lockss-debugpanel 0.8.2__tar.gz → 0.9.0.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/CHANGELOG.rst +7 -5
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/PKG-INFO +14 -11
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/README.rst +9 -4
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/pyproject.toml +5 -7
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/src/lockss/debugpanel/__init__.py +5 -33
- lockss_debugpanel-0.9.0.dev2/src/lockss/debugpanel/cli.py +400 -0
- lockss_debugpanel-0.8.2/src/lockss/debugpanel/cli.py +0 -379
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/LICENSE +0 -0
- {lockss_debugpanel-0.8.2 → lockss_debugpanel-0.9.0.dev2}/src/lockss/debugpanel/__main__.py +0 -0
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
Release Notes
|
|
3
3
|
=============
|
|
4
4
|
|
|
5
|
+
-----
|
|
6
|
+
0.9.0
|
|
7
|
+
-----
|
|
8
|
+
|
|
9
|
+
Released: NOT YET RELEASED
|
|
10
|
+
|
|
5
11
|
-----
|
|
6
12
|
0.8.2
|
|
7
13
|
-----
|
|
8
14
|
|
|
9
15
|
Released: 2026-02-03
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
* Requires Python 3.9-3.13.
|
|
12
18
|
|
|
13
19
|
-----
|
|
14
20
|
0.8.1
|
|
@@ -20,10 +26,6 @@ Released: 2025-08-13
|
|
|
20
26
|
|
|
21
27
|
* Fixed bug in the processing of ``--nodes`` and ``--auids`` options.
|
|
22
28
|
|
|
23
|
-
* **Version 0.8.1-post1** (released: 2026-02-03)
|
|
24
|
-
|
|
25
|
-
* Requires Python 3.9-3.13.
|
|
26
|
-
|
|
27
29
|
-----
|
|
28
30
|
0.8.0
|
|
29
31
|
-----
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lockss-debugpanel
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.9.0.dev2
|
|
4
|
+
Summary: Command line tool and Python library to interact with the LOCKSS 1.x DebugPanel servlet
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: Thib Guicherd-Callin
|
|
8
8
|
Author-email: thib@cs.stanford.edu
|
|
9
9
|
Maintainer: Thib Guicherd-Callin
|
|
10
10
|
Maintainer-email: thib@cs.stanford.edu
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Framework :: Pydantic :: 2
|
|
@@ -19,10 +19,8 @@ Classifier: Programming Language :: Python
|
|
|
19
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
20
20
|
Classifier: Topic :: System :: Archiving
|
|
21
21
|
Classifier: Topic :: Utilities
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist:
|
|
24
|
-
Requires-Dist: pydantic-argparse (>=0.10.0,<0.11.0)
|
|
25
|
-
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
22
|
+
Requires-Dist: click-extra (>=7.5.0,<7.6.0)
|
|
23
|
+
Requires-Dist: lockss-pybasic (==0.2.0-dev11)
|
|
26
24
|
Project-URL: Documentation, https://docs.lockss.org/en/latest/software/debugpanel
|
|
27
25
|
Project-URL: Repository, https://github.com/lockss/lockss-debugpanel
|
|
28
26
|
Project-URL: changelog, https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst
|
|
@@ -33,8 +31,8 @@ Description-Content-Type: text/x-rst
|
|
|
33
31
|
Debugpanel
|
|
34
32
|
==========
|
|
35
33
|
|
|
36
|
-
.. |RELEASE| replace:: 0.
|
|
37
|
-
.. |RELEASE_DATE| replace::
|
|
34
|
+
.. |RELEASE| replace:: 0.9.0-dev2
|
|
35
|
+
.. |RELEASE_DATE| replace:: NOT YET RELEASED
|
|
38
36
|
.. |DEBUGPANEL| replace:: **Debugpanel**
|
|
39
37
|
|
|
40
38
|
.. image:: https://assets.lockss.org/images/logos/debugpanel/debugpanel_128x128.png
|
|
@@ -67,9 +65,14 @@ Quick Start::
|
|
|
67
65
|
debugpanel reload-config -n lockss1.example.edu:8081
|
|
68
66
|
|
|
69
67
|
# Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
|
|
70
|
-
|
|
68
|
+
|
|
69
|
+
# ...First alternative: each node gets a -n option
|
|
71
70
|
debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
|
|
72
71
|
|
|
73
|
-
# ...Second alternative: each -n can have
|
|
72
|
+
# ...Second alternative: each -n option can have arguments
|
|
74
73
|
debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
|
|
75
74
|
|
|
75
|
+
# ...Third alternative: list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
|
|
76
|
+
debugpanel crawl -A list.txt -N nodes.txt
|
|
77
|
+
|
|
78
|
+
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Debugpanel
|
|
3
3
|
==========
|
|
4
4
|
|
|
5
|
-
.. |RELEASE| replace:: 0.
|
|
6
|
-
.. |RELEASE_DATE| replace::
|
|
5
|
+
.. |RELEASE| replace:: 0.9.0-dev2
|
|
6
|
+
.. |RELEASE_DATE| replace:: NOT YET RELEASED
|
|
7
7
|
.. |DEBUGPANEL| replace:: **Debugpanel**
|
|
8
8
|
|
|
9
9
|
.. image:: https://assets.lockss.org/images/logos/debugpanel/debugpanel_128x128.png
|
|
@@ -36,8 +36,13 @@ Quick Start::
|
|
|
36
36
|
debugpanel reload-config -n lockss1.example.edu:8081
|
|
37
37
|
|
|
38
38
|
# Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
|
|
39
|
-
|
|
39
|
+
|
|
40
|
+
# ...First alternative: each node gets a -n option
|
|
40
41
|
debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
|
|
41
42
|
|
|
42
|
-
# ...Second alternative: each -n can have
|
|
43
|
+
# ...Second alternative: each -n option can have arguments
|
|
43
44
|
debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
|
|
45
|
+
|
|
46
|
+
# ...Third alternative: list lockss1.example.edu:8081 and lockss2.example.edu:8081 in nodes.txt
|
|
47
|
+
debugpanel crawl -A list.txt -N nodes.txt
|
|
48
|
+
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
[project]
|
|
30
30
|
name = "lockss-debugpanel"
|
|
31
|
-
version = "0.
|
|
32
|
-
description = "
|
|
31
|
+
version = "0.9.0-dev2" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
|
|
32
|
+
description = "Command line tool and Python library to interact with the LOCKSS 1.x DebugPanel servlet"
|
|
33
33
|
license = { text = "BSD-3-Clause" }
|
|
34
34
|
readme = "README.rst"
|
|
35
|
-
requires-python = ">=3.
|
|
35
|
+
requires-python = ">=3.10,<4.0"
|
|
36
36
|
authors = [
|
|
37
37
|
{ name = "Thib Guicherd-Callin", email = "thib@cs.stanford.edu" },
|
|
38
38
|
]
|
|
@@ -40,10 +40,8 @@ maintainers = [
|
|
|
40
40
|
{ name = "Thib Guicherd-Callin", email = "thib@cs.stanford.edu" }
|
|
41
41
|
]
|
|
42
42
|
dependencies = [
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"pydantic-argparse (>=0.10.0,<0.11.0)",
|
|
46
|
-
"tabulate (>=0.9.0,<0.10.0)"
|
|
43
|
+
"click-extra (>=7.5.0,<7.6.0)",
|
|
44
|
+
"lockss-pybasic (==0.2.0-dev11)",
|
|
47
45
|
]
|
|
48
46
|
classifiers = [
|
|
49
47
|
"Development Status :: 5 - Production/Stable",
|
|
@@ -1,38 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
-
#
|
|
5
|
-
# Redistribution and use in source and binary forms, with or without
|
|
6
|
-
# modification, are permitted provided that the following conditions are met:
|
|
7
|
-
#
|
|
8
|
-
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
-
# this list of conditions and the following disclaimer.
|
|
10
|
-
#
|
|
11
|
-
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
-
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
-
# and/or other materials provided with the distribution.
|
|
14
|
-
#
|
|
15
|
-
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
-
# may be used to endorse or promote products derived from this software without
|
|
17
|
-
# specific prior written permission.
|
|
18
|
-
#
|
|
19
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
-
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
-
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
-
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
-
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
-
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
-
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
-
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
-
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
-
|
|
31
3
|
"""
|
|
32
4
|
Library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
|
|
33
5
|
"""
|
|
34
6
|
|
|
35
|
-
__version__ = '0.
|
|
7
|
+
__version__ = '0.9.0-dev2'
|
|
36
8
|
|
|
37
9
|
__copyright__ = '''
|
|
38
10
|
Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
@@ -69,7 +41,7 @@ POSSIBILITY OF SUCH DAMAGE.
|
|
|
69
41
|
|
|
70
42
|
from base64 import b64encode
|
|
71
43
|
from urllib.request import Request, urlopen
|
|
72
|
-
from typing import Any
|
|
44
|
+
from typing import Any
|
|
73
45
|
|
|
74
46
|
|
|
75
47
|
type RequestUrlOpenT = Any
|
|
@@ -303,7 +275,7 @@ def _auid_action(node: Node, auid: str, action: str, **kwargs) -> RequestUrlOpen
|
|
|
303
275
|
``Force Deep Crawl``.
|
|
304
276
|
:type action: str
|
|
305
277
|
:param kwargs: Key-value pairs of additional query string arguments.
|
|
306
|
-
:type kwargs:
|
|
278
|
+
:type kwargs: dict[str, Any]
|
|
307
279
|
:return: The result of calling `urllib.request.urlopen`` on an appropriate
|
|
308
280
|
URL.
|
|
309
281
|
:rtype: RequestUrlOpenT
|
|
@@ -326,7 +298,7 @@ def _make_request(node: Node, query: str, **kwargs) -> Request:
|
|
|
326
298
|
:type query: str
|
|
327
299
|
:param kwargs: Key-value pairs of additional query string arguments, e.g.
|
|
328
300
|
``(..., depth=99)`` to add ``"&depth=99"``.
|
|
329
|
-
:type kwargs:
|
|
301
|
+
:type kwargs: dict[str, Any]
|
|
330
302
|
:return: An authenticated ``Request`` instance (before
|
|
331
303
|
``urllib.request.urlopen`` is called).
|
|
332
304
|
:rtype: Request
|
|
@@ -350,7 +322,7 @@ def _node_action(node: Node, action: str, **kwargs) -> RequestUrlOpenT:
|
|
|
350
322
|
:type action: str
|
|
351
323
|
:param kwargs: Key-value pairs of additional query string arguments, e.g.
|
|
352
324
|
``(..., depth=99)`` to add ``"&depth=99"``.
|
|
353
|
-
:type kwargs:
|
|
325
|
+
:type kwargs: dict[str, Any]
|
|
354
326
|
:return: The result of calling `urllib.request.urlopen`` on an appropriate
|
|
355
327
|
URL.
|
|
356
328
|
:rtype: RequestUrlOpenT
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software without
|
|
17
|
+
# specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from collections.abc import Callable
|
|
36
|
+
from concurrent.futures import Executor, Future, ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
|
37
|
+
from enum import Enum
|
|
38
|
+
from itertools import chain
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Optional
|
|
41
|
+
|
|
42
|
+
from click_extra import ChoiceSource, EnumChoice, ExtraContext, HelpExtraFormatter, IntRange, Style, color_option, group, option, option_group, pass_context, password_option, progressbar, show_params_option, table_format_option
|
|
43
|
+
from click_extra.colorize import default_theme
|
|
44
|
+
from cloup.constraints import mutually_exclusive
|
|
45
|
+
|
|
46
|
+
from lockss.pybasic.cliutil import click_path
|
|
47
|
+
from lockss.pybasic.errorutil import InternalError
|
|
48
|
+
from lockss.pybasic.fileutil import file_lines, path
|
|
49
|
+
from . import Node, RequestUrlOpenT, check_substance, crawl, crawl_plugins, deep_crawl, disable_indexing, poll, reload_config, reindex_metadata, validate_files, DEFAULT_DEPTH, __copyright__, __license__, __version__
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _JobPoolType(Enum):
|
|
53
|
+
"""
|
|
54
|
+
An enum of job pool types.
|
|
55
|
+
|
|
56
|
+
See also ``_DEFAULT_JOB_POOL_TYPE``.
|
|
57
|
+
"""
|
|
58
|
+
THREAD_POOL = 'thread-pool'
|
|
59
|
+
PROCESS_POOL = 'process-pool'
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return self.value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_DEFAULT_JOB_POOL_TYPE: _JobPoolType = _JobPoolType.THREAD_POOL
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _DebugPanelCli(object):
|
|
69
|
+
|
|
70
|
+
def __init__(self):
|
|
71
|
+
super().__init__()
|
|
72
|
+
self._auids: Optional[list[str]] = None
|
|
73
|
+
self._auth: Optional[tuple[str, str]] = None
|
|
74
|
+
self._ctx: Optional[ExtraContext] = None
|
|
75
|
+
self._executor: Optional[Executor] = None
|
|
76
|
+
self._nodes: Optional[list[str]] = None
|
|
77
|
+
|
|
78
|
+
def _do_auid_command(self,
|
|
79
|
+
node_auid_func: Callable[[Node, str], RequestUrlOpenT],
|
|
80
|
+
**kwargs: dict[str, Any]) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Performs one AUID-centric command.
|
|
83
|
+
|
|
84
|
+
:param node_auid_func: A function that applies to a ``Node`` and an AUID
|
|
85
|
+
and returns what ``urllib.request.urlopen``
|
|
86
|
+
returns.
|
|
87
|
+
:type node_auid_func: ``RequestUrlOpenT``
|
|
88
|
+
:param kwargs: Keyword arguments (needed for the ``depth`` command).
|
|
89
|
+
:type kwargs: Dict[str, Any]
|
|
90
|
+
"""
|
|
91
|
+
node_objects = [Node(node, *self._auth) for node in self._nodes]
|
|
92
|
+
futures: dict[Future, tuple[str, str]] = {self._executor.submit(node_auid_func, node_object, auid, **kwargs): (node, auid) for auid in self._auids for node, node_object in zip(self._nodes, node_objects)}
|
|
93
|
+
results: dict[tuple[str, str], Any] = {}
|
|
94
|
+
with progressbar(as_completed(futures), length=len(futures), label='Progress') as bar:
|
|
95
|
+
for future in bar:
|
|
96
|
+
with future.result() as resp:
|
|
97
|
+
node_auid = futures[future]
|
|
98
|
+
try:
|
|
99
|
+
status: int = resp.status
|
|
100
|
+
reason: str = resp.reason
|
|
101
|
+
results[node_auid] = 'Requested' if status == 200 else reason
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
results[node_auid] = exc
|
|
104
|
+
self._ctx.print_table([[auid, *[results[(node, auid)] for node in self._nodes]] for auid in self._auids],
|
|
105
|
+
['AUID', *self._nodes])
|
|
106
|
+
|
|
107
|
+
def _do_node_command(self,
|
|
108
|
+
node_func: Callable[[Node], RequestUrlOpenT],
|
|
109
|
+
**kwargs: dict[str, Any]) -> None:
|
|
110
|
+
"""
|
|
111
|
+
Performs one node-centric command.
|
|
112
|
+
|
|
113
|
+
:param node_func: A function that applies to a ``Node`` and returns
|
|
114
|
+
what ``urllib.request.urlopen`` returns.
|
|
115
|
+
:type node_auid_func: ``RequestUrlOpenT``
|
|
116
|
+
:param kwargs: Keyword arguments (not currently needed by any command).
|
|
117
|
+
:type kwargs: Dict[str, Any]
|
|
118
|
+
"""
|
|
119
|
+
node_objects = [Node(node, *self._auth) for node in self._nodes]
|
|
120
|
+
futures: dict[Future, str] = {self._executor.submit(node_func, node_object, **kwargs): node for node, node_object in zip(self._nodes, node_objects)}
|
|
121
|
+
results: dict[str, Any] = {}
|
|
122
|
+
with progressbar(as_completed(futures), length=len(futures), label='Progress') as bar:
|
|
123
|
+
for future in bar:
|
|
124
|
+
with future.result() as resp:
|
|
125
|
+
node = futures[future]
|
|
126
|
+
try:
|
|
127
|
+
status: int = resp.status
|
|
128
|
+
reason: str = resp.reason
|
|
129
|
+
results[node] = 'Requested' if status == 200 else reason
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
results[node] = exc
|
|
132
|
+
self._ctx.print_table([[node, results[node]] for node in self._nodes],
|
|
133
|
+
['Node', 'Result'])
|
|
134
|
+
|
|
135
|
+
def _initialize_auid_operation(self,
|
|
136
|
+
cli_ctx: ExtraContext,
|
|
137
|
+
cli_node: tuple[str, ...],
|
|
138
|
+
cli_nodes: tuple[Path, ...],
|
|
139
|
+
cli_username: str,
|
|
140
|
+
cli_password: str,
|
|
141
|
+
cli_auid: tuple[str, ...],
|
|
142
|
+
cli_auids: tuple[Path, ...],
|
|
143
|
+
cli_pool_size: Optional[int],
|
|
144
|
+
cli_pool_type: _JobPoolType,
|
|
145
|
+
cli_process_pool: bool,
|
|
146
|
+
cli_thread_pool: bool) -> None:
|
|
147
|
+
self._ctx = self._ctx or cli_ctx
|
|
148
|
+
self._initialize_node_operation(cli_ctx,
|
|
149
|
+
cli_node,
|
|
150
|
+
cli_nodes,
|
|
151
|
+
cli_username,
|
|
152
|
+
cli_password,
|
|
153
|
+
cli_pool_size,
|
|
154
|
+
cli_pool_type,
|
|
155
|
+
cli_process_pool,
|
|
156
|
+
cli_thread_pool)
|
|
157
|
+
self._auids = [*cli_auid, *chain.from_iterable(file_lines(file_path) for file_path in cli_auids)]
|
|
158
|
+
if len(self._auids) == 0:
|
|
159
|
+
raise ValueError('The list of AUIDs to process is empty')
|
|
160
|
+
|
|
161
|
+
def _initialize_node_operation(self,
|
|
162
|
+
cli_ctx: ExtraContext,
|
|
163
|
+
cli_node: tuple[str, ...],
|
|
164
|
+
cli_nodes: tuple[Path, ...],
|
|
165
|
+
cli_username: str,
|
|
166
|
+
cli_password: str,
|
|
167
|
+
cli_pool_size: Optional[int],
|
|
168
|
+
cli_pool_type: _JobPoolType,
|
|
169
|
+
cli_process_pool: bool,
|
|
170
|
+
cli_thread_pool: bool) -> None:
|
|
171
|
+
self._ctx = self._ctx or cli_ctx
|
|
172
|
+
self._nodes = [*cli_node, *chain.from_iterable(file_lines(file_path) for file_path in cli_nodes)]
|
|
173
|
+
if len(self._nodes) == 0:
|
|
174
|
+
raise ValueError('The list of nodes to process is empty')
|
|
175
|
+
self._auth = (cli_username, cli_password)
|
|
176
|
+
if cli_process_pool:
|
|
177
|
+
cli_pool_type = _JobPoolType.PROCESS_POOL
|
|
178
|
+
elif cli_thread_pool:
|
|
179
|
+
cli_pool_type = _JobPoolType.THREAD_POOL
|
|
180
|
+
match cli_pool_type:
|
|
181
|
+
case _JobPoolType.PROCESS_POOL:
|
|
182
|
+
self._executor = ProcessPoolExecutor(max_workers=cli_pool_size)
|
|
183
|
+
case _JobPoolType.THREAD_POOL:
|
|
184
|
+
self._executor = ThreadPoolExecutor(max_workers=cli_pool_size)
|
|
185
|
+
case _:
|
|
186
|
+
raise InternalError() from ValueError(cli_pool_type)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
_node_options = option_group(
|
|
190
|
+
'Node options',
|
|
191
|
+
option('--node', '-n', metavar='NODE', multiple=True, help='Add NODE to the list of nodes to process.'),
|
|
192
|
+
option('--nodes', '-N', metavar='FILE', type=click_path('ferz'), multiple=True, help='Add the nodes in FILE to the list of nodes to process.'),
|
|
193
|
+
option('--username', '-u', metavar='USER', show_default='interactive prompt', help='Set the UI username to USER.', prompt='UI username'),
|
|
194
|
+
password_option('--password', '-p', metavar='PASS', show_default='interactive prompt', help='Set the UI password to PASS.', prompt='UI password', confirmation_prompt=False)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
_auid_options = option_group(
|
|
199
|
+
'AUID options',
|
|
200
|
+
option('--auid', '-a', metavar='AUID', multiple=True, help='Add AUID to the list of AUIDs to process.'),
|
|
201
|
+
option('--auids', '-A', metavar='FILE', type=click_path('ferz'), multiple=True, help='Add the AUIDs in FILE to the list of AUIDs to process.')
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
_pool_options = option_group(
|
|
206
|
+
'Job pool options',
|
|
207
|
+
option('--pool-size', metavar='SIZE', type=Optional[IntRange(1, None)], default=None, help='Set the job pool size to SIZE.', show_default='CPU-dependent'),
|
|
208
|
+
mutually_exclusive(
|
|
209
|
+
option('--pool-type', type=EnumChoice(choices=_JobPoolType, choice_source=ChoiceSource.VALUE), show_choices=True, default=_DEFAULT_JOB_POOL_TYPE, help=f'Set the job pool type to the given type.'),
|
|
210
|
+
option('--process-pool', is_flag=True, deprecated='Use --pool-type=process-pool instead.'),
|
|
211
|
+
option('--thread-pool', is_flag=True, deprecated='Use --pool-type=thread-pool instead.')
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
_table_format_option = table_format_option(help='Set the rendering of tables to the given style.')
|
|
217
|
+
|
|
218
|
+
@group('debugpanel', params=None,
|
|
219
|
+
context_settings=ExtraContext.settings(
|
|
220
|
+
formatter_settings=HelpExtraFormatter.settings(
|
|
221
|
+
theme=default_theme.with_(
|
|
222
|
+
invoked_command=Style(bold=True)
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
@color_option
|
|
228
|
+
@show_params_option
|
|
229
|
+
@pass_context
|
|
230
|
+
def _debugpanel(ctx: ExtraContext, **kwargs):
|
|
231
|
+
ctx.obj = _DebugPanelCli()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@_debugpanel.command('check-substance', aliases=['cs'], help='Cause nodes to check the substance of AUs.')
|
|
235
|
+
@_node_options
|
|
236
|
+
@_auid_options
|
|
237
|
+
@_pool_options
|
|
238
|
+
@_table_format_option
|
|
239
|
+
@pass_context
|
|
240
|
+
def _check_substance(ctx: ExtraContext, **kwargs) -> None:
|
|
241
|
+
cli: _DebugPanelCli = ctx.obj
|
|
242
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
243
|
+
try:
|
|
244
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
245
|
+
except ValueError as ve:
|
|
246
|
+
ctx.fail(str(ve))
|
|
247
|
+
cli._do_auid_command(check_substance)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@_debugpanel.command('copyright', help='Show the copyright then exit.')
|
|
251
|
+
def _copyright() -> None:
|
|
252
|
+
print(__copyright__)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@_debugpanel.command('crawl', aliases=['cr'], help='Cause nodes to crawl AUs.')
|
|
256
|
+
@_node_options
|
|
257
|
+
@_auid_options
|
|
258
|
+
@_pool_options
|
|
259
|
+
@table_format_option
|
|
260
|
+
@pass_context
|
|
261
|
+
def _crawl(ctx: ExtraContext, **kwargs) -> None:
|
|
262
|
+
cli: _DebugPanelCli = ctx.obj
|
|
263
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
264
|
+
try:
|
|
265
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
266
|
+
except ValueError as ve:
|
|
267
|
+
ctx.fail(str(ve))
|
|
268
|
+
cli._do_auid_command(crawl)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@_debugpanel.command('crawl-plugins', aliases=['cp'], help='Cause nodes to crawl plugins.')
|
|
272
|
+
@_node_options
|
|
273
|
+
@_pool_options
|
|
274
|
+
@table_format_option
|
|
275
|
+
@pass_context
|
|
276
|
+
def _crawl_plugins(ctx: ExtraContext, **kwargs) -> None:
|
|
277
|
+
cli: _DebugPanelCli = ctx.obj
|
|
278
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
279
|
+
try:
|
|
280
|
+
cli._initialize_node_operation(ctx, *args)
|
|
281
|
+
except ValueError as ve:
|
|
282
|
+
ctx.fail(str(ve))
|
|
283
|
+
cli._do_node_command(crawl_plugins)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@_debugpanel.command('deep-crawl', aliases=['dc'], help='Cause nodes to deep-crawl AUs.')
|
|
287
|
+
@_node_options
|
|
288
|
+
@_auid_options
|
|
289
|
+
@option_group(
|
|
290
|
+
'Depth options',
|
|
291
|
+
option('--depth', '-d', metavar='DEPTH', type=IntRange(1, None), default=DEFAULT_DEPTH, help='Set the crawl depth to DEPTH.')
|
|
292
|
+
)
|
|
293
|
+
@_pool_options
|
|
294
|
+
@table_format_option
|
|
295
|
+
@pass_context
|
|
296
|
+
def _deep_crawl(ctx: ExtraContext, **kwargs) -> None:
|
|
297
|
+
cli: _DebugPanelCli = ctx.obj
|
|
298
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
299
|
+
try:
|
|
300
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
301
|
+
except ValueError as ve:
|
|
302
|
+
ctx.fail(str(ve))
|
|
303
|
+
cli._do_auid_command(deep_crawl, depth=kwargs.get('depth'))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@_debugpanel.command('disable-indexing', aliases=['di'], help='Cause nodes to disable metadata indexing for AUs.')
|
|
307
|
+
@_node_options
|
|
308
|
+
@_auid_options
|
|
309
|
+
@_pool_options
|
|
310
|
+
@table_format_option
|
|
311
|
+
@pass_context
|
|
312
|
+
def _disable_indexing(ctx: ExtraContext, **kwargs) -> None:
|
|
313
|
+
cli: _DebugPanelCli = ctx.obj
|
|
314
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
315
|
+
try:
|
|
316
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
317
|
+
except ValueError as ve:
|
|
318
|
+
ctx.fail(str(ve))
|
|
319
|
+
cli._do_auid_command(disable_indexing)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@_debugpanel.command('license', help='Show the software license then exit.')
|
|
323
|
+
def license() -> None:
|
|
324
|
+
print(__license__)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@_debugpanel.command('poll', aliases=['po'], help='Cause nodes to poll AUs.')
|
|
328
|
+
@_node_options
|
|
329
|
+
@_auid_options
|
|
330
|
+
@_pool_options
|
|
331
|
+
@table_format_option
|
|
332
|
+
@pass_context
|
|
333
|
+
def _poll(ctx: ExtraContext, **kwargs) -> None:
|
|
334
|
+
cli: _DebugPanelCli = ctx.obj
|
|
335
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
336
|
+
try:
|
|
337
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
338
|
+
except ValueError as ve:
|
|
339
|
+
ctx.fail(str(ve))
|
|
340
|
+
cli._do_auid_command(poll)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@_debugpanel.command('reindex-metadata', aliases=['ri'], help='Cause nodes to reindex the metadata of AUs.')
|
|
344
|
+
@_node_options
|
|
345
|
+
@_auid_options
|
|
346
|
+
@_pool_options
|
|
347
|
+
@table_format_option
|
|
348
|
+
@pass_context
|
|
349
|
+
def _reindex_metadata(ctx: ExtraContext, **kwargs) -> None:
|
|
350
|
+
cli: _DebugPanelCli = ctx.obj
|
|
351
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
352
|
+
try:
|
|
353
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
354
|
+
except ValueError as ve:
|
|
355
|
+
ctx.fail(str(ve))
|
|
356
|
+
cli._do_auid_command(reindex_metadata)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@_debugpanel.command('reload-config', aliases=['rc'], help='Cause nodes to reload their configuration.')
|
|
360
|
+
@_node_options
|
|
361
|
+
@_pool_options
|
|
362
|
+
@table_format_option
|
|
363
|
+
@pass_context
|
|
364
|
+
def _reload_config(ctx: ExtraContext, **kwargs) -> None:
|
|
365
|
+
cli: _DebugPanelCli = ctx.obj
|
|
366
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
367
|
+
try:
|
|
368
|
+
cli._initialize_node_operation(ctx, *args)
|
|
369
|
+
except ValueError as ve:
|
|
370
|
+
ctx.fail(str(ve))
|
|
371
|
+
cli._do_node_command(reload_config)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@_debugpanel.command('validate-files', aliases=['vf'], help='Cause nodes to validate the files of AUs.')
|
|
375
|
+
@_node_options
|
|
376
|
+
@_auid_options
|
|
377
|
+
@_pool_options
|
|
378
|
+
@table_format_option
|
|
379
|
+
@pass_context
|
|
380
|
+
def _validate_files(ctx: ExtraContext, **kwargs) -> None:
|
|
381
|
+
cli: _DebugPanelCli = ctx.obj
|
|
382
|
+
args = [kwargs.get(k) for k in ['node', 'nodes', 'username', 'password', 'auid', 'auids', 'pool_size', 'pool_type', 'process_pool', 'thread_pool']]
|
|
383
|
+
try:
|
|
384
|
+
cli._initialize_auid_operation(ctx, *args)
|
|
385
|
+
except ValueError as ve:
|
|
386
|
+
ctx.fail(str(ve))
|
|
387
|
+
cli._do_auid_command(validate_files)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@_debugpanel.command('version', help='Show the version number then exit.')
|
|
391
|
+
def version() -> None:
|
|
392
|
+
print(__version__)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def main() -> None:
|
|
396
|
+
_debugpanel()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == '__main__':
|
|
400
|
+
main()
|
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
-
#
|
|
5
|
-
# Redistribution and use in source and binary forms, with or without
|
|
6
|
-
# modification, are permitted provided that the following conditions are met:
|
|
7
|
-
#
|
|
8
|
-
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
-
# this list of conditions and the following disclaimer.
|
|
10
|
-
#
|
|
11
|
-
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
-
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
-
# and/or other materials provided with the distribution.
|
|
14
|
-
#
|
|
15
|
-
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
-
# may be used to endorse or promote products derived from this software without
|
|
17
|
-
# specific prior written permission.
|
|
18
|
-
#
|
|
19
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
-
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
-
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
-
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
-
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
-
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
-
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
-
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
-
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
-
|
|
31
|
-
"""
|
|
32
|
-
Command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
from collections.abc import Callable
|
|
36
|
-
from concurrent.futures import Executor, Future, ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
|
37
|
-
from enum import Enum
|
|
38
|
-
from getpass import getpass
|
|
39
|
-
from itertools import chain
|
|
40
|
-
from pathlib import Path
|
|
41
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
42
|
-
|
|
43
|
-
from pydantic.v1 import BaseModel, Field, FilePath, root_validator, validator
|
|
44
|
-
from pydantic.v1.types import PositiveInt
|
|
45
|
-
from tabulate import tabulate
|
|
46
|
-
|
|
47
|
-
from lockss.pybasic.cliutil import BaseCli, StringCommand, at_most_one_from_enum, get_from_enum, COPYRIGHT_DESCRIPTION, LICENSE_DESCRIPTION, VERSION_DESCRIPTION
|
|
48
|
-
from lockss.pybasic.errorutil import InternalError
|
|
49
|
-
from lockss.pybasic.fileutil import file_lines, path
|
|
50
|
-
from lockss.pybasic.outpututil import OutputFormatOptions
|
|
51
|
-
from . import Node, RequestUrlOpenT, check_substance, crawl, crawl_plugins, deep_crawl, disable_indexing, poll, reload_config, reindex_metadata, validate_files, DEFAULT_DEPTH, __copyright__, __license__, __version__
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
class JobPool(Enum):
|
|
55
|
-
"""
|
|
56
|
-
An enum of job pool types.
|
|
57
|
-
|
|
58
|
-
See also ``DEFAULT_POOL_TYPE``.
|
|
59
|
-
"""
|
|
60
|
-
thread_pool = 'thread-pool'
|
|
61
|
-
process_pool = 'process-pool'
|
|
62
|
-
|
|
63
|
-
@staticmethod
|
|
64
|
-
def from_option(name: str) -> str:
|
|
65
|
-
"""
|
|
66
|
-
Given an option name with hyphens, return the enum constant name with
|
|
67
|
-
underscores.
|
|
68
|
-
|
|
69
|
-
:param name: An option name with hyphens.
|
|
70
|
-
:type name: str
|
|
71
|
-
:return: The corresponding enum constant name with underscores.
|
|
72
|
-
:rtype: str
|
|
73
|
-
"""
|
|
74
|
-
return JobPool(name.replace('-', '_'))
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
DEFAULT_POOL_SIZE: Optional[int] = None
|
|
78
|
-
DEFAULT_POOL_TYPE: JobPool = JobPool.thread_pool
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class NodesOptions(BaseModel):
|
|
82
|
-
"""
|
|
83
|
-
The --node/-n, --nodes/-N, --password/-p and --username/-u options.
|
|
84
|
-
"""
|
|
85
|
-
node: Optional[List[str]] = Field([], aliases=['-n'], description='(nodes) add one or more nodes to the set of nodes to process')
|
|
86
|
-
nodes: Optional[List[FilePath]] = Field([], aliases=['-N'], description='(nodes) add the nodes listed in one or more files to the set of nodes to process')
|
|
87
|
-
password: Optional[str] = Field(aliases=['-p'], description='(nodes) UI password; interactive prompt if not specified')
|
|
88
|
-
username: Optional[str] = Field(aliases=['-u'], description='(nodes) UI username; interactive prompt if not unspecified')
|
|
89
|
-
|
|
90
|
-
@validator('nodes', each_item=True, pre=True)
|
|
91
|
-
def _expand_each_nodes_path(cls, v: Path):
|
|
92
|
-
return path(v)
|
|
93
|
-
|
|
94
|
-
def get_nodes(self):
|
|
95
|
-
ret = [*self.node, *chain.from_iterable(file_lines(file_path) for file_path in self.nodes)]
|
|
96
|
-
if len(ret) == 0:
|
|
97
|
-
raise RuntimeError('empty list of nodes')
|
|
98
|
-
return ret
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
class AuidsOptions(BaseModel):
|
|
102
|
-
"""
|
|
103
|
-
The --auid/-a and --auids/-A options.
|
|
104
|
-
"""
|
|
105
|
-
auid: Optional[List[str]] = Field([], aliases=['-a'], description='(AUIDs) add one or more AUIDs to the set of AUIDs to process')
|
|
106
|
-
auids: Optional[List[FilePath]] = Field([], aliases=['-A'], description='(AUIDs) add the AUIDs listed in one or more files to the set of AUIDs to process')
|
|
107
|
-
|
|
108
|
-
@validator('auids', each_item=True, pre=True)
|
|
109
|
-
def _expand_each_auids_path(cls, v: Path):
|
|
110
|
-
return path(v)
|
|
111
|
-
|
|
112
|
-
def get_auids(self):
|
|
113
|
-
ret = [*self.auid, *chain.from_iterable(file_lines(file_path) for file_path in self.auids)]
|
|
114
|
-
if len(ret) == 0:
|
|
115
|
-
raise RuntimeError('empty list of AUIDs')
|
|
116
|
-
return ret
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
class DepthOptions(BaseModel):
|
|
120
|
-
"""
|
|
121
|
-
The --depth/-d option.
|
|
122
|
-
"""
|
|
123
|
-
depth: Optional[int] = Field(DEFAULT_DEPTH, aliases=['-d'], description='(deep crawl) set crawl depth')
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class JobPoolOptions(BaseModel):
|
|
127
|
-
"""
|
|
128
|
-
The --pool-size, --process-pool and --thread-pool options.
|
|
129
|
-
"""
|
|
130
|
-
pool_size: Optional[PositiveInt] = Field(description='(job pool) set the job pool size')
|
|
131
|
-
process_pool: Optional[bool] = Field(False, description='(job pool) use a process pool', enum=JobPool)
|
|
132
|
-
thread_pool: Optional[bool] = Field(False, description='(job pool) use a thread pool', enum=JobPool)
|
|
133
|
-
|
|
134
|
-
@root_validator
|
|
135
|
-
def _at_most_one_pool_type(cls, values):
|
|
136
|
-
return at_most_one_from_enum(cls, values, JobPool)
|
|
137
|
-
|
|
138
|
-
def get_pool_size(self) -> Optional[int]:
|
|
139
|
-
return self.pool_size if hasattr(self, 'pool_size') else DEFAULT_POOL_SIZE
|
|
140
|
-
|
|
141
|
-
def get_pool_type(self) -> JobPool:
|
|
142
|
-
return get_from_enum(self, JobPool, DEFAULT_POOL_TYPE)
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
class NodeCommand(OutputFormatOptions, JobPoolOptions, NodesOptions):
|
|
146
|
-
"""
|
|
147
|
-
A pydantic-argparse command for node commands.
|
|
148
|
-
"""
|
|
149
|
-
pass
|
|
150
|
-
|
|
151
|
-
class AuidCommand(NodeCommand, OutputFormatOptions, JobPoolOptions, AuidsOptions, NodesOptions):
|
|
152
|
-
"""
|
|
153
|
-
A pydantic-argparse command for AUID commands except deep-crawl.
|
|
154
|
-
"""
|
|
155
|
-
pass
|
|
156
|
-
|
|
157
|
-
class DeepCrawlCommand(AuidCommand, OutputFormatOptions, JobPoolOptions, DepthOptions, AuidsOptions, NodesOptions):
|
|
158
|
-
"""
|
|
159
|
-
A pydantic-argparse command for deep-crawl.
|
|
160
|
-
"""
|
|
161
|
-
pass
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class DebugPanelCommand(BaseModel):
|
|
165
|
-
"""
|
|
166
|
-
The pydantic-argparse model for the top-level debugpanel command.
|
|
167
|
-
"""
|
|
168
|
-
check_substance: Optional[AuidCommand] = Field(description='cause nodes to check the substance of AUs', alias='check-substance')
|
|
169
|
-
copyright: Optional[StringCommand.type(__copyright__)] = Field(description=COPYRIGHT_DESCRIPTION)
|
|
170
|
-
cp: Optional[NodeCommand] = Field(description='synonym for: crawl-plugins')
|
|
171
|
-
cr: Optional[AuidCommand] = Field(description='synonym for: crawl')
|
|
172
|
-
crawl: Optional[AuidCommand] = Field(description='cause nodes to crawl AUs')
|
|
173
|
-
crawl_plugins: Optional[NodeCommand] = Field(description='cause nodes to crawl plugins', alias='crawl-plugins')
|
|
174
|
-
cs: Optional[AuidCommand] = Field(description='synonym for: check-substance')
|
|
175
|
-
dc: Optional[DeepCrawlCommand] = Field(description='synonym for: deep-crawl')
|
|
176
|
-
deep_crawl: Optional[DeepCrawlCommand] = Field(description='cause nodes to deeply crawl AUs', alias='deep-crawl')
|
|
177
|
-
di: Optional[AuidCommand] = Field(description='synonym for: disable-indexing')
|
|
178
|
-
disable_indexing: Optional[AuidCommand] = Field(description='cause nodes to disable metadata indexing for AUs', alias='disable-indexing')
|
|
179
|
-
license: Optional[StringCommand.type(__license__)] = Field(description=LICENSE_DESCRIPTION)
|
|
180
|
-
po: Optional[AuidCommand] = Field(description='synonym for: poll')
|
|
181
|
-
poll: Optional[AuidCommand] = Field(description='cause nodes to poll AUs')
|
|
182
|
-
rc: Optional[NodeCommand] = Field(description='synonym for: reload-config')
|
|
183
|
-
reindex_metadata: Optional[AuidCommand] = Field(description='cause nodes to reindex the metadata of AUs', alias='reindex-metadata')
|
|
184
|
-
reload_config: Optional[NodeCommand] = Field(description='cause nodes to reload their configuration', alias='reload-config')
|
|
185
|
-
ri: Optional[AuidCommand] = Field(description='synonym for: reindex-metadata')
|
|
186
|
-
validate_files: Optional[AuidCommand] = Field(description='cause nodes to validate the files of AUs', alias='validate-files')
|
|
187
|
-
version: Optional[StringCommand.type(__version__)] = Field(description=VERSION_DESCRIPTION)
|
|
188
|
-
vf: Optional[AuidCommand] = Field(description='synonym for: validate-files')
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
class DebugPanelCli(BaseCli[DebugPanelCommand]):
|
|
192
|
-
"""
|
|
193
|
-
The debugpanel command line tool.
|
|
194
|
-
"""
|
|
195
|
-
|
|
196
|
-
def __init__(self):
|
|
197
|
-
"""
|
|
198
|
-
Constructs a new ``DebugPanelCli`` instance.
|
|
199
|
-
"""
|
|
200
|
-
super().__init__(model=DebugPanelCommand,
|
|
201
|
-
prog='debugpanel',
|
|
202
|
-
description='Tool to interact with the LOCKSS 1.x DebugPanel servlet')
|
|
203
|
-
self._auids: Optional[List[str]] = None
|
|
204
|
-
self._auth: Optional[Any] = None
|
|
205
|
-
self._executor: Optional[Executor] = None
|
|
206
|
-
self._nodes: Optional[List[str]] = None
|
|
207
|
-
|
|
208
|
-
def _check_substance(self, auid_command: AuidCommand) -> None:
|
|
209
|
-
self._do_auid_command(auid_command, check_substance)
|
|
210
|
-
|
|
211
|
-
def _copyright(self, string_command: StringCommand) -> None:
|
|
212
|
-
self._do_string_command(string_command)
|
|
213
|
-
|
|
214
|
-
def _cp(self, node_command: NodeCommand) -> None:
|
|
215
|
-
self._crawl_plugins(node_command)
|
|
216
|
-
|
|
217
|
-
def _cr(self, auid_command: AuidCommand) -> None:
|
|
218
|
-
self._crawl(auid_command)
|
|
219
|
-
|
|
220
|
-
def _crawl(self, auid_command: AuidCommand) -> None:
|
|
221
|
-
self._do_auid_command(auid_command, crawl)
|
|
222
|
-
|
|
223
|
-
def _crawl_plugins(self, node_command: NodeCommand) -> None:
|
|
224
|
-
self._do_node_command(node_command, crawl_plugins)
|
|
225
|
-
|
|
226
|
-
def _cs(self, auid_command: AuidCommand) -> None:
|
|
227
|
-
self._check_substance(auid_command)
|
|
228
|
-
|
|
229
|
-
def _dc(self, deep_crawl_command: DeepCrawlCommand) -> None:
|
|
230
|
-
self._deep_crawl(deep_crawl_command)
|
|
231
|
-
|
|
232
|
-
def _deep_crawl(self, deep_crawl_command: DeepCrawlCommand) -> None:
|
|
233
|
-
self._do_auid_command(deep_crawl_command, deep_crawl, depth=deep_crawl_command.depth)
|
|
234
|
-
|
|
235
|
-
def _di(self, auid_command: AuidCommand) -> None:
|
|
236
|
-
self._disable_indexing(auid_command)
|
|
237
|
-
|
|
238
|
-
def _disable_indexing(self, auid_command: AuidCommand) -> None:
|
|
239
|
-
self._do_auid_command(auid_command, disable_indexing)
|
|
240
|
-
|
|
241
|
-
def _do_auid_command(self, auid_command: AuidCommand, node_auid_func: Callable[[Node, str], RequestUrlOpenT], **kwargs: Dict[str, Any]) -> None:
|
|
242
|
-
"""
|
|
243
|
-
Performs one AUID-centric command.
|
|
244
|
-
|
|
245
|
-
:param auid_command: An ``AuidCommand`` model.
|
|
246
|
-
:type auid_command: AuidCommand
|
|
247
|
-
:param node_auid_func: A function that applies to a ``Node`` and an AUID
|
|
248
|
-
and returns what ``urllib.request.urlopen``
|
|
249
|
-
returns.
|
|
250
|
-
:type node_auid_func: ``RequestUrlOpenT``
|
|
251
|
-
:param kwargs: Keyword arguments (needed for the ``depth`` command).
|
|
252
|
-
:type kwargs: Dict[str, Any]
|
|
253
|
-
"""
|
|
254
|
-
self._initialize_auth(auid_command)
|
|
255
|
-
self._initialize_executor(auid_command)
|
|
256
|
-
self._nodes = auid_command.get_nodes()
|
|
257
|
-
self._auids = auid_command.get_auids()
|
|
258
|
-
node_objects = [Node(node, *self._auth) for node in self._nodes]
|
|
259
|
-
futures: Dict[Future, Tuple[str, str]] = {self._executor.submit(node_auid_func, node_object, auid, **kwargs): (node, auid) for auid in self._auids for node, node_object in zip(self._nodes, node_objects)}
|
|
260
|
-
results: Dict[Tuple[str, str], Any] = {}
|
|
261
|
-
for future in as_completed(futures):
|
|
262
|
-
node_auid = futures[future]
|
|
263
|
-
try:
|
|
264
|
-
resp: RequestUrlOpenT = future.result()
|
|
265
|
-
status: int = resp.status
|
|
266
|
-
reason: str = resp.reason
|
|
267
|
-
results[node_auid] = 'Requested' if status == 200 else reason
|
|
268
|
-
except Exception as exc:
|
|
269
|
-
results[node_auid] = exc
|
|
270
|
-
print(tabulate([[auid, *[results[(node, auid)] for node in self._nodes]] for auid in self._auids],
|
|
271
|
-
headers=['AUID', *self._nodes],
|
|
272
|
-
tablefmt=auid_command.output_format))
|
|
273
|
-
|
|
274
|
-
def _do_node_command(self, node_command: NodeCommand, node_func: Callable[[Node], RequestUrlOpenT], **kwargs: Dict[str, Any]) -> None:
|
|
275
|
-
"""
|
|
276
|
-
Performs one node-centric command.
|
|
277
|
-
|
|
278
|
-
:param node_command: A ``NodeCommand`` model.
|
|
279
|
-
:type auid_command: NodeCommand
|
|
280
|
-
:param node_func: A function that applies to a ``Node`` and returns
|
|
281
|
-
what ``urllib.request.urlopen`` returns.
|
|
282
|
-
:type node_auid_func: ``RequestUrlOpenT``
|
|
283
|
-
:param kwargs: Keyword arguments (not currently needed by any command).
|
|
284
|
-
:type kwargs: Dict[str, Any]
|
|
285
|
-
"""
|
|
286
|
-
self._initialize_auth(node_command)
|
|
287
|
-
self._initialize_executor(node_command)
|
|
288
|
-
self._nodes = node_command.get_nodes()
|
|
289
|
-
node_objects = [Node(node, *self._auth) for node in self._nodes]
|
|
290
|
-
futures: Dict[Future, str] = {self._executor.submit(node_func, node_object, **kwargs): node for node, node_object in zip(self._nodes, node_objects)}
|
|
291
|
-
results: Dict[str, Any] = {}
|
|
292
|
-
for future in as_completed(futures):
|
|
293
|
-
node = futures[future]
|
|
294
|
-
try:
|
|
295
|
-
resp: RequestUrlOpenT = future.result()
|
|
296
|
-
status: int = resp.status
|
|
297
|
-
reason: str = resp.reason
|
|
298
|
-
results[node] = 'Requested' if status == 200 else reason
|
|
299
|
-
except Exception as exc:
|
|
300
|
-
results[node] = exc
|
|
301
|
-
print(tabulate([[node, results[node]] for node in self._nodes],
|
|
302
|
-
headers=['Node', 'Result'],
|
|
303
|
-
tablefmt=node_command.output_format))
|
|
304
|
-
|
|
305
|
-
def _do_string_command(self, string_command: StringCommand) -> None:
|
|
306
|
-
"""
|
|
307
|
-
Performs one string command.
|
|
308
|
-
|
|
309
|
-
:param string_command: A ``StringCommand`` model.
|
|
310
|
-
:type auid_command: StringCommand
|
|
311
|
-
"""
|
|
312
|
-
string_command()
|
|
313
|
-
|
|
314
|
-
def _initialize_auth(self, nodes_options: NodesOptions) -> None:
|
|
315
|
-
"""
|
|
316
|
-
Computes the ``self._auth`` value, possibly after asking for interactive
|
|
317
|
-
input.
|
|
318
|
-
|
|
319
|
-
:param nodes_options: A ``NodesOptions`` model.
|
|
320
|
-
:type node_options: ``NodesOptions``
|
|
321
|
-
"""
|
|
322
|
-
_u = nodes_options.username or input('UI username: ')
|
|
323
|
-
_p = nodes_options.password or getpass('UI password: ')
|
|
324
|
-
self._auth = (_u, _p)
|
|
325
|
-
|
|
326
|
-
def _initialize_executor(self, job_pool_options: JobPoolOptions) -> None:
|
|
327
|
-
"""
|
|
328
|
-
Initializes the ``Executor``.
|
|
329
|
-
|
|
330
|
-
:param job_pool_options: A ``JobPoolOptions`` model.
|
|
331
|
-
:type job_pool_options: ``JobPoolOptions``.
|
|
332
|
-
"""
|
|
333
|
-
if job_pool_options.get_pool_type() == JobPool.thread_pool:
|
|
334
|
-
self._executor = ThreadPoolExecutor(max_workers=job_pool_options.get_pool_size())
|
|
335
|
-
elif job_pool_options.get_pool_type() == JobPool.process_pool:
|
|
336
|
-
self._executor = ProcessPoolExecutor(max_workers=job_pool_options.get_pool_size())
|
|
337
|
-
else:
|
|
338
|
-
raise InternalError()
|
|
339
|
-
|
|
340
|
-
def _license(self, string_command: StringCommand) -> None:
|
|
341
|
-
self._do_string_command(string_command)
|
|
342
|
-
|
|
343
|
-
def _po(self, auid_command: AuidCommand) -> None:
|
|
344
|
-
self._poll(auid_command)
|
|
345
|
-
|
|
346
|
-
def _poll(self, auid_command: AuidCommand) -> None:
|
|
347
|
-
self._do_auid_command(auid_command, poll)
|
|
348
|
-
|
|
349
|
-
def _rc(self, node_command: NodeCommand):
|
|
350
|
-
self._reload_config(node_command)
|
|
351
|
-
|
|
352
|
-
def _ri(self, auid_command: AuidCommand) -> None:
|
|
353
|
-
self._reindex_metadata(auid_command)
|
|
354
|
-
|
|
355
|
-
def _reindex_metadata(self, auid_command: AuidCommand) -> None:
|
|
356
|
-
self._do_auid_command(auid_command, reindex_metadata)
|
|
357
|
-
|
|
358
|
-
def _reload_config(self, node_command: NodeCommand):
|
|
359
|
-
self._do_node_command(node_command, reload_config)
|
|
360
|
-
|
|
361
|
-
def _validate_files(self, auid_command: AuidCommand) -> None:
|
|
362
|
-
self._do_auid_command(auid_command, validate_files)
|
|
363
|
-
|
|
364
|
-
def _vf(self, auid_command: AuidCommand) -> None:
|
|
365
|
-
self._validate_files(auid_command)
|
|
366
|
-
|
|
367
|
-
def _version(self, string_command: StringCommand) -> None:
|
|
368
|
-
self._do_string_command(string_command)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
def main() -> None:
|
|
372
|
-
"""
|
|
373
|
-
Entry point for the debugpanel command line tool.
|
|
374
|
-
"""
|
|
375
|
-
DebugPanelCli().run()
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if __name__ == '__main__':
|
|
379
|
-
main()
|
|
File without changes
|
|
File without changes
|