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/run/create.py
ADDED
@@ -0,0 +1,1116 @@
|
|
1
|
+
"""
|
2
|
+
Code for the `ybox-create` script that is used to create and configure a new ybox container.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import getpass
|
7
|
+
import grp
|
8
|
+
import os
|
9
|
+
import pwd
|
10
|
+
import re
|
11
|
+
import shutil
|
12
|
+
import stat
|
13
|
+
import subprocess
|
14
|
+
import sys
|
15
|
+
from collections import defaultdict
|
16
|
+
from configparser import ConfigParser, SectionProxy
|
17
|
+
from pathlib import Path
|
18
|
+
from textwrap import dedent
|
19
|
+
from typing import Optional
|
20
|
+
|
21
|
+
from ybox.cmd import PkgMgr, RepoCmd, YboxLabel, check_ybox_exists, run_command
|
22
|
+
from ybox.config import Consts, StaticConfiguration
|
23
|
+
from ybox.env import Environ, NotSupportedError, PathName
|
24
|
+
from ybox.filelock import FileLock
|
25
|
+
from ybox.pkg.inst import install_package, wrap_container_files
|
26
|
+
from ybox.print import (bgcolor, fgcolor, print_color, print_error, print_info,
|
27
|
+
print_warn)
|
28
|
+
from ybox.run.graphics import (add_env_option, add_mount_option, enable_dri,
|
29
|
+
enable_nvidia, enable_wayland, enable_x11)
|
30
|
+
from ybox.run.pkg import parse_args as pkg_parse_args
|
31
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
32
|
+
from ybox.util import (EnvInterpolation, config_reader,
|
33
|
+
copy_ybox_scripts_to_container, ini_file_reader,
|
34
|
+
select_item_from_menu, truncate_file,
|
35
|
+
wait_for_ybox_container, write_ybox_version)
|
36
|
+
|
37
|
+
_EXTRACT_PARENS_NAME = re.compile(r"^.*\(([^)]+)\)$")
|
38
|
+
_DEP_SUFFIX = re.compile(r"^(.*):dep\((.*)\)$")
|
39
|
+
_WS_RE = re.compile(r"\s+")
|
40
|
+
|
41
|
+
|
42
|
+
# Note: deliberately not using os.path.join for joining paths since the code only works on
|
43
|
+
# Linux/POSIX systems where path separator will always be "/" and explicitly forcing the same.
|
44
|
+
#
|
45
|
+
# Configuration files should be in $HOME/.config/ybox or ybox package installation directory.
|
46
|
+
|
47
|
+
def main() -> None:
|
48
|
+
"""main function for `ybox-create` script"""
|
49
|
+
main_argv(sys.argv[1:])
|
50
|
+
|
51
|
+
|
52
|
+
def main_argv(argv: list[str]) -> None:
|
53
|
+
"""
|
54
|
+
Main entrypoint of `ybox-create` that takes a list of arguments which are usually the
|
55
|
+
command-line arguments of the `main()` function. Pass ["-h"]/["--help"] to see all the
|
56
|
+
available arguments with help message for each.
|
57
|
+
|
58
|
+
:param argv: arguments to the function (main function passes `sys.argv[1:]`)
|
59
|
+
"""
|
60
|
+
args = parse_args(argv)
|
61
|
+
env = Environ()
|
62
|
+
docker_cmd = env.docker_cmd
|
63
|
+
|
64
|
+
# use provided distribution else let user select from available ones
|
65
|
+
distro = select_distribution(args, env)
|
66
|
+
# the profile used to build the podman/docker command-line which is either provided
|
67
|
+
# on command-line or else let user select from available ones in standard locations
|
68
|
+
profile = select_profile(args, env)
|
69
|
+
|
70
|
+
box_name = process_args(args, distro, profile)
|
71
|
+
print_color(f"Creating ybox container named '{box_name}' for distribution '{distro}' "
|
72
|
+
f"using profile '{profile}'", fg=fgcolor.green)
|
73
|
+
if check_ybox_exists(docker_cmd, box_name):
|
74
|
+
print_error(f"ybox container '{box_name}' already exists.")
|
75
|
+
sys.exit(1)
|
76
|
+
|
77
|
+
conf = StaticConfiguration(env, distro, box_name)
|
78
|
+
# read the distribution specific configuration
|
79
|
+
base_image_name, shared_root_dirs, secondary_groups, distro_config = read_distribution_config(
|
80
|
+
args, conf)
|
81
|
+
# setup entrypoint and related scripts to share with the container on a mount point
|
82
|
+
setup_ybox_scripts(conf, distro_config)
|
83
|
+
|
84
|
+
docker_full_args = [docker_cmd, "run", "-itd", f"--name={box_name}"]
|
85
|
+
# process the profile before any actions to ensure it is in proper shape
|
86
|
+
pkgmgr = distro_config["pkgmgr"]
|
87
|
+
shared_root, box_conf, apps_with_deps = process_sections(profile, conf, pkgmgr,
|
88
|
+
docker_full_args)
|
89
|
+
process_distribution_config(distro_config, docker_full_args)
|
90
|
+
current_user = getpass.getuser()
|
91
|
+
|
92
|
+
# The sequence for container creation and run is thus:
|
93
|
+
# 1) First start a basic container with the smallest upstream distro image (important to save
|
94
|
+
# space when `base.shared_root` is provided) with "entrypoint-base.sh" as the entrypoint
|
95
|
+
# script giving user/group arguments to be same as the user as on the host machine.
|
96
|
+
# 2) Next do a podman/docker commit and save the stopped container as local image which
|
97
|
+
# will be used henceforth. The main point of doing #1 is to ensure that a sudo enabled
|
98
|
+
# user is available which matches the current host user so that "--userns" option
|
99
|
+
# will not try to remap the image that can substantially increase the size of image.
|
100
|
+
# Either way, the user created by "--userns" in the container does not have sudo
|
101
|
+
# permissions, so temporarily need to run such a container as root user in any case.
|
102
|
+
# Hence, step 1 uses a cleaner and better option that also creates separate
|
103
|
+
# container-specific images that can be enhanced with more container-specific stuff
|
104
|
+
# later if required. Also change the USER and WORKDIR of the commit to point to the
|
105
|
+
# user added in step 1).
|
106
|
+
# 3) Start up the saved image of step 2 and run the distro specific entrypoint script
|
107
|
+
# that will do basic configuration and package installation (e.g. git/neovim/... on arch)
|
108
|
+
# followed by the generic "entrypoint.sh" script which will create the configuration
|
109
|
+
# file links (from [configs] section), install required apps (from [apps] section),
|
110
|
+
# followed by invoking the startup scripts from the [startup] section.
|
111
|
+
# 4) The container is now ready to use so 'ybox-cmd' will only do a podman/docker exec
|
112
|
+
# of /bin/bash to open a shell (or any given command).
|
113
|
+
# 5) Mounts and environment variables are set up for step 3 which are automatically also
|
114
|
+
# available in step 4, and hence no special setup is required in step 4.
|
115
|
+
#
|
116
|
+
# If `base.shared_root` is provided, the above sequence has the following changes:
|
117
|
+
# 1) First acquire a file/process lock so that no other container creation can interfere.
|
118
|
+
# 2) Once the lock has been acquired, check if the shared container image already exists.
|
119
|
+
# If it does exist, then skip to step 7.
|
120
|
+
# 3) If not, then start the basic container like in step 1) of previous sequence.
|
121
|
+
# 4) Like step 2) of the previous sequence, commit the container but with a temporary name.
|
122
|
+
# 5) Unlike step 3) of the previous sequence, do a temporary restart of the previous
|
123
|
+
# committed image with "--userns=keep-id" option (podman), and then copy the shared root
|
124
|
+
# directories to the shared mount point. This copying cannot be done in step 4) above
|
125
|
+
# because the file permissions are different with and without the --userns option.
|
126
|
+
# For docker that does not support the "--userns=keep-id" option, the container image
|
127
|
+
# needs to be run as root user that maps to host's user.
|
128
|
+
# 6) Stop the container and commit the image again with the final shared image name.
|
129
|
+
# Delete the previous temporary image. Release the file lock acquired in step 1).
|
130
|
+
# 7) Now start the shared image like in step 3) of the previous sequence but with the
|
131
|
+
# additional root directory mounts that were copied in step 5 above.
|
132
|
+
# Finally, continue with step 4) onwards of previous sequence.
|
133
|
+
|
134
|
+
# handle the shared_root case: acquire file lock and check if shared container image exists
|
135
|
+
if shared_root:
|
136
|
+
os.makedirs(os.path.dirname(shared_root),
|
137
|
+
mode=Consts.default_directory_mode(), exist_ok=True)
|
138
|
+
with FileLock(f"{shared_root}-image.lock"):
|
139
|
+
# if image already exists, then skip the subsequent steps
|
140
|
+
if subprocess.run([docker_cmd, "inspect", "--type=image",
|
141
|
+
"--format={{.Id}}", conf.box_image(True)], check=False,
|
142
|
+
stdout=subprocess.DEVNULL,
|
143
|
+
stderr=subprocess.DEVNULL).returncode != 0:
|
144
|
+
# run the "base" container with appropriate arguments for the current user to
|
145
|
+
# the 'entrypoint-base.sh' script to create the user and group in the container
|
146
|
+
run_base_container(base_image_name, current_user, secondary_groups, docker_cmd,
|
147
|
+
conf)
|
148
|
+
# commit the stopped container with a temporary name, then remove the container;
|
149
|
+
# keeping a separate tmp_image helps reduce size of final image a bit because
|
150
|
+
# this one is without --userns while the final shared image is with --userns
|
151
|
+
tmp_image = f"{conf.box_image(False)}__ybox_tmp"
|
152
|
+
commit_container(docker_cmd, tmp_image, conf)
|
153
|
+
# start a container using the temporary image with "--userns" option to make
|
154
|
+
# a copy of the container root directories to the shared location
|
155
|
+
run_shared_copy_container(docker_cmd, tmp_image, shared_root, shared_root_dirs,
|
156
|
+
conf, args.quiet)
|
157
|
+
# finally commit this container with the name of the shared image
|
158
|
+
commit_container(docker_cmd, conf.box_image(True), conf)
|
159
|
+
remove_image(docker_cmd, tmp_image)
|
160
|
+
# in case a shared root directory is not present but shared image is present,
|
161
|
+
# need to run the container to copy to shared root
|
162
|
+
elif any((not os.path.exists(f"{shared_root}{s_dir}") for s_dir in
|
163
|
+
shared_root_dirs.split(","))):
|
164
|
+
run_shared_copy_container(docker_cmd, conf.box_image(True), shared_root,
|
165
|
+
shared_root_dirs, conf, args.quiet)
|
166
|
+
remove_container(docker_cmd, conf)
|
167
|
+
else:
|
168
|
+
# for no shared_root case, its best to refresh the local image
|
169
|
+
run_command([docker_cmd, "pull", base_image_name],
|
170
|
+
error_msg="fetching container base image")
|
171
|
+
# run the "base" container with appropriate arguments for the current user to the
|
172
|
+
# 'entrypoint-base.sh' script to create the user and group in the container
|
173
|
+
run_base_container(base_image_name, current_user, secondary_groups, docker_cmd, conf)
|
174
|
+
# commit the stopped container, remove it, then start new container with the
|
175
|
+
# "--userns=keep-id" option (podman) for the required container state
|
176
|
+
commit_container(docker_cmd, conf.box_image(False), conf)
|
177
|
+
|
178
|
+
# there is one additional stop/start below because all packages are upgraded by the
|
179
|
+
# entrypoint script to pick up any important fixes which can lead to system libraries
|
180
|
+
# getting upgraded, so it's best to restart container for those to take effect properly
|
181
|
+
|
182
|
+
# set up the final container with all the required arguments
|
183
|
+
print_info(f"Initializing container for '{distro}' using '{profile}'")
|
184
|
+
start_container(docker_full_args, current_user, shared_root, shared_root_dirs, conf)
|
185
|
+
print_info("Waiting for the container to initialize (see "
|
186
|
+
f"'ybox-logs -f {box_name}' for detailed progress)")
|
187
|
+
# wait for container to initialize while printing out its progress from conf.status_file
|
188
|
+
wait_for_ybox_container(docker_cmd, conf)
|
189
|
+
|
190
|
+
# remove distribution specific scripts and restart container the final time
|
191
|
+
print_info(f"Restarting the final container '{box_name}'")
|
192
|
+
Path(f"{conf.scripts_dir}/{Consts.entrypoint_init_done_file()}").touch(mode=0o644)
|
193
|
+
restart_container(docker_cmd, conf)
|
194
|
+
print_info("Waiting for the container to be ready (see "
|
195
|
+
f"'ybox-logs -f {box_name}' for detailed progress)")
|
196
|
+
wait_for_ybox_container(docker_cmd, conf)
|
197
|
+
# truncate the app.list and config.list files so that those actions are skipped if the
|
198
|
+
# container is restarted later
|
199
|
+
if os.access(conf.app_list, os.W_OK):
|
200
|
+
truncate_file(conf.app_list)
|
201
|
+
if os.access(conf.config_list, os.W_OK):
|
202
|
+
truncate_file(conf.config_list)
|
203
|
+
|
204
|
+
# finally add the state and register the installed packages that were reassigned to this
|
205
|
+
# container (because the previously destroyed one has the same configuration and shared root)
|
206
|
+
with YboxStateManagement(env) as state:
|
207
|
+
owned_packages = state.register_container(box_name, distro, shared_root, box_conf,
|
208
|
+
args.force_own_orphans)
|
209
|
+
# create wrappers for owned_packages
|
210
|
+
if owned_packages:
|
211
|
+
list_cmd = pkgmgr[PkgMgr.LIST_FILES.value]
|
212
|
+
for package, (copy_type, app_flags) in owned_packages.items():
|
213
|
+
# skip packages already scheduled to be installed
|
214
|
+
if package in apps_with_deps:
|
215
|
+
continue
|
216
|
+
# skip all questions for -q/--quiet (equivalent to -qq to `ybox-pkg install`)
|
217
|
+
quiet = 2 if args.quiet else 0
|
218
|
+
# box_conf can be skipped in new state.db but not for pre 0.9.3 having empty flags
|
219
|
+
if local_copies := wrap_container_files(package, copy_type, app_flags, list_cmd,
|
220
|
+
docker_cmd, conf, box_conf, shared_root,
|
221
|
+
quiet):
|
222
|
+
# register the package again with the local_copies (no change to package_deps)
|
223
|
+
state.register_package(box_name, package, local_copies, copy_type, app_flags,
|
224
|
+
shared_root, dep_type=None, dep_of="")
|
225
|
+
if apps_with_deps:
|
226
|
+
runtime_conf = RuntimeConfiguration(box_name, distro, shared_root, box_conf)
|
227
|
+
for app, deps in apps_with_deps.items():
|
228
|
+
pkg_args = ["install", "-z", box_name, "-o", "-c"]
|
229
|
+
if args.quiet:
|
230
|
+
pkg_args.append("-qq")
|
231
|
+
if deps:
|
232
|
+
pkg_args.append("-w")
|
233
|
+
pkg_args.append(",".join(deps))
|
234
|
+
pkg_args.append(app)
|
235
|
+
parsed_args = pkg_parse_args(pkg_args)
|
236
|
+
install_package(parsed_args, pkgmgr, docker_cmd, conf, runtime_conf, state)
|
237
|
+
|
238
|
+
|
239
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
240
|
+
"""
|
241
|
+
Parse command-line arguments for the program and return the result :class:`argparse.Namespace`.
|
242
|
+
|
243
|
+
:param argv: the list of arguments to be parsed
|
244
|
+
:return: the result of parsing using the `argparse` library as a :class:`argparse.Namespace`
|
245
|
+
"""
|
246
|
+
parser = argparse.ArgumentParser(
|
247
|
+
description="""Create a new ybox container for given Linux distribution and configured
|
248
|
+
with given file in INI format. It allows for set up of various aspects of
|
249
|
+
the ybox including support for X11, Wayland, audio, video acceleration,
|
250
|
+
NVIDIA, dbus among others. It also allows controlling various parameters
|
251
|
+
of the container including directories to be shared, logging etc.
|
252
|
+
See src/ybox/conf/profiles/basic.ini in the distribution for all available
|
253
|
+
options with examples and comments having the explanations.""")
|
254
|
+
parser.add_argument("-n", "--name", type=str,
|
255
|
+
help="name of the ybox; default is ybox-<distribution>_<profile> "
|
256
|
+
"if not provided (removing the .ini suffix from <profile> file)")
|
257
|
+
parser.add_argument("-F", "--force-own-orphans", action="store_true",
|
258
|
+
help="force ownership of orphan packages on the same shared root even "
|
259
|
+
"if container configuration does not match, meaning the packages "
|
260
|
+
"on the shared root directory that got orphaned due to their "
|
261
|
+
"owner container being destroyed will be assigned to this new "
|
262
|
+
"container regardless of the container configuration")
|
263
|
+
parser.add_argument("-C", "--distribution-config", type=str,
|
264
|
+
help="path to distribution configuration file to use instead of the "
|
265
|
+
"`distro.ini` from user/system configuration paths")
|
266
|
+
parser.add_argument("-q", "--quiet", action="store_true",
|
267
|
+
help="proceed without asking any questions using defaults where possible; "
|
268
|
+
"this should usually be used with explicit specification of "
|
269
|
+
"distribution and profile arguments else the operation will fail if "
|
270
|
+
"there more than one of them available")
|
271
|
+
parser.add_argument("distribution", nargs="?", type=str,
|
272
|
+
help="short name of the distribution as listed in distros/supported.list "
|
273
|
+
"(either in ~/.config/ybox or package's ybox/conf); it is optional "
|
274
|
+
"and user is presented with selection menu if there are multiple "
|
275
|
+
"listed in the first supported.list file that is found")
|
276
|
+
parser.add_argument("profile", nargs="?", type=str,
|
277
|
+
help="the profile defined in INI file to use for creating the ybox "
|
278
|
+
"(can be a relative or absolute path, or be in user or system "
|
279
|
+
"configuration directory which are $HOME/.config/ybox/profiles and "
|
280
|
+
"package's ybox/conf/profiles directory respectively); it is "
|
281
|
+
"optional and user is presented with a selection menu of the "
|
282
|
+
"available profiles in the user or system profiles directory "
|
283
|
+
"whichever is found (in that order)")
|
284
|
+
return parser.parse_args(argv)
|
285
|
+
|
286
|
+
|
287
|
+
def quick_config_read(file: PathName) -> ConfigParser:
|
288
|
+
"""
|
289
|
+
Quickly read an INI file without processing `includes` or applying any value interpolation.
|
290
|
+
|
291
|
+
:param file: a `Path` or resource file from importlib (`Traversable`) for the configuration
|
292
|
+
:return: an object of :class:`ConfigParser` from parsing the configuration file
|
293
|
+
"""
|
294
|
+
with file.open("r", encoding="utf-8") as profile_fd:
|
295
|
+
return ini_file_reader(profile_fd, None)
|
296
|
+
|
297
|
+
|
298
|
+
def select_distribution(args: argparse.Namespace, env: Environ) -> str:
|
299
|
+
"""
|
300
|
+
Interactively select a Linux distribution from a menu among the ones supported by this
|
301
|
+
installation of ybox, or if there is only one supported distribution, then return its name.
|
302
|
+
User can also provide one explicitly on the command-line which will be returned if valid.
|
303
|
+
|
304
|
+
:param args: the parsed arguments passed to the invoking `ybox-create` script
|
305
|
+
:param env: an instance of the current :class:`Environ`
|
306
|
+
:raises ValueError: unexpected internal error in the name of the distribution
|
307
|
+
:return: name of the selected or provided distribution
|
308
|
+
"""
|
309
|
+
support_list = env.search_config_path("distros/supported.list", only_sys_conf=True)
|
310
|
+
with support_list.open("r", encoding="utf-8") as supp_file:
|
311
|
+
supported_distros = supp_file.read().splitlines()
|
312
|
+
if distro := args.distribution:
|
313
|
+
# check that the distribution is in supported.list
|
314
|
+
if distro in supported_distros:
|
315
|
+
return str(distro)
|
316
|
+
print_error(f"Distribution '{distro}' not supported in {support_list}")
|
317
|
+
sys.exit(1)
|
318
|
+
if len(supported_distros) == 1:
|
319
|
+
print_info(f"Using distribution '{supported_distros[0]}'")
|
320
|
+
return supported_distros[0]
|
321
|
+
if args.quiet:
|
322
|
+
print_error(
|
323
|
+
f"Expected one supported distribution but found: {', '.join(supported_distros)}")
|
324
|
+
sys.exit(1)
|
325
|
+
|
326
|
+
# show a menu to choose from if the number of supported distributions exceeds 1
|
327
|
+
distro_names: list[str] = []
|
328
|
+
for distro in supported_distros:
|
329
|
+
distro_config = quick_config_read(env.search_config_path(
|
330
|
+
StaticConfiguration.distribution_config(distro), only_sys_conf=True))
|
331
|
+
distro_names.append(f"{distro_config['base']['name']} ({distro})") # should always exist
|
332
|
+
print_info("Please select the distribution to use for the container:", file=sys.stderr)
|
333
|
+
if (distro_name := select_item_from_menu(distro_names)) is None:
|
334
|
+
sys.exit(1)
|
335
|
+
if match := _EXTRACT_PARENS_NAME.match(distro_name):
|
336
|
+
return match.group(1)
|
337
|
+
raise ValueError(f"Unexpected distribution name string: {distro_name}")
|
338
|
+
|
339
|
+
|
340
|
+
def select_profile(args: argparse.Namespace, env: Environ) -> PathName:
|
341
|
+
"""
|
342
|
+
Interactively select a profile for ybox container to use for its setup from a menu among the
|
343
|
+
ones provided by this installation of ybox, or those setup in user's configuration.
|
344
|
+
If there is only one available profile, then return its name. User can also provide one
|
345
|
+
explicitly on the command-line which will be returned if valid.
|
346
|
+
|
347
|
+
:param args: the parsed arguments passed to the invoking `ybox-create` script
|
348
|
+
:param env: an instance of the current :class:`Environ`
|
349
|
+
:raises ValueError: unexpected internal error in the name of the profile
|
350
|
+
:return: name of the selected or provided profile
|
351
|
+
"""
|
352
|
+
# the profile used to build the podman/docker command-line
|
353
|
+
if profile_arg := args.profile:
|
354
|
+
if os.access(profile_arg, os.R_OK):
|
355
|
+
return Path(profile_arg)
|
356
|
+
return env.search_config_path(f"profiles/{profile_arg}")
|
357
|
+
|
358
|
+
# search for available profiles in standard locations and provide a selection menu
|
359
|
+
# for the user
|
360
|
+
profile_names: list[str] = []
|
361
|
+
profiles_dir = env.search_config_path("profiles")
|
362
|
+
profiles = [file for file in profiles_dir.iterdir() if file.is_file()]
|
363
|
+
if len(profiles) == 1:
|
364
|
+
print_info(f"Using profile '{profiles[0]}'")
|
365
|
+
return profiles[0]
|
366
|
+
if len(profiles) == 0:
|
367
|
+
print_error(f"No valid profile found in '{profiles_dir}'")
|
368
|
+
sys.exit(1)
|
369
|
+
|
370
|
+
profiles.sort(key=str)
|
371
|
+
if args.quiet:
|
372
|
+
print_error(
|
373
|
+
f"Expected one configured profile but found: {', '.join([p.name for p in profiles])}")
|
374
|
+
sys.exit(1)
|
375
|
+
|
376
|
+
for profile in profiles:
|
377
|
+
profile_config = quick_config_read(profile)
|
378
|
+
profile_names.append(f"{profile_config['base']['name']} ({profile.name})")
|
379
|
+
print_info("Please select the profile to use for the container:", file=sys.stderr)
|
380
|
+
if (profile_name := select_item_from_menu(profile_names)) is None:
|
381
|
+
sys.exit(1)
|
382
|
+
if match := _EXTRACT_PARENS_NAME.match(profile_name):
|
383
|
+
return profiles_dir.joinpath(match.group(1))
|
384
|
+
raise ValueError(f"Unexpected profile name string: {profile_name}")
|
385
|
+
|
386
|
+
|
387
|
+
def process_args(args: argparse.Namespace, distro: str, profile: PathName) -> str:
|
388
|
+
"""
|
389
|
+
Initial processing of the provided command-line arguments to form the desired name of the
|
390
|
+
ybox container.
|
391
|
+
|
392
|
+
:param args: the parsed arguments passed to the invoking `ybox-create` script
|
393
|
+
:param distro: the Linux distribution name returned by :func:`select_distribution` to use for
|
394
|
+
the ybox container
|
395
|
+
:param profile: the profile file returned by :func:`select_profile` to use for ybox container
|
396
|
+
configuration as a `Path` or resource file from importlib (`Traversable`)
|
397
|
+
:return: the ybox container name to use
|
398
|
+
"""
|
399
|
+
ini_suffix = ".ini"
|
400
|
+
if args.name:
|
401
|
+
box_name = args.name
|
402
|
+
else:
|
403
|
+
def_name = profile.name
|
404
|
+
if def_name.endswith(ini_suffix):
|
405
|
+
def_name = def_name[:-len(ini_suffix)]
|
406
|
+
def_name = f"ybox-{distro}_{def_name}"
|
407
|
+
box_name = def_name if args.quiet else input(
|
408
|
+
f"Name of the container to create (default: {def_name}): ").strip()
|
409
|
+
if not box_name:
|
410
|
+
box_name = def_name
|
411
|
+
|
412
|
+
# don't allow spaces or weird characters in the name
|
413
|
+
if not re.fullmatch(r"[\w.\-]+", box_name):
|
414
|
+
print_error(f"Invalid container name '{box_name}' -- only alphanumeric, underscore and "
|
415
|
+
"hyphen characters are accepted")
|
416
|
+
sys.exit(1)
|
417
|
+
return box_name
|
418
|
+
|
419
|
+
|
420
|
+
def process_sections(profile: PathName, conf: StaticConfiguration, pkgmgr: SectionProxy,
|
421
|
+
docker_args: list[str]) -> tuple[str, ConfigParser, dict[str, list[str]]]:
|
422
|
+
"""
|
423
|
+
Process all the sections in the given profile file to return a tuple having:
|
424
|
+
* shared root to use for the container (if any)
|
425
|
+
* :class:`ConfigParser` object from parsing the ini format profile, and
|
426
|
+
* dictionary having the packages to be installed as specified in the `[apps]` section
|
427
|
+
of the profile mapped to list of dependent packages for each application.
|
428
|
+
|
429
|
+
:param profile: the profile file returned by :func:`select_profile` to use for ybox container
|
430
|
+
configuration as a `Path` or resource file from importlib (`Traversable`)
|
431
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
432
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
433
|
+
:param docker_args: list of arguments to be provided to podman/docker command for creating the
|
434
|
+
final ybox container which is populated with required options as per
|
435
|
+
the configuration in the given profile
|
436
|
+
:raises NotSupportedError: if there is an unknown section or key in the ini format profile
|
437
|
+
:return: tuple of container's shared root, :class:`ConfigParser` object from parsing the
|
438
|
+
profile, and dictionary of apps with dependencies to be installed in the container
|
439
|
+
from the `[apps]` section of the profile
|
440
|
+
"""
|
441
|
+
# Read the config file, recursively reading the includes if present,
|
442
|
+
# then replace the environment variables and the special ${NOW:...} from all values.
|
443
|
+
# Skip environment variable substitution for the "configs" section since the values
|
444
|
+
# there have to be written as is to 'config.list' file for the container (since the
|
445
|
+
# $HOME variable can be different inside the container).
|
446
|
+
env_interpolation = EnvInterpolation(conf.env, ["configs"])
|
447
|
+
config = config_reader(profile, env_interpolation)
|
448
|
+
# [base] section should always be present
|
449
|
+
if not config.has_section("base"):
|
450
|
+
raise NotSupportedError(f"Missing [base] section in profile '{profile}'")
|
451
|
+
shared_root, config_hardlinks = process_base_section(config["base"], profile, conf,
|
452
|
+
docker_args)
|
453
|
+
apps_with_deps: dict[str, list[str]] = {}
|
454
|
+
# finally process all the sections and the keys forming the podman/docker command-line
|
455
|
+
for section in config.sections():
|
456
|
+
if section == "security":
|
457
|
+
process_security_section(config["security"], profile, docker_args)
|
458
|
+
elif section == "mounts":
|
459
|
+
process_mounts_section(config["mounts"], docker_args)
|
460
|
+
elif section == "env":
|
461
|
+
process_env_section(config["env"], docker_args)
|
462
|
+
elif section == "configs":
|
463
|
+
if config_hardlinks is not None:
|
464
|
+
process_configs_section(config["configs"], config_hardlinks, conf, docker_args)
|
465
|
+
elif section == "apps":
|
466
|
+
apps_with_deps = process_apps_section(config["apps"], conf, pkgmgr)
|
467
|
+
elif section not in ("base", "app_flags", "startup"):
|
468
|
+
raise NotSupportedError(f"Unknown section [{section}] in '{profile}' "
|
469
|
+
"or one of its includes")
|
470
|
+
return shared_root, config, apps_with_deps
|
471
|
+
|
472
|
+
|
473
|
+
def read_distribution_config(args: argparse.Namespace,
|
474
|
+
conf: StaticConfiguration) -> tuple[str, str, str, ConfigParser]:
|
475
|
+
"""
|
476
|
+
Read and parse the Linux distribution's `distro.ini` file and return a tuple having:
|
477
|
+
* the container image name
|
478
|
+
* comma-separate list of directories shared if `shared_root` is provided for the container
|
479
|
+
* secondary groups of the user, and
|
480
|
+
* the result of parsing the `distro.ini` as an object of :class:`ConfigParser`.
|
481
|
+
|
482
|
+
:param args: the parsed arguments passed to the invoking `ybox-create` script
|
483
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
484
|
+
:return: a tuple of image name, shared root directories, secondary groups and an object of
|
485
|
+
:class:`ConfigParser` for the `distro.ini`
|
486
|
+
"""
|
487
|
+
env = conf.env
|
488
|
+
env_interpolation = EnvInterpolation(env, [])
|
489
|
+
distro_conf_file = args.distribution_config or conf.distribution_config(conf.distribution)
|
490
|
+
distro_config = config_reader(env.search_config_path(
|
491
|
+
distro_conf_file, only_sys_conf=True), env_interpolation)
|
492
|
+
distro_base_section = distro_config["base"]
|
493
|
+
image_name = distro_base_section["image"] # should always exist
|
494
|
+
shared_root_dirs = distro_base_section["shared_root_dirs"] # should always exist
|
495
|
+
secondary_groups = distro_base_section["secondary_groups"] # should always exist
|
496
|
+
return image_name, shared_root_dirs, secondary_groups, distro_config
|
497
|
+
|
498
|
+
|
499
|
+
def process_distribution_config(distro_config: ConfigParser, docker_args: list[str]) -> None:
|
500
|
+
"""
|
501
|
+
Process the Linux distribution's `distro.ini` file and populate relevant podman/docker options
|
502
|
+
in the given `docker_args` list.
|
503
|
+
|
504
|
+
:param distro_config: an object of :class:`ConfigParser` from parsing the Linux
|
505
|
+
distribution's `distro.ini`
|
506
|
+
:param docker_args: list of arguments to be provided to podman/docker command for creating the
|
507
|
+
final ybox container which is populated with required options
|
508
|
+
"""
|
509
|
+
if distro_config.getboolean("base", "configure_fastest_mirrors", fallback=False):
|
510
|
+
add_env_option(docker_args, "CONFIGURE_FASTEST_MIRRORS", "1")
|
511
|
+
if distro_config.has_section("packages"):
|
512
|
+
packages_section = distro_config["packages"]
|
513
|
+
for key, env_var in (("required", "REQUIRED_PKGS"), ("recommended", "RECOMMENDED_PKGS"),
|
514
|
+
("suggested", "SUGGESTED_PKGS"), ("required_deps", "REQUIRED_DEPS"),
|
515
|
+
("recommended_deps", "RECOMMENDED_DEPS"),
|
516
|
+
("suggested_deps", "SUGGESTED_DEPS"), ("extra", "EXTRA_PKGS")):
|
517
|
+
if value := packages_section.get(key):
|
518
|
+
add_env_option(docker_args, env_var, _WS_RE.sub(" ", value))
|
519
|
+
key_server = distro_config.get("repo", RepoCmd.DEFAULT_GPG_KEY_SERVER.value,
|
520
|
+
fallback=Consts.default_key_server())
|
521
|
+
add_env_option(docker_args, "DEFAULT_GPG_KEY_SERVER", key_server)
|
522
|
+
|
523
|
+
|
524
|
+
def process_base_section(base_section: SectionProxy, profile: PathName, conf: StaticConfiguration,
|
525
|
+
docker_args: list[str]) -> tuple[str, Optional[bool]]:
|
526
|
+
"""
|
527
|
+
Process the `[base]` section in the container profile to append required podman/docker
|
528
|
+
options in the list that has been passed, and return a tuple having the shared root to use for
|
529
|
+
the container (if any), and the value of `config_hardlinks` key in the section.
|
530
|
+
|
531
|
+
:param base_section: an object of :class:`SectionProxy` from parsing the `[base]` section
|
532
|
+
:param profile: the profile file returned by :func:`select_profile` to use for ybox container
|
533
|
+
configuration as a `Path` or resource file from importlib (`Traversable`)
|
534
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
535
|
+
:param docker_args: list of podman/docker arguments to which required options as per the
|
536
|
+
configuration in the `[base]` section are appended
|
537
|
+
:raises NotSupportedError: if there is an unknown key in the `[base]` section
|
538
|
+
:return: tuple of container's shared root and the value of `config_hardlinks` key
|
539
|
+
"""
|
540
|
+
env = conf.env
|
541
|
+
# shared root is disabled by default
|
542
|
+
shared_root = ""
|
543
|
+
# hard links are false by default (value of None means skip the [configs] section entirely)
|
544
|
+
config_hardlinks: Optional[bool] = False
|
545
|
+
# configure locale by default
|
546
|
+
config_locale = True
|
547
|
+
# DRI will be force enabled if NVIDIA support is enabled
|
548
|
+
dri = False
|
549
|
+
# NVIDIA is disabled by default
|
550
|
+
nvidia = False
|
551
|
+
nvidia_ctk = False
|
552
|
+
for key, val in base_section.items():
|
553
|
+
if key == "home":
|
554
|
+
if val:
|
555
|
+
# create the source directory if it does not exist
|
556
|
+
os.makedirs(val, mode=Consts.default_directory_mode(), exist_ok=True)
|
557
|
+
add_mount_option(docker_args, val, env.target_home)
|
558
|
+
elif key == "shared_root":
|
559
|
+
shared_root = val or ""
|
560
|
+
elif key == "config_hardlinks":
|
561
|
+
if val:
|
562
|
+
config_hardlinks = _get_boolean(val)
|
563
|
+
else:
|
564
|
+
config_hardlinks = None
|
565
|
+
elif key == "config_locale":
|
566
|
+
config_locale = _get_boolean(val)
|
567
|
+
elif key == "x11":
|
568
|
+
if _get_boolean(val):
|
569
|
+
enable_x11(docker_args, env)
|
570
|
+
elif key == "wayland":
|
571
|
+
if _get_boolean(val):
|
572
|
+
enable_wayland(docker_args, env)
|
573
|
+
elif key == "pulseaudio":
|
574
|
+
if _get_boolean(val):
|
575
|
+
enable_pulse(docker_args, env)
|
576
|
+
elif key == "dbus":
|
577
|
+
if _get_boolean(val):
|
578
|
+
enable_dbus(docker_args, base_section.getboolean("dbus_sys", fallback=False), env)
|
579
|
+
elif key == "dri":
|
580
|
+
dri = _get_boolean(val)
|
581
|
+
elif key == "nvidia":
|
582
|
+
nvidia = _get_boolean(val)
|
583
|
+
elif key == "nvidia_ctk":
|
584
|
+
nvidia_ctk = _get_boolean(val)
|
585
|
+
elif key == "shm_size":
|
586
|
+
if val:
|
587
|
+
docker_args.append(f"--shm-size={val}")
|
588
|
+
elif key == "pids_limit":
|
589
|
+
if val:
|
590
|
+
docker_args.append(f"--pids-limit={val}")
|
591
|
+
elif key == "log_driver":
|
592
|
+
if val:
|
593
|
+
docker_args.append(f"--log-driver={val}")
|
594
|
+
elif key == "log_opts":
|
595
|
+
add_multi_opt(docker_args, "log-opt", val)
|
596
|
+
# create the log directory if required
|
597
|
+
log_dirs = [mt.group(1) for mt in
|
598
|
+
(re.match("^--log-opt=path=(.*)/.*$", path) for path in docker_args) if mt]
|
599
|
+
for log_dir in log_dirs:
|
600
|
+
os.makedirs(log_dir, mode=Consts.default_directory_mode(), exist_ok=True)
|
601
|
+
elif key not in ("name", "dbus_sys", "includes"):
|
602
|
+
raise NotSupportedError(f"Unknown key '{key}' in the [base] of {profile} "
|
603
|
+
"or its includes")
|
604
|
+
if config_locale:
|
605
|
+
for lang_var in ("LANG", "LANGUAGE"):
|
606
|
+
add_env_option(docker_args, lang_var)
|
607
|
+
if dri or nvidia or nvidia_ctk:
|
608
|
+
enable_dri(docker_args)
|
609
|
+
if nvidia_ctk: # takes precedence over "nvidia" option
|
610
|
+
docker_args.append("--device=nvidia.com/gpu=all")
|
611
|
+
elif nvidia:
|
612
|
+
enable_nvidia(docker_args, conf)
|
613
|
+
return shared_root, config_hardlinks
|
614
|
+
|
615
|
+
|
616
|
+
def _get_boolean(value: str) -> bool:
|
617
|
+
"""
|
618
|
+
Convert a string to boolean else raise a `ValueError` if the value is not a boolean.
|
619
|
+
Recognizes the following values and is case-insensitive: 0/1, false/true, no/yes, off/on.
|
620
|
+
"""
|
621
|
+
if (result := ConfigParser.BOOLEAN_STATES.get(value.lower())) is not None:
|
622
|
+
return result
|
623
|
+
raise ValueError(f"Not a boolean: {value}")
|
624
|
+
|
625
|
+
|
626
|
+
def enable_pulse(docker_args: list[str], env: Environ) -> None:
|
627
|
+
"""
|
628
|
+
Append options to podman/docker arguments to share host machine's pulse/pipewire audio server
|
629
|
+
with the new ybox container.
|
630
|
+
|
631
|
+
:param docker_args: list of podman/docker arguments to which the options have to be appended
|
632
|
+
:param env: an instance of the current :class:`Environ`
|
633
|
+
"""
|
634
|
+
cookie = f"{env.home}/.config/pulse/cookie"
|
635
|
+
if os.access(cookie, os.R_OK):
|
636
|
+
add_mount_option(docker_args, cookie, f"{env.target_home}/.config/pulse/cookie", "ro")
|
637
|
+
if env.xdg_rt_dir:
|
638
|
+
pulse_native = f"{env.xdg_rt_dir}/pulse/native"
|
639
|
+
if os.access(pulse_native, os.W_OK):
|
640
|
+
add_mount_option(docker_args, pulse_native, f"{env.target_xdg_rt_dir}/pulse/native")
|
641
|
+
for pwf in [f for f in os.listdir(env.xdg_rt_dir) if re.match("pipewire-[0-9]+$", f)]:
|
642
|
+
pipewire_path = f"{env.xdg_rt_dir}/{pwf}"
|
643
|
+
if os.access(pipewire_path, os.W_OK):
|
644
|
+
add_mount_option(docker_args, pipewire_path, f"{env.target_xdg_rt_dir}/{pwf}")
|
645
|
+
|
646
|
+
|
647
|
+
def enable_dbus(docker_args: list[str], sys_enable: bool, env: Environ) -> None:
|
648
|
+
"""
|
649
|
+
Append options to podman/docker arguments to share host machine's dbus message bus
|
650
|
+
with the new ybox container.
|
651
|
+
|
652
|
+
:param docker_args: list of podman/docker arguments to which the options have to be appended
|
653
|
+
:param sys_enable: if True then also share host machine's system dbus message bus in addition
|
654
|
+
to the user dbus message bus
|
655
|
+
:param env: an instance of the current :class:`Environ`
|
656
|
+
"""
|
657
|
+
def replace_target_dir(src: str) -> str:
|
658
|
+
return src.replace(f"{env.xdg_rt_dir}/", f"{env.target_xdg_rt_dir}/")
|
659
|
+
if dbus_session := os.environ.get("DBUS_SESSION_BUS_ADDRESS"):
|
660
|
+
dbus_user = dbus_session[dbus_session.find("=") + 1:]
|
661
|
+
if (dbus_opts_idx := dbus_user.find(",")) != -1:
|
662
|
+
dbus_user = dbus_user[:dbus_opts_idx]
|
663
|
+
add_mount_option(docker_args, dbus_user, replace_target_dir(dbus_user))
|
664
|
+
add_env_option(docker_args, "DBUS_SESSION_BUS_ADDRESS", replace_target_dir(dbus_session))
|
665
|
+
if sys_enable:
|
666
|
+
dbus_sys = "/run/dbus/system_bus_socket"
|
667
|
+
dbus_sys2 = "/var/run/dbus/system_bus_socket"
|
668
|
+
if os.access(dbus_sys, os.W_OK):
|
669
|
+
add_mount_option(docker_args, dbus_sys, dbus_sys)
|
670
|
+
elif os.access(dbus_sys2, os.W_OK):
|
671
|
+
add_mount_option(docker_args, dbus_sys2, dbus_sys)
|
672
|
+
|
673
|
+
|
674
|
+
def add_multi_opt(docker_args: list[str], opt: str, val: Optional[str]) -> None:
|
675
|
+
"""
|
676
|
+
Append a comma-separated value in the profile as multiple options to podman/docker arguments.
|
677
|
+
|
678
|
+
:param docker_args: list of podman/docker arguments to which the options have to be appended
|
679
|
+
:param val: the comma-separated value
|
680
|
+
:param opt: the option name which is added as `--{opt}=...` to podman/docker arguments
|
681
|
+
"""
|
682
|
+
if val:
|
683
|
+
for opt_val in val.split(","):
|
684
|
+
docker_args.append(f"--{opt}={opt_val}")
|
685
|
+
|
686
|
+
|
687
|
+
def process_security_section(sec_section: SectionProxy, profile: PathName,
|
688
|
+
docker_args: list[str]) -> None:
|
689
|
+
"""
|
690
|
+
Process the `[security]` section in the container profile to append required podman/docker
|
691
|
+
options in the list that has been passed.
|
692
|
+
|
693
|
+
:param sec_section: an object of :class:`SectionProxy` from parsing the `[security]` section
|
694
|
+
:param profile: the profile file returned by :func:`select_profile` to use for ybox container
|
695
|
+
configuration as a `Path` or resource file from importlib (`Traversable`)
|
696
|
+
:param docker_args: list of podman/docker arguments to which required options as per the
|
697
|
+
configuration in the `[security]` section are appended
|
698
|
+
:raises NotSupportedError: if there is an unknown key in the `[security]` section
|
699
|
+
"""
|
700
|
+
sec_options = {"label", "apparmor", "seccomp", "mask", "umask", "proc_opts"}
|
701
|
+
single_options = {"seccomp_policy", "ipc", "cgroup_parent", "cgroupns", "cgroups"}
|
702
|
+
multi_options = {"caps_add": "cap-add", "caps_drop": "cap-drop", "ulimits": "ulimit",
|
703
|
+
"cgroup_confs": "cgroup-conf", "device_cgroup_rules": "device-cgroup-rule",
|
704
|
+
"secrets": "secret"}
|
705
|
+
for key, val in sec_section.items():
|
706
|
+
if key in sec_options:
|
707
|
+
if val:
|
708
|
+
docker_args.append(f"--security-opt={key.replace('_', '-')}={val}")
|
709
|
+
elif opt := multi_options.get(key):
|
710
|
+
add_multi_opt(docker_args, opt, val)
|
711
|
+
elif key in single_options:
|
712
|
+
if val:
|
713
|
+
docker_args.append(f"--{key.replace('_', '-')}={val}")
|
714
|
+
elif key == "no_new_privileges":
|
715
|
+
if _get_boolean(val):
|
716
|
+
docker_args.append("--security-opt=no-new-privileges")
|
717
|
+
else:
|
718
|
+
raise NotSupportedError(f"Unknown key '{key}' in the [security] of {profile} "
|
719
|
+
"or its includes")
|
720
|
+
|
721
|
+
|
722
|
+
def process_mounts_section(mounts_section: SectionProxy, docker_args: list[str]) -> None:
|
723
|
+
"""
|
724
|
+
Process the `[mounts]` section in the container profile to append required podman/docker
|
725
|
+
options in the list that has been passed.
|
726
|
+
|
727
|
+
:param mounts_section: an object of :class:`SectionProxy` from parsing the `[mounts]` section
|
728
|
+
:param docker_args: list of podman/docker arguments to which required options as per the
|
729
|
+
configuration in the `[mounts]` section are appended
|
730
|
+
"""
|
731
|
+
# keys here are only symbolic names and serve no purpose other than allowing
|
732
|
+
# later profile files to override previous ones
|
733
|
+
for _, val in mounts_section.items():
|
734
|
+
if val:
|
735
|
+
if "=" in val or "," in val:
|
736
|
+
docker_args.append(f"--mount={val}")
|
737
|
+
else:
|
738
|
+
docker_args.append(f"-v={val}")
|
739
|
+
|
740
|
+
|
741
|
+
def process_configs_section(configs_section: SectionProxy, config_hardlinks: bool,
|
742
|
+
conf: StaticConfiguration, docker_args: list[str]) -> None:
|
743
|
+
"""
|
744
|
+
Process the `[configs]` section in the container profile to append required podman/docker
|
745
|
+
options in the list that has been passed. This method also makes hard-links or copies of the
|
746
|
+
configuration files in local user's ybox data directory to mount inside the ybox container so
|
747
|
+
that the selected configuration files from host are available in the container.
|
748
|
+
|
749
|
+
:param configs_section: an object of :class:`SectionProxy` from parsing the `[configs]` section
|
750
|
+
:param config_hardlinks: the value of `config_hardlinks` key from the `[base]` section that
|
751
|
+
indicates whether the configuration files from host have to be made
|
752
|
+
available by creating hard-links to them or by making copies
|
753
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
754
|
+
:param docker_args: list of podman/docker arguments to which required options as per the
|
755
|
+
configuration in the `[configs]` section are appended
|
756
|
+
"""
|
757
|
+
# copy or link the mentioned files in [configs] section which can be either files
|
758
|
+
# or directories (recursively copy/link in the latter case)
|
759
|
+
# this is refreshed on every container start
|
760
|
+
|
761
|
+
# always recreate the directory to pick up any changes
|
762
|
+
if os.path.exists(conf.configs_dir):
|
763
|
+
shutil.rmtree(conf.configs_dir)
|
764
|
+
os.makedirs(conf.configs_dir, mode=Consts.default_directory_mode(), exist_ok=True)
|
765
|
+
if config_hardlinks:
|
766
|
+
print_info("Creating hard links to paths specified in [configs] ...")
|
767
|
+
else:
|
768
|
+
print_info("Creating a copy of paths specified in [configs] ...")
|
769
|
+
# write the links to be created in a file that will be passed to container
|
770
|
+
# entrypoint to create symlinks from container user's home to the mounted config files
|
771
|
+
with open(conf.config_list, "w", encoding="utf-8") as config_list_fd:
|
772
|
+
for key, val in configs_section.items():
|
773
|
+
# perform environment variable substitution now which was skipped earlier
|
774
|
+
f_val = os.path.expandvars(val)
|
775
|
+
split_idx = f_val.find("->")
|
776
|
+
if split_idx == -1:
|
777
|
+
raise NotSupportedError("Incorrect value format in [configs] section for "
|
778
|
+
f"'{key}'. Required: '{{src}} -> {{dest}}'")
|
779
|
+
src_path = os.path.realpath(f_val[:split_idx].strip())
|
780
|
+
dest_path = f"{conf.configs_dir}/{f_val[split_idx + 2:].strip()}"
|
781
|
+
if os.access(src_path, os.R_OK):
|
782
|
+
os.makedirs(os.path.dirname(dest_path),
|
783
|
+
mode=Consts.default_directory_mode(), exist_ok=True)
|
784
|
+
if os.path.isdir(src_path):
|
785
|
+
copytree(src_path, dest_path, hardlink=config_hardlinks)
|
786
|
+
else:
|
787
|
+
if config_hardlinks:
|
788
|
+
os.link(os.path.realpath(src_path), dest_path, follow_symlinks=True)
|
789
|
+
else:
|
790
|
+
shutil.copy2(src_path, dest_path, follow_symlinks=True)
|
791
|
+
config_list_fd.write(val)
|
792
|
+
config_list_fd.write("\n")
|
793
|
+
else:
|
794
|
+
print_warn(f"Skipping inaccessible configuration path '{src_path}'")
|
795
|
+
print_info("DONE.")
|
796
|
+
# finally mount the configs directory to corresponding directory in the target container
|
797
|
+
add_mount_option(docker_args, conf.configs_dir, conf.target_configs_dir, "ro")
|
798
|
+
|
799
|
+
|
800
|
+
def process_env_section(env_section: SectionProxy, docker_args: list[str]) -> None:
|
801
|
+
"""
|
802
|
+
Process the `[env]` section in the container profile to append required podman/docker
|
803
|
+
options in the list that has been passed.
|
804
|
+
|
805
|
+
:param env_section: an object of :class:`SectionProxy` from parsing the `[env]` section
|
806
|
+
:param docker_args: list of podman/docker arguments to which required options as per the
|
807
|
+
configuration in the `[env]` section are appended
|
808
|
+
"""
|
809
|
+
for key, val in env_section.items():
|
810
|
+
add_env_option(docker_args, key, val)
|
811
|
+
|
812
|
+
|
813
|
+
def process_apps_section(apps_section: SectionProxy, conf: StaticConfiguration,
|
814
|
+
pkgmgr: SectionProxy) -> dict[str, list[str]]:
|
815
|
+
"""
|
816
|
+
Process the `[apps]` section in the container profile to return a dictionary having packages
|
817
|
+
to be installed mapped to the list of dependencies to be installed along with.
|
818
|
+
|
819
|
+
:param apps_section: an object of :class:`SectionProxy` from parsing the `[apps]` section
|
820
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
821
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
822
|
+
:return: dictionary of package names mapped to their list of dependencies as specified
|
823
|
+
in the `[apps]` section
|
824
|
+
"""
|
825
|
+
if len(apps_section) == 0:
|
826
|
+
return {}
|
827
|
+
quiet_flag = pkgmgr[PkgMgr.QUIET_FLAG.value]
|
828
|
+
opt_dep_flag = pkgmgr[PkgMgr.OPT_DEP_FLAG.value]
|
829
|
+
install_cmd = pkgmgr[PkgMgr.INSTALL.value].format(quiet=quiet_flag, opt_dep="")
|
830
|
+
clean_cmd = pkgmgr[PkgMgr.CLEAN_QUIET.value]
|
831
|
+
if not install_cmd:
|
832
|
+
print_color("Skipping app installation since no 'pkgmgr.install' has "
|
833
|
+
"been defined in distro.ini or is empty",
|
834
|
+
fg=fgcolor.lightgray, bg=bgcolor.red)
|
835
|
+
return {}
|
836
|
+
# write pkgmgr.conf for entrypoint.sh
|
837
|
+
with open(f"{conf.scripts_dir}/pkgmgr.conf", "w", encoding="utf-8") as pkg_fd:
|
838
|
+
pkg_fd.write(f"PKGMGR_INSTALL='{install_cmd}'\n")
|
839
|
+
pkg_fd.write(f"PKGMGR_CLEAN='{clean_cmd}'\n")
|
840
|
+
apps_with_deps = defaultdict[str, list[str]](list[str])
|
841
|
+
|
842
|
+
def capture_dep(match: re.Match[str]) -> str:
|
843
|
+
dep = match.group(1)
|
844
|
+
apps_with_deps[match.group(2)].append(dep)
|
845
|
+
return dep
|
846
|
+
|
847
|
+
with open(conf.app_list, "w", encoding="utf-8") as apps_fd:
|
848
|
+
for _, val in apps_section.items():
|
849
|
+
apps = [app.strip() for app in val.split(",")]
|
850
|
+
deps = [capture_dep(match) for dep in apps if (match := _DEP_SUFFIX.match(dep))]
|
851
|
+
if deps:
|
852
|
+
apps = [app for app in apps if not _DEP_SUFFIX.match(app)]
|
853
|
+
apps_fd.write(f"{opt_dep_flag} {' '.join(deps)}\n")
|
854
|
+
if apps:
|
855
|
+
apps_fd.write(f"{' '.join(apps)}\n")
|
856
|
+
for app in apps:
|
857
|
+
assert apps_with_deps[app] is not None # insert with empty list if absent
|
858
|
+
return apps_with_deps
|
859
|
+
|
860
|
+
|
861
|
+
# The shutil.copytree(...) method does not work correctly for "symlinks=False" (or at least
|
862
|
+
# not like 'cp -rL' or 'cp -rlL') where it does not create the source symlinked file rather
|
863
|
+
# only the target one in the destination directory.
|
864
|
+
# This is a simplified version using os.walk(...) that works correctly that always has:
|
865
|
+
# a. follow_symlinks=True, and b. ignore_dangling_symlinks=True
|
866
|
+
def copytree(src: str, dest: str, hardlink: bool = False) -> None:
|
867
|
+
"""
|
868
|
+
Copy or create hard links to a source directory tree in the given destination directory.
|
869
|
+
Since hard links to directories are not supported, the destination will mirror the directories
|
870
|
+
of the source while the files inside will be either copies or hard links to the source.
|
871
|
+
|
872
|
+
:param src: the source directory tree
|
873
|
+
:param dest: the destination directory which should exist
|
874
|
+
:param hardlink: if True then create hard links to the files in the source (so it should
|
875
|
+
be in the same filesystem) else copy the files, defaults to False
|
876
|
+
"""
|
877
|
+
for src_dir, _, src_files in os.walk(src, followlinks=True):
|
878
|
+
# substitute 'src' prefix with 'dest'
|
879
|
+
dest_dir = f"{dest}{src_dir[len(src):]}"
|
880
|
+
os.mkdir(dest_dir, mode=stat.S_IMODE(os.stat(src_dir).st_mode))
|
881
|
+
for src_file in src_files:
|
882
|
+
src_path = f"{src_dir}/{src_file}"
|
883
|
+
if os.path.exists(src_path):
|
884
|
+
if hardlink:
|
885
|
+
try:
|
886
|
+
os.link(os.path.realpath(src_path), f"{dest_dir}/{src_file}",
|
887
|
+
follow_symlinks=True)
|
888
|
+
continue
|
889
|
+
except OSError:
|
890
|
+
# in case of error (likely due to cross-device link) fallback to copying
|
891
|
+
pass
|
892
|
+
shutil.copy2(src_path, f"{dest_dir}/{src_file}", follow_symlinks=True)
|
893
|
+
|
894
|
+
|
895
|
+
def setup_ybox_scripts(conf: StaticConfiguration, distro_config: ConfigParser) -> None:
|
896
|
+
"""
|
897
|
+
Create/copy various scripts required for the ybox container including entrypoint scripts,
|
898
|
+
Linux distribution specific scripts and other required executables (e.g. `run-in-dir`).
|
899
|
+
|
900
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
901
|
+
:param distro_config: an object of :class:`ConfigParser` from parsing the Linux
|
902
|
+
distribution's `distro.ini`
|
903
|
+
"""
|
904
|
+
# first create local mount directory having entrypoint and other scripts
|
905
|
+
if os.path.exists(conf.scripts_dir):
|
906
|
+
shutil.rmtree(conf.scripts_dir)
|
907
|
+
os.makedirs(conf.scripts_dir, exist_ok=True)
|
908
|
+
# allow for read/execute permissions for all since non-root user needs access with docker
|
909
|
+
os.chmod(conf.scripts_dir, mode=0o755)
|
910
|
+
copy_ybox_scripts_to_container(conf, distro_config)
|
911
|
+
# finally write the current version to "version" file in scripts directory of the container
|
912
|
+
write_ybox_version(conf)
|
913
|
+
|
914
|
+
|
915
|
+
def run_base_container(image_name: str, current_user: str, secondary_groups: str, docker_cmd: str,
|
916
|
+
conf: StaticConfiguration) -> None:
|
917
|
+
"""
|
918
|
+
Start a minimal container for the selected Linux distribution with the smallest upstream image
|
919
|
+
(important to save space when `base.shared_root` is provided) with `entrypoint-base.sh` as the
|
920
|
+
entrypoint script giving user/group arguments to be same as the user as on the host machine.
|
921
|
+
|
922
|
+
:param image_name: distribution image to use for the container as specified in `distro.ini`
|
923
|
+
:param current_user: the current user executing the `ybox-create` script
|
924
|
+
:param secondary_groups: secondary groups for the container user as specified in `distro.ini`
|
925
|
+
:param docker_cmd: the podman/docker executable to use
|
926
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
927
|
+
"""
|
928
|
+
# get current user and group details to pass to the entrypoint script
|
929
|
+
user_entry = pwd.getpwnam(current_user)
|
930
|
+
group_entry = grp.getgrgid(user_entry.pw_gid)
|
931
|
+
print_warn(f"Creating container specific image having sudo user '{current_user}'")
|
932
|
+
docker_run = [docker_cmd, "run", f"--name={conf.box_name}",
|
933
|
+
f"-v={conf.scripts_dir}:{conf.target_scripts_dir}:ro",
|
934
|
+
f"--label={YboxLabel.CONTAINER_BASE.value}",
|
935
|
+
f"--entrypoint={conf.target_scripts_dir}/{Consts.entrypoint_base()}",
|
936
|
+
image_name, "-u", current_user, "-U", str(user_entry.pw_uid),
|
937
|
+
"-n", user_entry.pw_gecos, "-g", group_entry.gr_name,
|
938
|
+
"-G", str(group_entry.gr_gid), "-s", secondary_groups]
|
939
|
+
if conf.localtime:
|
940
|
+
docker_run.append("-l")
|
941
|
+
docker_run.append(conf.localtime)
|
942
|
+
if conf.timezone:
|
943
|
+
docker_run.append("-z")
|
944
|
+
docker_run.append(conf.timezone)
|
945
|
+
run_command(docker_run, error_msg="running container with base image")
|
946
|
+
|
947
|
+
|
948
|
+
def run_shared_copy_container(docker_cmd: str, image_name: str, shared_root: str,
|
949
|
+
shared_root_dirs: str, conf: StaticConfiguration,
|
950
|
+
quiet: bool) -> None:
|
951
|
+
"""
|
952
|
+
Start a container from a base distribution image (with minimal configuration) when
|
953
|
+
`shared_root` has been provided for the container and copy a configured set of directories
|
954
|
+
(`shared_root_dirs` in `distro.ini`) from the container to the `shared_root` directory.
|
955
|
+
This directory is then used as the root directory for all the final containers that share the
|
956
|
+
same `shared_root`.
|
957
|
+
|
958
|
+
If the provided `shared_root` directory already exists, then user is interactively asked
|
959
|
+
whether to delete the existing directory before proceeding.
|
960
|
+
|
961
|
+
:param docker_cmd: the podman/docker executable to use
|
962
|
+
:param image_name: distribution image to use for the container as specified in `distro.ini`
|
963
|
+
:param shared_root: the shared root directory to use for the container
|
964
|
+
:param shared_root_dirs: comma-separate list of directories shared between containers having
|
965
|
+
the same `shared_root`
|
966
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
967
|
+
:param quiet: if True then don't ask for user confirmation but assume a `no`
|
968
|
+
"""
|
969
|
+
# if shared root copy exists locally, then prompt user to delete it or else exit
|
970
|
+
if os.path.exists(shared_root):
|
971
|
+
input_msg = f"""
|
972
|
+
The shared root directory for '{conf.distribution}' already exists in:
|
973
|
+
{shared_root}
|
974
|
+
However, the corresponding ybox container image for '{conf.distribution}' does not
|
975
|
+
exist. This can happen if an old copy of shared root directory is lying around and
|
976
|
+
is usually safe to remove, but you should be sure that no other ybox is running
|
977
|
+
for '{conf.distribution}' with 'shared_root' configuration that is using that
|
978
|
+
directory. Should the root directory be removed (y/N): """
|
979
|
+
response = "N" if quiet else input(dedent(input_msg))
|
980
|
+
if response.strip().lower() == "y":
|
981
|
+
try:
|
982
|
+
shutil.rmtree(shared_root)
|
983
|
+
except OSError:
|
984
|
+
# try with sudo
|
985
|
+
run_command(["/usr/bin/sudo", "/bin/rm", "-rf", shared_root],
|
986
|
+
error_msg="deleting directory")
|
987
|
+
else:
|
988
|
+
print_error(f"Aborting creation of ybox container '{conf.box_name}'")
|
989
|
+
# remove the temporary image before exit
|
990
|
+
remove_image(docker_cmd, image_name)
|
991
|
+
sys.exit(1)
|
992
|
+
os.makedirs(shared_root, mode=Consts.default_directory_mode())
|
993
|
+
# the entrypoint-cp.sh script requires two arguments: first is the comma separated
|
994
|
+
# list of directories to be copied, and second the target directory
|
995
|
+
docker_full_cmd = [docker_cmd, "run", f"--name={conf.box_name}",
|
996
|
+
f"-v={conf.scripts_dir}:{conf.target_scripts_dir}:ro",
|
997
|
+
f"-v={shared_root}:{Consts.shared_root_mount_dir()}",
|
998
|
+
f"--label={YboxLabel.CONTAINER_COPY.value}", "--user=0",
|
999
|
+
f"--entrypoint={conf.target_scripts_dir}/{Consts.entrypoint_cp()}"]
|
1000
|
+
if conf.env.uses_podman:
|
1001
|
+
docker_full_cmd.append("--userns=keep-id")
|
1002
|
+
docker_full_cmd.extend((image_name, shared_root_dirs, Consts.shared_root_mount_dir()))
|
1003
|
+
run_command(docker_full_cmd, error_msg="running container for copying shared root")
|
1004
|
+
|
1005
|
+
|
1006
|
+
def commit_container(docker_cmd: str, image_name: str, conf: StaticConfiguration) -> None:
|
1007
|
+
"""
|
1008
|
+
Commit the contents of a container as a new image. This also sets up the `USER` and `WORKDIR`
|
1009
|
+
properties of the image to those of target user's name and home respectively (as detected in
|
1010
|
+
the `Environ` object).
|
1011
|
+
|
1012
|
+
:param docker_cmd: the podman/docker executable to use
|
1013
|
+
:param image_name: name of the image to create
|
1014
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
1015
|
+
"""
|
1016
|
+
run_command([docker_cmd, "commit", "--change", f"USER {conf.env.target_user}",
|
1017
|
+
"--change", f"WORKDIR {conf.env.target_home}", conf.box_name, image_name],
|
1018
|
+
error_msg="container commit")
|
1019
|
+
remove_container(docker_cmd, conf)
|
1020
|
+
|
1021
|
+
|
1022
|
+
def remove_container(docker_cmd: str, conf: StaticConfiguration) -> None:
|
1023
|
+
"""remove a stopped podman/docker container"""
|
1024
|
+
run_command([docker_cmd, "container", "rm", conf.box_name], error_msg="container rm")
|
1025
|
+
|
1026
|
+
|
1027
|
+
def remove_image(docker_cmd: str, image_name: str) -> None:
|
1028
|
+
"""remove an unused podman/docker image"""
|
1029
|
+
run_command([docker_cmd, "image", "rm", image_name], exit_on_error=False,
|
1030
|
+
error_msg="image remove")
|
1031
|
+
|
1032
|
+
|
1033
|
+
def start_container(docker_full_cmd: list[str], current_user: str, shared_root: str,
|
1034
|
+
shared_root_dirs: str, conf: StaticConfiguration) -> None:
|
1035
|
+
"""
|
1036
|
+
Create and start the final ybox container applying all the provided configuration.
|
1037
|
+
The following characteristics of the container are noteworthy:
|
1038
|
+
* uses docker or podman to create the container that are required to be in `rootless` mode
|
1039
|
+
* maps the host environment user ID to the same UID in the container (`--userns=keep-id`)
|
1040
|
+
when using podman else maps to the root user for docker
|
1041
|
+
* sets the container user to host environment user using `--user=...` option but does not
|
1042
|
+
enforce the primary group in that option so that the container user can belong to other
|
1043
|
+
secondary groups that is required for some applications
|
1044
|
+
* as a result of above, applications that need user namespace support (like Steam) need
|
1045
|
+
to be started with explicit elevated capabilities e.g. using `setpriv --ambient-caps -all`
|
1046
|
+
(this can be specified in `[app_flags]` section of the container profile or when installing
|
1047
|
+
the application using `ybox-pkg install` which will add the same to the wrapper executable
|
1048
|
+
and desktop files)
|
1049
|
+
* containers with the same configured `shared_root` will share the system installation
|
1050
|
+
so any changes to system directories in one container will reflect in all others;
|
1051
|
+
such a configuration reduces memory and disk overheads of multiple containers significantly
|
1052
|
+
but users should also keep in mind that packages installed in any container will be
|
1053
|
+
visible across all containers so care should be taken to not use potentially risky
|
1054
|
+
programs from less secure containers; the `ybox-pkg` tool provided a convenient high-level
|
1055
|
+
package manager that users should use for managing packages in the containers which will
|
1056
|
+
help in exposing packages only in designated containers
|
1057
|
+
* systemd user service file can be generated for podman to start the container automatically
|
1058
|
+
on user login; docker installations run a background user service in any case which starts
|
1059
|
+
up the container without any additional setup
|
1060
|
+
|
1061
|
+
:param docker_full_cmd: the `docker`/`podman run -itd` command with all the options filled
|
1062
|
+
in from the container profile specification as a list of string
|
1063
|
+
:param current_user: the current user executing the `ybox-create` script
|
1064
|
+
:param shared_root: the shared root directory to use for the container
|
1065
|
+
:param shared_root_dirs: comma-separate list of directories shared between containers having
|
1066
|
+
the same `shared_root`
|
1067
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
1068
|
+
"""
|
1069
|
+
add_mount_option(docker_full_cmd, conf.scripts_dir, conf.target_scripts_dir, "ro")
|
1070
|
+
# touch the status file and mount it
|
1071
|
+
status_path = Path(conf.status_file)
|
1072
|
+
status_path.unlink(missing_ok=True)
|
1073
|
+
status_path.touch(mode=0o600, exist_ok=False)
|
1074
|
+
add_mount_option(docker_full_cmd, conf.status_file, Consts.status_target_file())
|
1075
|
+
|
1076
|
+
if shared_root:
|
1077
|
+
for shared_dir in shared_root_dirs.split(","):
|
1078
|
+
add_mount_option(docker_full_cmd, f"{shared_root}{shared_dir}", shared_dir)
|
1079
|
+
docker_full_cmd.append(f"-e=XDG_RUNTIME_DIR={conf.env.target_xdg_rt_dir}")
|
1080
|
+
docker_full_cmd.append("-e=YBOX_TARGET_SCRIPTS_DIR") # pass this along for container scripts
|
1081
|
+
docker_full_cmd.append(f"--label={YboxLabel.CONTAINER_PRIMARY.value}")
|
1082
|
+
docker_full_cmd.append(f"--label={YboxLabel.CONTAINER_DISTRIBUTION.value}={conf.distribution}")
|
1083
|
+
docker_full_cmd.append(f"--entrypoint={conf.target_scripts_dir}/{Consts.entrypoint()}")
|
1084
|
+
# bubblewrap and thereby programs like steam do not work without --user
|
1085
|
+
# (https://github.com/containers/bubblewrap/issues/380#issuecomment-648169485)
|
1086
|
+
user_entry = pwd.getpwnam(current_user)
|
1087
|
+
user_uid = user_entry.pw_uid
|
1088
|
+
user_gid = user_entry.pw_gid
|
1089
|
+
if conf.env.uses_podman:
|
1090
|
+
docker_full_cmd.append(f"--user={user_uid}")
|
1091
|
+
docker_full_cmd.append("--userns=keep-id")
|
1092
|
+
else:
|
1093
|
+
docker_full_cmd.append("--user=0")
|
1094
|
+
docker_full_cmd.append(f"-e=YBOX_HOST_UID={user_uid}")
|
1095
|
+
docker_full_cmd.append(f"-e=YBOX_HOST_GID={user_gid}")
|
1096
|
+
docker_full_cmd.append(conf.box_image(bool(shared_root)))
|
1097
|
+
if os.access(conf.config_list, os.R_OK):
|
1098
|
+
docker_full_cmd.extend(["-c", f"{conf.target_scripts_dir}/config.list",
|
1099
|
+
"-d", conf.target_configs_dir])
|
1100
|
+
if os.access(conf.app_list, os.R_OK):
|
1101
|
+
docker_full_cmd.append("-a")
|
1102
|
+
docker_full_cmd.append(f"{conf.target_scripts_dir}/app.list")
|
1103
|
+
docker_full_cmd.append(conf.box_name)
|
1104
|
+
|
1105
|
+
if (code := int(run_command(docker_full_cmd, exit_on_error=False,
|
1106
|
+
error_msg="container launch"))) != 0:
|
1107
|
+
print_error(f"Also check 'ybox-logs {conf.box_name}' for details")
|
1108
|
+
sys.exit(code)
|
1109
|
+
|
1110
|
+
|
1111
|
+
def restart_container(docker_cmd: str, conf: StaticConfiguration) -> None:
|
1112
|
+
"""restart a stopped podman/docker container"""
|
1113
|
+
if (code := int(run_command([docker_cmd, "container", "start", conf.box_name],
|
1114
|
+
exit_on_error=False, error_msg="container restart"))) != 0:
|
1115
|
+
print_error(f"Also check 'ybox-logs {conf.box_name}' for details")
|
1116
|
+
sys.exit(code)
|