ybox 0.9.9__py3-none-any.whl → 0.9.10__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/basic.ini +29 -16
- ybox/conf/profiles/dev.ini +0 -6
- ybox/conf/resources/entrypoint-root.sh +1 -0
- ybox/conf/resources/entrypoint-user.sh +5 -3
- ybox/conf/resources/entrypoint.sh +22 -13
- ybox/conf/resources/prime-run +0 -2
- ybox/conf/resources/run-in-dir +4 -0
- ybox/conf/resources/run-user-bash-cmd +17 -1
- ybox/conf/resources/ybox-systemd.template +22 -0
- ybox/config.py +9 -1
- ybox/pkg/clean.py +1 -7
- ybox/pkg/info.py +1 -7
- ybox/pkg/inst.py +20 -14
- ybox/pkg/list.py +1 -6
- ybox/pkg/repair.py +4 -0
- ybox/pkg/search.py +1 -7
- ybox/run/cmd.py +2 -1
- ybox/run/control.py +91 -24
- ybox/run/create.py +186 -48
- ybox/run/destroy.py +67 -4
- ybox/run/logs.py +2 -1
- ybox/run/ls.py +2 -1
- ybox/run/pkg.py +46 -4
- ybox/state.py +22 -3
- ybox/util.py +5 -5
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/METADATA +35 -12
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/RECORD +38 -37
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/WHEEL +1 -1
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/LICENSE +0 -0
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/entry_points.txt +0 -0
- {ybox-0.9.9.dist-info → ybox-0.9.10.dist-info}/top_level.txt +0 -0
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,25 +6,30 @@ 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
35
|
print_color(f"Ybox container '{container_name}' already active", fg=fgcolor.cyan)
|
@@ -33,24 +38,38 @@ def start_container(docker_cmd: str, container_name: str):
|
|
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,31 @@ 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
|
+
|
66
110
|
def main_argv(argv: list[str]) -> None:
|
67
111
|
"""
|
68
112
|
Main entrypoint of `ybox-control` that takes a list of arguments which are usually the
|
@@ -73,19 +117,7 @@ def main_argv(argv: list[str]) -> None:
|
|
73
117
|
"""
|
74
118
|
args = parse_args(argv)
|
75
119
|
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")
|
120
|
+
args.func(docker_cmd, args)
|
89
121
|
|
90
122
|
|
91
123
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
@@ -96,7 +128,42 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
96
128
|
:return: the result of parsing using the `argparse` library as a :class:`argparse.Namespace`
|
97
129
|
"""
|
98
130
|
parser = argparse.ArgumentParser(description="control ybox containers")
|
99
|
-
parser.
|
100
|
-
|
101
|
-
|
131
|
+
operations = parser.add_subparsers(title="Operations", required=True, metavar="OPERATION",
|
132
|
+
help="DESCRIPTION")
|
133
|
+
|
134
|
+
start = operations.add_parser("start", help="start a ybox container")
|
135
|
+
_add_subparser_args(start, 60, "time in seconds to wait for a container to start")
|
136
|
+
start.set_defaults(func=start_container)
|
137
|
+
|
138
|
+
stop = operations.add_parser("stop", help="stop a ybox container")
|
139
|
+
_add_subparser_args(stop, 10,
|
140
|
+
"time in seconds to wait for a container to stop before killing it")
|
141
|
+
stop.add_argument("--ignore-stopped", action="store_true",
|
142
|
+
help="don't fail on an already stopped container")
|
143
|
+
stop.set_defaults(func=stop_container)
|
144
|
+
|
145
|
+
restart = operations.add_parser("restart", help="restart a ybox container")
|
146
|
+
_add_subparser_args(restart, 60, "time in seconds to wait for a container to restart")
|
147
|
+
restart.set_defaults(func=restart_container)
|
148
|
+
|
149
|
+
status = operations.add_parser("status", help="show status of a ybox container")
|
150
|
+
_add_subparser_args(status, 0, "")
|
151
|
+
status.set_defaults(func=show_container_status)
|
152
|
+
|
153
|
+
parser_version_check(parser, argv)
|
102
154
|
return parser.parse_args(argv)
|
155
|
+
|
156
|
+
|
157
|
+
def _add_subparser_args(subparser: argparse.ArgumentParser, timeout_default: int,
|
158
|
+
timeout_help: str) -> None:
|
159
|
+
"""
|
160
|
+
Add arguments for the sub-operation of the ybox-control command.
|
161
|
+
|
162
|
+
:param subparser: the :class:`argparse.ArgumentParser` object for the sub-command
|
163
|
+
:param timeout_default: default value for the -t/--timeout argument, or 0 to skip the argument
|
164
|
+
:param timeout_help: help string for the -t/--timeout argument
|
165
|
+
"""
|
166
|
+
if timeout_default != 0:
|
167
|
+
subparser.add_argument("-t", "--timeout", type=int, default=timeout_default,
|
168
|
+
help=timeout_help)
|
169
|
+
subparser.add_argument("container", help="name of the ybox")
|
ybox/run/create.py
CHANGED
@@ -18,13 +18,16 @@ from pathlib import Path
|
|
18
18
|
from textwrap import dedent
|
19
19
|
from typing import Optional
|
20
20
|
|
21
|
-
from ybox
|
21
|
+
from ybox import __version__ as product_version
|
22
|
+
from ybox.cmd import (PkgMgr, RepoCmd, YboxLabel, check_ybox_exists,
|
23
|
+
parser_version_check, run_command)
|
22
24
|
from ybox.config import Consts, StaticConfiguration
|
23
25
|
from ybox.env import Environ, NotSupportedError, PathName
|
24
26
|
from ybox.filelock import FileLock
|
25
27
|
from ybox.pkg.inst import install_package, wrap_container_files
|
26
28
|
from ybox.print import (bgcolor, fgcolor, print_color, print_error, print_info,
|
27
|
-
print_warn)
|
29
|
+
print_notice, print_warn)
|
30
|
+
from ybox.run.destroy import get_all_containers, remove_orphans_from_db
|
28
31
|
from ybox.run.graphics import (add_env_option, add_mount_option, enable_dri,
|
29
32
|
enable_nvidia, enable_wayland, enable_x11)
|
30
33
|
from ybox.run.pkg import parse_args as pkg_parse_args
|
@@ -181,19 +184,24 @@ def main_argv(argv: list[str]) -> None:
|
|
181
184
|
|
182
185
|
# set up the final container with all the required arguments
|
183
186
|
print_info(f"Initializing container for '{distro}' using '{profile}'")
|
184
|
-
|
187
|
+
run_container(docker_full_args, current_user, shared_root, shared_root_dirs, conf)
|
185
188
|
print_info("Waiting for the container to initialize (see "
|
186
189
|
f"'ybox-logs -f {box_name}' for detailed progress)")
|
187
190
|
# wait for container to initialize while printing out its progress from conf.status_file
|
188
|
-
wait_for_ybox_container(docker_cmd, conf)
|
191
|
+
wait_for_ybox_container(docker_cmd, conf, 600)
|
189
192
|
|
190
193
|
# remove distribution specific scripts and restart container the final time
|
191
|
-
print_info(f"
|
194
|
+
print_info(f"Starting the final container '{box_name}'")
|
192
195
|
Path(f"{conf.scripts_dir}/{Consts.entrypoint_init_done_file()}").touch(mode=0o644)
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
196
|
+
wait_msg = ("Waiting for the container to be ready "
|
197
|
+
f"(see ybox-logs -f {box_name}' for detailed progress)")
|
198
|
+
if args.systemd_service and (sys_path := os.pathsep.join(Consts.sys_bin_dirs())) and (
|
199
|
+
systemctl := shutil.which("systemctl", path=sys_path)):
|
200
|
+
create_and_start_service(box_name, env, systemctl, sys_path, wait_msg)
|
201
|
+
else:
|
202
|
+
start_container(docker_cmd, conf)
|
203
|
+
print_info(wait_msg)
|
204
|
+
wait_for_ybox_container(docker_cmd, conf, 120)
|
197
205
|
# truncate the app.list and config.list files so that those actions are skipped if the
|
198
206
|
# container is restarted later
|
199
207
|
if os.access(conf.app_list, os.W_OK):
|
@@ -201,11 +209,17 @@ def main_argv(argv: list[str]) -> None:
|
|
201
209
|
if os.access(conf.config_list, os.W_OK):
|
202
210
|
truncate_file(conf.config_list)
|
203
211
|
|
212
|
+
# check and remove any dangling container references in state database
|
213
|
+
valid_containers = set(get_all_containers(docker_cmd))
|
214
|
+
|
204
215
|
# finally add the state and register the installed packages that were reassigned to this
|
205
216
|
# container (because the previously destroyed one has the same configuration and shared root)
|
206
217
|
with YboxStateManagement(env) as state:
|
218
|
+
state.begin_transaction()
|
219
|
+
remove_orphans_from_db(valid_containers, state)
|
207
220
|
owned_packages = state.register_container(box_name, distro, shared_root, box_conf,
|
208
221
|
args.force_own_orphans)
|
222
|
+
state.commit()
|
209
223
|
# create wrappers for owned_packages
|
210
224
|
if owned_packages:
|
211
225
|
list_cmd = pkgmgr[PkgMgr.LIST_FILES.value]
|
@@ -254,6 +268,9 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
254
268
|
parser.add_argument("-n", "--name", type=str,
|
255
269
|
help="name of the ybox; default is ybox-<distribution>_<profile> "
|
256
270
|
"if not provided (removing the .ini suffix from <profile> file)")
|
271
|
+
parser.add_argument("-S", "--systemd-service", action="store_true",
|
272
|
+
help="create/overwrite user systemd service file for the container in "
|
273
|
+
"~/.config/systemd/user and enable it by default")
|
257
274
|
parser.add_argument("-F", "--force-own-orphans", action="store_true",
|
258
275
|
help="force ownership of orphan packages on the same shared root even "
|
259
276
|
"if container configuration does not match, meaning the packages "
|
@@ -262,7 +279,14 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
262
279
|
"container regardless of the container configuration")
|
263
280
|
parser.add_argument("-C", "--distribution-config", type=str,
|
264
281
|
help="path to distribution configuration file to use instead of the "
|
265
|
-
"
|
282
|
+
"'distro.ini' from user/system configuration paths")
|
283
|
+
parser.add_argument("--distribution-image", type=str,
|
284
|
+
help="custom container image to use that overrides the one specified in "
|
285
|
+
"the distribution's 'distro.ini'; note that the distribution "
|
286
|
+
"configuration scripts make assumptions on the available utilities "
|
287
|
+
"in the image so you should ensure that the provided image is "
|
288
|
+
"compatible with and a superset of the base image specified in the "
|
289
|
+
"builtin profile of the distribution in the installed version")
|
266
290
|
parser.add_argument("-q", "--quiet", action="store_true",
|
267
291
|
help="proceed without asking any questions using defaults where possible; "
|
268
292
|
"this should usually be used with explicit specification of "
|
@@ -281,6 +305,7 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
281
305
|
"optional and user is presented with a selection menu of the "
|
282
306
|
"available profiles in the user or system profiles directory "
|
283
307
|
"whichever is found (in that order)")
|
308
|
+
parser_version_check(parser, argv)
|
284
309
|
return parser.parse_args(argv)
|
285
310
|
|
286
311
|
|
@@ -363,7 +388,7 @@ def select_profile(args: argparse.Namespace, env: Environ) -> PathName:
|
|
363
388
|
if len(profiles) == 1:
|
364
389
|
print_info(f"Using profile '{profiles[0]}'")
|
365
390
|
return profiles[0]
|
366
|
-
if
|
391
|
+
if not profiles:
|
367
392
|
print_error(f"No valid profile found in '{profiles_dir}'")
|
368
393
|
sys.exit(1)
|
369
394
|
|
@@ -491,6 +516,12 @@ def read_distribution_config(args: argparse.Namespace,
|
|
491
516
|
distro_conf_file, only_sys_conf=True), env_interpolation)
|
492
517
|
distro_base_section = distro_config["base"]
|
493
518
|
image_name = distro_base_section["image"] # should always exist
|
519
|
+
if args.distribution_image:
|
520
|
+
print()
|
521
|
+
print_notice(f"Overriding distribution's container image '{image_name}' with the one "
|
522
|
+
f"provided on the command-line: {args.distribution_image}")
|
523
|
+
print()
|
524
|
+
image_name = args.distribution_image
|
494
525
|
shared_root_dirs = distro_base_section["shared_root_dirs"] # should always exist
|
495
526
|
secondary_groups = distro_base_section["secondary_groups"] # should always exist
|
496
527
|
return image_name, shared_root_dirs, secondary_groups, distro_config
|
@@ -759,7 +790,7 @@ def process_configs_section(configs_section: SectionProxy, config_hardlinks: boo
|
|
759
790
|
# this is refreshed on every container start
|
760
791
|
|
761
792
|
# always recreate the directory to pick up any changes
|
762
|
-
if os.path.
|
793
|
+
if os.path.isdir(conf.configs_dir):
|
763
794
|
shutil.rmtree(conf.configs_dir)
|
764
795
|
os.makedirs(conf.configs_dir, mode=Consts.default_directory_mode(), exist_ok=True)
|
765
796
|
if config_hardlinks:
|
@@ -777,19 +808,36 @@ def process_configs_section(configs_section: SectionProxy, config_hardlinks: boo
|
|
777
808
|
raise NotSupportedError("Incorrect value format in [configs] section for "
|
778
809
|
f"'{key}'. Required: '{{src}} -> {{dest}}'")
|
779
810
|
src_path = os.path.realpath(f_val[:split_idx].strip())
|
780
|
-
|
811
|
+
dest_rel_path = f_val[split_idx + 2:].strip()
|
812
|
+
dest_path = f"{conf.configs_dir}/{dest_rel_path}"
|
781
813
|
if os.access(src_path, os.R_OK):
|
782
|
-
os.
|
783
|
-
|
814
|
+
if os.path.exists(dest_path):
|
815
|
+
if os.path.isdir(dest_path):
|
816
|
+
shutil.rmtree(dest_path)
|
817
|
+
else:
|
818
|
+
os.unlink(dest_path)
|
819
|
+
else:
|
820
|
+
os.makedirs(os.path.dirname(dest_path),
|
821
|
+
mode=Consts.default_directory_mode(), exist_ok=True)
|
784
822
|
if os.path.isdir(src_path):
|
785
823
|
copytree(src_path, dest_path, hardlink=config_hardlinks)
|
786
824
|
else:
|
787
825
|
if config_hardlinks:
|
788
|
-
|
826
|
+
try:
|
827
|
+
os.link(os.path.realpath(src_path), dest_path, follow_symlinks=True)
|
828
|
+
except OSError:
|
829
|
+
# in case of error (likely due to cross-device link) fallback to copy
|
830
|
+
shutil.copy2(src_path, dest_path, follow_symlinks=True)
|
789
831
|
else:
|
790
832
|
shutil.copy2(src_path, dest_path, follow_symlinks=True)
|
791
|
-
|
792
|
-
|
833
|
+
# - if key has ":copy", then indicate creation of copies in the target
|
834
|
+
# - if key has ":dir", then indicate replication of directory structure with links
|
835
|
+
# for individual files
|
836
|
+
# - else a symlink should be created
|
837
|
+
# handled by "replicate_config_files" function in entrypoint.sh
|
838
|
+
prefix = "COPY" if key.endswith(":copy") else (
|
839
|
+
"LINK_DIR" if key.endswith(":dir") else "LINK")
|
840
|
+
config_list_fd.write(f"{prefix}:{dest_rel_path}\n")
|
793
841
|
else:
|
794
842
|
print_warn(f"Skipping inaccessible configuration path '{src_path}'")
|
795
843
|
print_info("DONE.")
|
@@ -822,7 +870,7 @@ def process_apps_section(apps_section: SectionProxy, conf: StaticConfiguration,
|
|
822
870
|
:return: dictionary of package names mapped to their list of dependencies as specified
|
823
871
|
in the `[apps]` section
|
824
872
|
"""
|
825
|
-
if
|
873
|
+
if not apps_section:
|
826
874
|
return {}
|
827
875
|
quiet_flag = pkgmgr[PkgMgr.QUIET_FLAG.value]
|
828
876
|
opt_dep_flag = pkgmgr[PkgMgr.OPT_DEP_FLAG.value]
|
@@ -859,37 +907,73 @@ def process_apps_section(apps_section: SectionProxy, conf: StaticConfiguration,
|
|
859
907
|
|
860
908
|
|
861
909
|
# The shutil.copytree(...) method does not work correctly for "symlinks=False" (or at least
|
862
|
-
# not like 'cp -
|
863
|
-
# only the target one in the destination directory
|
864
|
-
#
|
865
|
-
#
|
866
|
-
|
910
|
+
# not like 'cp -aL' or 'cp -alL') where it does not create the source symlinked file rather
|
911
|
+
# only the target one in the destination directory, and neither does it provide the option to
|
912
|
+
# create hardlinks.
|
913
|
+
#
|
914
|
+
# This is a simplified version using recursive os.scandir(...) that works correctly so that the
|
915
|
+
# copy will continue to work even if source disappears in all cases but still avoid making copies
|
916
|
+
# for all symlinks. So it behaves like follow_symlinks=True if the symlink destination is outside
|
917
|
+
# the "src_path" else it is False.
|
918
|
+
def copytree(src_path: str, dest: str, hardlink: bool = False,
|
919
|
+
src_root: Optional[str] = None) -> None:
|
867
920
|
"""
|
868
921
|
Copy or create hard links to a source directory tree in the given destination directory.
|
869
922
|
Since hard links to directories are not supported, the destination will mirror the directories
|
870
923
|
of the source while the files inside will be either copies or hard links to the source.
|
924
|
+
Symlinks are copied as such if the source ones point within the tree, else the target is
|
925
|
+
followed and copied recursively.
|
871
926
|
|
872
|
-
:
|
927
|
+
Note: this function only handles regular files and directories (and hard/symbolic links to
|
928
|
+
them) and will skip special files like device files, fifos etc.
|
929
|
+
|
930
|
+
:param src_path: the resolved source directory (using `os.path.realpath` or `Path.resolve`)
|
873
931
|
:param dest: the destination directory which should exist
|
874
932
|
:param hardlink: if True then create hard links to the files in the source (so it should
|
875
933
|
be in the same filesystem) else copy the files, defaults to False
|
934
|
+
:param src_root: the resolved root source directory (same as `src_path` if `None`)
|
876
935
|
"""
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
936
|
+
src_root = src_root or src_path
|
937
|
+
os.mkdir(dest, mode=stat.S_IMODE(os.stat(src_path).st_mode))
|
938
|
+
# follow symlink if it leads to outside the "src" tree, else copy as a symlink which
|
939
|
+
# ensures that all destination files are always accessible regardless of source going
|
940
|
+
# away (for example), and also reduces the size with hardlink=False as much as possible
|
941
|
+
with os.scandir(src_path) as src_it:
|
942
|
+
for entry in src_it:
|
943
|
+
entry_path = ""
|
944
|
+
entry_st_mode = 0
|
945
|
+
dest_path = f"{dest}/{entry.name}"
|
946
|
+
try:
|
947
|
+
if entry.is_symlink():
|
948
|
+
# check if entry is a symlink inside the tree or outside
|
949
|
+
l_name = os.readlink(entry.path)
|
950
|
+
if "/" not in l_name: # shortcut check for links in the same directory
|
951
|
+
os.symlink(l_name, dest_path)
|
888
952
|
continue
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
953
|
+
entry_path = os.path.realpath(entry.path)
|
954
|
+
if entry_path.startswith(src_root) and entry_path[len(src_root)] == "/":
|
955
|
+
rpath = entry_path[len(src_root) + 1:]
|
956
|
+
os.symlink(("../" * rpath.count("/")) + rpath, dest_path)
|
957
|
+
continue
|
958
|
+
entry_st_mode = os.stat(entry_path).st_mode
|
959
|
+
entry_path = entry_path or entry.path
|
960
|
+
if stat.S_ISREG(entry_st_mode) or (entry_st_mode == 0 and entry.is_file()):
|
961
|
+
if hardlink:
|
962
|
+
try:
|
963
|
+
os.link(entry_path, dest_path)
|
964
|
+
continue
|
965
|
+
except OSError:
|
966
|
+
# in case of error (likely due to cross-device link) fallback to copy
|
967
|
+
pass
|
968
|
+
shutil.copy2(entry_path, dest_path)
|
969
|
+
elif stat.S_ISDIR(entry_st_mode) or (entry_st_mode == 0 and entry.is_dir()):
|
970
|
+
copytree(entry_path, dest_path, hardlink,
|
971
|
+
entry_path if entry_st_mode else src_root)
|
972
|
+
else:
|
973
|
+
print_warn(f"Skipping copy/link of special file (fifo/dev/...) '{entry_path}'")
|
974
|
+
except OSError as err:
|
975
|
+
# ignore permission and related errors and continue
|
976
|
+
print_warn(f"Skipping copy/link of '{entry_path}' due to error: {err}")
|
893
977
|
|
894
978
|
|
895
979
|
def setup_ybox_scripts(conf: StaticConfiguration, distro_config: ConfigParser) -> None:
|
@@ -902,7 +986,7 @@ def setup_ybox_scripts(conf: StaticConfiguration, distro_config: ConfigParser) -
|
|
902
986
|
distribution's `distro.ini`
|
903
987
|
"""
|
904
988
|
# first create local mount directory having entrypoint and other scripts
|
905
|
-
if os.path.
|
989
|
+
if os.path.isdir(conf.scripts_dir):
|
906
990
|
shutil.rmtree(conf.scripts_dir)
|
907
991
|
os.makedirs(conf.scripts_dir, exist_ok=True)
|
908
992
|
# allow for read/execute permissions for all since non-root user needs access with docker
|
@@ -1030,8 +1114,8 @@ def remove_image(docker_cmd: str, image_name: str) -> None:
|
|
1030
1114
|
error_msg="image remove")
|
1031
1115
|
|
1032
1116
|
|
1033
|
-
def
|
1034
|
-
|
1117
|
+
def run_container(docker_full_cmd: list[str], current_user: str, shared_root: str,
|
1118
|
+
shared_root_dirs: str, conf: StaticConfiguration) -> None:
|
1035
1119
|
"""
|
1036
1120
|
Create and start the final ybox container applying all the provided configuration.
|
1037
1121
|
The following characteristics of the container are noteworthy:
|
@@ -1054,9 +1138,8 @@ def start_container(docker_full_cmd: list[str], current_user: str, shared_root:
|
|
1054
1138
|
programs from less secure containers; the `ybox-pkg` tool provided a convenient high-level
|
1055
1139
|
package manager that users should use for managing packages in the containers which will
|
1056
1140
|
help in exposing packages only in designated containers
|
1057
|
-
* systemd user service file can be generated for podman to start the container
|
1058
|
-
on user login
|
1059
|
-
up the container without any additional setup
|
1141
|
+
* systemd user service file can be generated for podman/docker to start the container
|
1142
|
+
automatically on user login when -S/--systemd-service option has been provided
|
1060
1143
|
|
1061
1144
|
:param docker_full_cmd: the `docker`/`podman run -itd` command with all the options filled
|
1062
1145
|
in from the container profile specification as a list of string
|
@@ -1089,8 +1172,10 @@ def start_container(docker_full_cmd: list[str], current_user: str, shared_root:
|
|
1089
1172
|
if conf.env.uses_podman:
|
1090
1173
|
docker_full_cmd.append(f"--user={user_uid}")
|
1091
1174
|
docker_full_cmd.append("--userns=keep-id")
|
1175
|
+
docker_full_cmd.append(f"-e=USER={current_user}")
|
1092
1176
|
else:
|
1093
1177
|
docker_full_cmd.append("--user=0")
|
1178
|
+
docker_full_cmd.append("-e=USER=root")
|
1094
1179
|
docker_full_cmd.append(f"-e=YBOX_HOST_UID={user_uid}")
|
1095
1180
|
docker_full_cmd.append(f"-e=YBOX_HOST_GID={user_gid}")
|
1096
1181
|
docker_full_cmd.append(conf.box_image(bool(shared_root)))
|
@@ -1108,8 +1193,61 @@ def start_container(docker_full_cmd: list[str], current_user: str, shared_root:
|
|
1108
1193
|
sys.exit(code)
|
1109
1194
|
|
1110
1195
|
|
1111
|
-
def
|
1112
|
-
|
1196
|
+
def create_and_start_service(box_name: str, env: Environ, systemctl: str, sys_path: str,
|
1197
|
+
wait_msg: str) -> None:
|
1198
|
+
"""
|
1199
|
+
Create, enable and start systemd service for a ybox container.
|
1200
|
+
|
1201
|
+
:param box_name: name of the ybox container
|
1202
|
+
:param env: an instance of the current :class:`Environ`
|
1203
|
+
:param systemctl: resolved path to the `systemctl` utility
|
1204
|
+
:param sys_path: PATH used for searching system utilities
|
1205
|
+
:param wait_msg: message to output before waiting for the service to start
|
1206
|
+
"""
|
1207
|
+
svc_file = env.search_config_path("resources/ybox-systemd.template", only_sys_conf=True)
|
1208
|
+
with svc_file.open("r", encoding="utf-8") as svc_fd:
|
1209
|
+
svc_tmpl = svc_fd.read()
|
1210
|
+
pid_file = ""
|
1211
|
+
if env.uses_podman:
|
1212
|
+
manager_name = "Podman"
|
1213
|
+
docker_requires = ""
|
1214
|
+
res = run_command([env.docker_cmd, "container", "inspect", "--format={{.ConmonPidFile}}",
|
1215
|
+
box_name], capture_output=True, exit_on_error=False)
|
1216
|
+
if isinstance(res, str):
|
1217
|
+
pid_file = f"PIDFile={res}"
|
1218
|
+
else:
|
1219
|
+
manager_name = "Docker"
|
1220
|
+
docker_requires = "After=docker.service\nRequires=docker.service\n"
|
1221
|
+
systemd_dir = f"{env.home}/.config/systemd/user"
|
1222
|
+
ybox_svc = f"ybox-{box_name}.service"
|
1223
|
+
ybox_env = f"{systemd_dir}/.ybox-{box_name}.env"
|
1224
|
+
formatted_now = env.now.astimezone().strftime("%a %d %b %Y %H:%M:%S %Z")
|
1225
|
+
svc_content = svc_tmpl.format(name=box_name, version=product_version, date=formatted_now,
|
1226
|
+
manager_name=manager_name, docker_requires=docker_requires,
|
1227
|
+
env_file=ybox_env, pid_file=pid_file)
|
1228
|
+
env_content = f"""
|
1229
|
+
PATH={sys_path}:{env.home}/.local/bin
|
1230
|
+
SLEEP_SECS={{sleep_secs}}
|
1231
|
+
# set the container manager to the one configured during ybox-create
|
1232
|
+
YBOX_CONTAINER_MANAGER={env.docker_cmd}
|
1233
|
+
"""
|
1234
|
+
os.makedirs(systemd_dir, Consts.default_directory_mode(), exist_ok=True)
|
1235
|
+
print_color(f"Generating user systemd service '{ybox_svc}' and reloading daemon", fgcolor.cyan)
|
1236
|
+
with open(f"{systemd_dir}/{ybox_svc}", "w", encoding="utf-8") as svc_fd:
|
1237
|
+
svc_fd.write(svc_content)
|
1238
|
+
with open(ybox_env, "w", encoding="utf-8") as env_fd:
|
1239
|
+
env_fd.write(dedent(env_content.format(sleep_secs=0))) # don't sleep for the start below
|
1240
|
+
run_command([systemctl, "--user", "daemon-reload"], exit_on_error=False)
|
1241
|
+
run_command([systemctl, "--user", "enable", ybox_svc], exit_on_error=True)
|
1242
|
+
print_info(wait_msg)
|
1243
|
+
run_command([systemctl, "--user", "start", ybox_svc], exit_on_error=True)
|
1244
|
+
# change SLEEP_SECS to 7 for subsequent starts
|
1245
|
+
with open(ybox_env, "w", encoding="utf-8") as env_fd:
|
1246
|
+
env_fd.write(dedent(env_content.format(sleep_secs=7)))
|
1247
|
+
|
1248
|
+
|
1249
|
+
def start_container(docker_cmd: str, conf: StaticConfiguration) -> None:
|
1250
|
+
"""start a stopped podman/docker container"""
|
1113
1251
|
if (code := int(run_command([docker_cmd, "container", "start", conf.box_name],
|
1114
1252
|
exit_on_error=False, error_msg="container restart"))) != 0:
|
1115
1253
|
print_error(f"Also check 'ybox-logs {conf.box_name}' for details")
|