lockss-debugpanel 0.8.2__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.
@@ -0,0 +1,85 @@
1
+ =============
2
+ Release Notes
3
+ =============
4
+
5
+ -----
6
+ 0.8.2
7
+ -----
8
+
9
+ Released: 2026-02-03
10
+
11
+ * Requires Python 3.9-3.13.
12
+
13
+ -----
14
+ 0.8.1
15
+ -----
16
+
17
+ Released: 2025-08-13
18
+
19
+ * **Bug Fixes**
20
+
21
+ * Fixed bug in the processing of ``--nodes`` and ``--auids`` options.
22
+
23
+ * **Version 0.8.1-post1** (released: 2026-02-03)
24
+
25
+ * Requires Python 3.9-3.13.
26
+
27
+ -----
28
+ 0.8.0
29
+ -----
30
+
31
+ Released: 2025-07-01
32
+
33
+ * **Features**
34
+
35
+ * Now using *lockss-pybasic* and *pydantic-argparse* internally.
36
+
37
+ * **Changes**
38
+
39
+ * Bare arguments are no longer allowed and treated as node references; all node references must be specified via ``--node/-n`` or ``--nodes/-N`` options.
40
+
41
+ * The ``usage`` command has been removed.
42
+
43
+ -----
44
+ 0.7.0
45
+ -----
46
+
47
+ Released: 2023-05-02
48
+
49
+ * **Features**
50
+
51
+ * CLI improvements.
52
+
53
+ -----
54
+ 0.6.1
55
+ -----
56
+
57
+ Released: 2023-03-16
58
+
59
+ * **Bug Fixes**
60
+
61
+ * Files from ``--auids`` were appended to nodes.
62
+
63
+ -----
64
+ 0.6.0
65
+ -----
66
+
67
+ Released: 2023-03-15
68
+
69
+ * **Features**
70
+
71
+ * Now providing a Python library.
72
+
73
+ -----
74
+ 0.5.0
75
+ -----
76
+
77
+ Released: 2023-03-10
78
+
79
+ * **Features**
80
+
81
+ * Completely refactored to be in the package ``lockss.debugpanel``.
82
+
83
+ * Using Poetry to make uploadable to and installable from PyPI as `lockss-debugpanel <https://pypi.org/project/lockss-debugpanel>`_.
84
+
85
+ * Added the ``verify-files`` command.
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice,
7
+ this list of conditions and the following disclaimer.
8
+
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ 3. Neither the name of the copyright holder nor the names of its contributors
14
+ may be used to endorse or promote products derived from this software without
15
+ specific prior written permission.
16
+
17
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: lockss-debugpanel
3
+ Version: 0.8.2
4
+ Summary: Library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet
5
+ License: BSD-3-Clause
6
+ License-File: LICENSE
7
+ Author: Thib Guicherd-Callin
8
+ Author-email: thib@cs.stanford.edu
9
+ Maintainer: Thib Guicherd-Callin
10
+ Maintainer-email: thib@cs.stanford.edu
11
+ Requires-Python: >=3.9,<3.14
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Environment :: Console
14
+ Classifier: Framework :: Pydantic :: 2
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: BSD License
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Archiving
21
+ Classifier: Topic :: Utilities
22
+ Requires-Dist: lockss-pybasic (>=0.1.0,<0.2.0)
23
+ Requires-Dist: pydantic (>=2.11.0,<3.0.0)
24
+ Requires-Dist: pydantic-argparse (>=0.10.0,<0.11.0)
25
+ Requires-Dist: tabulate (>=0.9.0,<0.10.0)
26
+ Project-URL: Documentation, https://docs.lockss.org/en/latest/software/debugpanel
27
+ Project-URL: Repository, https://github.com/lockss/lockss-debugpanel
28
+ Project-URL: changelog, https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst
29
+ Project-URL: issues, https://github.com/lockss/lockss-debugpanel/issues
30
+ Description-Content-Type: text/x-rst
31
+
32
+ ==========
33
+ Debugpanel
34
+ ==========
35
+
36
+ .. |RELEASE| replace:: 0.8.2
37
+ .. |RELEASE_DATE| replace:: 2026-02-03
38
+ .. |DEBUGPANEL| replace:: **Debugpanel**
39
+
40
+ .. image:: https://assets.lockss.org/images/logos/debugpanel/debugpanel_128x128.png
41
+ :alt: Debugpanel logo
42
+ :align: right
43
+
44
+ |DEBUGPANEL| is a library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
45
+
46
+ :Latest release: |RELEASE| (|RELEASE_DATE|)
47
+ :Documentation: https://docs.lockss.org/en/latest/software/debugpanel
48
+ :Release notes: `CHANGELOG.rst <https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst>`_
49
+ :License: `LICENSE <https://github.com/lockss/lockss-debugpanel/blob/main/LICENSE>`_
50
+ :Repository: https://github.com/lockss/lockss-debugpanel
51
+ :Issues: https://github.com/lockss/lockss-debugpanel/issues
52
+
53
+ ----
54
+
55
+ Quick Start::
56
+
57
+ # Requires Python 3.9-3.13
58
+ python --version
59
+
60
+ # Install with pipx
61
+ pipx install lockss-debugpanel
62
+
63
+ # Verify installation and discover all the commands
64
+ debugpanel --help
65
+
66
+ # Reload config on lockss1.example.edu:8081
67
+ debugpanel reload-config -n lockss1.example.edu:8081
68
+
69
+ # Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
70
+ # ...First alternative: each node gets a -n
71
+ debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
72
+
73
+ # ...Second alternative: each -n can have more than argument
74
+ debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
75
+
@@ -0,0 +1,43 @@
1
+ ==========
2
+ Debugpanel
3
+ ==========
4
+
5
+ .. |RELEASE| replace:: 0.8.2
6
+ .. |RELEASE_DATE| replace:: 2026-02-03
7
+ .. |DEBUGPANEL| replace:: **Debugpanel**
8
+
9
+ .. image:: https://assets.lockss.org/images/logos/debugpanel/debugpanel_128x128.png
10
+ :alt: Debugpanel logo
11
+ :align: right
12
+
13
+ |DEBUGPANEL| is a library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
14
+
15
+ :Latest release: |RELEASE| (|RELEASE_DATE|)
16
+ :Documentation: https://docs.lockss.org/en/latest/software/debugpanel
17
+ :Release notes: `CHANGELOG.rst <https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst>`_
18
+ :License: `LICENSE <https://github.com/lockss/lockss-debugpanel/blob/main/LICENSE>`_
19
+ :Repository: https://github.com/lockss/lockss-debugpanel
20
+ :Issues: https://github.com/lockss/lockss-debugpanel/issues
21
+
22
+ ----
23
+
24
+ Quick Start::
25
+
26
+ # Requires Python 3.9-3.13
27
+ python --version
28
+
29
+ # Install with pipx
30
+ pipx install lockss-debugpanel
31
+
32
+ # Verify installation and discover all the commands
33
+ debugpanel --help
34
+
35
+ # Reload config on lockss1.example.edu:8081
36
+ debugpanel reload-config -n lockss1.example.edu:8081
37
+
38
+ # Crawl AUIDs from list.txt on lockss1.example.edu:8081 and lockss2.example.edu:8081
39
+ # ...First alternative: each node gets a -n
40
+ debugpanel crawl -A list.txt -n lockss1.example.edu:8081 -n lockss2.example.edu:8081
41
+
42
+ # ...Second alternative: each -n can have more than argument
43
+ debugpanel crawl -A list.txt -n lockss1.example.edu:8081 lockss2.example.edu:8081
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice,
7
+ # this list of conditions and the following disclaimer.
8
+ #
9
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ # this list of conditions and the following disclaimer in the documentation
11
+ # and/or other materials provided with the distribution.
12
+ #
13
+ # 3. Neither the name of the copyright holder nor the names of its contributors
14
+ # may be used to endorse or promote products derived from this software without
15
+ # specific prior written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
+ # POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ [project]
30
+ name = "lockss-debugpanel"
31
+ version = "0.8.2" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
32
+ description = "Library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet"
33
+ license = { text = "BSD-3-Clause" }
34
+ readme = "README.rst"
35
+ requires-python = ">=3.9,<3.14"
36
+ authors = [
37
+ { name = "Thib Guicherd-Callin", email = "thib@cs.stanford.edu" },
38
+ ]
39
+ maintainers = [
40
+ { name = "Thib Guicherd-Callin", email = "thib@cs.stanford.edu" }
41
+ ]
42
+ dependencies = [
43
+ "lockss-pybasic (>=0.1.0,<0.2.0)",
44
+ "pydantic (>=2.11.0,<3.0.0)",
45
+ "pydantic-argparse (>=0.10.0,<0.11.0)",
46
+ "tabulate (>=0.9.0,<0.10.0)"
47
+ ]
48
+ classifiers = [
49
+ "Development Status :: 5 - Production/Stable",
50
+ "Environment :: Console",
51
+ "Framework :: Pydantic :: 2",
52
+ "Intended Audience :: Developers",
53
+ "Intended Audience :: System Administrators",
54
+ "License :: OSI Approved :: BSD License",
55
+ "Programming Language :: Python",
56
+ "Topic :: Software Development :: Libraries",
57
+ "Topic :: System :: Archiving",
58
+ "Topic :: Utilities",
59
+ ]
60
+
61
+ [project.urls]
62
+ changelog = "https://github.com/lockss/lockss-debugpanel/blob/main/CHANGELOG.rst"
63
+ documentation = "https://docs.lockss.org/en/latest/software/debugpanel"
64
+ issues = "https://github.com/lockss/lockss-debugpanel/issues"
65
+ repository = "https://github.com/lockss/lockss-debugpanel"
66
+
67
+ [tool.poetry]
68
+ include = [
69
+ "CHANGELOG.rst",
70
+ "LICENSE",
71
+ "README.rst",
72
+ ]
73
+ packages = [
74
+ { include = "lockss", from = "src" }
75
+ ]
76
+
77
+ [tool.poetry.scripts]
78
+ debugpanel = 'lockss.debugpanel.cli:main'
79
+
80
+ [build-system]
81
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
82
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,361 @@
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
+ Library and command line tool to interact with the LOCKSS 1.x DebugPanel servlet.
33
+ """
34
+
35
+ __version__ = '0.8.2'
36
+
37
+ __copyright__ = '''
38
+ Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
39
+ '''.strip()
40
+
41
+ __license__ = __copyright__ + '\n\n' + '''
42
+ Redistribution and use in source and binary forms, with or without
43
+ modification, are permitted provided that the following conditions are met:
44
+
45
+ 1. Redistributions of source code must retain the above copyright notice,
46
+ this list of conditions and the following disclaimer.
47
+
48
+ 2. Redistributions in binary form must reproduce the above copyright notice,
49
+ this list of conditions and the following disclaimer in the documentation
50
+ and/or other materials provided with the distribution.
51
+
52
+ 3. Neither the name of the copyright holder nor the names of its contributors
53
+ may be used to endorse or promote products derived from this software without
54
+ specific prior written permission.
55
+
56
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
57
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
58
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
59
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
60
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
61
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
62
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
63
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
64
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
65
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
66
+ POSSIBILITY OF SUCH DAMAGE.
67
+ '''.strip()
68
+
69
+
70
+ from base64 import b64encode
71
+ from urllib.request import Request, urlopen
72
+ from typing import Any, Dict
73
+
74
+
75
+ type RequestUrlOpenT = Any
76
+
77
+
78
+ DEFAULT_DEPTH = 123
79
+
80
+
81
+ class Node(object):
82
+ """
83
+ Connector to a LOCKSS 1.x node.
84
+ """
85
+
86
+ DEFAULT_PROTOCOL = 'http'
87
+
88
+ def __init__(self, node_reference: str, u: str, p: str) -> None:
89
+ """
90
+ Constructs a new ``Node`` instance.
91
+
92
+ :param node_reference: A LOCKSS 1.x node reference, typically of the
93
+ form ``http://lockss.university.edu:8081``. If
94
+ no protocol is specified,
95
+ ``Node.DEFAULT_PROTOCOL`` is assumed. If a final
96
+ slash is included, it is ignored.
97
+ :type node_reference: str
98
+ :param u: The username for the given node's Web user interface.
99
+ :type u: str
100
+ :param p: The password for the given node's Web user interface.
101
+ :type p: str
102
+ """
103
+ super().__init__()
104
+ if '://' not in node_reference:
105
+ node_reference = f'{Node.DEFAULT_PROTOCOL}://{node_reference}'
106
+ if node_reference.endswith('/'):
107
+ node_reference = node_reference[:-1]
108
+ self._url: str = node_reference
109
+ self._basic: str = b64encode(f'{u}:{p}'.encode('utf-8')).decode('utf-8')
110
+
111
+ def authenticate(self, req: Request) -> None:
112
+ """
113
+ Does what is necessary to authenticate with the given ``Request``
114
+ object.
115
+
116
+ :param req: A ``Request`` instance.
117
+ :type req: Request
118
+ """
119
+ req.add_header('Authorization', f'Basic {self._basic}')
120
+
121
+ def get_url(self) -> str:
122
+ """
123
+ Returns the full URL corresponding to this node (with protocol but no
124
+ final slash).
125
+
126
+ :return: The full URL corresponding to this node.
127
+ :rtype: str
128
+ """
129
+ return self._url
130
+
131
+
132
+ def check_substance(node: Node, auid: str) -> RequestUrlOpenT:
133
+ """
134
+ Performs the DebugPanel servlet "Check Substance" operation on the given
135
+ node for the given AUID.
136
+
137
+ :param node: A ``Node`` instance.
138
+ :type node: Node
139
+ :param auid: An AUID.
140
+ :type auid: str
141
+ :return: The result of ``urllib.request.urlopen``.
142
+ :rtype: RequestUrlOpenT
143
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
144
+ """
145
+ return _auid_action(node, auid, 'Check Substance')
146
+
147
+
148
+ def crawl(node: Node, auid: str) -> RequestUrlOpenT:
149
+ """
150
+ Performs the DebugPanel servlet "Force Start Crawl" operation on the given
151
+ node for the given AUID.
152
+
153
+ :param node: A ``Node`` instance.
154
+ :type node: Node
155
+ :param auid: An AUID.
156
+ :type auid: str
157
+ :return: The result of ``urllib.request.urlopen``.
158
+ :rtype: RequestUrlOpenT
159
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
160
+ """
161
+ return _auid_action(node, auid, 'Force Start Crawl')
162
+
163
+
164
+ def crawl_plugins(node: Node) -> RequestUrlOpenT:
165
+ """
166
+ Performs the DebugPanel servlet "Crawl Plugins" operation on the given
167
+ node.
168
+
169
+ :param node: A ``Node`` instance.
170
+ :type node: Node
171
+ :return: The result of ``urllib.request.urlopen``.
172
+ :rtype: RequestUrlOpenT
173
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
174
+ """
175
+ return _node_action(node, 'Crawl Plugins')
176
+
177
+
178
+ def deep_crawl(node: Node, auid: str, depth: int=DEFAULT_DEPTH) -> RequestUrlOpenT:
179
+ """
180
+ Performs the DebugPanel servlet "Force Deep Crawl" operation on the given
181
+ node for the given AUID, with the given depth (default ``DEFAULT_DEPTH``).
182
+
183
+ :param node: A ``Node`` instance.
184
+ :type node: Node
185
+ :param auid: An AUID.
186
+ :type auid: str
187
+ :param depth: A strictly positive refetch depth.
188
+ :type auid: int
189
+ :return: The result of ``urllib.request.urlopen``.
190
+ :rtype: RequestUrlOpenT
191
+ :raises ValueError: If depth is negative or zero.
192
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
193
+ """
194
+ if depth < 1:
195
+ raise ValueError(f'depth must be a strictly positive integer, got {depth}')
196
+ return _auid_action(node, auid, 'Force Deep Crawl', depth=depth)
197
+
198
+
199
+ def disable_indexing(node: Node, auid: str) -> RequestUrlOpenT:
200
+ """
201
+ Performs the DebugPanel servlet "Disable Indexing" operation on the given
202
+ node for the given AUID.
203
+
204
+ :param node: A ``Node`` instance.
205
+ :type node: Node
206
+ :param auid: An AUID.
207
+ :type auid: str
208
+ :return: The result of ``urllib.request.urlopen``.
209
+ :rtype: RequestUrlOpenT
210
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
211
+ """
212
+ return _auid_action(node, auid, 'Disable Indexing')
213
+
214
+
215
+ def node(node_reference: str, u: str, p: str) -> Node:
216
+ """
217
+ DEPRECATED: Constructs a new ``Node`` instance (see the ``Node``
218
+ constructor); THIS FUNCTION WILL BE REMOVED IN VERSION 0.9.0.
219
+
220
+ :param node_reference: See the ``Node`` constructor.
221
+ :type node_reference: str
222
+ :param u: See the ``Node`` constructor.
223
+ :type node_reference: str
224
+ :param p: See the ``Node`` constructor.
225
+ :type node_reference: str
226
+ :return: See the ``Node`` constructor.
227
+ :rtype: Node
228
+ """
229
+ return Node(node_reference, u, p)
230
+
231
+
232
+ def poll(node: Node, auid: str) -> RequestUrlOpenT:
233
+ """
234
+ Performs the DebugPanel servlet "Start V3 Poll" operation on the given
235
+ node for the given AUID.
236
+
237
+ :param node: A ``Node`` instance.
238
+ :type node: Node
239
+ :param auid: An AUID.
240
+ :type auid: str
241
+ :return: The result of ``urllib.request.urlopen``.
242
+ :rtype: RequestUrlOpenT
243
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
244
+ """
245
+ return _auid_action(node, auid, 'Start V3 Poll')
246
+
247
+
248
+ def reindex_metadata(node: Node, auid: str) -> RequestUrlOpenT:
249
+ """
250
+ Performs the DebugPanel servlet "Force Reindex Metadata" operation on the
251
+ given node for the given AUID.
252
+
253
+ :param node: A ``Node`` instance.
254
+ :type node: Node
255
+ :param auid: An AUID.
256
+ :type auid: str
257
+ :return: The result of ``urllib.request.urlopen``.
258
+ :rtype: RequestUrlOpenT
259
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
260
+ """
261
+ return _auid_action(node, auid, 'Force Reindex Metadata')
262
+
263
+
264
+ def reload_config(node: Node) -> RequestUrlOpenT:
265
+ """
266
+ Performs the DebugPanel servlet "Reload Config" operation on the given
267
+ node.
268
+
269
+ :param node: A ``Node`` instance.
270
+ :type node: Node
271
+ :return: The result of ``urllib.request.urlopen``.
272
+ :rtype: RequestUrlOpenT
273
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
274
+ """
275
+ return _node_action(node, 'Reload Config')
276
+
277
+
278
+ def validate_files(node: Node, auid: str) -> RequestUrlOpenT:
279
+ """
280
+ Performs the DebugPanel servlet "Validate Files" operation on the given
281
+ node for the given AUID.
282
+
283
+ :param node: A ``Node`` instance.
284
+ :type node: Node
285
+ :param auid: An AUID.
286
+ :type auid: str
287
+ :return: The result of ``urllib.request.urlopen``.
288
+ :rtype: RequestUrlOpenT
289
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
290
+ """
291
+ return _auid_action(node, auid, 'Validate Files')
292
+
293
+
294
+ def _auid_action(node: Node, auid: str, action: str, **kwargs) -> RequestUrlOpenT:
295
+ """
296
+ Performs one AUID-centric action.
297
+
298
+ :param node: A ``Node`` instance.
299
+ :type node: Node
300
+ :param auid: An AUID.
301
+ :type auid: str
302
+ :param action: An AUID-oriented DebugPanel servlet action string, e.g.
303
+ ``Force Deep Crawl``.
304
+ :type action: str
305
+ :param kwargs: Key-value pairs of additional query string arguments.
306
+ :type kwargs: Dict[str, Any]
307
+ :return: The result of calling `urllib.request.urlopen`` on an appropriate
308
+ URL.
309
+ :rtype: RequestUrlOpenT
310
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
311
+ """
312
+ action_encoded = action.replace(" ", "%20")
313
+ auid_encoded = auid.replace('%', '%25').replace('|', '%7C').replace('&', '%26').replace('~', '%7E')
314
+ req = _make_request(node, f'action={action_encoded}&auid={auid_encoded}', **kwargs)
315
+ return urlopen(req)
316
+
317
+
318
+ def _make_request(node: Node, query: str, **kwargs) -> Request:
319
+ """
320
+ Constructs and authenticates an HTTP request.
321
+
322
+ :param node: A ``Node`` instance.
323
+ :type node: Node
324
+ :param query: A primary ampersand-separated query string, e.g.
325
+ ``"action=MyAction&auid=MyAuid"``.
326
+ :type query: str
327
+ :param kwargs: Key-value pairs of additional query string arguments, e.g.
328
+ ``(..., depth=99)`` to add ``"&depth=99"``.
329
+ :type kwargs: Dict[str, Any]
330
+ :return: An authenticated ``Request`` instance (before
331
+ ``urllib.request.urlopen`` is called).
332
+ :rtype: Request
333
+ """
334
+ for key, val in kwargs.items():
335
+ query = f'{query}&{key}={val}'
336
+ url = f'{node.get_url()}/DebugPanel?{query}'
337
+ req: Request = Request(url)
338
+ node.authenticate(req)
339
+ return req
340
+
341
+
342
+ def _node_action(node: Node, action: str, **kwargs) -> RequestUrlOpenT:
343
+ """
344
+ Performs one node-centric action.
345
+
346
+ :param node: A ``Node`` instance.
347
+ :type node: Node
348
+ :param action: A node-oriented DebugPanel servlet action string, e.g.
349
+ ``Reload Config``.
350
+ :type action: str
351
+ :param kwargs: Key-value pairs of additional query string arguments, e.g.
352
+ ``(..., depth=99)`` to add ``"&depth=99"``.
353
+ :type kwargs: Dict[str, Any]
354
+ :return: The result of calling `urllib.request.urlopen`` on an appropriate
355
+ URL.
356
+ :rtype: RequestUrlOpenT
357
+ :raises Exception: Whatever ``urllib.request.urlopen`` might raise.
358
+ """
359
+ action_encoded = action.replace(" ", "%20")
360
+ req = _make_request(node, f'action={action_encoded}', **kwargs)
361
+ return urlopen(req)
@@ -0,0 +1,37 @@
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
+ Entry point for the ``lockss.debugpanel`` module.
33
+ """
34
+
35
+ from .cli import main
36
+
37
+ main()
@@ -0,0 +1,379 @@
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()