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/config.py
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
"""
|
2
|
+
Configuration locations, distribution and box name of ybox container.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import os
|
6
|
+
import re
|
7
|
+
from typing import Iterable, Optional
|
8
|
+
|
9
|
+
from .env import Environ
|
10
|
+
|
11
|
+
|
12
|
+
class StaticConfiguration:
|
13
|
+
"""
|
14
|
+
Configuration paths for a ybox container, its name and distribution.
|
15
|
+
This class also setups up related environment variables which can be used by the
|
16
|
+
INI configuration files.
|
17
|
+
"""
|
18
|
+
|
19
|
+
def __init__(self, env: Environ, distribution: str, box_name: str):
|
20
|
+
self._env = env
|
21
|
+
# set up the additional environment variables
|
22
|
+
os.environ["YBOX_DISTRIBUTION_NAME"] = distribution
|
23
|
+
os.environ["YBOX_CONTAINER_NAME"] = box_name
|
24
|
+
self._distribution = distribution
|
25
|
+
self._box_name = box_name
|
26
|
+
self._box_image = f"{Consts.image_prefix()}/{distribution}/{box_name}"
|
27
|
+
self._shared_box_image = f"{Consts.shared_image_prefix()}/{distribution}"
|
28
|
+
# timezone properties
|
29
|
+
self._localtime = None
|
30
|
+
self._timezone = None
|
31
|
+
if os.path.islink("/etc/localtime"):
|
32
|
+
self._localtime = os.readlink("/etc/localtime")
|
33
|
+
if os.path.exists("/etc/timezone"):
|
34
|
+
with open("/etc/timezone", "r", encoding="utf-8") as timezone:
|
35
|
+
self._timezone = timezone.read().rstrip("\n")
|
36
|
+
self._pager = os.environ.get("YBOX_PAGER", Consts.default_pager())
|
37
|
+
container_dir = f"{env.data_dir}/{box_name}"
|
38
|
+
os.environ["YBOX_CONTAINER_DIR"] = container_dir
|
39
|
+
self._configs_dir = f"{container_dir}/configs"
|
40
|
+
self._target_configs_dir = f"{env.target_data_dir}/{box_name}/configs"
|
41
|
+
self._scripts_dir = f"{container_dir}/ybox-scripts"
|
42
|
+
self._target_scripts_dir = "/usr/local/ybox"
|
43
|
+
os.environ["YBOX_TARGET_SCRIPTS_DIR"] = self._target_scripts_dir
|
44
|
+
self._status_file = f"{container_dir}/status"
|
45
|
+
self._config_list = f"{self._scripts_dir}/config.list"
|
46
|
+
self._app_list = f"{self._scripts_dir}/app.list"
|
47
|
+
|
48
|
+
@property
|
49
|
+
def env(self) -> Environ:
|
50
|
+
"""the `Environ` object used for this configuration"""
|
51
|
+
return self._env
|
52
|
+
|
53
|
+
@property
|
54
|
+
def distribution(self) -> str:
|
55
|
+
"""linux distribution being used by the ybox container"""
|
56
|
+
return self._distribution
|
57
|
+
|
58
|
+
@staticmethod
|
59
|
+
def distribution_config(distribution: str, config_file: str = "distro.ini") -> str:
|
60
|
+
"""
|
61
|
+
Relative configuration file path for the Linux distribution being used.
|
62
|
+
|
63
|
+
:param distribution: name of the Linux distribution
|
64
|
+
:param config_file: name of the configuration file, defaults to "distro.ini"
|
65
|
+
:return: relative path of the configuration file
|
66
|
+
"""
|
67
|
+
return f"distros/{distribution}/{config_file}"
|
68
|
+
|
69
|
+
@property
|
70
|
+
def box_name(self) -> str:
|
71
|
+
"""name of the ybox container"""
|
72
|
+
return self._box_name
|
73
|
+
|
74
|
+
def box_image(self, has_shared_root: bool) -> str:
|
75
|
+
"""
|
76
|
+
Container image created with basic required user configuration from base image.
|
77
|
+
This can either be container specific, or if `base.shared_root` is provided, then
|
78
|
+
it will be common for all such images for the same distribution.
|
79
|
+
|
80
|
+
:param has_shared_root: whether `base.shared_root` is provided in configuration file
|
81
|
+
:return: the podman/docker image to be created and used for the ybox
|
82
|
+
"""
|
83
|
+
return self._shared_box_image if has_shared_root else self._box_image
|
84
|
+
|
85
|
+
@property
|
86
|
+
def localtime(self) -> Optional[str]:
|
87
|
+
"""the target link for /etc/localtime"""
|
88
|
+
return self._localtime
|
89
|
+
|
90
|
+
@property
|
91
|
+
def timezone(self) -> Optional[str]:
|
92
|
+
"""the contents of /etc/timezone"""
|
93
|
+
return self._timezone
|
94
|
+
|
95
|
+
@property
|
96
|
+
def pager(self) -> str:
|
97
|
+
"""pager command to show output one screenful at a time on the terminal"""
|
98
|
+
return self._pager
|
99
|
+
|
100
|
+
@property
|
101
|
+
def configs_dir(self) -> str:
|
102
|
+
"""user directory where configuration files specified in [configs] are copied or
|
103
|
+
hard-linked for sharing with the container"""
|
104
|
+
return self._configs_dir
|
105
|
+
|
106
|
+
@property
|
107
|
+
def target_configs_dir(self) -> str:
|
108
|
+
"""target container directory where shared [configs] are mounted in the container"""
|
109
|
+
return self._target_configs_dir
|
110
|
+
|
111
|
+
@property
|
112
|
+
def scripts_dir(self) -> str:
|
113
|
+
"""local directory where scripts to be shared with container are copied"""
|
114
|
+
return self._scripts_dir
|
115
|
+
|
116
|
+
@property
|
117
|
+
def target_scripts_dir(self) -> str:
|
118
|
+
"""target container directory where shared scripts are mounted"""
|
119
|
+
return self._target_scripts_dir
|
120
|
+
|
121
|
+
@property
|
122
|
+
def status_file(self) -> str:
|
123
|
+
"""local status file to communicate when the container is ready for use"""
|
124
|
+
return self._status_file
|
125
|
+
|
126
|
+
# file containing list of configuration files to be linked on that container to host
|
127
|
+
# as mentioned in the [configs] section
|
128
|
+
@property
|
129
|
+
def config_list(self) -> str:
|
130
|
+
"""file containing list of configuration files to be linked on that container to host
|
131
|
+
as mentioned in the [configs] section"""
|
132
|
+
return self._config_list
|
133
|
+
|
134
|
+
@property
|
135
|
+
def app_list(self) -> str:
|
136
|
+
"""file containing list of applications to be installed in the container"""
|
137
|
+
return self._app_list
|
138
|
+
|
139
|
+
|
140
|
+
class Consts:
|
141
|
+
"""
|
142
|
+
Defines fixed file/path and other names used by ybox that are not configurable.
|
143
|
+
"""
|
144
|
+
|
145
|
+
_MAN_DIRS_PATTERN = re.compile(r"/usr(/local)?(/share)?/man(/[^/]*)?/man[0-9][a-zA-Z_]*")
|
146
|
+
|
147
|
+
@staticmethod
|
148
|
+
def image_prefix() -> str:
|
149
|
+
"""prefix used for the non-shared root images"""
|
150
|
+
return "ybox-local"
|
151
|
+
|
152
|
+
@staticmethod
|
153
|
+
def shared_image_prefix() -> str:
|
154
|
+
"""prefix used for the shared root images"""
|
155
|
+
return "ybox-shared-local"
|
156
|
+
|
157
|
+
@staticmethod
|
158
|
+
def default_directory_mode() -> int:
|
159
|
+
"""return the default mode to use for new directories"""
|
160
|
+
return 0o750
|
161
|
+
|
162
|
+
@staticmethod
|
163
|
+
def entrypoint_base() -> str:
|
164
|
+
"""entrypoint script name for the base container (which is booted to configure
|
165
|
+
the final container)"""
|
166
|
+
return "entrypoint-base.sh"
|
167
|
+
|
168
|
+
@staticmethod
|
169
|
+
def entrypoint_cp() -> str:
|
170
|
+
"""entrypoint script name for the "copy" container that copies files to shared root"""
|
171
|
+
return "entrypoint-cp.sh"
|
172
|
+
|
173
|
+
@staticmethod
|
174
|
+
def entrypoint() -> str:
|
175
|
+
"""entrypoint script name for the final ybox container"""
|
176
|
+
return "entrypoint.sh"
|
177
|
+
|
178
|
+
@staticmethod
|
179
|
+
def run_user_bash_cmd() -> str:
|
180
|
+
"""script used to force run a command as non-root user using `sudo` with `/bin/bash`"""
|
181
|
+
return "run-user-bash-cmd"
|
182
|
+
|
183
|
+
@staticmethod
|
184
|
+
def resource_scripts() -> Iterable[str]:
|
185
|
+
"""all the scripts in the resources directory"""
|
186
|
+
return (Consts.entrypoint_base(), Consts.entrypoint_cp(), Consts.entrypoint(),
|
187
|
+
"entrypoint-common.sh", "entrypoint-root.sh", "entrypoint-user.sh",
|
188
|
+
"prime-run", "run-in-dir", Consts.run_user_bash_cmd())
|
189
|
+
|
190
|
+
@staticmethod
|
191
|
+
def shared_root_mount_dir() -> str:
|
192
|
+
"""directory where shared root directory is mounted in a container during setup"""
|
193
|
+
return "/ybox-root"
|
194
|
+
|
195
|
+
@staticmethod
|
196
|
+
def status_target_file() -> str:
|
197
|
+
"""target location where status_file is mounted in container"""
|
198
|
+
return "/usr/local/ybox-status" # this should match the one in entrypoint-common.sh
|
199
|
+
|
200
|
+
@staticmethod
|
201
|
+
def entrypoint_init_done_file() -> str:
|
202
|
+
"""file that indicates completion of first run initialization by entrypoint.sh script"""
|
203
|
+
return "ybox-init.done"
|
204
|
+
|
205
|
+
@staticmethod
|
206
|
+
def container_desktop_dirs() -> Iterable[str]:
|
207
|
+
"""directories on the container that has desktop files that may need to be wrapped"""
|
208
|
+
return ("/usr/share/applications",)
|
209
|
+
|
210
|
+
@staticmethod
|
211
|
+
def container_icon_dirs() -> Iterable[str]:
|
212
|
+
"""directories on the container (as regexes) that may have application icons"""
|
213
|
+
return ("/usr/share/icons/hicolor/scalable/.*", "/usr/share/icons/hicolor/([1-9]+)x.*",
|
214
|
+
"/usr/share/icons/hicolor/symbolic/.*", "/usr/share/icons", "/usr/share/pixmaps")
|
215
|
+
|
216
|
+
@staticmethod
|
217
|
+
def container_bin_dirs() -> Iterable[str]:
|
218
|
+
"""directories on the container that has executables that may need to be wrapped"""
|
219
|
+
return ("/usr/bin", "/usr/sbin", "/bin", "/sbin", "/usr/local/bin", "/usr/local/sbin")
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def container_man_dir_pattern() -> re.Pattern[str]:
|
223
|
+
"""directory regex pattern on the container having man-pages that may need to be linked"""
|
224
|
+
return Consts._MAN_DIRS_PATTERN
|
225
|
+
|
226
|
+
@staticmethod
|
227
|
+
def nvidia_target_base_dir() -> str:
|
228
|
+
"""base directory path where NVIDIA libs/data are linked in the container"""
|
229
|
+
return "/usr/local/nvidia"
|
230
|
+
|
231
|
+
@staticmethod
|
232
|
+
def nvidia_setup_script() -> str:
|
233
|
+
"""
|
234
|
+
name of the NVIDIA setup script in the container
|
235
|
+
(location is `StaticConfiguration.target_scripts_dir`)
|
236
|
+
"""
|
237
|
+
return "nvidia-setup.sh"
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def default_pager() -> str:
|
241
|
+
"""
|
242
|
+
default pager to show output one screenful at a time on the terminal when YBOX_PAGER
|
243
|
+
environment variable is not set
|
244
|
+
"""
|
245
|
+
return "/usr/bin/less -RLFXK"
|
246
|
+
|
247
|
+
@staticmethod
|
248
|
+
def default_field_separator() -> str:
|
249
|
+
"""default separator used between the fields in output of podman/docker exec commands"""
|
250
|
+
return "::::"
|
251
|
+
|
252
|
+
@staticmethod
|
253
|
+
def default_key_server() -> str:
|
254
|
+
"""default gpg key server to use when not specified in the distribution's `distro.ini`"""
|
255
|
+
return "hkps://keys.openpgp.org"
|
ybox/env.py
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
"""
|
2
|
+
Useful user environment settings.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import getpass
|
6
|
+
import os
|
7
|
+
import pwd
|
8
|
+
import site
|
9
|
+
import subprocess
|
10
|
+
from datetime import datetime
|
11
|
+
from importlib.abc import Traversable
|
12
|
+
from importlib.resources import files
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Optional, Union
|
15
|
+
|
16
|
+
from .print import print_error, print_notice
|
17
|
+
|
18
|
+
PathName = Union[Path, Traversable]
|
19
|
+
|
20
|
+
|
21
|
+
def get_docker_command() -> str:
|
22
|
+
"""
|
23
|
+
If a custom podman/docker executable is defined by YBOX_CONTAINER_MANAGER environment variable,
|
24
|
+
then return it else check for podman and docker (in that order) in the standard /usr/bin path.
|
25
|
+
|
26
|
+
:return: the podman/docker executable specified in arguments or defined by
|
27
|
+
YBOX_CONTAINER_MANAGER environment variable
|
28
|
+
"""
|
29
|
+
# check for podman first then docker
|
30
|
+
if cmd := os.environ.get("YBOX_CONTAINER_MANAGER"):
|
31
|
+
if os.access(cmd, os.X_OK):
|
32
|
+
return cmd
|
33
|
+
raise PermissionError(
|
34
|
+
f"Cannot execute '{cmd}' provided in YBOX_CONTAINER_MANAGER environment variable")
|
35
|
+
if os.access("/usr/bin/podman", os.X_OK):
|
36
|
+
return "/usr/bin/podman"
|
37
|
+
if os.access("/usr/bin/docker", os.X_OK):
|
38
|
+
return "/usr/bin/docker"
|
39
|
+
raise FileNotFoundError(
|
40
|
+
"No podman/docker found in /usr/bin and $YBOX_CONTAINER_MANAGER not defined")
|
41
|
+
|
42
|
+
|
43
|
+
class NotSupportedError(Exception):
|
44
|
+
"""Raised when an operation or configuration is not supported or invalid."""
|
45
|
+
|
46
|
+
|
47
|
+
class Environ:
|
48
|
+
"""
|
49
|
+
Holds common environment variables useful for the scripts like $HOME, $XDG_RUNTIME_DIR.
|
50
|
+
It sets up the $TARGET_HOME environment variable which is the $HOME inside the container.
|
51
|
+
Also captures the current time and sets up the $NOW environment variable.
|
52
|
+
"""
|
53
|
+
|
54
|
+
def __init__(self, docker_cmd: Optional[str] = None, home_dir: Optional[str] = None):
|
55
|
+
"""
|
56
|
+
Initialize the `Environ` object providing the podman/docker command to use.
|
57
|
+
|
58
|
+
:param docker_cmd: the podman/docker executable to use,
|
59
|
+
defaults to :func:`get_docker_command()`
|
60
|
+
:param home_dir: if a non-default user home directory has to be set
|
61
|
+
"""
|
62
|
+
self._home_dir = home_dir or os.path.expanduser("~")
|
63
|
+
self._docker_cmd = docker_cmd or get_docker_command()
|
64
|
+
cmd_version = subprocess.check_output([self._docker_cmd, "--version"])
|
65
|
+
self._uses_podman = "podman" in cmd_version.decode("utf-8").lower()
|
66
|
+
# local user home might be in a different location than /home but target user in the
|
67
|
+
# container will always be in /home with podman else /root for the root user with docker
|
68
|
+
# as ensured by entrypoint-base.sh script
|
69
|
+
target_uid = 0
|
70
|
+
if self._uses_podman:
|
71
|
+
self._target_user = getpass.getuser()
|
72
|
+
target_uid = pwd.getpwnam(self._target_user).pw_uid
|
73
|
+
self._target_home = f"/home/{self._target_user}"
|
74
|
+
else:
|
75
|
+
self._target_user = "root"
|
76
|
+
self._target_home = "/root"
|
77
|
+
# confirm that docker is being used in rootless mode (not required for podman because
|
78
|
+
# it runs as rootless when run by a non-root user in any case without explicit sudo
|
79
|
+
# which the ybox tools don't use)
|
80
|
+
if (docker_ctx := subprocess.check_output(
|
81
|
+
[self._docker_cmd, "context", "show"]).decode("utf-8")).strip() != "rootless":
|
82
|
+
raise NotSupportedError("docker should use the rootless mode (see "
|
83
|
+
"https://docs.docker.com/engine/security/rootless/) "
|
84
|
+
f"but the current context is '{docker_ctx}'")
|
85
|
+
os.environ["TARGET_HOME"] = self._target_home
|
86
|
+
self._user_base = user_base = site.getuserbase()
|
87
|
+
target_user_base = f"{self._target_home}/.local"
|
88
|
+
self._data_dir = f"{user_base}/share/ybox"
|
89
|
+
self._target_data_dir = f"{target_user_base}/share/ybox"
|
90
|
+
self._xdg_rt_dir = os.environ.get("XDG_RUNTIME_DIR", "")
|
91
|
+
# the container user's one can be different because it is the root user for docker
|
92
|
+
self._target_xdg_rt_dir = f"/run/user/{target_uid}"
|
93
|
+
self._now = datetime.now()
|
94
|
+
os.environ["NOW"] = str(self._now)
|
95
|
+
sys_conf_dir = files("ybox").joinpath("conf")
|
96
|
+
os.environ["YBOX_SYS_CONF_DIR"] = str(sys_conf_dir)
|
97
|
+
self._sys_conf_dirs = [sys_conf_dir]
|
98
|
+
self._root_dir = [Path("/")]
|
99
|
+
self._configuration_dirs: list[PathName] = []
|
100
|
+
# for tests, only the bundled configurations should be tested
|
101
|
+
if os.environ.get("YBOX_TESTING"):
|
102
|
+
print_notice("Running with YBOX_TESTING enabled")
|
103
|
+
self._configuration_dirs = self._sys_conf_dirs
|
104
|
+
else:
|
105
|
+
self._configuration_dirs = [Path(f"{self._home_dir}/.config/ybox"),
|
106
|
+
sys_conf_dir]
|
107
|
+
self._user_applications_dir = f"{user_base}/share/applications"
|
108
|
+
self._user_executables_dir = f"{user_base}/bin"
|
109
|
+
|
110
|
+
def search_config_path(self, conf_path: str, only_sys_conf: bool = False,
|
111
|
+
quiet: bool = False) -> PathName:
|
112
|
+
"""
|
113
|
+
Search for given configuration path in user and system configuration directories
|
114
|
+
(in that order). The path may refer to a file or a subdirectory.
|
115
|
+
|
116
|
+
:param conf_path: the configuration file to search (expected to be a relative path)
|
117
|
+
:param only_sys_conf: if True then search only system configuration directory else
|
118
|
+
search for user configuration directory first then the system one
|
119
|
+
:param quiet: if False then prints an error message on standard output on failure
|
120
|
+
:return: the path of the configuration file as `Path` or resource file from
|
121
|
+
importlib (i.e. `Traversable`)
|
122
|
+
"""
|
123
|
+
if os.path.isabs(conf_path):
|
124
|
+
conf_dirs = self._root_dir
|
125
|
+
else:
|
126
|
+
conf_dirs = self._sys_conf_dirs if only_sys_conf else self._configuration_dirs
|
127
|
+
for config_dir in conf_dirs:
|
128
|
+
path = config_dir.joinpath(conf_path)
|
129
|
+
if os.access(path, os.R_OK): # type: ignore
|
130
|
+
return path
|
131
|
+
search_dirs = ', '.join([str(file) for file in conf_dirs])
|
132
|
+
if not quiet:
|
133
|
+
print_error(f"Configuration file '{conf_path}' not found in [{search_dirs}]")
|
134
|
+
raise FileNotFoundError(f"Missing configuration file '{conf_path}'")
|
135
|
+
|
136
|
+
@property
|
137
|
+
def home(self) -> str:
|
138
|
+
"""home directory of the current user"""
|
139
|
+
return self._home_dir
|
140
|
+
|
141
|
+
@property
|
142
|
+
def docker_cmd(self) -> str:
|
143
|
+
"""path of the podman/docker executable to use for all the commands"""
|
144
|
+
return self._docker_cmd
|
145
|
+
|
146
|
+
@property
|
147
|
+
def uses_podman(self) -> bool:
|
148
|
+
"""if podman is the container manager being used"""
|
149
|
+
return self._uses_podman
|
150
|
+
|
151
|
+
@property
|
152
|
+
def target_user(self) -> str:
|
153
|
+
"""username of the container user (which is the same as the current user for podman
|
154
|
+
and root for docker)"""
|
155
|
+
return self._target_user
|
156
|
+
|
157
|
+
# home directory of the container user (which is $TARGET_HOME=/home/$USER for podman
|
158
|
+
# and /root for docker)
|
159
|
+
@property
|
160
|
+
def target_home(self) -> str:
|
161
|
+
"""home directory of the container user (which is $TARGET_HOME=/home/$USER for podman
|
162
|
+
and /root for docker)"""
|
163
|
+
return self._target_home
|
164
|
+
|
165
|
+
@property
|
166
|
+
def data_dir(self) -> str:
|
167
|
+
"""base user directory where runtime data related to all the containers is
|
168
|
+
stored in subdirectories"""
|
169
|
+
return self._data_dir
|
170
|
+
|
171
|
+
@property
|
172
|
+
def target_data_dir(self) -> str:
|
173
|
+
"""base user directory of the container user where runtime data related to all
|
174
|
+
the containers is stored"""
|
175
|
+
return self._target_data_dir
|
176
|
+
|
177
|
+
@property
|
178
|
+
def xdg_rt_dir(self) -> str:
|
179
|
+
"""value of $XDG_RUNTIME_DIR in the current session"""
|
180
|
+
return self._xdg_rt_dir
|
181
|
+
|
182
|
+
@property
|
183
|
+
def target_xdg_rt_dir(self) -> str:
|
184
|
+
"""value of $XDG_RUNTIME_DIR for the user in the container"""
|
185
|
+
return self._target_xdg_rt_dir
|
186
|
+
|
187
|
+
@property
|
188
|
+
def now(self) -> datetime:
|
189
|
+
"""current time as captured during Environ object creation"""
|
190
|
+
return self._now
|
191
|
+
|
192
|
+
@property
|
193
|
+
def user_base(self) -> str:
|
194
|
+
"""User's local base data directory which is typically ~/.local"""
|
195
|
+
return self._user_base
|
196
|
+
|
197
|
+
@property
|
198
|
+
def user_applications_dir(self) -> str:
|
199
|
+
"""User's local applications directory that holds the .desktop files"""
|
200
|
+
return self._user_applications_dir
|
201
|
+
|
202
|
+
@property
|
203
|
+
def user_executables_dir(self) -> str:
|
204
|
+
"""User's local executables directory which should be in the $PATH"""
|
205
|
+
return self._user_executables_dir
|
ybox/filelock.py
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
"""
|
2
|
+
Helper class for file locking with timeout support.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import errno
|
6
|
+
import fcntl
|
7
|
+
import time
|
8
|
+
from datetime import datetime
|
9
|
+
from io import IOBase
|
10
|
+
from typing import Optional
|
11
|
+
|
12
|
+
|
13
|
+
class FileLock:
|
14
|
+
"""
|
15
|
+
A simple file locker class that takes an `fcntl()` lock on given file with polling and timeout.
|
16
|
+
The lock file should always be separate from the resource being locked (if the resource
|
17
|
+
is also a file).
|
18
|
+
|
19
|
+
The file is created on first access or truncated if it exists, and never removed thereafter
|
20
|
+
to avoid any complications. Lock files on NFS files may or may not work as expected
|
21
|
+
depending on the NFS server characteristics, so this class can safely be used only
|
22
|
+
with the lock file on the local filesystem.
|
23
|
+
|
24
|
+
Usage:
|
25
|
+
with FileLock("file.lock", timeout_secs=100):
|
26
|
+
<code>
|
27
|
+
"""
|
28
|
+
|
29
|
+
def __init__(self, lock_file: str, timeout_secs: float = 300.0, poll_interval: float = 1.0):
|
30
|
+
"""
|
31
|
+
Initialize the lock giving a file which should be a separate lock file from the
|
32
|
+
actual resource to be locked. This file is created or truncated on acquisition.
|
33
|
+
|
34
|
+
:param lock_file: the lock file which can be any unique file corresponding to
|
35
|
+
the resource being locked
|
36
|
+
:param timeout_secs: lock timeout in seconds (use negative for infinite wait)
|
37
|
+
:param poll_interval: polling interval at which to check for lock to be available
|
38
|
+
"""
|
39
|
+
self._lock_file = lock_file
|
40
|
+
self._lock_fd: Optional[IOBase] = None
|
41
|
+
self._timeout = timeout_secs
|
42
|
+
self._poll = poll_interval
|
43
|
+
|
44
|
+
def __enter__(self):
|
45
|
+
success = False
|
46
|
+
self._lock_fd = open(self._lock_file, "w+", encoding="utf-8")
|
47
|
+
try:
|
48
|
+
start_time: Optional[datetime] = None
|
49
|
+
remaining_time = self._timeout
|
50
|
+
while remaining_time != 0:
|
51
|
+
try:
|
52
|
+
fcntl.lockf(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
53
|
+
success = True
|
54
|
+
return
|
55
|
+
except OSError as ex:
|
56
|
+
if ex.errno in (errno.EACCES, errno.EAGAIN):
|
57
|
+
# start proper timing only after first failure
|
58
|
+
if not start_time:
|
59
|
+
start_time = datetime.now()
|
60
|
+
# wait for poll time, then try again
|
61
|
+
time.sleep(self._poll)
|
62
|
+
# treat -ve timeout as infinite where remaining_time will never reach 0
|
63
|
+
if remaining_time > 0:
|
64
|
+
remaining_time -= self._poll
|
65
|
+
remaining_time = max(remaining_time, 0)
|
66
|
+
else:
|
67
|
+
raise
|
68
|
+
wait_time = (datetime.now() - start_time).total_seconds() if start_time else 0.0
|
69
|
+
raise TimeoutError(f"Failed to lock '{self._lock_file}' in {wait_time} seconds")
|
70
|
+
finally:
|
71
|
+
if not success:
|
72
|
+
self._lock_fd.close()
|
73
|
+
|
74
|
+
def __exit__(self, ex_type, ex_value, ex_traceback): # type: ignore
|
75
|
+
if self._lock_fd:
|
76
|
+
fcntl.lockf(self._lock_fd, fcntl.LOCK_UN)
|
77
|
+
self._lock_fd.close()
|
@@ -0,0 +1,33 @@
|
|
1
|
+
"""
|
2
|
+
Migrate from 0.9.0 upwards to 0.9.7 that requires copying updated scripts to the container.
|
3
|
+
|
4
|
+
Invoke this script using `exec` passing the `StaticConfiguration` object as `conf` local variable
|
5
|
+
and parsed distribution configuration `ConfigParser` object as `distro_config` local variable.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import subprocess
|
9
|
+
from configparser import ConfigParser
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import cast
|
12
|
+
|
13
|
+
from ybox.config import Consts, StaticConfiguration
|
14
|
+
from ybox.util import copy_ybox_scripts_to_container
|
15
|
+
|
16
|
+
# the two variables below should be passed as local variables to `exec`
|
17
|
+
static_conf = cast(StaticConfiguration, conf) # type: ignore # noqa: F821
|
18
|
+
distro_conf = cast(ConfigParser, distro_config) # type: ignore # noqa: F821
|
19
|
+
copy_ybox_scripts_to_container(static_conf, distro_conf)
|
20
|
+
|
21
|
+
# rename PKGMGR_CLEANUP to PKGMGR_CLEAN in pkgmgr.conf
|
22
|
+
scripts_dir = static_conf.scripts_dir
|
23
|
+
pkgmgr_conf = f"{scripts_dir}/pkgmgr.conf"
|
24
|
+
with open(pkgmgr_conf, "r", encoding="utf-8") as pkgmgr_file:
|
25
|
+
pkgmgr_data = pkgmgr_file.read()
|
26
|
+
with open(pkgmgr_conf, "w", encoding="utf-8") as pkgmgr_file:
|
27
|
+
pkgmgr_file.write(pkgmgr_data.replace("PKGMGR_CLEANUP", "PKGMGR_CLEAN"))
|
28
|
+
# run entrypoint-root.sh again to refresh scripts and configuration
|
29
|
+
subprocess.run([static_conf.env.docker_cmd, "exec", "-it", static_conf.box_name, "/usr/bin/sudo",
|
30
|
+
"/bin/bash", f"{static_conf.target_scripts_dir}/entrypoint-root.sh"])
|
31
|
+
|
32
|
+
# touch the file to indicate that first run initialization of entrypoint.sh is complete
|
33
|
+
Path(f"{scripts_dir}/{Consts.entrypoint_init_done_file()}").touch(mode=0o644)
|
ybox/pkg/__init__.py
ADDED
File without changes
|
ybox/pkg/clean.py
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
"""
|
2
|
+
Clean package cache and related intermediate files of an active ybox container.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
from configparser import SectionProxy
|
7
|
+
|
8
|
+
from ybox.cmd import PkgMgr, build_shell_command, run_command
|
9
|
+
from ybox.config import StaticConfiguration
|
10
|
+
from ybox.print import print_info
|
11
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
12
|
+
|
13
|
+
|
14
|
+
# noinspection PyUnusedLocal
|
15
|
+
def clean_cache(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
16
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
17
|
+
state: YboxStateManagement) -> int:
|
18
|
+
# pylint: disable=unused-argument
|
19
|
+
"""
|
20
|
+
Clean package cache and related intermediate files.
|
21
|
+
|
22
|
+
:param args: arguments having `quiet` and all other attributes passed by the user
|
23
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
24
|
+
:param docker_cmd: the podman/docker executable to use
|
25
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
26
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
27
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
28
|
+
:return: integer exit status of clean command where 0 represents success
|
29
|
+
"""
|
30
|
+
print_info(f"Cleaning package cache in container '{conf.box_name}'")
|
31
|
+
clean_cmd = pkgmgr[PkgMgr.CLEAN_QUIET.value] if args.quiet else pkgmgr[PkgMgr.CLEAN.value]
|
32
|
+
return int(run_command(build_shell_command(docker_cmd, conf.box_name, clean_cmd),
|
33
|
+
exit_on_error=False, error_msg="cleaning package cache"))
|
ybox/pkg/info.py
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
"""
|
2
|
+
Show detailed information of installed or repository packages in an active ybox container.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import argparse
|
6
|
+
import sys
|
7
|
+
from configparser import SectionProxy
|
8
|
+
|
9
|
+
from ybox.cmd import PkgMgr, page_command
|
10
|
+
from ybox.config import StaticConfiguration
|
11
|
+
from ybox.state import RuntimeConfiguration, YboxStateManagement
|
12
|
+
|
13
|
+
|
14
|
+
# noinspection PyUnusedLocal
|
15
|
+
def info_packages(args: argparse.Namespace, pkgmgr: SectionProxy, docker_cmd: str,
|
16
|
+
conf: StaticConfiguration, runtime_conf: RuntimeConfiguration,
|
17
|
+
state: YboxStateManagement) -> int:
|
18
|
+
# pylint: disable=unused-argument
|
19
|
+
"""
|
20
|
+
Show detailed information of an installed or repository package(s).
|
21
|
+
|
22
|
+
:param args: arguments having `packages` and all other attributes passed by the user
|
23
|
+
:param pkgmgr: the `[pkgmgr]` section from `distro.ini` configuration file of the distribution
|
24
|
+
:param docker_cmd: the podman/docker executable to use
|
25
|
+
:param conf: the :class:`StaticConfiguration` for the container
|
26
|
+
:param runtime_conf: the `RuntimeConfiguration` of the container
|
27
|
+
:param state: instance of `YboxStateManagement` having the state of all ybox containers
|
28
|
+
:return: integer exit status of info command where 0 represents success
|
29
|
+
"""
|
30
|
+
quiet_flag = pkgmgr[PkgMgr.QUIET_DETAILS_FLAG.value] if args.quiet else ""
|
31
|
+
packages: list[str] = args.packages
|
32
|
+
info_cmd = pkgmgr[PkgMgr.INFO_ALL.value] if args.all else pkgmgr[PkgMgr.INFO.value]
|
33
|
+
info_cmd = info_cmd.format(quiet=quiet_flag, packages=" ".join(packages))
|
34
|
+
docker_args = [docker_cmd, "exec"]
|
35
|
+
if sys.stdout.isatty(): # don't act as a terminal if it is being redirected
|
36
|
+
docker_args.append("-it")
|
37
|
+
docker_args.extend([conf.box_name, "/bin/bash", "-c", info_cmd])
|
38
|
+
# empty pager argument is a valid one and indicates no pagination, hence the `is None` check
|
39
|
+
pager: str = args.pager if args.pager is not None else conf.pager
|
40
|
+
return page_command(docker_args, pager, error_msg="SKIP")
|