lockss-turtles 0.3.0.dev2__py3-none-any.whl → 0.3.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/turtles.py DELETED
@@ -1,1109 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- __copyright__ = '''\
4
- Copyright (c) 2000-2022, Board of Trustees of Leland Stanford Jr. University
5
- '''
6
-
7
- __license__ = '''\
8
- Redistribution and use in source and binary forms, with or without
9
- modification, are permitted provided that the following conditions are met:
10
-
11
- 1. Redistributions of source code must retain the above copyright notice,
12
- this list of conditions and the following disclaimer.
13
-
14
- 2. Redistributions in binary form must reproduce the above copyright notice,
15
- this list of conditions and the following disclaimer in the documentation
16
- and/or other materials provided with the distribution.
17
-
18
- 3. Neither the name of the copyright holder nor the names of its contributors
19
- may be used to endorse or promote products derived from this software without
20
- specific prior written permission.
21
-
22
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
26
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
- POSSIBILITY OF SUCH DAMAGE.
33
- '''
34
-
35
- __version__ = '0.3.0-dev'
36
-
37
- import argparse
38
- import getpass
39
- import java_manifest
40
- import os
41
- from pathlib import Path, PurePath
42
- import shlex
43
- import subprocess
44
- import sys
45
- import tabulate
46
- import xdg
47
- import xml.etree.ElementTree as ET
48
- import yaml
49
- import zipfile
50
-
51
- PROG = 'turtles.sh'
52
-
53
- def _file_lines(path):
54
- f = None
55
- try:
56
- f = open(_path(path), 'r') if path != '-' else sys.stdin
57
- return [line for line in [line.partition('#')[0].strip() for line in f] if len(line) > 0]
58
- finally:
59
- if f is not None and path != '-':
60
- f.close()
61
-
62
- def _path(purepath_or_string):
63
- if not issubclass(type(purepath_or_string), PurePath):
64
- purepath_or_string=Path(purepath_or_string)
65
- return purepath_or_string.expanduser().resolve()
66
-
67
-
68
- class Plugin(object):
69
-
70
- @staticmethod
71
- def from_jar(jar_path):
72
- jar_path = _path(jar_path) # in case it's a string
73
- plugin_id = Plugin.id_from_jar(jar_path)
74
- plugin_fstr = str(Plugin.id_to_file(plugin_id))
75
- with zipfile.ZipFile(jar_path, 'r') as zip_file:
76
- with zip_file.open(plugin_fstr, 'r') as plugin_file:
77
- return Plugin(plugin_file, plugin_fstr)
78
-
79
- @staticmethod
80
- def from_path(path):
81
- path = _path(path) # in case it's a string
82
- with open(path, 'r') as input_file:
83
- return Plugin(input_file, path)
84
-
85
- @staticmethod
86
- def file_to_id(plugin_fstr):
87
- return plugin_fstr.replace('/', '.')[:-4] # 4 is len('.xml')
88
-
89
- @staticmethod
90
- def id_from_jar(jar_path):
91
- jar_path = _path(jar_path) # in case it's a string
92
- manifest = java_manifest.from_jar(jar_path)
93
- for entry in manifest:
94
- if entry.get('Lockss-Plugin') == 'true':
95
- name = entry.get('Name')
96
- if name is None:
97
- raise Exception(f'{jar_path!s}: Lockss-Plugin entry in META-INF/MANIFEST.MF has no Name value')
98
- return Plugin.file_to_id(name)
99
- else:
100
- raise Exception(f'{jar_path!s}: no Lockss-Plugin entry in META-INF/MANIFEST.MF')
101
-
102
- @staticmethod
103
- def id_to_dir(plugin_id):
104
- return Plugin.id_to_file(plugin_id).parent
105
-
106
- @staticmethod
107
- def id_to_file(plugin_id):
108
- return Path(f'{plugin_id.replace(".", "/")}.xml')
109
-
110
- def __init__(self, plugin_file, plugin_path):
111
- super().__init__()
112
- self._path = plugin_path
113
- self._parsed = ET.parse(plugin_file).getroot()
114
- tag = self._parsed.tag
115
- if tag != 'map':
116
- raise RuntimeError(f'{plugin_path!s}: invalid root element: {tag}')
117
-
118
- def name(self):
119
- return self._only_one('plugin_name')
120
-
121
- def identifier(self):
122
- return self._only_one('plugin_identifier')
123
-
124
- def parent_identifier(self):
125
- return self._only_one('plugin_parent')
126
-
127
- def parent_version(self):
128
- return self._only_one('plugin_parent_version', int)
129
-
130
- def version(self):
131
- return self._only_one('plugin_version', int)
132
-
133
- def _only_one(self, key, result=str):
134
- lst = [x[1].text for x in self._parsed.findall('entry') if x[0].tag == 'string' and x[0].text == key]
135
- if lst is None or len(lst) < 1:
136
- return None
137
- if len(lst) > 1:
138
- raise ValueError(f'plugin declares {len(lst)} entries for {key}')
139
- return result(lst[0])
140
-
141
-
142
- class PluginRegistry(object):
143
-
144
- KIND = 'PluginRegistry'
145
-
146
- @staticmethod
147
- def from_path(path):
148
- path = _path(path)
149
- with path.open('r') as f:
150
- return [PluginRegistry.from_yaml(parsed, path) for parsed in yaml.safe_load_all(f)]
151
-
152
- @staticmethod
153
- def from_yaml(parsed, path):
154
- kind = parsed.get('kind')
155
- if kind is None:
156
- raise RuntimeError(f'{path}: kind is not defined')
157
- elif kind != PluginRegistry.KIND:
158
- raise RuntimeError(f'{path}: not of kind {PluginRegistry.KIND}: {kind}')
159
- layout = parsed.get('layout')
160
- if layout is None:
161
- raise RuntimeError(f'{path}: layout is not defined')
162
- typ = layout.get('type')
163
- if typ is None:
164
- raise RuntimeError(f'{path}: layout type is not defined')
165
- elif typ == DirectoryPluginRegistry.LAYOUT:
166
- return DirectoryPluginRegistry(parsed)
167
- elif typ == RcsPluginRegistry.LAYOUT:
168
- return RcsPluginRegistry(parsed)
169
- else:
170
- raise RuntimeError(f'{path}: unknown layout type: {typ}')
171
-
172
- def __init__(self, parsed):
173
- super().__init__()
174
- self._parsed = parsed
175
-
176
- def get_layer(self, layerid):
177
- for layer in self.get_layers():
178
- if layer.id() == layerid:
179
- return layer
180
- return None
181
-
182
- def get_layer_ids(self):
183
- return [layer.id() for layer in self.get_layers()]
184
-
185
- def get_layers(self):
186
- return [self._make_layer(layer_elem) for layer_elem in self._parsed['layers']]
187
-
188
- def has_plugin(self, plugid):
189
- return plugid in self.plugin_identifiers()
190
-
191
- def id(self):
192
- return self._parsed['id']
193
-
194
- def layout_type(self):
195
- return self._parsed['layout']['type']
196
-
197
- def layout_options(self):
198
- return self._parsed['layout'].get('options', dict())
199
-
200
- def name(self):
201
- return self._parsed['name']
202
-
203
- def plugin_identifiers(self):
204
- return self._parsed['plugin-identifiers']
205
-
206
- def _make_layer(self, parsed):
207
- raise NotImplementedError('_make_layer')
208
-
209
-
210
- class PluginRegistryLayer(object):
211
-
212
- PRODUCTION = 'production'
213
- TESTING = 'testing'
214
-
215
- def __init__(self, plugin_registry, parsed):
216
- super().__init__()
217
- self._parsed = parsed
218
- self._plugin_registry = plugin_registry
219
-
220
- # Returns (dst_path, plugin)
221
- def deploy_plugin(self, plugin_id, jar_path, interactive=False):
222
- raise NotImplementedError('deploy_plugin')
223
-
224
- def get_file_for(self, plugin_id):
225
- raise NotImplementedError('get_file_for')
226
-
227
- def get_jars(self):
228
- raise NotImplementedError('get_jars')
229
-
230
- def id(self):
231
- return self._parsed['id']
232
-
233
- def name(self):
234
- return self._parsed['name']
235
-
236
- def path(self):
237
- return _path(self._parsed['path'])
238
-
239
- def plugin_registry(self):
240
- return self._plugin_registry
241
-
242
-
243
- class DirectoryPluginRegistry(PluginRegistry):
244
-
245
- LAYOUT = 'directory'
246
-
247
- def __init__(self, parsed):
248
- super().__init__(parsed)
249
-
250
- def _make_layer(self, parsed):
251
- return DirectoryPluginRegistryLayer(self, parsed)
252
-
253
-
254
- class DirectoryPluginRegistryLayer(PluginRegistryLayer):
255
-
256
- def __init__(self, plugin_registry, parsed):
257
- super().__init__(plugin_registry, parsed)
258
-
259
- def deploy_plugin(self, plugin_id, src_path, interactive=False):
260
- src_path = _path(src_path) # in case it's a string
261
- dst_path = self._get_dstpath(plugin_id)
262
- if not self._proceed_copy(src_path, dst_path, interactive=interactive):
263
- return None
264
- self._copy_jar(src_path, dst_path, interactive=interactive)
265
- return (dst_path, Plugin.from_jar(src_path))
266
-
267
- def get_file_for(self, plugin_id):
268
- jar_path = self._get_dstpath(plugin_id)
269
- return jar_path if jar_path.is_file() else None
270
-
271
- def get_jars(self):
272
- return sorted(self.path().glob('*.jar'))
273
-
274
- def _copy_jar(self, src_path, dst_path, interactive=False):
275
- basename = dst_path.name
276
- subprocess.run(['cp', str(src_path), str(dst_path)], check=True, cwd=self.path())
277
- if subprocess.run('command -v selinuxenabled > /dev/null && selinuxenabled && command -v chcon > /dev/null',
278
- shell=True).returncode == 0:
279
- cmd = ['chcon', '-t', 'httpd_sys_content_t', basename]
280
- subprocess.run(cmd, check=True, cwd=self.path())
281
-
282
- def _get_dstpath(self, plugin_id):
283
- return Path(self.path(), self._get_dstfile(plugin_id))
284
-
285
- def _get_dstfile(self, plugin_id):
286
- return f'{plugin_id}.jar'
287
-
288
- def _proceed_copy(self, src_path, dst_path, interactive=False):
289
- if not dst_path.exists():
290
- if interactive:
291
- i = input(
292
- f'{dst_path} does not exist in {self.plugin_registry().id()}:{self.id()} ({self.name()}); create it (y/n)? [n] ').lower() or 'n'
293
- if i != 'y':
294
- return False
295
- return True
296
-
297
-
298
- class RcsPluginRegistry(DirectoryPluginRegistry):
299
-
300
- LAYOUT = 'rcs'
301
-
302
- FULL = 'full'
303
- ABBREVIATED = 'abbreviated'
304
-
305
- def __init__(self, parsed):
306
- super().__init__(parsed)
307
-
308
- def _make_layer(self, parsed):
309
- return RcsPluginRegistryLayer(self, parsed)
310
-
311
-
312
- class RcsPluginRegistryLayer(DirectoryPluginRegistryLayer):
313
-
314
- def __init__(self, plugin_registry, parsed):
315
- super().__init__(plugin_registry, parsed)
316
-
317
- def _copy_jar(self, src_path, dst_path, interactive=False):
318
- basename = dst_path.name
319
- plugin = Plugin.from_jar(src_path)
320
- rcs_path = self.path().joinpath('RCS', f'{basename},v')
321
- # Maybe do co -l before the parent's copy
322
- if dst_path.exists() and rcs_path.is_file():
323
- cmd = ['co', '-l', basename]
324
- subprocess.run(cmd, check=True, cwd=self.path())
325
- # Do the parent's copy
326
- super()._copy_jar(src_path, dst_path)
327
- # Do ci -u after the aprent's copy
328
- cmd = ['ci', '-u', f'-mVersion {plugin.version()}']
329
- if not rcs_path.is_file():
330
- cmd.append(f'-t-{plugin.name()}')
331
- cmd.append(basename)
332
- subprocess.run(cmd, check=True, cwd=self.path())
333
-
334
- def _get_dstfile(self, plugid):
335
- conv = self.plugin_registry().layout_options().get('file-naming-convention')
336
- if conv == RcsPluginRegistry.ABBREVIATED:
337
- return f'{plugid.split(".")[-1]}.jar'
338
- elif conv == RcsPluginRegistry.FULL or conv is None:
339
- return super()._get_dstfile(plugid)
340
- else:
341
- raise RuntimeError(f'{self.plugin_registry().id()}: unknown file naming convention in layout options: {conv}')
342
-
343
-
344
- class PluginSet(object):
345
-
346
- KIND = 'PluginSet'
347
-
348
- @staticmethod
349
- def from_path(path):
350
- path = _path(path)
351
- with path.open('r') as f:
352
- return [PluginSet.from_yaml(parsed, path) for parsed in yaml.safe_load_all(f)]
353
-
354
- @staticmethod
355
- def from_yaml(parsed, path):
356
- kind = parsed.get('kind')
357
- if kind is None:
358
- raise RuntimeError(f'{path}: kind is not defined')
359
- elif kind != PluginSet.KIND:
360
- raise RuntimeError(f'{path}: not of kind {PluginSet.KIND}: {kind}')
361
- builder = parsed.get('builder')
362
- if builder is None:
363
- raise RuntimeError(f'{path}: builder is not defined')
364
- typ = builder.get('type')
365
- if typ is None:
366
- raise RuntimeError(f'{path}: builder type is not defined')
367
- elif typ == AntPluginSet.TYPE:
368
- return AntPluginSet(parsed, path)
369
- elif typ == 'mvn':
370
- return MavenPluginSet(parsed, path)
371
- else:
372
- raise RuntimeError(f'{path}: unknown builder type: {typ}')
373
-
374
- def __init__(self, parsed):
375
- super().__init__()
376
- self._parsed = parsed
377
-
378
- # Returns (jar_path, plugin)
379
- def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
380
- raise NotImplementedError('build_plugin')
381
-
382
- def builder_type(self):
383
- return self._parsed['builder']['type']
384
-
385
- def builder_options(self):
386
- return self._parsed['builder'].get('options', dict())
387
-
388
- def has_plugin(self, plugid):
389
- raise NotImplementedError('has_plugin')
390
-
391
- def id(self):
392
- return self._parsed['id']
393
-
394
- def make_plugin(self, plugid):
395
- raise NotImplementedError('make_plugin')
396
-
397
- def name(self):
398
- return self._parsed['name']
399
-
400
-
401
- class AntPluginSet(PluginSet):
402
-
403
- TYPE = 'ant'
404
-
405
- def __init__(self, parsed, path):
406
- super().__init__(parsed)
407
- self._built = False
408
- self._root = path.parent
409
-
410
- # Returns (jar_path, plugin)
411
- def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
412
- # Prerequisites
413
- if 'JAVA_HOME' not in os.environ:
414
- raise RuntimeError('error: JAVA_HOME must be set in your environment')
415
- # Big build (maybe)
416
- self._big_build()
417
- # Little build
418
- return self._little_build(plugin_id, keystore_path, keystore_alias, keystore_password=keystore_password)
419
-
420
- def has_plugin(self, plugin_id):
421
- return self._plugin_path(plugin_id).is_file()
422
-
423
- def main(self):
424
- return self._parsed.get('main', 'plugins/src')
425
-
426
- def main_path(self):
427
- return self.root_path().joinpath(self.main())
428
-
429
- def make_plugin(self, plugin_id):
430
- return Plugin.from_path(self._plugin_path(plugin_id))
431
-
432
- def root(self):
433
- return self._root
434
-
435
- def root_path(self):
436
- return Path(self.root()).expanduser().resolve()
437
-
438
- def test(self):
439
- return self._parsed.get('test', 'plugins/test/src')
440
-
441
- def test_path(self):
442
- return self.root_path().joinpath(self.test())
443
-
444
- def _big_build(self):
445
- if not self._built:
446
- # Do build
447
- subprocess.run('ant load-plugins',
448
- shell=True, cwd=self.root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
449
- self._built = True
450
-
451
- # Returns (jar_path, plugin)
452
- def _little_build(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
453
- plugin = self.make_plugin(plugin_id)
454
- # Get all directories for jarplugin -d
455
- dirs = list()
456
- cur_id = plugin_id
457
- while cur_id is not None:
458
- cur_dir = Plugin.id_to_dir(cur_id)
459
- if cur_dir not in dirs:
460
- dirs.append(cur_dir)
461
- cur_id = self.make_plugin(cur_id).parent_identifier()
462
- # Invoke jarplugin
463
- jar_fstr = Plugin.id_to_file(plugin_id)
464
- jar_path = self.root_path().joinpath('plugins/jars', f'{plugin_id}.jar')
465
- jar_path.parent.mkdir(parents=True, exist_ok=True)
466
- cmd = ['test/scripts/jarplugin',
467
- '-j', str(jar_path),
468
- '-p', str(jar_fstr)]
469
- for dir in dirs:
470
- cmd.extend(['-d', dir])
471
- subprocess.run(cmd, cwd=self.root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
472
- # Invoke signplugin
473
- cmd = ['test/scripts/signplugin',
474
- '--jar', str(jar_path),
475
- '--alias', keystore_alias,
476
- '--keystore', str(keystore_path)]
477
- if keystore_password is not None:
478
- cmd.extend(['--password', keystore_password])
479
- try:
480
- subprocess.run(cmd, cwd=self.root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
481
- except subprocess.CalledProcessError as cpe:
482
- raise self._sanitize(cpe)
483
- if not jar_path.is_file():
484
- raise FileNotFoundError(str(jar_path))
485
- return (jar_path, plugin)
486
-
487
- def _plugin_path(self, plugin_id):
488
- return Path(self.main_path()).joinpath(Plugin.id_to_file(plugin_id))
489
-
490
- def _sanitize(self, called_process_error):
491
- cmd = called_process_error.cmd[:]
492
- i = 0
493
- for i in range(len(cmd)):
494
- if i > 1 and cmd[i - 1] == '--password':
495
- cmd[i] = '<password>'
496
- called_process_error.cmd = ' '.join([shlex.quote(c) for c in cmd])
497
- return called_process_error
498
-
499
-
500
- class MavenPluginSet(PluginSet):
501
-
502
- TYPE = 'maven'
503
-
504
- def __init__(self, parsed, path):
505
- super().__init__(parsed)
506
- self._built = False
507
- self._root = path.parent
508
-
509
- # Returns (jar_path, plugin)
510
- def build_plugin(self, plugin_id, keystore_path, keystore_alias, keystore_password=None):
511
- self._big_build(keystore_path, keystore_alias, keystore_password=keystore_password)
512
- return self._little_build(plugin_id)
513
-
514
- def has_plugin(self, plugid):
515
- return self._plugin_path(plugid).is_file()
516
-
517
- def main(self):
518
- return self._parsed.get('main', 'src/main/java')
519
-
520
- def main_path(self):
521
- return self.root_path().joinpath(self.main())
522
-
523
- def make_plugin(self, plugid):
524
- return Plugin.from_path(self._plugin_path(plugid))
525
-
526
- def root(self):
527
- return self._root
528
-
529
- def root_path(self):
530
- return Path(self.root()).expanduser().resolve()
531
-
532
- def test(self):
533
- return self._parsed.get('test', 'src/test/java')
534
-
535
- def test_path(self):
536
- return self.root_path().joinpath(self.test())
537
-
538
- def _big_build(self, keystore_path, keystore_alias, keystore_password=None):
539
- if not self._built:
540
- # Do build
541
- cmd = ['mvn', 'package',
542
- f'-Dkeystore.file={keystore_path!s}',
543
- f'-Dkeystore.alias={keystore_alias}',
544
- f'-Dkeystore.password={keystore_password}']
545
- try:
546
- subprocess.run(cmd, cwd=self.root_path(), check=True, stdout=sys.stdout, stderr=sys.stderr)
547
- except subprocess.CalledProcessError as cpe:
548
- raise self._sanitize(cpe)
549
- self._built = True
550
-
551
- # Returns (jar_path, plugin)
552
- def _little_build(self, plugin_id):
553
- jar_path = Path(self.root_path(), 'target', 'pluginjars', f'{plugin_id}.jar')
554
- if not jar_path.is_file():
555
- raise Exception(f'{plugin_id}: built JAR not found: {jar_path!s}')
556
- return (jar_path, Plugin.from_jar(jar_path))
557
-
558
- def _plugin_path(self, plugin_id):
559
- return Path(self.main_path()).joinpath(Plugin.id_to_file(plugin_id))
560
-
561
- def _sanitize(self, called_process_error):
562
- cmd = called_process_error.cmd[:]
563
- i = 0
564
- for i in range(len(cmd)):
565
- if cmd[i].startswith('-Dkeystore.password='):
566
- cmd[i] = '-Dkeystore.password=<password>'
567
- called_process_error.cmd = ' '.join([shlex.quote(c) for c in cmd])
568
- return called_process_error
569
-
570
-
571
- class Turtles(object):
572
-
573
- def __init__(self):
574
- super().__init__()
575
- self._password = None
576
- self._plugin_sets = list()
577
- self._plugin_registries = list()
578
- self._settings = dict()
579
-
580
- # Returns plugin_id -> (set_id, jar_path, plugin)
581
- def build_plugin(self, plugin_ids):
582
- return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
583
-
584
- # Returns (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
585
- def deploy_plugin(self, src_paths, layer_ids, interactive=False):
586
- plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths]
587
- return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
588
- plugin_id,
589
- layer_ids,
590
- interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
591
-
592
- def load_plugin_registries(self, path):
593
- path = _path(path)
594
- if not path.is_file():
595
- raise FileNotFoundError(str(path))
596
- parsed = None
597
- with path.open('r') as f:
598
- parsed = yaml.safe_load(f)
599
- kind = parsed.get('kind')
600
- if kind is None:
601
- raise Exception(f'{path!s}: kind is not defined')
602
- elif kind != 'Settings':
603
- raise Exception(f'{path!s}: not of kind Settings: {kind}')
604
- paths = parsed.get('plugin-registries')
605
- if paths is None:
606
- raise Exception(f'{path!s}: undefined plugin-registries')
607
- self._plugin_registries = list()
608
- for p in paths:
609
- self._plugin_registries.extend(PluginRegistry.from_path(p))
610
-
611
- def load_plugin_sets(self, path):
612
- path = _path(path)
613
- if not path.is_file():
614
- raise FileNotFoundError(str(path))
615
- parsed = None
616
- with path.open('r') as f:
617
- parsed = yaml.safe_load(f)
618
- kind = parsed.get('kind')
619
- if kind is None:
620
- raise Exception(f'{path!s}: kind is not defined')
621
- elif kind != 'Settings':
622
- raise Exception(f'{path!s}: not of kind Settings: {kind}')
623
- paths = parsed.get('plugin-sets')
624
- if paths is None:
625
- raise Exception(f'{path!s}: plugin-sets is not defined')
626
- self.plugin_sets = list()
627
- for p in paths:
628
- self._plugin_sets.extend(PluginSet.from_path(p))
629
-
630
- def load_settings(self, path):
631
- path = _path(path)
632
- if not path.is_file():
633
- raise FileNotFoundError(str(path))
634
- with path.open('r') as f:
635
- parsed = yaml.safe_load(f)
636
- kind = parsed.get('kind')
637
- if kind is None:
638
- raise Exception(f'{path!s}: kind is not defined')
639
- elif kind != 'Settings':
640
- raise Exception(f'{path!s}: not of kind Settings: {kind}')
641
- self._settings = parsed
642
-
643
- # Returns plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
644
- def release_plugin(self, plugin_ids, layer_ids, interactive=False):
645
- # ... plugin_id -> (set_id, jar_path, plugin)
646
- ret1 = self.build_plugin(plugin_ids)
647
- jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
648
- # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
649
- ret2 = self.deploy_plugin(jar_paths,
650
- layer_ids,
651
- interactive=interactive)
652
- return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
653
-
654
- def set_password(self, obj):
655
- self._password = obj() if callable(obj) else obj
656
-
657
- # Returns (set_id, jar_path, plugin)
658
- def _build_one_plugin(self, plugin_id):
659
- """
660
- Returns a (plugsetid, plujarpath) tuple
661
- """
662
- for plugin_set in self._plugin_sets:
663
- if plugin_set.has_plugin(plugin_id):
664
- return (plugin_set.id(),
665
- *plugin_set.build_plugin(plugin_id,
666
- self._get_plugin_signing_keystore(),
667
- self._get_plugin_signing_alias(),
668
- self._get_plugin_signing_password()))
669
- raise Exception(f'{plugin_id}: not found in any plugin set')
670
-
671
- # Returns list of (registry_id, layer_id, dst_path, plugin)
672
- def _deploy_one_plugin(self, src_jar, plugin_id, layer_ids, interactive=False):
673
- ret = list()
674
- for plugin_registry in self._plugin_registries:
675
- if plugin_registry.has_plugin(plugin_id):
676
- for layer_id in layer_ids:
677
- layer = plugin_registry.get_layer(layer_id)
678
- if layer is not None:
679
- ret.append((plugin_registry.id(),
680
- layer.id(),
681
- *layer.deploy_plugin(plugin_id,
682
- src_jar,
683
- interactive=interactive)))
684
- if len(ret) == 0:
685
- raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
686
- return ret
687
-
688
- def _get_plugin_signing_alias(self):
689
- ret = self._settings.get('plugin-signing-alias')
690
- if ret is None:
691
- raise Exception('plugin-signing-alias is not defined in the settings')
692
- return ret
693
-
694
- def _get_plugin_signing_keystore(self):
695
- ret = self._settings.get('plugin-signing-keystore')
696
- if ret is None:
697
- raise Exception('plugin-signing-keystore is not defined in the settings')
698
- return _path(ret)
699
-
700
- def _get_plugin_signing_password(self):
701
- return self._password
702
-
703
-
704
- class TurtlesCli(Turtles):
705
-
706
- XDG_CONFIG_DIR=xdg.xdg_config_home().joinpath(PROG)
707
- GLOBAL_CONFIG_DIR=Path('/etc', PROG)
708
- CONFIG_DIRS=[XDG_CONFIG_DIR, GLOBAL_CONFIG_DIR]
709
-
710
- PLUGIN_REGISTRIES='plugin-registries.yaml'
711
- PLUGIN_SETS='plugin-sets.yaml'
712
- SETTINGS='settings.yaml'
713
-
714
- @staticmethod
715
- def _config_files(name):
716
- return [Path(base, name) for base in TurtlesCli.CONFIG_DIRS]
717
-
718
- @staticmethod
719
- def _list_config_files(name):
720
- return ' or '.join(str(x) for x in TurtlesCli._config_files(name))
721
-
722
- @staticmethod
723
- def _select_config_file(name):
724
- for x in TurtlesCli._config_files(name):
725
- if x.is_file():
726
- return x
727
- return None
728
-
729
- def __init__(self):
730
- super().__init__()
731
- self._args = None
732
- self._identifiers = None
733
- self._jars = None
734
- self._layers = None
735
- self._parser = None
736
- self._subparsers = None
737
-
738
- def run(self):
739
- self._make_parser()
740
- self._args = self._parser.parse_args()
741
- if self._args.debug_cli:
742
- print(self._args)
743
- self._args.fun()
744
-
745
- def _analyze_registry(self):
746
- # Prerequisites
747
- self.load_settings(self._args.settings or TurtlesCli._select_config_file(TurtlesCli.SETTINGS))
748
- self.load_plugin_registries(self._args.plugin_registries or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_REGISTRIES))
749
- self.load_plugin_sets(self._args.plugin_sets or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_SETS))
750
-
751
- #####
752
- title = 'Plugins declared in a plugin registry but not found in any plugin set'
753
- result = list()
754
- headers = ['Plugin registry', 'Plugin identifier']
755
- for plugin_registry in self._plugin_registries:
756
- for plugin_id in plugin_registry.plugin_identifiers():
757
- for plugin_set in self._plugin_sets:
758
- if plugin_set.has_plugin(plugin_id):
759
- break
760
- else: # No plugin set matched
761
- result.append([plugin_registry.id(), plugin_id])
762
- if len(result) > 0:
763
- self._tabulate(title, result, headers)
764
-
765
- #####
766
- title = 'Plugins declared in a plugin registry but with missing JARs'
767
- result = list()
768
- headers = ['Plugin registry', 'Plugin registry layer', 'Plugin identifier']
769
- for plugin_registry in self._plugin_registries:
770
- for plugin_id in plugin_registry.plugin_identifiers():
771
- for layer_id in plugin_registry.get_layer_ids():
772
- if plugin_registry.get_layer(layer_id).get_file_for(plugin_id) is None:
773
- result.append([plugin_registry.id(), layer_id, plugin_id])
774
- if len(result) > 0:
775
- self._tabulate(title, result, headers)
776
-
777
- #####
778
- title = 'Plugin JARs not declared in any plugin registry'
779
- result = list()
780
- headers = ['Plugin registry', 'Plugin registry layer', 'Plugin JAR', 'Plugin identifier']
781
- # Map from layer path to the layers that have that path
782
- pathlayers = dict()
783
- for plugin_registry in self._plugin_registries:
784
- for layer_id in plugin_registry.get_layer_ids():
785
- layer_id = plugin_registry.get_layer(layer_id)
786
- path = layer_id.path()
787
- pathlayers.setdefault(path, list()).append(layer_id)
788
- # Do report, taking care of not processing a path twice if overlapping
789
- visited = set()
790
- for plugin_registry in self._plugin_registries:
791
- for layer_id in plugin_registry.get_layer_ids():
792
- layer_id = plugin_registry.get_layer(layer_id)
793
- if layer_id.path() not in visited:
794
- visited.add(layer_id.path())
795
- for jar_path in layer_id.get_jars():
796
- if jar_path.stat().st_size > 0:
797
- plugin_id = Plugin.id_from_jar(jar_path)
798
- if not any([lay.plugin_registry().has_plugin(plugin_id) for lay in pathlayers[layer_id.path()]]):
799
- result.append([plugin_registry.id(), layer_id, jar_path, plugin_id])
800
- if len(result) > 0:
801
- self._tabulate(title, result, headers)
802
-
803
- def _build_plugin(self):
804
- # Prerequisites
805
- self.load_settings(self._args.settings or TurtlesCli._select_config_file(TurtlesCli.SETTINGS))
806
- self.load_plugin_sets(self._args.plugin_sets or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_SETS))
807
- self._obtain_password()
808
- # Action
809
- # ... plugin_id -> (set_id, jar_path, plugin)
810
- ret = self.build_plugin(self._get_identifiers())
811
- # Output
812
- print(tabulate.tabulate([[plugin_id, plugin.version(), set_id, jar_path] for plugin_id, (set_id, jar_path, plugin) in ret.items()],
813
- headers=['Plugin identifier', 'Plugin version', 'Plugin set', 'Plugin JAR'],
814
- tablefmt=self._args.output_format))
815
-
816
- def _copyright(self):
817
- print(__copyright__)
818
-
819
- def _deploy_plugin(self):
820
- # Prerequisites
821
- self.load_plugin_registries(self._args.plugin_registries or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_REGISTRIES))
822
- # Action
823
- # ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
824
- ret = self.deploy_plugin(self._get_jars(),
825
- self._get_layers(),
826
- interactive=self._args.interactive)
827
- # Output
828
- print(tabulate.tabulate([[src_path, plugin_id, plugin.version(), registry_id, layer_id, dst_path] for (src_path, plugin_id), val in ret.items() for registry_id, layer_id, dst_path, plugin in val],
829
- headers=['Plugin JAR', 'Plugin identifier', 'Plugin version', 'Plugin registry', 'Plugin registry layer', 'Deployed JAR'],
830
- tablefmt=self._args.output_format))
831
-
832
- def _get_identifiers(self):
833
- if self._identifiers is None:
834
- self._identifiers = list()
835
- self._identifiers.extend(self._args.remainder)
836
- self._identifiers.extend(self._args.identifier)
837
- for path in self._args.identifiers:
838
- self._identifiers.extend(_file_lines(path))
839
- if len(self._identifiers) == 0:
840
- self._parser.error('list of plugin identifiers to build is empty')
841
- return self._identifiers
842
-
843
- def _get_jars(self):
844
- if self._jars is None:
845
- self._jars = list()
846
- self._jars.extend(self._args.remainder)
847
- self._jars.extend(self._args.jar)
848
- for path in self._args.jars:
849
- self._jars.extend(_file_lines(path))
850
- if len(self._jars) == 0:
851
- self._parser.error('list of plugin JARs to deploy is empty')
852
- return self._jars
853
-
854
- def _get_layers(self):
855
- if self._layers is None:
856
- self._layers = list()
857
- self._layers.extend(self._args.layer)
858
- for path in self._args.layers:
859
- self._layers.extend(_file_lines(path))
860
- if len(self._layers) == 0:
861
- self._parser.error('list of plugin registry layers to process is empty')
862
- return self._layers
863
-
864
- def _license(self):
865
- print(__license__)
866
-
867
- def _make_option_debug_cli(self, container):
868
- container.add_argument('--debug-cli',
869
- action='store_true',
870
- help='print the result of parsing command line arguments')
871
-
872
- def _make_option_non_interactive(self, container):
873
- container.add_argument('--non-interactive', '-n',
874
- dest='interactive',
875
- action='store_false', # note: default True
876
- help='disallow interactive prompts (default: allow)')
877
-
878
- def _make_option_output_format(self, container):
879
- container.add_argument('--output-format',
880
- metavar='FMT',
881
- choices=tabulate.tabulate_formats,
882
- default='simple',
883
- help='set tabular output format to %(metavar)s (default: %(default)s; choices: %(choices)s)')
884
-
885
- def _make_option_password(self, container):
886
- container.add_argument('--password',
887
- metavar='PASS',
888
- help='set the plugin signing password')
889
-
890
- def _make_option_plugin_registries(self, container):
891
- container.add_argument('--plugin-registries',
892
- metavar='FILE',
893
- type=Path,
894
- help=f'load plugin registries from %(metavar)s (default: {TurtlesCli._list_config_files(TurtlesCli.PLUGIN_REGISTRIES)})')
895
-
896
- def _make_option_plugin_sets(self, container):
897
- container.add_argument('--plugin-sets',
898
- metavar='FILE',
899
- type=Path,
900
- help=f'load plugin sets from %(metavar)s (default: {TurtlesCli._list_config_files(TurtlesCli.PLUGIN_SETS)})')
901
-
902
- def _make_option_production(self, container):
903
- container.add_argument('--production', '-p',
904
- dest='layer',
905
- action='append_const',
906
- const=PluginRegistryLayer.PRODUCTION,
907
- help="synonym for --layer=%(const)s (i.e. add '%(const)s' to the list of plugin registry layers to process)")
908
-
909
- def _make_option_settings(self, container):
910
- container.add_argument('--settings',
911
- metavar='FILE',
912
- type=Path,
913
- help=f'load settings from %(metavar)s (default: {TurtlesCli._list_config_files(TurtlesCli.SETTINGS)})')
914
-
915
- def _make_option_testing(self, container):
916
- container.add_argument('--testing', '-t',
917
- dest='layer',
918
- action='append_const',
919
- const=PluginRegistryLayer.TESTING,
920
- help="synonym for --layer=%(const)s (i.e. add '%(const)s' to the list of plugin registry layers to process)")
921
-
922
- def _make_options_identifiers(self, container):
923
- container.add_argument('--identifier', '-i',
924
- metavar='PLUGID',
925
- action='append',
926
- default=list(),
927
- help='add %(metavar)s to the list of plugin identifiers to build')
928
- container.add_argument('--identifiers', '-I',
929
- metavar='FILE',
930
- action='append',
931
- default=list(),
932
- help='add the plugin identifiers in %(metavar)s to the list of plugin identifiers to build')
933
- container.add_argument('remainder',
934
- metavar='PLUGID',
935
- nargs='*',
936
- help='plugin identifier to build')
937
-
938
- def _make_options_jars(self, container):
939
- container.add_argument('--jar', '-j',
940
- metavar='PLUGJAR',
941
- type=Path,
942
- action='append',
943
- default=list(),
944
- help='add %(metavar)s to the list of plugin JARs to deploy')
945
- container.add_argument('--jars', '-J',
946
- metavar='FILE',
947
- action='append',
948
- default=list(),
949
- help='add the plugin JARs in %(metavar)s to the list of plugin JARs to deploy')
950
- container.add_argument('remainder',
951
- metavar='PLUGJAR',
952
- nargs='*',
953
- help='plugin JAR to deploy')
954
-
955
- def _make_options_layers(self, container):
956
- container.add_argument('--layer', '-l',
957
- metavar='LAYER',
958
- action='append',
959
- default=list(),
960
- help='add %(metavar)s to the list of plugin registry layers to process')
961
- container.add_argument('--layers', '-L',
962
- metavar='FILE',
963
- action='append',
964
- default=list(),
965
- help='add the layers in %(metavar)s to the list of plugin registry layers to process')
966
-
967
- def _make_parser(self):
968
- self._parser = argparse.ArgumentParser(prog=PROG)
969
- self._subparsers = self._parser.add_subparsers(title='commands',
970
- description="Add --help to see the command's own help message",
971
- # In subparsers, metavar is also used as the heading of the column of subcommands
972
- metavar='COMMAND',
973
- # In subparsers, help is used as the heading of the column of subcommand descriptions
974
- help='DESCRIPTION')
975
- self._make_option_debug_cli(self._parser)
976
- self._make_option_non_interactive(self._parser)
977
- self._make_option_output_format(self._parser)
978
- self._make_parser_analyze_registry(self._subparsers)
979
- self._make_parser_build_plugin(self._subparsers)
980
- self._make_parser_copyright(self._subparsers)
981
- self._make_parser_deploy_plugin(self._subparsers)
982
- self._make_parser_license(self._subparsers)
983
- self._make_parser_release_plugin(self._subparsers)
984
- self._make_parser_usage(self._subparsers)
985
- self._make_parser_version(self._subparsers)
986
-
987
- def _make_parser_analyze_registry(self, container):
988
- parser = container.add_parser('analyze-registry', aliases=['ar'],
989
- description='Analyze plugin registries',
990
- help='analyze plugin registries')
991
- parser.set_defaults(fun=self._analyze_registry)
992
- self._make_option_plugin_registries(parser)
993
- self._make_option_plugin_sets(parser)
994
- self._make_option_settings(parser)
995
-
996
- def _make_parser_build_plugin(self, container):
997
- parser = container.add_parser('build-plugin', aliases=['bp'],
998
- description='Build (package and sign) plugins',
999
- help='build (package and sign) plugins')
1000
- parser.set_defaults(fun=self._build_plugin)
1001
- self._make_options_identifiers(parser)
1002
- self._make_option_password(parser)
1003
- self._make_option_plugin_sets(parser)
1004
- self._make_option_settings(parser)
1005
-
1006
- def _make_parser_copyright(self, container):
1007
- parser = container.add_parser('copyright',
1008
- description='Show copyright and exit',
1009
- help='show copyright and exit')
1010
- parser.set_defaults(fun=self._copyright)
1011
-
1012
- def _make_parser_deploy_plugin(self, container):
1013
- parser = container.add_parser('deploy-plugin', aliases=['dp'],
1014
- description='Deploy plugins',
1015
- help='deploy plugins')
1016
- parser.set_defaults(fun=self._deploy_plugin)
1017
- self._make_options_jars(parser)
1018
- self._make_options_layers(parser)
1019
- self._make_option_plugin_registries(parser)
1020
- self._make_option_production(parser)
1021
- self._make_option_testing(parser)
1022
-
1023
- def _make_parser_license(self, container):
1024
- parser = container.add_parser('license',
1025
- description='Show license and exit',
1026
- help='show license and exit')
1027
- parser.set_defaults(fun=self._license)
1028
-
1029
- def _make_parser_release_plugin(self, container):
1030
- parser = container.add_parser('release-plugin', aliases=['rp'],
1031
- description='Release (build and deploy) plugins',
1032
- help='release (build and deploy) plugins')
1033
- parser.set_defaults(fun=self._release_plugin)
1034
- self._make_options_identifiers(parser)
1035
- self._make_options_layers(parser)
1036
- self._make_option_password(parser)
1037
- self._make_option_plugin_registries(parser)
1038
- self._make_option_plugin_sets(parser)
1039
- self._make_option_production(parser)
1040
- self._make_option_settings(parser)
1041
- self._make_option_testing(parser)
1042
-
1043
- def _make_parser_usage(self, container):
1044
- parser = container.add_parser('usage',
1045
- description='Show usage and exit',
1046
- help='show detailed usage and exit')
1047
- parser.set_defaults(fun=self._usage)
1048
-
1049
- def _make_parser_version(self, container):
1050
- parser = container.add_parser('version',
1051
- description='Show version and exit',
1052
- help='show version and exit')
1053
- parser.set_defaults(fun=self._version)
1054
-
1055
- def _obtain_password(self):
1056
- if self._args.password is not None:
1057
- _p = self._args.password
1058
- elif self._args.interactive:
1059
- _p = getpass.getpass('Plugin signing password: ')
1060
- else:
1061
- self._parser.error('no plugin signing password specified while in non-interactive mode')
1062
- self.set_password(lambda: _p)
1063
-
1064
- def _release_plugin(self):
1065
- # Prerequisites
1066
- self.load_settings(self._args.settings or TurtlesCli._select_config_file(TurtlesCli.SETTINGS))
1067
- self.load_plugin_sets(self._args.plugin_sets or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_SETS))
1068
- self.load_plugin_registries(self._args.plugin_registries or TurtlesCli._select_config_file(TurtlesCli.PLUGIN_REGISTRIES))
1069
- self._obtain_password()
1070
- # Action
1071
- # ... plugin_id -> list of (registry_id, layer_id, dst_path, plugin)
1072
- ret = self.release_plugin(self._get_identifiers(),
1073
- self._get_layers(),
1074
- interactive=self._args.interactive)
1075
- # Output
1076
- print(tabulate.tabulate([[plugin_id, plugin.version(), registry_id, layer_id, dst_path] for plugin_id, val in ret.items() for registry_id, layer_id, dst_path, plugin in val],
1077
- headers=['Plugin identifier', 'Plugin version', 'Plugin registry', 'Plugin registry layer', 'Deployed JAR'],
1078
- tablefmt=self._args.output_format))
1079
-
1080
- def _tabulate(self, title, data, headers):
1081
- print(self._title(title))
1082
- print(tabulate.tabulate(data, headers=headers, tablefmt=self._args.output_format))
1083
- print()
1084
-
1085
- def _title(self, s):
1086
- return f'{"=" * len(s)}\n{s}\n{"=" * len(s)}\n'
1087
-
1088
- def _usage(self):
1089
- self._parser.print_usage()
1090
- print()
1091
- uniq = set()
1092
- for cmd, par in self._subparsers.choices.items():
1093
- if par not in uniq:
1094
- uniq.add(par)
1095
- for s in par.format_usage().split('\n'):
1096
- usage = 'usage: '
1097
- print(f'{" " * len(usage)}{s[len(usage):]}' if s.startswith(usage) else s)
1098
-
1099
- def _version(self):
1100
- print(__version__)
1101
-
1102
- #
1103
- # Main entry point
1104
- #
1105
-
1106
- if __name__ == '__main__':
1107
- if sys.version_info < (3, 6):
1108
- sys.exit('Requires Python 3.6 or greater; currently {}'.format(sys.version))
1109
- TurtlesCli().run()