lockss-turtles 0.5.0.dev3__py3-none-any.whl → 0.6.0__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.
- lockss/turtles/__init__.py +10 -30
- lockss/turtles/__main__.py +7 -3
- lockss/turtles/app.py +520 -109
- lockss/turtles/cli.py +540 -333
- lockss/turtles/plugin.py +207 -50
- lockss/turtles/plugin_registry.py +617 -189
- lockss/turtles/plugin_set.py +534 -187
- lockss/turtles/plugin_signing_credentials.py +84 -0
- lockss/turtles/util.py +70 -21
- {lockss_turtles-0.5.0.dev3.dist-info → lockss_turtles-0.6.0.dist-info}/LICENSE +1 -1
- lockss_turtles-0.6.0.dist-info/METADATA +64 -0
- lockss_turtles-0.6.0.dist-info/RECORD +18 -0
- {lockss_turtles-0.5.0.dev3.dist-info → lockss_turtles-0.6.0.dist-info}/WHEEL +1 -1
- unittest/lockss/turtles/__init__.py +106 -0
- unittest/lockss/turtles/test_plugin_registry.py +417 -0
- unittest/lockss/turtles/test_plugin_set.py +274 -0
- unittest/lockss/turtles/test_plugin_signing_credentials.py +102 -0
- CHANGELOG.rst +0 -113
- lockss/turtles/resources/__init__.py +0 -29
- lockss/turtles/resources/plugin-registry-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-registry-schema.json +0 -115
- lockss/turtles/resources/plugin-set-catalog-schema.json +0 -27
- lockss/turtles/resources/plugin-set-schema.json +0 -92
- lockss/turtles/resources/plugin-signing-credentials-schema.json +0 -27
- lockss_turtles-0.5.0.dev3.dist-info/METADATA +0 -1041
- lockss_turtles-0.5.0.dev3.dist-info/RECORD +0 -20
- {lockss_turtles-0.5.0.dev3.dist-info → lockss_turtles-0.6.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
4
|
#
|
|
5
5
|
# Redistribution and use in source and binary forms, with or without
|
|
6
6
|
# modification, are permitted provided that the following conditions are met:
|
|
@@ -28,225 +28,653 @@
|
|
|
28
28
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
29
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
from lockss.turtles.plugin import Plugin
|
|
36
|
-
import lockss.turtles.resources
|
|
37
|
-
from lockss.turtles.util import _load_and_validate, _path
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
class PluginRegistryCatalog(object):
|
|
41
|
-
|
|
42
|
-
PLUGIN_REGISTRY_CATALOG_SCHEMA = 'plugin-registry-catalog-schema.json'
|
|
31
|
+
"""
|
|
32
|
+
Module to represent plugin registries and plugin registry catalogs.
|
|
33
|
+
"""
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
plugin_registry_catalog_path = _path(plugin_registry_catalog_path)
|
|
47
|
-
with importlib.resources.path(lockss.turtles.resources, PluginRegistryCatalog.PLUGIN_REGISTRY_CATALOG_SCHEMA) as plugin_registry_catalog_schema_path:
|
|
48
|
-
parsed = _load_and_validate(plugin_registry_catalog_schema_path, plugin_registry_catalog_path)
|
|
49
|
-
return PluginRegistryCatalog(parsed)
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
36
|
+
from __future__ import annotations
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
38
|
+
from abc import ABC, abstractmethod
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
import subprocess
|
|
41
|
+
from typing import Annotated, Any, ClassVar, Literal, Optional, Union
|
|
42
|
+
|
|
43
|
+
from lockss.pybasic.errorutil import InternalError
|
|
44
|
+
from lockss.pybasic.fileutil import path
|
|
45
|
+
from pydantic import BaseModel, Field
|
|
46
|
+
|
|
47
|
+
from .plugin import Plugin, PluginIdentifier
|
|
48
|
+
from .util import BaseModelWithRoot
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
#: A type alias for the plugin registry catalog kind.
|
|
52
|
+
PluginRegistryCatalogKind = Literal['PluginRegistryCatalog']
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PluginRegistryCatalog(BaseModelWithRoot):
|
|
56
|
+
"""
|
|
57
|
+
A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
|
|
58
|
+
plugin registry catalog.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
#: This object's kind.
|
|
62
|
+
kind: PluginRegistryCatalogKind = Field(title='Kind',
|
|
63
|
+
description="This object's kind")
|
|
64
|
+
|
|
65
|
+
#: A non-empty list of plugin registry files.
|
|
66
|
+
plugin_registry_files: list[str] = Field(alias='plugin-registry-files',
|
|
67
|
+
min_length=1,
|
|
68
|
+
title='Plugin Registry Files',
|
|
69
|
+
description="A non-empty list of plugin registry files")
|
|
70
|
+
|
|
71
|
+
def get_plugin_registry_files(self) -> list[Path]:
|
|
72
|
+
"""
|
|
73
|
+
Returns the list of plugin registry files in this catalog, relative to
|
|
74
|
+
the plugin registry catalog file if applicable.
|
|
75
|
+
|
|
76
|
+
:return: A non-null list of plugin registry file paths.
|
|
77
|
+
:rtype: list[Path]
|
|
78
|
+
"""
|
|
79
|
+
return [self.get_root().joinpath(pstr) for pstr in self.plugin_registry_files]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
#: A type alias for the two plugin registry layout types.
|
|
83
|
+
PluginRegistryLayoutType = Literal['directory', 'rcs']
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
#: A type alias for the three plugin registry layout file naming conventions.
|
|
87
|
+
PluginRegistryLayoutFileNamingConvention = Literal['abbreviated', 'identifier', 'underscore']
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BasePluginRegistryLayout(BaseModel, ABC):
|
|
91
|
+
"""
|
|
92
|
+
An abstract Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to
|
|
93
|
+
represent a plugin registry layout, with concrete implementations
|
|
94
|
+
``DirectoryPluginRegistryLayout`` and ``RcsPluginRegistryLayout``.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
#: Pydantic definition of the ``type`` field.
|
|
98
|
+
TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout Type',
|
|
99
|
+
description='A plugin registry layout type')
|
|
100
|
+
|
|
101
|
+
#: Default file naming convention.
|
|
102
|
+
FILE_NAMING_CONVENTION_DEFAULT: ClassVar[PluginRegistryLayoutFileNamingConvention] = 'identifier'
|
|
103
|
+
|
|
104
|
+
#: Pydantic definition of the ``file_naming_convention`` field.
|
|
105
|
+
FILE_NAMING_CONVENTION_FIELD: ClassVar[dict[str, str]] = dict(alias='file-naming-convention',
|
|
106
|
+
title='Plugin Registry Layout File Naming Convention',
|
|
107
|
+
description='A file naming convention for the plugin registry layout')
|
|
108
|
+
|
|
109
|
+
#: Internal backreference to the enclosing plugin registry; see ``initialize``.
|
|
110
|
+
_plugin_registry: Optional[PluginRegistry]
|
|
111
|
+
|
|
112
|
+
def deploy_plugin(self,
|
|
113
|
+
plugin_id: PluginIdentifier,
|
|
114
|
+
layer: PluginRegistryLayer,
|
|
115
|
+
src_path: Path,
|
|
116
|
+
interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
|
|
117
|
+
"""
|
|
118
|
+
Deploys the given plugin to the target plugin registry layer according to
|
|
119
|
+
this plugin registry layout's file naming convention.
|
|
120
|
+
|
|
121
|
+
See ``_copy_jar``.
|
|
122
|
+
|
|
123
|
+
:param plugin_id: A plugin identifier.
|
|
124
|
+
:type plugin_id: PluginIdentifier
|
|
125
|
+
:param layer: A plugin registry layer.
|
|
126
|
+
:type layer: PluginRegistryLayer
|
|
127
|
+
:param src_path: The path of the plugin JAR.
|
|
128
|
+
:type src_path: Path
|
|
129
|
+
:param interactive: If False (the default), no interactive confirmation
|
|
130
|
+
will occur. If True, and the given plugin is being
|
|
131
|
+
deployed to the target layer for the very first
|
|
132
|
+
time, the user will be prompted interactively to
|
|
133
|
+
confirm.
|
|
134
|
+
:type interactive: bool
|
|
135
|
+
:return: A tuple of the path of the deployed JAR and a Plugin object
|
|
136
|
+
instantiated from the source JAR, or None if the user was
|
|
137
|
+
prompted for confirmation and responded negatively.
|
|
138
|
+
:rtype: Optional[tuple[Path, Plugin]]
|
|
139
|
+
"""
|
|
140
|
+
src_path = path(src_path) # in case it's a string
|
|
141
|
+
dst_path = self._get_dstpath(plugin_id, layer)
|
|
142
|
+
if not self._proceed_copy(src_path, dst_path, layer, interactive=interactive):
|
|
143
|
+
return None
|
|
144
|
+
self._copy_jar(src_path, dst_path)
|
|
145
|
+
return dst_path, Plugin.from_jar(src_path)
|
|
146
|
+
|
|
147
|
+
# Believed to be abandoned:
|
|
148
|
+
|
|
149
|
+
# def get_file_for(self,
|
|
150
|
+
# plugin_id,
|
|
151
|
+
# layer: PluginRegistryLayer) -> Optional[Path]:
|
|
152
|
+
# """
|
|
153
|
+
#
|
|
154
|
+
# :param plugin_id:
|
|
155
|
+
# :type plugin_id:
|
|
156
|
+
# :param layer:
|
|
157
|
+
# :type layer:
|
|
158
|
+
# :return:
|
|
159
|
+
# :rtype:
|
|
160
|
+
# """
|
|
161
|
+
# jar_path = self._get_dstpath(plugin_id, layer)
|
|
162
|
+
# return jar_path if jar_path.is_file() else None
|
|
163
|
+
|
|
164
|
+
def get_file_naming_convention(self) -> PluginRegistryLayoutFileNamingConvention:
|
|
165
|
+
"""
|
|
166
|
+
Returns the concrete implementation's ``file_naming`convention`` field.
|
|
167
|
+
|
|
168
|
+
:return: This plugin registry layout's file naming convention.
|
|
169
|
+
:rtype: PluginRegistryLayoutFileNamingConvention
|
|
170
|
+
"""
|
|
171
|
+
return getattr(self, 'file_naming_convention')
|
|
172
|
+
|
|
173
|
+
def get_plugin_registry(self) -> PluginRegistry:
|
|
174
|
+
"""
|
|
175
|
+
Returns the enclosing plugin registry.
|
|
176
|
+
|
|
177
|
+
See ``initialize``.
|
|
178
|
+
|
|
179
|
+
:return: The enclosing plugin registry.
|
|
180
|
+
:rtype: PluginRegistry
|
|
181
|
+
:raises ValueError: If ``initialize`` was not called on the object.
|
|
182
|
+
"""
|
|
183
|
+
if self._plugin_registry is None:
|
|
184
|
+
raise ValueError('Uninitialized plugin registry')
|
|
185
|
+
return self._plugin_registry
|
|
69
186
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
187
|
+
def get_type(self) -> PluginRegistryLayoutType:
|
|
188
|
+
"""
|
|
189
|
+
Returns the concrete implementation's ``type`` field.
|
|
190
|
+
|
|
191
|
+
:return: This plugin registry layout's type.
|
|
192
|
+
:rtype: PluginRegistryLayoutType
|
|
193
|
+
"""
|
|
194
|
+
return getattr(self, 'type')
|
|
195
|
+
|
|
196
|
+
def initialize(self,
|
|
197
|
+
plugin_registry: PluginRegistry) -> BasePluginRegistryLayout:
|
|
198
|
+
"""
|
|
199
|
+
Initializes the plugin registry backreference. Mandatory call after
|
|
200
|
+
object creation.
|
|
201
|
+
|
|
202
|
+
:param plugin_registry: The enclosing plugin registry.
|
|
203
|
+
:type plugin_registry: PluginRegistry
|
|
204
|
+
:return: This object (for chaining).
|
|
205
|
+
:rtype: BasePluginRegistryLayout
|
|
206
|
+
"""
|
|
207
|
+
self._plugin_registry = plugin_registry
|
|
208
|
+
return self
|
|
209
|
+
|
|
210
|
+
def model_post_init(self,
|
|
211
|
+
context: Any) -> None:
|
|
212
|
+
"""
|
|
213
|
+
Pydantic post-initialization method, to create the ``_plugin_registry``
|
|
214
|
+
backreference.
|
|
215
|
+
|
|
216
|
+
See ``initialize``.
|
|
217
|
+
|
|
218
|
+
:param context: The Pydantic context.
|
|
219
|
+
:type context: Any
|
|
220
|
+
"""
|
|
221
|
+
super().model_post_init(context)
|
|
222
|
+
self._plugin_registry = None
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def _copy_jar(self,
|
|
226
|
+
src_path: Path,
|
|
227
|
+
dst_path: Path) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Implementation-specific copy of the plugin JAR from a source path to its
|
|
230
|
+
intended deployed path.
|
|
231
|
+
|
|
232
|
+
:param src_path: The path of the plugin JAR to be deployed.
|
|
233
|
+
:type src_path: Path
|
|
234
|
+
:param dst_path: The intended path of the deployed JAR.
|
|
235
|
+
:type dst_path: Path
|
|
236
|
+
"""
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
def _get_dstfile(self,
|
|
240
|
+
plugin_id: PluginIdentifier) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Computes the destination file name (not path) based on this layout's
|
|
243
|
+
file naming convention.
|
|
244
|
+
|
|
245
|
+
Implemented here because common to both concrete implementations.
|
|
246
|
+
|
|
247
|
+
:param plugin_id: A plugin identifier.
|
|
248
|
+
:type plugin_id: PluginIdentifier
|
|
249
|
+
:return: A file name string consistent with the file naming convention.
|
|
250
|
+
For a plugin identifier ``org.myproject.plugin.MyPlugin``, the
|
|
251
|
+
result is ``MyPlugin.jar`` for ``abbreviated``,
|
|
252
|
+
``org.myproject.plugin.MyPlugin.jar`` for ``identifier`` and
|
|
253
|
+
``org_myproject_plugin_MyPlugin.jar`` for ``underscore``.
|
|
254
|
+
:rtype: str
|
|
255
|
+
"""
|
|
256
|
+
if (conv := self.get_file_naming_convention()) == 'abbreviated':
|
|
257
|
+
return f'{plugin_id.split(".")[-1]}.jar'
|
|
258
|
+
elif conv == 'identifier':
|
|
259
|
+
return f'{plugin_id}.jar'
|
|
260
|
+
elif conv == 'underscore':
|
|
261
|
+
return f'{plugin_id.replace(".", "_")}.jar'
|
|
77
262
|
else:
|
|
78
|
-
raise
|
|
263
|
+
raise InternalError()
|
|
264
|
+
|
|
265
|
+
def _get_dstpath(self,
|
|
266
|
+
plugin_id: PluginIdentifier,
|
|
267
|
+
layer: PluginRegistryLayer) -> Path:
|
|
268
|
+
"""
|
|
269
|
+
Computes the destination path for the given plugin being deployed to the
|
|
270
|
+
target layer, using the layout's file naming convention.
|
|
271
|
+
|
|
272
|
+
:param plugin_id: A plugin identifier.
|
|
273
|
+
:type plugin_id: PluginIdentifier
|
|
274
|
+
:param layer: A target plugin registry layer.
|
|
275
|
+
:type layer: PluginRegistryLayer
|
|
276
|
+
:return: The would-be destination of the deployed JAR.
|
|
277
|
+
:rtype: Path
|
|
278
|
+
"""
|
|
279
|
+
return layer.get_path().joinpath(self._get_dstfile(plugin_id))
|
|
280
|
+
|
|
281
|
+
def _proceed_copy(self,
|
|
282
|
+
src_path: Path,
|
|
283
|
+
dst_path: Path,
|
|
284
|
+
layer: PluginRegistryLayer,
|
|
285
|
+
interactive: bool=False) -> bool:
|
|
286
|
+
"""
|
|
287
|
+
Determines whether the copy of the JAR should proceed.
|
|
288
|
+
|
|
289
|
+
:param src_path: The path of the JAR being deployed.
|
|
290
|
+
:type src_path: Path
|
|
291
|
+
:param dst_path: The path of the intended deployed JAR.
|
|
292
|
+
:type dst_path: Path
|
|
293
|
+
:param layer: The target plugin registry layer.
|
|
294
|
+
:type layer: PluginRegistryLayer
|
|
295
|
+
:param interactive: Whether interactive prompts are allowed (False by
|
|
296
|
+
default)
|
|
297
|
+
:type interactive: bool
|
|
298
|
+
:return: True, unless the destination file does not exist yet, the
|
|
299
|
+
interactive flag is True, and the user does not respond
|
|
300
|
+
positively to the confirmation prompt.
|
|
301
|
+
:rtype: bool
|
|
302
|
+
"""
|
|
303
|
+
if not dst_path.exists():
|
|
304
|
+
if interactive:
|
|
305
|
+
i = input(f'{dst_path} does not exist in {self.get_plugin_registry().get_id()}:{layer.get_id()} ({layer.get_name()}); create it (y/n)? [n] ').lower() or 'n'
|
|
306
|
+
if i != 'y':
|
|
307
|
+
return False
|
|
308
|
+
return True
|
|
79
309
|
|
|
80
|
-
def __init__(self, parsed):
|
|
81
|
-
super().__init__()
|
|
82
|
-
self._parsed = parsed
|
|
83
310
|
|
|
84
|
-
|
|
85
|
-
|
|
311
|
+
class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
|
|
312
|
+
"""
|
|
313
|
+
A plugin registry layout that keeps plugin JARs in a single directory.
|
|
314
|
+
"""
|
|
86
315
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if layer.get_id() == layer_id:
|
|
90
|
-
return layer
|
|
91
|
-
return None
|
|
316
|
+
#: This plugin registry layout's type.
|
|
317
|
+
type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
|
|
92
318
|
|
|
93
|
-
|
|
94
|
-
|
|
319
|
+
#: This plugin registry layout's file naming convention.
|
|
320
|
+
file_naming_convention: PluginRegistryLayoutFileNamingConvention = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT,
|
|
321
|
+
**BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
|
|
95
322
|
|
|
96
|
-
def
|
|
97
|
-
|
|
323
|
+
def _copy_jar(self,
|
|
324
|
+
src_path: Path,
|
|
325
|
+
dst_path: Path) -> None:
|
|
326
|
+
"""
|
|
327
|
+
Copies the plugin JAR from a source path to its intended deployed path.
|
|
98
328
|
|
|
99
|
-
|
|
100
|
-
|
|
329
|
+
Additionally, if SELinux is enabled, sets the type of the security
|
|
330
|
+
context of the deployed path to ``httpd_sys_content_t``.
|
|
101
331
|
|
|
102
|
-
|
|
103
|
-
|
|
332
|
+
:param src_path: The path of the plugin JAR to be deployed.
|
|
333
|
+
:type src_path: Path
|
|
334
|
+
:param dst_path: The intended path of the deployed JAR.
|
|
335
|
+
:type dst_path: Path
|
|
336
|
+
:raises subprocess.CalledProcessError: If an invoked subprocess fails.
|
|
337
|
+
"""
|
|
338
|
+
dst_dir, dst_file = dst_path.parent, dst_path.name
|
|
339
|
+
subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=dst_dir)
|
|
340
|
+
if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
|
|
341
|
+
cmd = ['chcon', '-t', 'httpd_sys_content_t', dst_file]
|
|
342
|
+
subprocess.run(cmd, check=True, cwd=dst_dir)
|
|
104
343
|
|
|
105
|
-
def get_plugin_identifiers(self):
|
|
106
|
-
return self._parsed['plugin-identifiers']
|
|
107
344
|
|
|
108
|
-
|
|
109
|
-
|
|
345
|
+
class RcsPluginRegistryLayout(DirectoryPluginRegistryLayout):
|
|
346
|
+
"""
|
|
347
|
+
A plugin registry layout that is like ``DirectoryPluginRegistryLayout`` but
|
|
348
|
+
also uses `GNU RCS <https://www.gnu.org/software/rcs/>`_ to keep a record of
|
|
349
|
+
successive plugin versions in an ``RCS`` subdirectory.
|
|
350
|
+
"""
|
|
110
351
|
|
|
111
|
-
|
|
112
|
-
|
|
352
|
+
#: This plugin registry layout's type. Shadows that of ``DirectoryPluginRegistryLayout`` due to inheritance.
|
|
353
|
+
type: Literal['rcs'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
|
|
113
354
|
|
|
355
|
+
# Believed to be unnecessary:
|
|
114
356
|
|
|
115
|
-
|
|
357
|
+
#file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
|
|
116
358
|
|
|
117
|
-
|
|
359
|
+
def _copy_jar(self,
|
|
360
|
+
src_path: Path,
|
|
361
|
+
dst_path: Path) -> None:
|
|
362
|
+
"""
|
|
363
|
+
Copies the plugin JAR from a source path to its intended deployed path.
|
|
118
364
|
|
|
119
|
-
|
|
365
|
+
Does ``co -l`` if applicable, does the same copy as the parent
|
|
366
|
+
``DirectoryPluginRegistryLayout._copy_jar``, then does ``ci -u``.
|
|
120
367
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
368
|
+
:param src_path: The path of the plugin JAR to be deployed.
|
|
369
|
+
:type src_path: Path
|
|
370
|
+
:param dst_path: The intended path of the deployed JAR.
|
|
371
|
+
:type dst_path: Path
|
|
372
|
+
:raises subprocess.CalledProcessError: If an invoked subprocess fails.
|
|
373
|
+
"""
|
|
374
|
+
dst_dir, dst_file = dst_path.parent, dst_path.name
|
|
375
|
+
plugin = Plugin.from_jar(src_path)
|
|
376
|
+
rcs_path = dst_dir.joinpath('RCS', f'{dst_file},v')
|
|
377
|
+
# Maybe do co -l before the parent's copy
|
|
378
|
+
if dst_path.exists() and rcs_path.is_file():
|
|
379
|
+
cmd = ['co', '-l', dst_file]
|
|
380
|
+
subprocess.run(cmd, check=True, cwd=dst_dir)
|
|
381
|
+
# Do the parent's copy
|
|
382
|
+
super()._copy_jar(src_path, dst_path)
|
|
383
|
+
# Do ci -u after the parent's copy
|
|
384
|
+
cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
|
|
385
|
+
if not rcs_path.is_file():
|
|
386
|
+
cmd.append(f'-t-{plugin.get_name()}')
|
|
387
|
+
cmd.append(dst_file)
|
|
388
|
+
subprocess.run(cmd, check=True, cwd=dst_dir)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
#: A type alias for plugin registry layouts, which is the union of
|
|
392
|
+
#: ``DirectoryPluginRegistryLayout`` and ``RcsPluginRegistryLayout`` using
|
|
393
|
+
#: ``type`` as the discriminator field.
|
|
394
|
+
PluginRegistryLayout = Annotated[Union[DirectoryPluginRegistryLayout, RcsPluginRegistryLayout], Field(discriminator='type')]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
#: A type alias for plugin registry layer identifiers.
|
|
398
|
+
PluginRegistryLayerIdentifier = str
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class PluginRegistryLayer(BaseModel):
|
|
402
|
+
"""
|
|
403
|
+
A Pydantic model to represent a plugin registry layer.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
#: This plugin registry layer's identifier.
|
|
407
|
+
id: PluginRegistryLayerIdentifier = Field(title='Plugin Registry Layer Identifier',
|
|
408
|
+
description='An identifier for the plugin registry layer')
|
|
409
|
+
|
|
410
|
+
#: This plugin registry layer's name.
|
|
411
|
+
name: str = Field(title='Plugin Registry Layer Name',
|
|
412
|
+
description='A name for the plugin registry layer')
|
|
413
|
+
|
|
414
|
+
#: This plugin registry layer's path.
|
|
415
|
+
path: str = Field(title='Plugin Registry Layer Path',
|
|
416
|
+
description='A root path for the plugin registry layer')
|
|
417
|
+
|
|
418
|
+
#: Internal backreference to the enclosing plugin registry; see ``initialize``.
|
|
419
|
+
_plugin_registry: Optional[PluginRegistry]
|
|
420
|
+
|
|
421
|
+
def deploy_plugin(self,
|
|
422
|
+
plugin_id: PluginIdentifier,
|
|
423
|
+
src_path: Path,
|
|
424
|
+
interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
|
|
425
|
+
"""
|
|
426
|
+
Deploys the given plugin to this plugin registry layer according to
|
|
427
|
+
this plugin registry layout's file naming convention.
|
|
428
|
+
|
|
429
|
+
:param plugin_id: A plugin identifier.
|
|
430
|
+
:type plugin_id: PluginIdentifier
|
|
431
|
+
:param src_path: The path of the plugin JAR.
|
|
432
|
+
:type src_path: Path
|
|
433
|
+
:param interactive: If False (the default), no interactive confirmation
|
|
434
|
+
will occur. If True, and the given plugin is being
|
|
435
|
+
deployed to this layer for the very first time, the
|
|
436
|
+
user will be prompted interactively to confirm.
|
|
437
|
+
:type interactive: bool
|
|
438
|
+
:return: A tuple of the path of the deployed JAR and a Plugin object
|
|
439
|
+
instantiated from the source JAR, or None if the user was
|
|
440
|
+
prompted for confirmation and responded negatively.
|
|
441
|
+
:rtype: Optional[tuple[Path, Plugin]]
|
|
442
|
+
"""
|
|
443
|
+
return self.get_plugin_registry().get_layout().deploy_plugin(plugin_id, self, src_path, interactive)
|
|
444
|
+
|
|
445
|
+
def get_id(self) -> PluginRegistryLayerIdentifier:
|
|
446
|
+
"""
|
|
447
|
+
Returns this plugin registry layer's identifier.
|
|
448
|
+
|
|
449
|
+
:return: This plugin registry layer's identifier.
|
|
450
|
+
:rtype: PluginRegistryLayerIdentifier
|
|
451
|
+
"""
|
|
452
|
+
return self.id
|
|
453
|
+
|
|
454
|
+
def get_jars(self) -> list[Path]:
|
|
455
|
+
"""
|
|
456
|
+
Returns the list of this plugin registry layer's JAR file paths.
|
|
457
|
+
|
|
458
|
+
:return: A sorted list of JAR file paths.
|
|
459
|
+
:rtype: list[Path]
|
|
460
|
+
"""
|
|
461
|
+
# FIXME Strictly speaking this should be in the layout
|
|
462
|
+
return sorted(self.get_path().glob('*.jar'))
|
|
125
463
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
464
|
+
def get_name(self) -> str:
|
|
465
|
+
"""
|
|
466
|
+
Returns this plugin registry layer's name.
|
|
129
467
|
|
|
130
|
-
|
|
131
|
-
|
|
468
|
+
:return: This plugin registry layer's name.
|
|
469
|
+
:rtype: str
|
|
470
|
+
"""
|
|
471
|
+
return self.name
|
|
132
472
|
|
|
133
|
-
def
|
|
134
|
-
|
|
473
|
+
def get_path(self) -> Path:
|
|
474
|
+
"""
|
|
475
|
+
Returns this plugin registry layer's path.
|
|
135
476
|
|
|
136
|
-
|
|
137
|
-
|
|
477
|
+
:return: This plugin registry layer's path.
|
|
478
|
+
:rtype: Path
|
|
479
|
+
"""
|
|
480
|
+
return self.get_plugin_registry().get_root().joinpath(self.path)
|
|
138
481
|
|
|
139
|
-
def
|
|
140
|
-
|
|
482
|
+
def get_plugin_registry(self) -> PluginRegistry:
|
|
483
|
+
"""
|
|
484
|
+
Returns the enclosing plugin registry.
|
|
141
485
|
|
|
142
|
-
|
|
143
|
-
return _path(self._parsed['path'])
|
|
486
|
+
See ``initialize``.
|
|
144
487
|
|
|
145
|
-
|
|
488
|
+
:return: The enclosing plugin registry.
|
|
489
|
+
:rtype: PluginRegistry
|
|
490
|
+
:raises ValueError: If ``initialize`` was not called on the object.
|
|
491
|
+
"""
|
|
492
|
+
if self._plugin_registry is None:
|
|
493
|
+
raise ValueError('Uninitialized plugin registry')
|
|
146
494
|
return self._plugin_registry
|
|
147
495
|
|
|
496
|
+
def initialize(self,
|
|
497
|
+
plugin_registry: PluginRegistry) -> PluginRegistryLayer:
|
|
498
|
+
"""
|
|
499
|
+
Initializes the plugin registry backreference. Mandatory call after
|
|
500
|
+
object creation.
|
|
501
|
+
|
|
502
|
+
:param plugin_registry: The enclosing plugin registry.
|
|
503
|
+
:type plugin_registry: PluginRegistry
|
|
504
|
+
:return: This object (for chaining).
|
|
505
|
+
:rtype: BasePluginRegistryLayout
|
|
506
|
+
"""
|
|
507
|
+
self._plugin_registry = plugin_registry
|
|
508
|
+
return self
|
|
509
|
+
|
|
510
|
+
def model_post_init(self,
|
|
511
|
+
context: Any) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Pydantic post-initialization method, to create the ``_plugin_registry``
|
|
514
|
+
backreference.
|
|
515
|
+
|
|
516
|
+
See ``initialize``.
|
|
517
|
+
|
|
518
|
+
:param context: The Pydantic context.
|
|
519
|
+
:type context: Any
|
|
520
|
+
"""
|
|
521
|
+
super().model_post_init(context)
|
|
522
|
+
self._plugin_registry = None
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
#: A type alias for the plugin registry kind.
|
|
526
|
+
PluginRegistryKind = Literal['PluginRegistry']
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
#: A type alias for plugin registry identifiers.
|
|
530
|
+
PluginRegistryIdentifier = str
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class PluginRegistry(BaseModelWithRoot):
|
|
534
|
+
"""
|
|
535
|
+
A Pydantic model (``lockss.turtles.util.BaseModelWithRoot``) to represent a
|
|
536
|
+
plugin registry.
|
|
537
|
+
"""
|
|
538
|
+
|
|
539
|
+
#: This object's kind.
|
|
540
|
+
kind: PluginRegistryKind = Field(title='Kind',
|
|
541
|
+
description="This object's kind")
|
|
542
|
+
|
|
543
|
+
#: This plugin registry's identifier.
|
|
544
|
+
id: PluginRegistryIdentifier = Field(title='Plugin Registry Identifier',
|
|
545
|
+
description='An identifier for the plugin set')
|
|
546
|
+
|
|
547
|
+
#: This plugin registry's name.
|
|
548
|
+
name: str = Field(title='Plugin Registry Name',
|
|
549
|
+
description='A name for the plugin set')
|
|
550
|
+
|
|
551
|
+
#: This plugin registry's layout.
|
|
552
|
+
layout: PluginRegistryLayout = Field(title='Plugin Registry Layout',
|
|
553
|
+
description='A layout for the plugin registry')
|
|
554
|
+
|
|
555
|
+
#: This plugin registry's layers.
|
|
556
|
+
layers: list[PluginRegistryLayer] = Field(min_length=1,
|
|
557
|
+
title='Plugin Registry Layers',
|
|
558
|
+
description="A non-empty list of plugin registry layers")
|
|
559
|
+
|
|
560
|
+
#: The plugin identifiers in this registry.
|
|
561
|
+
plugin_identifiers: list[PluginIdentifier] = Field(alias='plugin-identifiers',
|
|
562
|
+
min_length=1,
|
|
563
|
+
title='Plugin Identifiers',
|
|
564
|
+
description="A non-empty list of plugin identifiers")
|
|
565
|
+
|
|
566
|
+
#: The suppressed plugin identifiers, formerly in this plugin registry.
|
|
567
|
+
suppressed_plugin_identifiers: list[PluginIdentifier] = Field([],
|
|
568
|
+
alias='suppressed-plugin-identifiers',
|
|
569
|
+
title='Suppressed Plugin Identifiers',
|
|
570
|
+
description="A list of suppressed plugin identifiers")
|
|
571
|
+
|
|
572
|
+
def get_id(self) -> PluginRegistryIdentifier:
|
|
573
|
+
"""
|
|
574
|
+
Returns this plugin registry's identifier.
|
|
575
|
+
|
|
576
|
+
:return: This plugin registry's identifier.
|
|
577
|
+
:rtype: PluginRegistryIdentifier
|
|
578
|
+
"""
|
|
579
|
+
return self.id
|
|
580
|
+
|
|
581
|
+
def get_layer(self,
|
|
582
|
+
layer_id: PluginRegistryLayerIdentifier) -> Optional[PluginRegistryLayer]:
|
|
583
|
+
"""
|
|
584
|
+
Returns the plugin registry layer with the given identifier.
|
|
585
|
+
|
|
586
|
+
:param layer_id: A plugin registry layer identifier.
|
|
587
|
+
:type layer_id: PluginRegistryLayerIdentifier
|
|
588
|
+
:return: The plugin registry layer from this registry with the given
|
|
589
|
+
identifier, or None if there is no such layer.
|
|
590
|
+
:rtype: Optional[PluginRegistryLayer]
|
|
591
|
+
"""
|
|
592
|
+
for layer in self.get_layers():
|
|
593
|
+
if layer.get_id() == layer_id:
|
|
594
|
+
return layer
|
|
595
|
+
return None
|
|
148
596
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
FILE_NAMING_CONVENTION_ABBREVIATED = 'abbreviated'
|
|
154
|
-
|
|
155
|
-
FILE_NAMING_CONVENTION_IDENTIFIER = 'identifier'
|
|
156
|
-
|
|
157
|
-
FILE_NAMING_CONVENTION_UNDERSCORE = 'underscore'
|
|
158
|
-
|
|
159
|
-
DEFAULT_FILE_NAMING_CONVENTION = FILE_NAMING_CONVENTION_IDENTIFIER
|
|
160
|
-
|
|
161
|
-
def __init__(self, parsed):
|
|
162
|
-
super().__init__(parsed)
|
|
163
|
-
|
|
164
|
-
def _make_layer(self, parsed):
|
|
165
|
-
return DirectoryPluginRegistryLayer(self, parsed)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class DirectoryPluginRegistryLayer(PluginRegistryLayer):
|
|
169
|
-
|
|
170
|
-
def __init__(self, plugin_registry, parsed):
|
|
171
|
-
super().__init__(plugin_registry, parsed)
|
|
172
|
-
|
|
173
|
-
def deploy_plugin(self, plugin_id, src_path, interactive=False):
|
|
174
|
-
src_path = _path(src_path) # in case it's a string
|
|
175
|
-
dst_path = self._get_dstpath(plugin_id)
|
|
176
|
-
if not self._proceed_copy(src_path, dst_path, interactive=interactive):
|
|
177
|
-
return None
|
|
178
|
-
self._copy_jar(src_path, dst_path, interactive=interactive)
|
|
179
|
-
return (dst_path, Plugin.from_jar(src_path))
|
|
180
|
-
|
|
181
|
-
def get_file_for(self, plugin_id):
|
|
182
|
-
jar_path = self._get_dstpath(plugin_id)
|
|
183
|
-
return jar_path if jar_path.is_file() else None
|
|
184
|
-
|
|
185
|
-
def get_file_naming_convention(self):
|
|
186
|
-
return self.get_plugin_registry()._parsed['layout'].get('file-naming-convention', DirectoryPluginRegistry.DEFAULT_FILE_NAMING_CONVENTION)
|
|
187
|
-
|
|
188
|
-
def get_jars(self):
|
|
189
|
-
return sorted(self.get_path().glob('*.jar'))
|
|
190
|
-
|
|
191
|
-
def _copy_jar(self, src_path, dst_path, interactive=False):
|
|
192
|
-
basename = dst_path.name
|
|
193
|
-
subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
|
|
194
|
-
if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
|
|
195
|
-
cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
|
|
196
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
197
|
-
|
|
198
|
-
def _get_dstpath(self, plugin_id):
|
|
199
|
-
return Path(self.get_path(), self._get_dstfile(plugin_id))
|
|
200
|
-
|
|
201
|
-
def _get_dstfile(self, plugin_id):
|
|
202
|
-
conv = self.get_file_naming_convention()
|
|
203
|
-
if conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_IDENTIFIER:
|
|
204
|
-
return f'{plugin_id}.jar'
|
|
205
|
-
elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_UNDERSCORE:
|
|
206
|
-
return f'{plugin_id.replace(".", "_")}.jar'
|
|
207
|
-
elif conv == DirectoryPluginRegistry.FILE_NAMING_CONVENTION_ABBREVIATED:
|
|
208
|
-
return f'{plugin_id.split(".")[-1]}.jar'
|
|
209
|
-
else:
|
|
210
|
-
raise RuntimeError(f'{self.get_plugin_registry().get_id()}: unknown file naming convention: {conv}')
|
|
211
|
-
|
|
212
|
-
def _proceed_copy(self, src_path, dst_path, interactive=False):
|
|
213
|
-
if not dst_path.exists():
|
|
214
|
-
if interactive:
|
|
215
|
-
i = input(f'{dst_path} does not exist in {self.get_plugin_registry().get_id()}:{self.get_id()} ({self.get_name()}); create it (y/n)? [n] ').lower() or 'n'
|
|
216
|
-
if i != 'y':
|
|
217
|
-
return False
|
|
218
|
-
return True
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
class RcsPluginRegistry(DirectoryPluginRegistry):
|
|
222
|
-
|
|
223
|
-
LAYOUT = 'rcs'
|
|
224
|
-
|
|
225
|
-
def __init__(self, parsed):
|
|
226
|
-
super().__init__(parsed)
|
|
597
|
+
def get_layer_ids(self) -> list[PluginRegistryLayerIdentifier]:
|
|
598
|
+
"""
|
|
599
|
+
Returns a list of all the plugin registry layer identifiers in this
|
|
600
|
+
registry.
|
|
227
601
|
|
|
228
|
-
|
|
229
|
-
|
|
602
|
+
:return: A list of plugin registry layer identifiers.
|
|
603
|
+
:rtype: list[PluginRegistryLayerIdentifier]
|
|
604
|
+
"""
|
|
605
|
+
return [layer.get_id() for layer in self.get_layers()]
|
|
230
606
|
|
|
607
|
+
def get_layers(self) -> list[PluginRegistryLayer]:
|
|
608
|
+
"""
|
|
609
|
+
Returns a list of all the plugin registry layers in this registry.
|
|
610
|
+
|
|
611
|
+
:return: A list of plugin registry layers.
|
|
612
|
+
:rtype: list[PluginRegistryLayer]
|
|
613
|
+
"""
|
|
614
|
+
return self.layers
|
|
615
|
+
|
|
616
|
+
def get_layout(self) -> BasePluginRegistryLayout:
|
|
617
|
+
"""
|
|
618
|
+
Returns this plugin registry's layout.
|
|
619
|
+
|
|
620
|
+
:return: A list of plugin registry layers.
|
|
621
|
+
:rtype: list[PluginRegistryLayer]
|
|
622
|
+
"""
|
|
623
|
+
return self.layout
|
|
624
|
+
|
|
625
|
+
def get_name(self) -> str:
|
|
626
|
+
"""
|
|
627
|
+
Returns this plugin registry's name.
|
|
628
|
+
|
|
629
|
+
:return: This plugin registry's name.
|
|
630
|
+
:rtype: str
|
|
631
|
+
"""
|
|
632
|
+
return self.name
|
|
633
|
+
|
|
634
|
+
def get_plugin_identifiers(self) -> list[PluginIdentifier]:
|
|
635
|
+
"""
|
|
636
|
+
Returns the list of plugin identifiers in this registry.
|
|
637
|
+
|
|
638
|
+
:return: The list of plugin identifiers in this registry.
|
|
639
|
+
:rtype: list[PluginIdentifier]
|
|
640
|
+
"""
|
|
641
|
+
return self.plugin_identifiers
|
|
642
|
+
|
|
643
|
+
def get_suppressed_plugin_identifiers(self) -> list[PluginIdentifier]:
|
|
644
|
+
"""
|
|
645
|
+
Returns the list of suppressed plugin identifiers in this registry.
|
|
646
|
+
|
|
647
|
+
:return: The list of suppressed plugin identifiers in this registry.
|
|
648
|
+
:rtype: list[PluginIdentifier]
|
|
649
|
+
"""
|
|
650
|
+
return self.suppressed_plugin_identifiers
|
|
651
|
+
|
|
652
|
+
def has_plugin(self,
|
|
653
|
+
plugin_id: PluginIdentifier) -> bool:
|
|
654
|
+
"""
|
|
655
|
+
Determines if a given plugin identifier is in this registry.
|
|
656
|
+
|
|
657
|
+
:param plugin_id: A plugin identifier.
|
|
658
|
+
:type plugin_id: PluginIdentifier
|
|
659
|
+
:return: True if and only if the given plugin identifier is in this
|
|
660
|
+
registry.
|
|
661
|
+
:rtype: bool
|
|
662
|
+
"""
|
|
663
|
+
return plugin_id in self.get_plugin_identifiers()
|
|
231
664
|
|
|
232
|
-
|
|
665
|
+
def model_post_init(self,
|
|
666
|
+
context: Any) -> None:
|
|
667
|
+
"""
|
|
668
|
+
Pydantic post-initialization method to initialize the layout and all the
|
|
669
|
+
layers with this registry as the enclosing registry.
|
|
233
670
|
|
|
234
|
-
|
|
235
|
-
|
|
671
|
+
See ``BasePluginRegistrLayout.initialize`` and
|
|
672
|
+
``PluginRegistryLayout.initialize``.
|
|
236
673
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
245
|
-
# Do the parent's copy
|
|
246
|
-
super()._copy_jar(src_path, dst_path)
|
|
247
|
-
# Do ci -u after the parent's copy
|
|
248
|
-
cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
|
|
249
|
-
if not rcs_path.is_file():
|
|
250
|
-
cmd.append(f'-t-{plugin.get_name()}')
|
|
251
|
-
cmd.append(basename)
|
|
252
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
674
|
+
:param context: The Pydantic context.
|
|
675
|
+
:type context: Any
|
|
676
|
+
"""
|
|
677
|
+
super().model_post_init(context)
|
|
678
|
+
self.get_layout().initialize(self)
|
|
679
|
+
for layer in self.get_layers():
|
|
680
|
+
layer.initialize(self)
|