ybox 0.9.8__py3-none-any.whl

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