lockss-turtles 0.6.0.dev2__tar.gz → 0.6.0.dev4__tar.gz
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.
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/PKG-INFO +2 -3
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/pyproject.toml +4 -4
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/__init__.py +1 -1
- lockss_turtles-0.6.0.dev4/src/lockss/turtles/app.py +245 -0
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/cli.py +82 -42
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/plugin.py +18 -13
- lockss_turtles-0.6.0.dev4/src/lockss/turtles/plugin_registry.py +238 -0
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/plugin_set.py +102 -137
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/util.py +28 -17
- lockss_turtles-0.6.0.dev4/tests/unittest/lockss/turtles/__init__.py +65 -0
- lockss_turtles-0.6.0.dev4/tests/unittest/lockss/turtles/test_plugin_set.py +62 -0
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/app.py +0 -192
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/plugin_registry.py +0 -263
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/__init__.py +0 -29
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-registry-catalog-schema.json +0 -27
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-registry-schema.json +0 -115
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-set-catalog-schema.json +0 -27
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-set-schema.json +0 -92
- lockss_turtles-0.6.0.dev2/src/lockss/turtles/resources/plugin-signing-credentials-schema.json +0 -27
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/CHANGELOG.rst +0 -0
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/LICENSE +0 -0
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/README.rst +0 -0
- {lockss_turtles-0.6.0.dev2 → lockss_turtles-0.6.0.dev4}/src/lockss/turtles/__main__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: lockss-turtles
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev4
|
|
4
4
|
Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Author: Thib Guicherd-Callin
|
|
@@ -19,9 +19,8 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
19
19
|
Classifier: Topic :: System :: Archiving
|
|
20
20
|
Classifier: Topic :: Utilities
|
|
21
21
|
Requires-Dist: java-manifest (>=1.1.0,<1.2.0)
|
|
22
|
-
Requires-Dist: jsonschema (>=4.24.0,<4.25.0)
|
|
23
22
|
Requires-Dist: lockss-pybasic (>=0.1.0,<0.2.0)
|
|
24
|
-
Requires-Dist: pydantic (>=2.11.
|
|
23
|
+
Requires-Dist: pydantic (>=2.11.0,<3.0.0)
|
|
25
24
|
Requires-Dist: pyyaml (>=6.0.0,<6.1.0)
|
|
26
25
|
Requires-Dist: xdg (>=6.0.0,<6.1.0)
|
|
27
26
|
Project-URL: Documentation, https://docs.lockss.org/en/latest/software/turtles
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
[project]
|
|
30
30
|
name = "lockss-turtles"
|
|
31
|
-
version = "0.6.0-
|
|
31
|
+
version = "0.6.0-dev4" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
|
|
32
32
|
description = "Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries"
|
|
33
33
|
license = { text = "BSD-3-Clause" }
|
|
34
34
|
readme = "README.rst"
|
|
@@ -41,11 +41,10 @@ maintainers = [
|
|
|
41
41
|
]
|
|
42
42
|
dependencies = [
|
|
43
43
|
"java-manifest (>=1.1.0,<1.2.0)",
|
|
44
|
-
"jsonschema (>=4.24.0,<4.25.0)",
|
|
45
44
|
"lockss-pybasic (>=0.1.0,<0.2.0)",
|
|
46
45
|
"pyyaml (>=6.0.0,<6.1.0)",
|
|
47
46
|
"xdg (>=6.0.0,<6.1.0)",
|
|
48
|
-
"pydantic (>=2.11.
|
|
47
|
+
"pydantic (>=2.11.0,<3.0.0)",
|
|
49
48
|
]
|
|
50
49
|
classifiers = [
|
|
51
50
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -74,7 +73,8 @@ include = [
|
|
|
74
73
|
"README.rst",
|
|
75
74
|
]
|
|
76
75
|
packages = [
|
|
77
|
-
{ include = "lockss", from = "src" }
|
|
76
|
+
{ include = "lockss", from = "src" },
|
|
77
|
+
{ include = "unittest", from = "tests" },
|
|
78
78
|
]
|
|
79
79
|
|
|
80
80
|
[tool.poetry.scripts]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software without
|
|
17
|
+
# specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
# Remove in Python 3.14
|
|
32
|
+
# See https://stackoverflow.com/questions/33533148/how-do-i-type-hint-a-method-with-the-type-of-the-enclosing-class/33533514#33533514
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from collections.abc import Callable, Iterable
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import ClassVar, Literal, Optional, Union
|
|
38
|
+
|
|
39
|
+
import yaml
|
|
40
|
+
from lockss.pybasic.fileutil import path
|
|
41
|
+
from pydantic import BaseModel, Field
|
|
42
|
+
import xdg
|
|
43
|
+
|
|
44
|
+
from .plugin import Plugin, PluginIdentifier
|
|
45
|
+
from .plugin_registry import PluginRegistry, PluginRegistryCatalog, PluginRegistryCatalogKind, PluginRegistryKind, PluginRegistryLayerIdentifier
|
|
46
|
+
from .plugin_set import PluginSet, PluginSetCatalog, PluginSetCatalogKind, PluginSetKind
|
|
47
|
+
from .util import BaseModelWithRoot, PathOrStr
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PluginSigningCredentialsKind = Literal['PluginSigningCredentials']
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PluginSigningCredentials(BaseModelWithRoot):
|
|
55
|
+
kind: PluginSigningCredentialsKind = Field(description="This object's kind")
|
|
56
|
+
plugin_signing_keystore: str = Field(title='Plugin Signing Keystore', description='A path to the plugin signing keystore')
|
|
57
|
+
plugin_signing_alias: str = Field(title='Plugin Signing Alias', description='The plugin signing alias to use')
|
|
58
|
+
|
|
59
|
+
def get_plugin_signing_alias(self) -> str:
|
|
60
|
+
return self.plugin_signing_alias
|
|
61
|
+
|
|
62
|
+
def get_plugin_signing_keystore(self) -> Path:
|
|
63
|
+
return self.get_root().joinpath(self.plugin_signing_keystore)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TurtlesApp(object):
|
|
67
|
+
|
|
68
|
+
CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
|
|
69
|
+
|
|
70
|
+
XDG_CONFIG_DIR: ClassVar[Path] = Path(xdg.xdg_config_home(), CONFIG_DIR_NAME)
|
|
71
|
+
|
|
72
|
+
USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
|
|
73
|
+
|
|
74
|
+
ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
|
|
75
|
+
|
|
76
|
+
CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, USR_CONFIG_DIR, ETC_CONFIG_DIR)
|
|
77
|
+
|
|
78
|
+
PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
|
|
79
|
+
|
|
80
|
+
PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
|
|
81
|
+
|
|
82
|
+
PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
super().__init__()
|
|
86
|
+
self._password: Optional[Callable[[], str]] = None
|
|
87
|
+
self._plugin_registries: list[PluginRegistry] = list()
|
|
88
|
+
self._plugin_registry_catalogs: list[PluginRegistryCatalog] = list()
|
|
89
|
+
self._plugin_set_catalogs: list[PluginSetCatalog] = list()
|
|
90
|
+
self._plugin_sets: list[PluginSet] = list()
|
|
91
|
+
self._plugin_signing_credentials: Optional[PluginSigningCredentials] = None
|
|
92
|
+
|
|
93
|
+
def build_plugin(self, plugin_ids: list[PluginIdentifier]) -> dict[str, tuple[str, Path, Plugin]]:
|
|
94
|
+
return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
|
|
95
|
+
|
|
96
|
+
def deploy_plugin(self, src_paths: list[Path], layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> dict[tuple[Path, str], list[tuple[str, str, Optional[Path], Optional[Plugin]]]]:
|
|
97
|
+
plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
|
|
98
|
+
return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
|
|
99
|
+
plugin_id,
|
|
100
|
+
layer_ids,
|
|
101
|
+
interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
|
|
102
|
+
|
|
103
|
+
def load_plugin_registries(self, plugin_registry_path_or_str: PathOrStr) -> TurtlesApp:
|
|
104
|
+
plugin_registry_path = path(plugin_registry_path_or_str)
|
|
105
|
+
if plugin_registry_path in map(lambda pr: pr.get_root(), self._plugin_registries):
|
|
106
|
+
raise ValueError(f'Plugin registries already loaded from: {plugin_registry_path!s}')
|
|
107
|
+
with plugin_registry_path.open('r') as fpr:
|
|
108
|
+
for yaml_obj in yaml.safe_load_all(fpr):
|
|
109
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryKind.__args__:
|
|
110
|
+
plugin_registry = PluginRegistry(**yaml_obj).initialize(plugin_registry_path)
|
|
111
|
+
self._plugin_registries.append(plugin_registry)
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def load_plugin_registry_catalogs(self, plugin_registry_catalog_path_or_str: PathOrStr) -> TurtlesApp:
|
|
115
|
+
plugin_registry_catalog_path = path(plugin_registry_catalog_path_or_str)
|
|
116
|
+
if plugin_registry_catalog_path in map(lambda prc: prc.get_root(), self._plugin_registry_catalogs):
|
|
117
|
+
raise ValueError(f'Plugin registry catalogs already loaded from: {plugin_registry_catalog_path!s}')
|
|
118
|
+
with plugin_registry_catalog_path.open('r') as fprc:
|
|
119
|
+
for yaml_obj in yaml.safe_load_all(fprc):
|
|
120
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryCatalogKind.__args__:
|
|
121
|
+
plugin_registry_catalog = PluginRegistryCatalog(**yaml_obj).initialize(plugin_registry_catalog_path)
|
|
122
|
+
self._plugin_registry_catalogs.append(plugin_registry_catalog)
|
|
123
|
+
for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
|
|
124
|
+
self.load_plugin_registries(plugin_registry_catalog_path.joinpath(plugin_registry_file))
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def load_plugin_set_catalogs(self, plugin_set_catalog_path_or_str: PathOrStr) -> TurtlesApp:
|
|
128
|
+
plugin_set_catalog_path = path(plugin_set_catalog_path_or_str)
|
|
129
|
+
if plugin_set_catalog_path in map(lambda psc: psc.get_root(), self._plugin_set_catalogs):
|
|
130
|
+
raise ValueError(f'Plugin set catalogs already loaded from: {plugin_set_catalog_path!s}')
|
|
131
|
+
with plugin_set_catalog_path.open('r') as fpsc:
|
|
132
|
+
for yaml_obj in yaml.safe_load_all(fpsc):
|
|
133
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetCatalogKind.__args__:
|
|
134
|
+
plugin_set_catalog = PluginSetCatalog(**yaml_obj).initialize(plugin_set_catalog_path)
|
|
135
|
+
self._plugin_set_catalogs.append(plugin_set_catalog)
|
|
136
|
+
for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
|
|
137
|
+
self.load_plugin_sets(plugin_set_catalog_path.joinpath(plugin_set_file))
|
|
138
|
+
return self
|
|
139
|
+
|
|
140
|
+
def load_plugin_sets(self, plugin_set_path_or_str: PathOrStr) -> TurtlesApp:
|
|
141
|
+
plugin_set_path = path(plugin_set_path_or_str)
|
|
142
|
+
if plugin_set_path in map(lambda ps: ps.get_root(), self._plugin_sets):
|
|
143
|
+
raise ValueError(f'Plugin sets already loaded from: {plugin_set_path!s}')
|
|
144
|
+
with plugin_set_path.open('r') as fps:
|
|
145
|
+
for yaml_obj in yaml.safe_load_all(fps):
|
|
146
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetKind.__args__:
|
|
147
|
+
plugin_set = PluginSet(**yaml_obj).initialize(plugin_set_path)
|
|
148
|
+
self._plugin_sets.append(plugin_set)
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
def load_plugin_signing_credentials(self, plugin_signing_credentials_path_or_str: PathOrStr) -> TurtlesApp:
|
|
152
|
+
plugin_signing_credentials_path = path(plugin_signing_credentials_path_or_str)
|
|
153
|
+
if self._plugin_signing_credentials:
|
|
154
|
+
raise ValueError(f'Plugin signing credentials already loaded from: {self._plugin_signing_credentials.get_root()!s}')
|
|
155
|
+
with plugin_signing_credentials_path.open('r') as fpsc:
|
|
156
|
+
self._plugin_signing_credentials = PluginSigningCredentials(**yaml.safe_load(fpsc)).initialize(plugin_signing_credentials_path)
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def release_plugin(self, plugin_ids: list[PluginIdentifier], layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> dict[str, list[tuple[str, str, Path, Plugin]]]:
|
|
160
|
+
# ... plugin_id -> (set_id, jar_path, plugin)
|
|
161
|
+
ret1 = self.build_plugin(plugin_ids)
|
|
162
|
+
jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
|
|
163
|
+
# ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
|
|
164
|
+
ret2 = self.deploy_plugin(jar_paths,
|
|
165
|
+
layer_ids,
|
|
166
|
+
interactive=interactive)
|
|
167
|
+
return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
|
|
168
|
+
|
|
169
|
+
def set_password(self, pw: Union[Callable[[], str], str]) -> None:
|
|
170
|
+
self._password = pw if callable(pw) else lambda: pw
|
|
171
|
+
|
|
172
|
+
def _build_one_plugin(self, plugin_id: str) -> tuple[str, Optional[Path], Optional[Plugin]]:
|
|
173
|
+
for plugin_set in self._plugin_sets:
|
|
174
|
+
if plugin_set.has_plugin(plugin_id):
|
|
175
|
+
bp = plugin_set.build_plugin(plugin_id,
|
|
176
|
+
self._get_plugin_signing_keystore(),
|
|
177
|
+
self._get_plugin_signing_alias(),
|
|
178
|
+
self._get_plugin_signing_password())
|
|
179
|
+
return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
|
|
180
|
+
raise Exception(f'{plugin_id}: not found in any plugin set')
|
|
181
|
+
|
|
182
|
+
def _deploy_one_plugin(self, src_jar: Path, plugin_id: PluginIdentifier, layer_ids: list[PluginRegistryLayerIdentifier], interactive: bool=False) -> list[tuple[str, str, Optional[Path], Optional[Plugin]]]:
|
|
183
|
+
ret = list()
|
|
184
|
+
for plugin_registry in self._plugin_registries:
|
|
185
|
+
if plugin_registry.has_plugin(plugin_id):
|
|
186
|
+
for layer_id in layer_ids:
|
|
187
|
+
layer = plugin_registry.get_layer(layer_id)
|
|
188
|
+
if layer is not None:
|
|
189
|
+
dp = layer.deploy_plugin(plugin_id,
|
|
190
|
+
src_jar,
|
|
191
|
+
interactive=interactive)
|
|
192
|
+
ret.append((plugin_registry.get_id(),
|
|
193
|
+
layer.get_id(),
|
|
194
|
+
dp[0] if dp else None,
|
|
195
|
+
dp[1] if dp else None))
|
|
196
|
+
if len(ret) == 0:
|
|
197
|
+
raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
|
|
198
|
+
return ret
|
|
199
|
+
|
|
200
|
+
def _get_password(self) -> Optional[str]:
|
|
201
|
+
return self._password() if self._password else None
|
|
202
|
+
|
|
203
|
+
def _get_plugin_signing_alias(self) -> str:
|
|
204
|
+
return self._plugin_signing_credentials.get_plugin_signing_alias()
|
|
205
|
+
|
|
206
|
+
def _get_plugin_signing_keystore(self) -> Path:
|
|
207
|
+
return self._plugin_signing_credentials.get_plugin_signing_keystore()
|
|
208
|
+
|
|
209
|
+
def _get_plugin_signing_password(self) -> str:
|
|
210
|
+
return self._get_password()
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
|
|
214
|
+
return TurtlesApp._default_files(TurtlesApp.PLUGIN_REGISTRY_CATALOG)
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
|
|
218
|
+
return TurtlesApp._default_files(TurtlesApp.PLUGIN_SET_CATALOG)
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
|
|
222
|
+
return TurtlesApp._default_files(TurtlesApp.PLUGIN_SIGNING_CREDENTIALS)
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def select_default_plugin_registry_catalog() -> Optional[Path]:
|
|
226
|
+
return TurtlesApp._select_file(TurtlesApp.default_plugin_registry_catalog_choices())
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def select_default_plugin_set_catalog() -> Optional[Path]:
|
|
230
|
+
return TurtlesApp._select_file(TurtlesApp.default_plugin_set_catalog_choices())
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def select_default_plugin_signing_credentials() -> Optional[Path]:
|
|
234
|
+
return TurtlesApp._select_file(TurtlesApp.default_plugin_signing_credentials_choices())
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _default_files(file_str) -> tuple[Path, ...]:
|
|
238
|
+
return tuple(dir_path.joinpath(file_str) for dir_path in TurtlesApp.CONFIG_DIRS)
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _select_file(choices: Iterable[Path]) -> Optional[Path]:
|
|
242
|
+
for p in choices:
|
|
243
|
+
if p.is_file():
|
|
244
|
+
return p
|
|
245
|
+
return None
|
|
@@ -41,73 +41,104 @@ from lockss.pybasic.outpututil import OutputFormatOptions
|
|
|
41
41
|
from pydantic.v1 import BaseModel, Field, FilePath
|
|
42
42
|
from pydantic.v1.class_validators import validator
|
|
43
43
|
import tabulate
|
|
44
|
-
from typing import
|
|
44
|
+
from typing import Optional
|
|
45
45
|
|
|
46
46
|
from . import __copyright__, __license__, __version__
|
|
47
47
|
from .app import TurtlesApp
|
|
48
|
+
from .plugin_registry import PluginRegistryLayerIdentifier
|
|
49
|
+
from .util import file_or
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class PluginBuildingOptions(BaseModel):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
plugin_set: Optional[list[FilePath]] = Field(aliases=['-s'], description=f'(plugin sets) add one or more plugin sets to the loaded plugin sets')
|
|
54
|
+
plugin_set_catalog: Optional[list[FilePath]] = Field(aliases=['-S'], description=f'(plugin sets) add one or more plugin set catalogs to the loaded plugin set catalogs; if no plugin set catalogs or plugin sets are specified, load {file_or(TurtlesApp.default_plugin_set_catalog_choices())}')
|
|
55
|
+
plugin_signing_credentials: Optional[FilePath] = Field(aliases=['-c'], description=f'(plugin signing credentials) load the plugin signing credentials from the given file, or if none, from {file_or(TurtlesApp.default_plugin_signing_credentials_choices())}')
|
|
53
56
|
plugin_signing_password: Optional[str] = Field(description='(plugin signing credentials) set the plugin signing password, or if none, prompt interactively')
|
|
54
57
|
|
|
58
|
+
def get_plugin_sets(self) -> list[Path]:
|
|
59
|
+
return [path(p) for p in self.plugin_set or []]
|
|
60
|
+
|
|
61
|
+
def get_plugin_set_catalogs(self) -> list[Path]:
|
|
62
|
+
if self.plugin_set or self.plugin_set_catalog:
|
|
63
|
+
return [path(p) for p in self.plugin_set_catalog or []]
|
|
64
|
+
if single := TurtlesApp.select_default_plugin_set_catalog():
|
|
65
|
+
return [single]
|
|
66
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
|
|
67
|
+
|
|
68
|
+
def get_plugin_signing_credentials(self) -> Path:
|
|
69
|
+
if self.plugin_signing_credentials:
|
|
70
|
+
return path(self.plugin_signing_credentials)
|
|
71
|
+
if ret := TurtlesApp.select_default_plugin_signing_credentials():
|
|
72
|
+
return ret
|
|
73
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_signing_credentials_choices()))
|
|
74
|
+
|
|
55
75
|
|
|
56
76
|
class PluginDeploymentOptions(BaseModel):
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
plugin_registry: Optional[list[FilePath]] = Field(aliases=['-r'], description=f'(plugin registry) add one or more plugin registries to the loaded plugin registries')
|
|
78
|
+
plugin_registry_catalog: Optional[list[FilePath]] = Field(aliases=['-R'], description=f'(plugin registry) add one or more plugin registry catalogs to the loaded plugin registry catalogs; if no plugin registry catalogs or plugin registries are specified, load {file_or(TurtlesApp.default_plugin_registry_catalog_choices())}')
|
|
79
|
+
plugin_registry_layer: Optional[list[str]] = Field(aliases=['-l'], description='(plugin registry layers) add one or more plugin registry layers to the set of plugin registry layers to process')
|
|
80
|
+
plugin_registry_layers: Optional[list[FilePath]] = Field(aliases=['-L'], description='(plugin registry layers) add the plugin registry layers listed in one or more files to the set of plugin registry layers to process')
|
|
60
81
|
testing: Optional[bool] = Field(False, aliases=['-t'], description='(plugin registry layers) synonym for --plugin-registry-layer testing (i.e. add "testing" to the list of plugin registry layers to process)')
|
|
61
82
|
production: Optional[bool] = Field(False, aliases=['-p'], description='(plugin registry layers) synonym for --plugin-registry-layer production (i.e. add "production" to the list of plugin registry layers to process)')
|
|
62
83
|
|
|
63
84
|
@validator('plugin_registry_layers', each_item=True, pre=True)
|
|
64
|
-
def _expand_each_plugin_registry_layers_path(cls, v: Path):
|
|
85
|
+
def _expand_each_plugin_registry_layers_path(cls, v: Path) -> Path:
|
|
65
86
|
return path(v)
|
|
66
87
|
|
|
67
|
-
def
|
|
68
|
-
|
|
88
|
+
def get_plugin_registries(self) -> list[Path]:
|
|
89
|
+
return [path(p) for p in self.plugin_registry or []]
|
|
90
|
+
|
|
91
|
+
def get_plugin_registry_catalogs(self) -> list[Path]:
|
|
92
|
+
if self.plugin_registry or self.plugin_registry_catalog:
|
|
93
|
+
return [path(p) for p in self.plugin_registry_catalog or []]
|
|
94
|
+
if single := TurtlesApp.select_default_plugin_registry_catalog():
|
|
95
|
+
return [single]
|
|
96
|
+
raise FileNotFoundError(file_or(TurtlesApp.default_plugin_set_catalog_choices()))
|
|
97
|
+
|
|
98
|
+
def get_plugin_registry_layers(self) -> list[PluginRegistryLayerIdentifier]:
|
|
99
|
+
ret = [*(self.plugin_registry_layer or []), *[file_lines(file_path) for file_path in self.plugin_registry_layers or []]]
|
|
69
100
|
for layer in reversed(['testing', 'production']):
|
|
70
101
|
if getattr(self, layer, False):
|
|
71
102
|
ret.insert(0, layer)
|
|
72
|
-
if
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
if ret:
|
|
104
|
+
return ret
|
|
105
|
+
raise ValueError('Empty list of plugin registry layers')
|
|
75
106
|
|
|
76
107
|
|
|
77
108
|
class PluginIdentifierOptions(BaseModel):
|
|
78
109
|
"""
|
|
79
110
|
The --identifier/-i and --identifiers/-I options.
|
|
80
111
|
"""
|
|
81
|
-
plugin_identifier: Optional[
|
|
82
|
-
plugin_identifiers: Optional[
|
|
112
|
+
plugin_identifier: Optional[list[str]] = Field(aliases=['-i'], description='(plugin identifiers) add one or more plugin identifiers to the set of plugin identifiers to process')
|
|
113
|
+
plugin_identifiers: Optional[list[FilePath]] = Field(aliases=['-I'], description='(plugin identifiers) add the plugin identifiers listed in one or more files to the set of plugin identifiers to process')
|
|
83
114
|
|
|
84
115
|
@validator('plugin_identifiers', each_item=True, pre=True)
|
|
85
|
-
def _expand_each_plugin_identifiers_path(cls, v: Path):
|
|
116
|
+
def _expand_each_plugin_identifiers_path(cls, v: Path) -> Path:
|
|
86
117
|
return path(v)
|
|
87
118
|
|
|
88
|
-
def get_plugin_identifiers(self) ->
|
|
89
|
-
ret = [*self.plugin_identifier[
|
|
90
|
-
if
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
def get_plugin_identifiers(self) -> list[str]:
|
|
120
|
+
ret = [*(self.plugin_identifier or []), *[file_lines(file_path) for file_path in self.plugin_identifiers or []]]
|
|
121
|
+
if ret:
|
|
122
|
+
return ret
|
|
123
|
+
raise ValueError('Empty list of plugin identifiers')
|
|
93
124
|
|
|
94
125
|
|
|
95
126
|
class PluginJarOptions(BaseModel):
|
|
96
127
|
"""
|
|
97
128
|
The --plugin-jar/-j and --plugin-jars/-J options.
|
|
98
129
|
"""
|
|
99
|
-
plugin_jar: Optional[
|
|
100
|
-
plugin_jars: Optional[
|
|
130
|
+
plugin_jar: Optional[list[FilePath]] = Field(aliases=['-j'], description='(plugin JARs) add one or more plugin JARs to the set of plugin JARs to process')
|
|
131
|
+
plugin_jars: Optional[list[FilePath]] = Field(aliases=['-J'], description='(plugin JARs) add the plugin JARs listed in one or more files to the set of plugin JARs to process')
|
|
101
132
|
|
|
102
133
|
@validator('plugin_jar', 'plugin_jars', each_item=True, pre=True)
|
|
103
|
-
def _expand_each_plugin_jars_path(cls, v: Path):
|
|
134
|
+
def _expand_each_plugin_jars_path(cls, v: Path) -> Path:
|
|
104
135
|
return path(v)
|
|
105
136
|
|
|
106
137
|
def get_plugin_jars(self):
|
|
107
|
-
ret = [*self.plugin_jar[
|
|
108
|
-
if len(ret)
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
ret = [*(self.plugin_jar or []), *[file_lines(file_path) for file_path in self.plugin_jars or []]]
|
|
139
|
+
if len(ret):
|
|
140
|
+
return ret
|
|
141
|
+
raise ValueError('Empty list of plugin JARs')
|
|
111
142
|
|
|
112
143
|
|
|
113
144
|
class NonInteractiveOptions(BaseModel):
|
|
@@ -208,10 +239,12 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
208
239
|
return self._build_plugin(build_plugin_command)
|
|
209
240
|
|
|
210
241
|
def _build_plugin(self, build_plugin_command: BuildPluginCommand) -> None:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
242
|
+
for psc in build_plugin_command.get_plugin_set_catalogs():
|
|
243
|
+
self._app.load_plugin_set_catalogs(psc)
|
|
244
|
+
for ps in build_plugin_command.get_plugin_sets():
|
|
245
|
+
self._app.load_plugin_sets(ps)
|
|
246
|
+
self._app.load_plugin_signing_credentials(build_plugin_command.get_plugin_signing_credentials())
|
|
247
|
+
self._obtain_password(build_plugin_command, non_interactive=build_plugin_command.non_interactive)
|
|
215
248
|
# Action
|
|
216
249
|
# ... plugin_id -> (set_id, jar_path, plugin)
|
|
217
250
|
ret = self._app.build_plugin(build_plugin_command.get_plugin_identifiers())
|
|
@@ -224,8 +257,10 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
224
257
|
self._do_string_command(string_command)
|
|
225
258
|
|
|
226
259
|
def _deploy_plugin(self, deploy_plugin_command: DeployPluginCommand) -> None:
|
|
227
|
-
|
|
228
|
-
|
|
260
|
+
for prc in deploy_plugin_command.get_plugin_registry_catalogs():
|
|
261
|
+
self._app.load_plugin_registry_catalogs(prc)
|
|
262
|
+
for pr in deploy_plugin_command.get_plugin_registries():
|
|
263
|
+
self._app.load_plugin_registries(pr)
|
|
229
264
|
# Action
|
|
230
265
|
# ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
|
|
231
266
|
ret = self._app.deploy_plugin(deploy_plugin_command.get_plugin_jars(),
|
|
@@ -245,21 +280,26 @@ class TurtlesCli(BaseCli[TurtlesCommand]):
|
|
|
245
280
|
def _license(self, string_command: StringCommand) -> None:
|
|
246
281
|
self._do_string_command(string_command)
|
|
247
282
|
|
|
248
|
-
def _obtain_password(self,
|
|
249
|
-
if
|
|
250
|
-
_p =
|
|
251
|
-
elif
|
|
283
|
+
def _obtain_password(self, plugin_building_options: PluginBuildingOptions, non_interactive: bool=False) -> None:
|
|
284
|
+
if plugin_building_options.plugin_signing_password:
|
|
285
|
+
_p = plugin_building_options.plugin_signing_password
|
|
286
|
+
elif not non_interactive:
|
|
252
287
|
_p = getpass('Plugin signing password: ')
|
|
253
288
|
else:
|
|
254
289
|
self._parser.error('no plugin signing password specified while in non-interactive mode')
|
|
255
290
|
self._app.set_password(lambda: _p)
|
|
256
291
|
|
|
257
292
|
def _release_plugin(self, release_plugin_command: ReleasePluginCommand) -> None:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
293
|
+
for psc in release_plugin_command.get_plugin_set_catalogs():
|
|
294
|
+
self._app.load_plugin_set_catalogs(psc)
|
|
295
|
+
for ps in release_plugin_command.get_plugin_sets():
|
|
296
|
+
self._app.load_plugin_sets(ps)
|
|
297
|
+
for prc in release_plugin_command.get_plugin_registry_catalogs():
|
|
298
|
+
self._app.load_plugin_registry_catalogs(prc)
|
|
299
|
+
for pr in release_plugin_command.get_plugin_registries():
|
|
300
|
+
self._app.load_plugin_registries(pr)
|
|
301
|
+
self._app.load_plugin_signing_credentials(release_plugin_command.get_plugin_signing_credentials())
|
|
302
|
+
self._obtain_password(release_plugin_command, non_interactive=release_plugin_command.non_interactive)
|
|
263
303
|
# Action
|
|
264
304
|
# ... plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
|
|
265
305
|
ret = self._app.release_plugin(release_plugin_command.get_plugin_identifiers(),
|
|
@@ -37,14 +37,19 @@ Library to represent a LOCKSS plugin.
|
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
39
39
|
from collections.abc import Callable
|
|
40
|
-
from pathlib import Path
|
|
41
|
-
from typing import Any,
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Optional
|
|
42
42
|
import xml.etree.ElementTree as ET
|
|
43
43
|
from zipfile import ZipFile
|
|
44
44
|
|
|
45
|
-
import java_manifest
|
|
45
|
+
import java_manifest
|
|
46
46
|
from lockss.pybasic.fileutil import path
|
|
47
47
|
|
|
48
|
+
from .util import PathOrStr
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PluginIdentifier = str
|
|
52
|
+
|
|
48
53
|
|
|
49
54
|
class Plugin(object):
|
|
50
55
|
|
|
@@ -56,7 +61,7 @@ class Plugin(object):
|
|
|
56
61
|
if tag != 'map':
|
|
57
62
|
raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
|
|
58
63
|
|
|
59
|
-
def get_aux_packages(self) ->
|
|
64
|
+
def get_aux_packages(self) -> list[str]:
|
|
60
65
|
key = 'plugin_aux_packages'
|
|
61
66
|
lst = [x[1] for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
|
|
62
67
|
if lst is None or len(lst) < 1:
|
|
@@ -65,13 +70,13 @@ class Plugin(object):
|
|
|
65
70
|
raise ValueError(f'plugin declares {len(lst)} entries for {key}')
|
|
66
71
|
return [x.text for x in lst[0].findall('string')]
|
|
67
72
|
|
|
68
|
-
def get_identifier(self) -> Optional[
|
|
73
|
+
def get_identifier(self) -> Optional[PluginIdentifier]:
|
|
69
74
|
return self._only_one('plugin_identifier')
|
|
70
75
|
|
|
71
76
|
def get_name(self) -> Optional[str]:
|
|
72
77
|
return self._only_one('plugin_name')
|
|
73
78
|
|
|
74
|
-
def get_parent_identifier(self) -> Optional[
|
|
79
|
+
def get_parent_identifier(self) -> Optional[PluginIdentifier]:
|
|
75
80
|
return self._only_one('plugin_parent')
|
|
76
81
|
|
|
77
82
|
def get_parent_version(self) -> Optional[int]:
|
|
@@ -89,7 +94,7 @@ class Plugin(object):
|
|
|
89
94
|
return result(lst[0])
|
|
90
95
|
|
|
91
96
|
@staticmethod
|
|
92
|
-
def from_jar(jar_path:
|
|
97
|
+
def from_jar(jar_path: PathOrStr) -> Plugin:
|
|
93
98
|
jar_path = path(jar_path) # in case it's a string
|
|
94
99
|
plugin_id = Plugin.id_from_jar(jar_path)
|
|
95
100
|
plugin_fstr = str(Plugin.id_to_file(plugin_id))
|
|
@@ -98,19 +103,19 @@ class Plugin(object):
|
|
|
98
103
|
return Plugin(plugin_file, plugin_fstr)
|
|
99
104
|
|
|
100
105
|
@staticmethod
|
|
101
|
-
def from_path(fpath:
|
|
106
|
+
def from_path(fpath: PathOrStr) -> Plugin:
|
|
102
107
|
fpath = path(fpath) # in case it's a string
|
|
103
108
|
with open(fpath, 'r') as input_file:
|
|
104
109
|
return Plugin(input_file, fpath)
|
|
105
110
|
|
|
106
111
|
@staticmethod
|
|
107
|
-
def file_to_id(plugin_fstr: str) ->
|
|
112
|
+
def file_to_id(plugin_fstr: str) -> PluginIdentifier:
|
|
108
113
|
return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
|
|
109
114
|
|
|
110
115
|
@staticmethod
|
|
111
|
-
def id_from_jar(jar_path:
|
|
116
|
+
def id_from_jar(jar_path: PathOrStr) -> PluginIdentifier:
|
|
112
117
|
jar_path = path(jar_path) # in case it's a string
|
|
113
|
-
manifest =
|
|
118
|
+
manifest = java_manifest.from_jar(jar_path)
|
|
114
119
|
for entry in manifest:
|
|
115
120
|
if entry.get('Lockss-Plugin') == 'true':
|
|
116
121
|
name = entry.get('Name')
|
|
@@ -121,9 +126,9 @@ class Plugin(object):
|
|
|
121
126
|
raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
|
|
122
127
|
|
|
123
128
|
@staticmethod
|
|
124
|
-
def id_to_dir(plugin_id:
|
|
129
|
+
def id_to_dir(plugin_id: PluginIdentifier) -> Path:
|
|
125
130
|
return Plugin.id_to_file(plugin_id).parent
|
|
126
131
|
|
|
127
132
|
@staticmethod
|
|
128
|
-
def id_to_file(plugin_id:
|
|
133
|
+
def id_to_file(plugin_id: PluginIdentifier) -> Path:
|
|
129
134
|
return Path(f'{plugin_id.replace(".", "/")}.xml')
|