lockss-turtles 0.6.0.dev2__py3-none-any.whl → 0.6.0.dev4__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 +1 -1
- lockss/turtles/app.py +127 -74
- lockss/turtles/cli.py +82 -42
- lockss/turtles/plugin.py +18 -13
- lockss/turtles/plugin_registry.py +140 -165
- lockss/turtles/plugin_set.py +102 -137
- lockss/turtles/util.py +28 -17
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/METADATA +2 -3
- lockss_turtles-0.6.0.dev4.dist-info/RECORD +15 -0
- unittest/lockss/turtles/__init__.py +65 -0
- unittest/lockss/turtles/test_plugin_set.py +62 -0
- 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.6.0.dev2.dist-info/RECORD +0 -19
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/LICENSE +0 -0
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/WHEEL +0 -0
- {lockss_turtles-0.6.0.dev2.dist-info → lockss_turtles-0.6.0.dev4.dist-info}/entry_points.txt +0 -0
|
@@ -33,231 +33,206 @@
|
|
|
33
33
|
from __future__ import annotations
|
|
34
34
|
|
|
35
35
|
from abc import ABC, abstractmethod
|
|
36
|
-
import importlib.resources as IR
|
|
37
36
|
from pathlib import Path
|
|
38
37
|
import subprocess
|
|
39
|
-
from typing import
|
|
38
|
+
from typing import Annotated, Any, ClassVar, Literal, Optional, Union
|
|
40
39
|
|
|
40
|
+
from lockss.pybasic.errorutil import InternalError
|
|
41
41
|
from lockss.pybasic.fileutil import path
|
|
42
|
+
from pydantic import BaseModel, Field
|
|
42
43
|
|
|
43
|
-
from . import
|
|
44
|
-
from .
|
|
45
|
-
from .util import YamlT, load_and_validate
|
|
44
|
+
from .plugin import Plugin, PluginIdentifier
|
|
45
|
+
from .util import BaseModelWithRoot
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class PluginRegistryCatalog(object):
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
PluginRegistryCatalogKind = Literal['PluginRegistryCatalog']
|
|
51
50
|
|
|
52
|
-
def __init__(self, parsed: YamlT) -> None:
|
|
53
|
-
super().__init__()
|
|
54
|
-
self._parsed: YamlT = parsed
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
class PluginRegistryCatalog(BaseModelWithRoot):
|
|
53
|
+
kind: PluginRegistryCatalogKind = Field(description="This object's kind")
|
|
54
|
+
plugin_registry_files: list[str] = Field(min_length=1, description="A non-empty list of plugin registry files", title='Plugin Registry Files', alias='plugin-registry-files')
|
|
58
55
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
plugin_registry_catalog_path = path(plugin_registry_catalog_path)
|
|
62
|
-
with IR.path(__resources__, PluginRegistryCatalog.PLUGIN_REGISTRY_CATALOG_SCHEMA) as plugin_registry_catalog_schema_path:
|
|
63
|
-
parsed = load_and_validate(plugin_registry_catalog_schema_path, plugin_registry_catalog_path)
|
|
64
|
-
return PluginRegistryCatalog(parsed)
|
|
56
|
+
def get_plugin_registry_files(self) -> list[Path]:
|
|
57
|
+
return [self.get_root().joinpath(pstr) for pstr in self.plugin_registry_files]
|
|
65
58
|
|
|
66
59
|
|
|
67
|
-
|
|
60
|
+
PluginRegistryLayoutType = Literal['directory', 'rcs']
|
|
68
61
|
|
|
69
|
-
PLUGIN_REGISTRY_SCHEMA = 'plugin-registry-schema.json'
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
super().__init__()
|
|
73
|
-
self._parsed: YamlT = parsed
|
|
63
|
+
PluginRegistryLayoutFileNamingConvention = Literal['abbreviated', 'identifier', 'underscore']
|
|
74
64
|
|
|
75
|
-
def get_id(self) -> str:
|
|
76
|
-
return self._parsed['id']
|
|
77
65
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return None
|
|
83
|
-
|
|
84
|
-
def get_layer_ids(self) -> List[str]:
|
|
85
|
-
return [layer.get_id() for layer in self.get_layers()]
|
|
86
|
-
|
|
87
|
-
def get_layers(self) -> List[PluginRegistryLayer]:
|
|
88
|
-
return [self._make_layer(layer_elem) for layer_elem in self._parsed['layers']]
|
|
66
|
+
class BasePluginRegistryLayout(BaseModel, ABC):
|
|
67
|
+
TYPE_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout Type', description='A plugin registry layout type')
|
|
68
|
+
FILE_NAMING_CONVENTION_DEFAULT: ClassVar[PluginRegistryLayoutFileNamingConvention] = 'identifier'
|
|
69
|
+
FILE_NAMING_CONVENTION_FIELD: ClassVar[dict[str, str]] = dict(title='Plugin Registry Layout File Naming Convention', description='A file naming convention for the plugin registry layout', alias='file-naming-convention')
|
|
89
70
|
|
|
90
|
-
|
|
91
|
-
return self._parsed['layout']['type']
|
|
71
|
+
_plugin_registry: Optional[PluginRegistry]
|
|
92
72
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return plugin_id in self.get_plugin_identifiers()
|
|
101
|
-
|
|
102
|
-
@abstractmethod
|
|
103
|
-
def _make_layer(self, parsed: YamlT) -> PluginRegistryLayer:
|
|
104
|
-
pass
|
|
73
|
+
def deploy_plugin(self, plugin_id: PluginIdentifier, layer: PluginRegistryLayer, src_path: Path, interactive: bool=False) -> Optional[tuple[Path, Plugin]]:
|
|
74
|
+
src_path = path(src_path) # in case it's a string
|
|
75
|
+
dst_path = self._get_dstpath(plugin_id, layer)
|
|
76
|
+
if not self._proceed_copy(src_path, dst_path, layer, interactive=interactive):
|
|
77
|
+
return None
|
|
78
|
+
self._copy_jar(src_path, dst_path, interactive=interactive)
|
|
79
|
+
return dst_path, Plugin.from_jar(src_path)
|
|
105
80
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
with IR.path(__resources__, PluginRegistry.PLUGIN_REGISTRY_SCHEMA) as plugin_registry_schema_path:
|
|
110
|
-
lst = load_and_validate(plugin_registry_schema_path, plugin_registry_file_path, multiple=True)
|
|
111
|
-
return [PluginRegistry._from_obj(parsed, plugin_registry_file_path) for parsed in lst]
|
|
112
|
-
|
|
113
|
-
@staticmethod
|
|
114
|
-
def _from_obj(parsed: YamlT, plugin_registry_file_path: Path) -> PluginRegistry:
|
|
115
|
-
typ = parsed['layout']['type']
|
|
116
|
-
if typ == DirectoryPluginRegistry.LAYOUT:
|
|
117
|
-
return DirectoryPluginRegistry(parsed)
|
|
118
|
-
elif typ == RcsPluginRegistry.LAYOUT:
|
|
119
|
-
return RcsPluginRegistry(parsed)
|
|
120
|
-
else:
|
|
121
|
-
raise RuntimeError(f'{plugin_registry_file_path!s}: unknown layout type: {typ}')
|
|
81
|
+
def get_file_for(self, plugin_id, layer: PluginRegistryLayer) -> Optional[Path]:
|
|
82
|
+
jar_path = self._get_dstpath(plugin_id, layer)
|
|
83
|
+
return jar_path if jar_path.is_file() else None
|
|
122
84
|
|
|
85
|
+
def get_file_naming_convention(self) -> PluginRegistryLayoutFileNamingConvention:
|
|
86
|
+
return getattr(self, 'file_naming_convention')
|
|
123
87
|
|
|
124
|
-
|
|
88
|
+
def get_plugin_registry(self) -> PluginRegistry:
|
|
89
|
+
if self._plugin_registry is None:
|
|
90
|
+
raise RuntimeError('Uninitialized plugin registry')
|
|
91
|
+
return self._plugin_registry
|
|
125
92
|
|
|
126
|
-
|
|
93
|
+
def get_type(self) -> PluginRegistryLayoutType:
|
|
94
|
+
return getattr(self, 'type')
|
|
127
95
|
|
|
128
|
-
|
|
96
|
+
def initialize(self, plugin_registry: PluginRegistry) -> BasePluginRegistryLayout:
|
|
97
|
+
self._plugin_registry = plugin_registry
|
|
98
|
+
return self
|
|
129
99
|
|
|
130
|
-
def
|
|
131
|
-
super().
|
|
132
|
-
self.
|
|
133
|
-
self._plugin_registry: PluginRegistry = plugin_registry
|
|
100
|
+
def model_post_init(self, context: Any) -> None:
|
|
101
|
+
super().model_post_init(context)
|
|
102
|
+
self._plugin_registry = None
|
|
134
103
|
|
|
135
104
|
@abstractmethod
|
|
136
|
-
def
|
|
105
|
+
def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
|
|
137
106
|
pass
|
|
138
107
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
108
|
+
def _get_dstfile(self, plugin_id: PluginIdentifier) -> str:
|
|
109
|
+
if (conv := self.get_file_naming_convention()) == 'abbreviated':
|
|
110
|
+
return f'{plugin_id.split(".")[-1]}.jar'
|
|
111
|
+
elif conv == 'identifier':
|
|
112
|
+
return f'{plugin_id}.jar'
|
|
113
|
+
elif conv == 'underscore':
|
|
114
|
+
return f'{plugin_id.replace(".", "_")}.jar'
|
|
115
|
+
else:
|
|
116
|
+
raise InternalError()
|
|
142
117
|
|
|
143
|
-
def
|
|
144
|
-
return self.
|
|
118
|
+
def _get_dstpath(self, plugin_id: PluginIdentifier, layer: PluginRegistryLayer) -> Path:
|
|
119
|
+
return layer.get_path().joinpath(self._get_dstfile(plugin_id))
|
|
145
120
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
def _proceed_copy(self, src_path: Path, dst_path: Path, layer: PluginRegistryLayer, interactive: bool=False) -> bool:
|
|
122
|
+
if not dst_path.exists():
|
|
123
|
+
if interactive:
|
|
124
|
+
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'
|
|
125
|
+
if i != 'y':
|
|
126
|
+
return False
|
|
127
|
+
return True
|
|
149
128
|
|
|
150
|
-
def get_name(self) -> str:
|
|
151
|
-
return self._parsed['name']
|
|
152
129
|
|
|
153
|
-
|
|
154
|
-
|
|
130
|
+
class DirectoryPluginRegistryLayout(BasePluginRegistryLayout):
|
|
131
|
+
type: Literal['directory'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
|
|
132
|
+
file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
|
|
155
133
|
|
|
156
|
-
def
|
|
157
|
-
|
|
134
|
+
def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
|
|
135
|
+
basename = dst_path.name
|
|
136
|
+
subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
|
|
137
|
+
if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
|
|
138
|
+
cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
|
|
139
|
+
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
158
140
|
|
|
159
141
|
|
|
160
|
-
class
|
|
142
|
+
class RcsPluginRegistryLayout(DirectoryPluginRegistryLayout):
|
|
143
|
+
type: Literal['rcs'] = Field(**BasePluginRegistryLayout.TYPE_FIELD)
|
|
144
|
+
file_naming_convention: Optional[PluginRegistryLayoutFileNamingConvention] = Field(BasePluginRegistryLayout.FILE_NAMING_CONVENTION_DEFAULT, **BasePluginRegistryLayout.FILE_NAMING_CONVENTION_FIELD)
|
|
145
|
+
|
|
146
|
+
def _copy_jar(self, src_path: Path, dst_path: Path, interactive: bool=False) -> None:
|
|
147
|
+
basename = dst_path.name
|
|
148
|
+
plugin = Plugin.from_jar(src_path)
|
|
149
|
+
rcs_path = self.get_path().joinpath('RCS', f'{basename},v')
|
|
150
|
+
# Maybe do co -l before the parent's copy
|
|
151
|
+
if dst_path.exists() and rcs_path.is_file():
|
|
152
|
+
cmd = ['co', '-l', basename]
|
|
153
|
+
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
154
|
+
# Do the parent's copy
|
|
155
|
+
super()._copy_jar(src_path, dst_path)
|
|
156
|
+
# Do ci -u after the parent's copy
|
|
157
|
+
cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
|
|
158
|
+
if not rcs_path.is_file():
|
|
159
|
+
cmd.append(f'-t-{plugin.get_name()}')
|
|
160
|
+
cmd.append(basename)
|
|
161
|
+
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
161
162
|
|
|
162
|
-
LAYOUT = 'directory'
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
PluginRegistryLayout = Annotated[Union[DirectoryPluginRegistryLayout, RcsPluginRegistryLayout], Field(discriminator='type')]
|
|
165
165
|
|
|
166
|
-
FILE_NAMING_CONVENTION_IDENTIFIER = 'identifier'
|
|
167
166
|
|
|
168
|
-
|
|
167
|
+
PluginRegistryLayerIdentifier = str
|
|
169
168
|
|
|
170
|
-
DEFAULT_FILE_NAMING_CONVENTION = FILE_NAMING_CONVENTION_IDENTIFIER
|
|
171
169
|
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
class PluginRegistryLayer(BaseModel, ABC):
|
|
171
|
+
PRODUCTION: ClassVar[str] = 'production'
|
|
172
|
+
TESTING: ClassVar[str] = 'testing'
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
id: PluginRegistryLayerIdentifier = Field(title='Plugin Registry Layer Identifier', description='An identifier for the plugin registry layer')
|
|
175
|
+
name: str = Field(title='Plugin Registry Layer Name', description='A name for the plugin registry layer')
|
|
176
|
+
path: str = Field(title='Plugin Registry Layer Path', description='A root path for the plugin registry layer')
|
|
177
177
|
|
|
178
|
+
def get_id(self) -> PluginRegistryLayerIdentifier:
|
|
179
|
+
return self.id
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
def get_jars(self) -> list[Path]:
|
|
182
|
+
return sorted(self.get_path().glob('*.jar'))
|
|
180
183
|
|
|
181
|
-
def
|
|
182
|
-
|
|
184
|
+
def get_name(self) -> str:
|
|
185
|
+
return self.name
|
|
183
186
|
|
|
184
|
-
def
|
|
185
|
-
|
|
186
|
-
dst_path = self._get_dstpath(plugin_id)
|
|
187
|
-
if not self._proceed_copy(src_path, dst_path, interactive=interactive):
|
|
188
|
-
return None
|
|
189
|
-
self._copy_jar(src_path, dst_path, interactive=interactive)
|
|
190
|
-
return dst_path, Plugin.from_jar(src_path)
|
|
187
|
+
def get_path(self) -> Path:
|
|
188
|
+
return path(self.path)
|
|
191
189
|
|
|
192
|
-
def get_file_for(self, plugin_id) -> Optional[Path]:
|
|
193
|
-
jar_path = self._get_dstpath(plugin_id)
|
|
194
|
-
return jar_path if jar_path.is_file() else None
|
|
195
190
|
|
|
196
|
-
|
|
197
|
-
return self.get_plugin_registry()._parsed['layout'].get('file-naming-convention', DirectoryPluginRegistry.DEFAULT_FILE_NAMING_CONVENTION)
|
|
191
|
+
PluginRegistryKind = Literal['PluginRegistry']
|
|
198
192
|
|
|
199
|
-
def get_jars(self) -> List[Path]:
|
|
200
|
-
return sorted(self.get_path().glob('*.jar'))
|
|
201
193
|
|
|
202
|
-
|
|
203
|
-
basename = dst_path.name
|
|
204
|
-
subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.get_path())
|
|
205
|
-
if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null', shell=True).returncode == 0:
|
|
206
|
-
cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
|
|
207
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
194
|
+
PluginRegistryIdentifier = str
|
|
208
195
|
|
|
209
|
-
def _get_dstpath(self, plugin_id: str) -> Path:
|
|
210
|
-
return self.get_path().joinpath(self._get_dstfile(plugin_id))
|
|
211
196
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
else:
|
|
221
|
-
raise RuntimeError(f'{self.get_plugin_registry().get_id()}: unknown file naming convention: {conv}')
|
|
197
|
+
class PluginRegistry(BaseModelWithRoot):
|
|
198
|
+
kind: PluginRegistryKind = Field(description="This object's kind")
|
|
199
|
+
id: PluginRegistryIdentifier = Field(title='Plugin Registry Identifier', description='An identifier for the plugin set')
|
|
200
|
+
name: str = Field(title='Plugin Registry Name', description='A name for the plugin set')
|
|
201
|
+
layout: PluginRegistryLayout = Field(title='Plugin Registry Layout', description='A layout for the plugin registry')
|
|
202
|
+
layers: list[PluginRegistryLayer] = Field(min_length=1, title='Plugin Registry Layers', description="A non-empty list of plugin registry layers", alias='plugin-registry-layers')
|
|
203
|
+
plugin_identifiers: list[PluginIdentifier] = Field(min_length=1, title='Plugin Identifiers', description="A non-empty list of plugin identifiers", alias='plugin-identifiers')
|
|
204
|
+
suppressed_plugin_identifiers: list[PluginIdentifier] = Field([], title='Suppressed Plugin Identifiers', description="A list of suppressed plugin identifiers", alias='suppressed-plugin-identifiers')
|
|
222
205
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
if interactive:
|
|
226
|
-
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'
|
|
227
|
-
if i != 'y':
|
|
228
|
-
return False
|
|
229
|
-
return True
|
|
206
|
+
def get_id(self) -> PluginRegistryIdentifier:
|
|
207
|
+
return self.id
|
|
230
208
|
|
|
209
|
+
def get_layer(self, layer_id: PluginRegistryLayerIdentifier) -> Optional[PluginRegistryLayer]:
|
|
210
|
+
for layer in self.get_layers():
|
|
211
|
+
if layer.get_id() == layer_id:
|
|
212
|
+
return layer
|
|
213
|
+
return None
|
|
231
214
|
|
|
232
|
-
|
|
215
|
+
def get_layer_ids(self) -> list[PluginRegistryLayerIdentifier]:
|
|
216
|
+
return [layer.get_id() for layer in self.get_layers()]
|
|
233
217
|
|
|
234
|
-
|
|
218
|
+
def get_layers(self) -> list[PluginRegistryLayer]:
|
|
219
|
+
return self.layers
|
|
235
220
|
|
|
236
|
-
def
|
|
237
|
-
|
|
221
|
+
def get_layout(self) -> BasePluginRegistryLayout:
|
|
222
|
+
return self.layout
|
|
238
223
|
|
|
239
|
-
def
|
|
240
|
-
return
|
|
224
|
+
def get_name(self) -> str:
|
|
225
|
+
return self.name
|
|
241
226
|
|
|
227
|
+
def get_plugin_identifiers(self) -> list[PluginIdentifier]:
|
|
228
|
+
return self.plugin_identifiers
|
|
242
229
|
|
|
243
|
-
|
|
230
|
+
def get_suppressed_plugin_identifiers(self) -> list[PluginIdentifier]:
|
|
231
|
+
return self.suppressed_plugin_identifiers
|
|
244
232
|
|
|
245
|
-
def
|
|
246
|
-
|
|
233
|
+
def has_plugin(self, plugin_id: PluginIdentifier) -> bool:
|
|
234
|
+
return plugin_id in self.get_plugin_identifiers()
|
|
247
235
|
|
|
248
|
-
def
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
rcs_path = self.get_path().joinpath('RCS', f'{basename},v')
|
|
252
|
-
# Maybe do co -l before the parent's copy
|
|
253
|
-
if dst_path.exists() and rcs_path.is_file():
|
|
254
|
-
cmd = ['co', '-l', basename]
|
|
255
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
256
|
-
# Do the parent's copy
|
|
257
|
-
super()._copy_jar(src_path, dst_path)
|
|
258
|
-
# Do ci -u after the parent's copy
|
|
259
|
-
cmd = ['ci', '-u', f'-mVersion {plugin.get_version()}']
|
|
260
|
-
if not rcs_path.is_file():
|
|
261
|
-
cmd.append(f'-t-{plugin.get_name()}')
|
|
262
|
-
cmd.append(basename)
|
|
263
|
-
subprocess.run(cmd, check=True, cwd=self.get_path())
|
|
236
|
+
def model_post_init(self, context: Any) -> None:
|
|
237
|
+
super().model_post_init(context)
|
|
238
|
+
self.get_layout().initialize(self)
|