python-rac 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyrac/__init__.py +4 -0
- pyrac/__main__.py +4 -0
- pyrac/cli.py +921 -0
- pyrac/client.py +2382 -0
- pyrac/desktop.py +5003 -0
- pyrac/legacy.py +8 -0
- pyrac/mcp_server.py +3845 -0
- pyrac/protocol/__init__.py +0 -0
- pyrac/protocol/v851.py +32 -0
- pyrac/protocol/v851_client.py +3453 -0
- pyrac/read_service.py +831 -0
- pyrac/v8311.py +1400 -0
- pyrac/web/__init__.py +0 -0
- pyrac/web/__main__.py +355 -0
- pyrac/web/app.py +2835 -0
- pyrac/web/workspace.py +1905 -0
- python_rac-0.2.0.dist-info/METADATA +843 -0
- python_rac-0.2.0.dist-info/RECORD +22 -0
- python_rac-0.2.0.dist-info/WHEEL +5 -0
- python_rac-0.2.0.dist-info/entry_points.txt +5 -0
- python_rac-0.2.0.dist-info/licenses/LICENSE +21 -0
- python_rac-0.2.0.dist-info/top_level.txt +1 -0
pyrac/web/workspace.py
ADDED
|
@@ -0,0 +1,1905 @@
|
|
|
1
|
+
"""Local multi-RAS workspace. Endpoints live in memory; no credentials are stored."""
|
|
2
|
+
import html
|
|
3
|
+
import hmac
|
|
4
|
+
import re
|
|
5
|
+
import asyncio
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
from urllib.parse import parse_qs, quote, unquote_to_bytes, urlencode, urlsplit
|
|
8
|
+
|
|
9
|
+
from ..client import RacClient
|
|
10
|
+
from ..read_service import PROFILE_ACL_LISTS
|
|
11
|
+
from .app import create_app, render_document
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _search_text(value):
|
|
15
|
+
if not isinstance(value, str) or '\x00' in value:
|
|
16
|
+
return False
|
|
17
|
+
try:
|
|
18
|
+
return len(value.encode('utf-8')) <= 1024
|
|
19
|
+
except UnicodeError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
_ENDPOINT = re.compile(r'^/ras/([0-9]+)/?(.*)$')
|
|
23
|
+
_LOOPBACK = re.compile(r'^(localhost|127\.0\.0\.1|\[::1\])(?::[0-9]{1,5})?$')
|
|
24
|
+
_HOST = re.compile(r'^[A-Za-z0-9][A-Za-z0-9.-]{0,252}$')
|
|
25
|
+
_RAS_SERVER = re.compile(r'([A-Za-z0-9][A-Za-z0-9.-]{0,252}):([0-9]+)/(?:(16\.0)|(10\.0)|(auto))\Z')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_ras_server(spec):
|
|
29
|
+
"""Parse a startup workspace endpoint without connecting to it."""
|
|
30
|
+
match = _RAS_SERVER.fullmatch(spec)
|
|
31
|
+
if match is None or not 1 <= int(match.group(2)) <= 65535:
|
|
32
|
+
raise ValueError('Invalid RAS endpoint (expected HOST:PORT/16.0|10.0|auto)')
|
|
33
|
+
return match.group(1), int(match.group(2)), next(part for part in match.groups()[2:] if part)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def startup_endpoints(host, port, protocol, specs):
|
|
37
|
+
endpoints = [(host, port, protocol)]
|
|
38
|
+
endpoints.extend(parse_ras_server(spec) for spec in specs)
|
|
39
|
+
if len(endpoints) > 64 or len(set(endpoints)) != len(endpoints):
|
|
40
|
+
raise ValueError('Invalid RAS endpoint list (maximum 64 unique servers)')
|
|
41
|
+
return endpoints
|
|
42
|
+
SEARCH_ENDPOINT_TIMEOUT = 30.0
|
|
43
|
+
SEARCH_CLUSTER_GROUPS = {
|
|
44
|
+
'connections': ('connection', True), 'managers': ('manager', True),
|
|
45
|
+
'limits': ('name', False), 'counters': ('name', False),
|
|
46
|
+
}
|
|
47
|
+
SEARCH_SERVER_GROUPS = {'rules': ('list_rules', 'rule', 'object-type'),
|
|
48
|
+
'service-settings': ('list_service_settings', 'setting', 'service-name')}
|
|
49
|
+
SEARCH_PROFILE_ACL = {'profile-directories': ('directory', 'alias'),
|
|
50
|
+
**{'profile-' + kind: (kind, 'name')
|
|
51
|
+
for kind in PROFILE_ACL_LISTS if kind != 'directory'}}
|
|
52
|
+
SEARCH_KIND_NAMES = {
|
|
53
|
+
'clusters': 'Кластеры', 'infobases': 'Базы данных',
|
|
54
|
+
'agent-admins': 'Администраторы агента',
|
|
55
|
+
'cluster-admins': 'Администраторы кластера',
|
|
56
|
+
'profile-directories': 'Каталоги профиля',
|
|
57
|
+
**{'profile-' + kind: 'Профиль: ' + label for kind, label in {
|
|
58
|
+
'com': 'COM', 'addin': 'внешние компоненты', 'module': 'модули',
|
|
59
|
+
'app': 'приложения', 'inet': 'Интернет',
|
|
60
|
+
}.items()},
|
|
61
|
+
}
|
|
62
|
+
SECTION_NAMES = {
|
|
63
|
+
'servers': 'Рабочие серверы', 'processes': 'Рабочие процессы',
|
|
64
|
+
'infobases': 'Базы данных', 'sessions': 'Сеансы',
|
|
65
|
+
'connections': 'Соединения', 'profiles': 'Профили безопасности',
|
|
66
|
+
'counters': 'Счётчики', 'limits': 'Ограничения', 'managers': 'Менеджеры',
|
|
67
|
+
'services': 'Сервисы', 'locks': 'Блокировки', 'rules': 'Правила',
|
|
68
|
+
'service-settings': 'Настройки сервисов', 'transfer-directories': 'Каталоги переноса',
|
|
69
|
+
'summary': 'Краткая информация', 'info': 'Сведения',
|
|
70
|
+
'binary-storages': 'Двоичные хранилища',
|
|
71
|
+
'directory': 'Каталоги', 'com': 'COM', 'addin': 'Внешние компоненты',
|
|
72
|
+
'module': 'Модули', 'app': 'Приложения', 'inet': 'Интернет',
|
|
73
|
+
}
|
|
74
|
+
INSPECTOR_SCRIPT = '''"use strict";
|
|
75
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
76
|
+
const panel = document.querySelector(".console-inspector");
|
|
77
|
+
if (!panel) return;
|
|
78
|
+
const fields = [["RAS", "ras"], ["Protocol", "protocol"], ["Type", "type"],
|
|
79
|
+
["Object", "object"]];
|
|
80
|
+
function select(row) {
|
|
81
|
+
document.querySelectorAll(".selectable-row, [data-searchable-row]").forEach(item => {
|
|
82
|
+
item.classList.toggle("is-selected", item === row);
|
|
83
|
+
item.setAttribute("aria-selected", item === row ? "true" : "false");
|
|
84
|
+
});
|
|
85
|
+
panel.replaceChildren();
|
|
86
|
+
const title = document.createElement("h2");
|
|
87
|
+
title.textContent = "Свойства";
|
|
88
|
+
panel.append(title);
|
|
89
|
+
const properties = document.createElement("dl");
|
|
90
|
+
const visible = row.matches("[data-searchable-row]") && !row.hasAttribute("data-inspect-type")
|
|
91
|
+
? Array.from(row.querySelectorAll("td")).map((cell, index) => [
|
|
92
|
+
row.closest("table").querySelectorAll("thead th")[index]?.textContent || "Field",
|
|
93
|
+
cell.textContent
|
|
94
|
+
]).filter(([label]) => label !== "Actions")
|
|
95
|
+
: fields.map(([label, key]) => [label,
|
|
96
|
+
row.dataset["inspect" + key[0].toUpperCase() + key.slice(1)] || "—"]);
|
|
97
|
+
visible.forEach(([label, text]) => {
|
|
98
|
+
const term = document.createElement("dt");
|
|
99
|
+
const value = document.createElement("dd");
|
|
100
|
+
term.textContent = label;
|
|
101
|
+
value.textContent = text;
|
|
102
|
+
properties.append(term, value);
|
|
103
|
+
});
|
|
104
|
+
panel.append(properties);
|
|
105
|
+
}
|
|
106
|
+
document.querySelectorAll(".selectable-row, [data-searchable-row]").forEach(row => {
|
|
107
|
+
if (row.matches("[data-searchable-row]")) {
|
|
108
|
+
row.classList.add("selectable-row");
|
|
109
|
+
row.tabIndex = 0;
|
|
110
|
+
row.setAttribute("aria-selected", "false");
|
|
111
|
+
}
|
|
112
|
+
row.addEventListener("click", event => {
|
|
113
|
+
if (!event.target.closest("a,button")) select(row);
|
|
114
|
+
});
|
|
115
|
+
row.addEventListener("keydown", event => {
|
|
116
|
+
if (event.target !== row) return;
|
|
117
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
118
|
+
event.preventDefault();
|
|
119
|
+
select(row);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
'''
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def create_workspace_app(endpoints, *, csrf_token):
|
|
128
|
+
"""Wrap the read-only dashboard without changing its mutation guards."""
|
|
129
|
+
initial = list(endpoints)
|
|
130
|
+
if not initial or not isinstance(csrf_token, str) or len(csrf_token) < 16:
|
|
131
|
+
raise ValueError('Workspace needs an endpoint and CSRF token')
|
|
132
|
+
if len(initial) > 64 or any(
|
|
133
|
+
not isinstance(endpoint, (tuple, list)) or len(endpoint) != 3 or
|
|
134
|
+
not isinstance(endpoint[0], str) or not _HOST.fullmatch(endpoint[0]) or
|
|
135
|
+
type(endpoint[1]) is not int or not 1 <= endpoint[1] <= 65535 or
|
|
136
|
+
endpoint[2] not in ('16.0', '10.0', 'auto') for endpoint in initial):
|
|
137
|
+
raise ValueError('Invalid initial RAS endpoint')
|
|
138
|
+
servers = dict(enumerate(initial))
|
|
139
|
+
next_index = len(servers)
|
|
140
|
+
|
|
141
|
+
def safe_tree_name(value, fallback):
|
|
142
|
+
if not isinstance(value, str) or not value.strip() or '\x00' in value:
|
|
143
|
+
return fallback
|
|
144
|
+
try:
|
|
145
|
+
return value if len(value.encode('utf-8')) <= 1024 else fallback
|
|
146
|
+
except UnicodeError:
|
|
147
|
+
return fallback
|
|
148
|
+
|
|
149
|
+
def navigation(expanded_index=None, clusters=(), expanded_cluster=None, servers_for_cluster=(),
|
|
150
|
+
expanded_server=None, processes_for_server=(), expanded_process=None,
|
|
151
|
+
connections_for_process=(), profiles_for_cluster=()):
|
|
152
|
+
from ..read_service import CLUSTER_LISTS, SERVER_LISTS
|
|
153
|
+
entries = []
|
|
154
|
+
for index, (host, port, protocol) in servers.items():
|
|
155
|
+
label = html.escape(f'{host}:{port}')
|
|
156
|
+
children = ''
|
|
157
|
+
if index == expanded_index:
|
|
158
|
+
for cluster_id, name, infobases in clusters:
|
|
159
|
+
server_children = ''
|
|
160
|
+
if cluster_id == expanded_cluster:
|
|
161
|
+
for server_id, server_name in servers_for_cluster:
|
|
162
|
+
base = f'/ras/{index}/clusters/{cluster_id}/servers/{server_id}'
|
|
163
|
+
processes = (''.join(
|
|
164
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/processes/{process_id}">'
|
|
165
|
+
f'{"Working process" if pid == "Working process" else "PID " + html.escape(pid)}</a><ul><li><a href="{base}/processes/'
|
|
166
|
+
f'{process_id}/connections/tree">Expand connections</a><ul>' +
|
|
167
|
+
(''.join(f'<li><a href="/ras/{index}/clusters/{cluster_id}/connections/'
|
|
168
|
+
f'{connection_id}">{html.escape(connection_host)}</a><ul>'
|
|
169
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/connections/'
|
|
170
|
+
f'{connection_id}/locks">Locks</a></li></ul></li>'
|
|
171
|
+
for connection_id, connection_host in connections_for_process)
|
|
172
|
+
if process_id == expanded_process else '') +
|
|
173
|
+
f'</ul></li><li><a href="/ras/{index}/clusters/{cluster_id}/processes/'
|
|
174
|
+
f'{process_id}/connections">Connections</a></li></ul></li>'
|
|
175
|
+
for process_id, pid in processes_for_server)
|
|
176
|
+
if server_id == expanded_server else '')
|
|
177
|
+
server_children += (f'<li><a href="{base}">{html.escape(server_name)}</a><ul>' +
|
|
178
|
+
f'<li><a href="{base}/processes/tree">Развернуть процессы</a>'
|
|
179
|
+
f'<ul>{processes}</ul></li>' +
|
|
180
|
+
''.join(f'<li><a href="{base}/{kind}">'
|
|
181
|
+
f'{SECTION_NAMES.get(kind, kind)}'
|
|
182
|
+
'</a></li>' for kind in ('processes', *SERVER_LISTS)) +
|
|
183
|
+
'</ul></li>')
|
|
184
|
+
bases = ''.join(
|
|
185
|
+
(f'<li><a href="/ras/{index}/clusters/{cluster_id}/infobases/{base_id}/summary">'
|
|
186
|
+
f'{html.escape(base_name)}</a><ul>' + ''.join(
|
|
187
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/infobases/{base_id}/{kind}">'
|
|
188
|
+
f'{SECTION_NAMES.get(kind, kind)}</a></li>'
|
|
189
|
+
for kind in ('summary', 'info', 'sessions', 'connections',
|
|
190
|
+
'locks', 'binary-storages')) + '</ul></li>' if base_id is not None else
|
|
191
|
+
f'<li>{html.escape(base_name)}</li>')
|
|
192
|
+
for base_id, base_name in infobases)
|
|
193
|
+
profile_children = ''
|
|
194
|
+
if cluster_id == expanded_cluster:
|
|
195
|
+
for profile_name in profiles_for_cluster:
|
|
196
|
+
profile_url = (f'/ras/{index}/clusters/{cluster_id}/profiles/'
|
|
197
|
+
f'{quote(profile_name, safe="")}')
|
|
198
|
+
profile_children += (f'<li><a href="/ras/{index}/clusters/{cluster_id}'
|
|
199
|
+
f'/profiles/overview?{urlencode({"name": profile_name})}">'
|
|
200
|
+
f'{html.escape(profile_name)}</a><ul>' + ''.join(
|
|
201
|
+
f'<li><a href="{profile_url}/acl/{kind}">'
|
|
202
|
+
f'{SECTION_NAMES.get(kind, kind)}</a></li>'
|
|
203
|
+
for kind in PROFILE_ACL_LISTS) + '</ul></li>')
|
|
204
|
+
children += (f'<li><a href="/ras/{index}/clusters/{cluster_id}">'
|
|
205
|
+
f'{html.escape(name)}</a><ul>' + ''.join(
|
|
206
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/{kind}">'
|
|
207
|
+
f'{SECTION_NAMES.get(kind, kind)}</a></li>'
|
|
208
|
+
for kind in CLUSTER_LISTS) +
|
|
209
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/profiles/tree">'
|
|
210
|
+
'Развернуть профили безопасности</a><ul>' + profile_children + '</ul></li>' +
|
|
211
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/servers/tree">'
|
|
212
|
+
'Развернуть рабочие серверы</a><ul>' + server_children + '</ul></li>' +
|
|
213
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/admins">'
|
|
214
|
+
'Администраторы</a></li>' +
|
|
215
|
+
f'<li><a href="/ras/{index}/clusters/{cluster_id}/info">'
|
|
216
|
+
'Сведения о кластере</a></li>' + bases + '</ul></li>')
|
|
217
|
+
entries.append(f'<li><a class="tree-node" href="/ras/{index}/tree">'
|
|
218
|
+
f'{"▾" if index == expanded_index else "▸"} {label}</a><ul>'
|
|
219
|
+
f'<li><a href="/ras/{index}/clusters">Кластеры</a></li>' + children +
|
|
220
|
+
f'<li><a href="/ras/{index}/agent/admins">Администраторы агента</a></li>'
|
|
221
|
+
f'<li><a href="/ras/{index}/agent/version">Версия агента</a></li>'
|
|
222
|
+
f'<li><small>{html.escape(protocol)}</small></li></ul></li>')
|
|
223
|
+
return '<h2>Подключения</h2><a href="/ras"><span class="ui-icon" aria-hidden="true">▣</span> Все серверы RAS</a><ul>' + ''.join(entries) + '</ul>'
|
|
224
|
+
|
|
225
|
+
def page(environ, start_response, body, status='200 OK', *, inspector=None,
|
|
226
|
+
expanded_index=None, clusters=(), expanded_cluster=None, servers_for_cluster=(),
|
|
227
|
+
expanded_server=None, processes_for_server=(), expanded_process=None,
|
|
228
|
+
connections_for_process=(), profiles_for_cluster=()):
|
|
229
|
+
start_response(status, [('Content-Type', 'text/html; charset=utf-8'),
|
|
230
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
231
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
232
|
+
return [render_document('RAS workspace', body,
|
|
233
|
+
workspace_tree=navigation(expanded_index, clusters,
|
|
234
|
+
expanded_cluster, servers_for_cluster,
|
|
235
|
+
expanded_server, processes_for_server,
|
|
236
|
+
expanded_process, connections_for_process,
|
|
237
|
+
profiles_for_cluster),
|
|
238
|
+
inspector=inspector).encode('utf-8')]
|
|
239
|
+
|
|
240
|
+
def inventory_filter(label):
|
|
241
|
+
name = html.escape(label)
|
|
242
|
+
return ('<label>Search ' + name.lower() +
|
|
243
|
+
' <input type="search" aria-label="Search ' + name.lower() +
|
|
244
|
+
'" oninput="this.closest(\'section\').querySelectorAll(\'[data-searchable-row]\')'
|
|
245
|
+
'.forEach(row=>row.hidden=!(row.textContent.toLocaleLowerCase().includes('
|
|
246
|
+
'this.value.toLocaleLowerCase())||row.dataset.searchKey?.toLocaleLowerCase().includes('
|
|
247
|
+
'this.value.toLocaleLowerCase())))" placeholder="Name or UUID"></label>')
|
|
248
|
+
|
|
249
|
+
def workspace(environ, start_response):
|
|
250
|
+
nonlocal next_index
|
|
251
|
+
path = environ.get('PATH_INFO', '/')
|
|
252
|
+
method = environ.get('REQUEST_METHOD', 'GET')
|
|
253
|
+
if (environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1') or
|
|
254
|
+
not _LOOPBACK.fullmatch(environ.get('HTTP_HOST', ''))):
|
|
255
|
+
start_response('403 Forbidden', [('Content-Type', 'text/plain')])
|
|
256
|
+
return [b'Local access only']
|
|
257
|
+
if path == '/ras/inspector.js':
|
|
258
|
+
if method != 'GET':
|
|
259
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
260
|
+
return [b'']
|
|
261
|
+
start_response('200 OK', [('Content-Type', 'text/javascript; charset=utf-8'),
|
|
262
|
+
('Cache-Control', 'no-store'),
|
|
263
|
+
('X-Content-Type-Options', 'nosniff')])
|
|
264
|
+
return [INSPECTOR_SCRIPT.encode('utf-8')]
|
|
265
|
+
if path == '/ras/remove':
|
|
266
|
+
if method != 'POST':
|
|
267
|
+
start_response('405 Method Not Allowed', [('Allow', 'POST')])
|
|
268
|
+
return [b'']
|
|
269
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
270
|
+
if (origin.scheme != 'http' or origin.netloc != environ.get('HTTP_HOST') or
|
|
271
|
+
origin.path or origin.query or origin.fragment or
|
|
272
|
+
environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded'):
|
|
273
|
+
return page(environ, start_response, '<p>Invalid origin</p>', '403 Forbidden')
|
|
274
|
+
try:
|
|
275
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
276
|
+
if not 0 <= length <= 2048:
|
|
277
|
+
raise ValueError('length')
|
|
278
|
+
raw = environ['wsgi.input'].read(length)
|
|
279
|
+
if len(raw) != length:
|
|
280
|
+
raise ValueError('truncated')
|
|
281
|
+
fields = parse_qs(raw.decode('utf-8'), strict_parsing=True)
|
|
282
|
+
if set(fields) != {'csrf_token', 'index', 'confirm'} or any(
|
|
283
|
+
len(values) != 1 for values in fields.values()):
|
|
284
|
+
raise ValueError('fields')
|
|
285
|
+
if not hmac.compare_digest(fields['csrf_token'][0], csrf_token):
|
|
286
|
+
raise ValueError('token')
|
|
287
|
+
index_text = fields['index'][0]
|
|
288
|
+
if not index_text.isascii() or not index_text.isdecimal() or len(index_text) > 16:
|
|
289
|
+
raise ValueError('index')
|
|
290
|
+
index = int(index_text)
|
|
291
|
+
if index not in servers or len(servers) <= 1:
|
|
292
|
+
raise ValueError('index')
|
|
293
|
+
host, port, protocol = servers[index]
|
|
294
|
+
if not hmac.compare_digest(fields['confirm'][0], f'{host}:{port}/{protocol}'):
|
|
295
|
+
raise ValueError('confirmation')
|
|
296
|
+
del servers[index]
|
|
297
|
+
except (ValueError, UnicodeError, KeyError):
|
|
298
|
+
return page(environ, start_response, '<p>Invalid RAS endpoint</p>', '403 Forbidden')
|
|
299
|
+
return page(environ, start_response, '<p>RAS endpoint removed from this workspace. '
|
|
300
|
+
'<a href="/ras">Back to servers</a></p>')
|
|
301
|
+
if path == '/ras/clusters':
|
|
302
|
+
if method != 'GET':
|
|
303
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
304
|
+
return [b'']
|
|
305
|
+
|
|
306
|
+
async def inventory():
|
|
307
|
+
semaphore = asyncio.Semaphore(4)
|
|
308
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
309
|
+
|
|
310
|
+
async def list_one(index, host, port, protocol):
|
|
311
|
+
found = []
|
|
312
|
+
try:
|
|
313
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
314
|
+
async with RacClient(host, port, protocol) as client:
|
|
315
|
+
for row in await client.list_clusters():
|
|
316
|
+
if not isinstance(row, dict):
|
|
317
|
+
continue
|
|
318
|
+
try:
|
|
319
|
+
cluster = str(UUID(str(row['cluster'])))
|
|
320
|
+
except (KeyError, TypeError, ValueError):
|
|
321
|
+
continue
|
|
322
|
+
name, object_host = row.get('name'), row.get('host')
|
|
323
|
+
if (not isinstance(name, str) or not name.strip()
|
|
324
|
+
or not _search_text(name)):
|
|
325
|
+
name = (object_host if isinstance(object_host, str)
|
|
326
|
+
and object_host.strip() and _search_text(object_host)
|
|
327
|
+
else 'Unnamed cluster')
|
|
328
|
+
found.append((index, host, port, protocol, cluster, name))
|
|
329
|
+
except Exception:
|
|
330
|
+
pass # Keep healthy RAS; do not render exception text.
|
|
331
|
+
return found
|
|
332
|
+
|
|
333
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
334
|
+
for index, endpoint in tuple(servers.items())))
|
|
335
|
+
return [row for group in groups for row in group]
|
|
336
|
+
|
|
337
|
+
rows = asyncio.run(inventory())
|
|
338
|
+
items = ''.join(
|
|
339
|
+
f'<tr data-searchable-row data-search-key="{cluster}"><td>'
|
|
340
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
341
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(name)}</a>'
|
|
342
|
+
'</td></tr>' for index, host, port, protocol, cluster, name in rows)
|
|
343
|
+
body = ('<section><h1>Кластеры всех RAS</h1><p>'
|
|
344
|
+
'<a href="/ras">Все серверы RAS</a></p>' + inventory_filter('Кластеры') +
|
|
345
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
346
|
+
'<th>RAS</th><th>Протокол</th><th>Кластер</th>'
|
|
347
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
348
|
+
return page(environ, start_response, body)
|
|
349
|
+
if path == '/ras/servers':
|
|
350
|
+
if method != 'GET':
|
|
351
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
352
|
+
return [b'']
|
|
353
|
+
|
|
354
|
+
async def inventory():
|
|
355
|
+
semaphore = asyncio.Semaphore(4)
|
|
356
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
357
|
+
|
|
358
|
+
async def list_one(index, host, port, protocol):
|
|
359
|
+
found = []
|
|
360
|
+
try:
|
|
361
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
362
|
+
async with RacClient(host, port, protocol) as client:
|
|
363
|
+
for record in await client.list_clusters():
|
|
364
|
+
if not isinstance(record, dict):
|
|
365
|
+
continue
|
|
366
|
+
try:
|
|
367
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
368
|
+
except (KeyError, TypeError, ValueError):
|
|
369
|
+
continue
|
|
370
|
+
cluster_name = record.get('name')
|
|
371
|
+
cluster_host = record.get('host')
|
|
372
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
373
|
+
or not _search_text(cluster_name)):
|
|
374
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
375
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
376
|
+
else 'Unnamed cluster')
|
|
377
|
+
try:
|
|
378
|
+
for row in await client.list_servers(UUID(cluster)):
|
|
379
|
+
if not isinstance(row, dict):
|
|
380
|
+
continue
|
|
381
|
+
try:
|
|
382
|
+
server_id = str(UUID(str(row['server'])))
|
|
383
|
+
except (KeyError, TypeError, ValueError):
|
|
384
|
+
continue
|
|
385
|
+
name, server_host = row.get('name'), row.get('host_name')
|
|
386
|
+
if (not isinstance(name, str) or not name.strip()
|
|
387
|
+
or not _search_text(name)):
|
|
388
|
+
name = (server_host if isinstance(server_host, str)
|
|
389
|
+
and server_host.strip() and _search_text(server_host)
|
|
390
|
+
else 'Unnamed server')
|
|
391
|
+
found.append((index, host, port, protocol, cluster,
|
|
392
|
+
cluster_name, server_id, name))
|
|
393
|
+
except Exception:
|
|
394
|
+
continue
|
|
395
|
+
except Exception:
|
|
396
|
+
pass # Preserve healthy clusters without rendering private errors.
|
|
397
|
+
return found
|
|
398
|
+
|
|
399
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
400
|
+
for index, endpoint in tuple(servers.items())))
|
|
401
|
+
return [row for group in groups for row in group]
|
|
402
|
+
|
|
403
|
+
rows = asyncio.run(inventory())
|
|
404
|
+
items = ''.join(
|
|
405
|
+
f'<tr data-searchable-row data-search-key="{server_id}"><td>'
|
|
406
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
407
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
408
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/servers/{server_id}">'
|
|
409
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
410
|
+
for index, host, port, protocol, cluster, cluster_name, server_id, name in rows)
|
|
411
|
+
body = ('<section><h1>Working servers across RAS</h1><p>'
|
|
412
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Working servers') +
|
|
413
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
414
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Working server</th>'
|
|
415
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
416
|
+
return page(environ, start_response, body)
|
|
417
|
+
if path == '/ras/processes':
|
|
418
|
+
if method != 'GET':
|
|
419
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
420
|
+
return [b'']
|
|
421
|
+
|
|
422
|
+
async def inventory():
|
|
423
|
+
semaphore = asyncio.Semaphore(4)
|
|
424
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
425
|
+
|
|
426
|
+
async def list_one(index, host, port, protocol):
|
|
427
|
+
found = []
|
|
428
|
+
try:
|
|
429
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
430
|
+
async with RacClient(host, port, protocol) as client:
|
|
431
|
+
for record in await client.list_clusters():
|
|
432
|
+
if not isinstance(record, dict):
|
|
433
|
+
continue
|
|
434
|
+
try:
|
|
435
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
436
|
+
except (KeyError, TypeError, ValueError):
|
|
437
|
+
continue
|
|
438
|
+
cluster_name, cluster_host = record.get('name'), record.get('host')
|
|
439
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
440
|
+
or not _search_text(cluster_name)):
|
|
441
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
442
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
443
|
+
else 'Unnamed cluster')
|
|
444
|
+
try:
|
|
445
|
+
for row in await client.list_processes(UUID(cluster)):
|
|
446
|
+
if not isinstance(row, dict):
|
|
447
|
+
continue
|
|
448
|
+
try:
|
|
449
|
+
process = str(UUID(str(row['process'])))
|
|
450
|
+
except (KeyError, TypeError, ValueError):
|
|
451
|
+
continue
|
|
452
|
+
pid = row.get('pid')
|
|
453
|
+
name = (f'PID {pid}' if type(pid) is str and pid.isascii()
|
|
454
|
+
and pid.isdecimal() and len(pid) <= 20
|
|
455
|
+
else 'Working process')
|
|
456
|
+
found.append((index, host, port, protocol, cluster,
|
|
457
|
+
cluster_name, process, name))
|
|
458
|
+
except Exception:
|
|
459
|
+
continue
|
|
460
|
+
except Exception:
|
|
461
|
+
pass # Preserve completed clusters; do not expose private errors.
|
|
462
|
+
return found
|
|
463
|
+
|
|
464
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
465
|
+
for index, endpoint in tuple(servers.items())))
|
|
466
|
+
return [row for group in groups for row in group]
|
|
467
|
+
|
|
468
|
+
rows = asyncio.run(inventory())
|
|
469
|
+
items = ''.join(
|
|
470
|
+
f'<tr data-searchable-row data-search-key="{process}"><td>'
|
|
471
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
472
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
473
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/processes/{process}">'
|
|
474
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
475
|
+
for index, host, port, protocol, cluster, cluster_name, process, name in rows)
|
|
476
|
+
body = ('<section><h1>Working processes across RAS</h1><p>'
|
|
477
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Working processes') +
|
|
478
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
479
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Working process</th>'
|
|
480
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
481
|
+
return page(environ, start_response, body)
|
|
482
|
+
if path == '/ras/sessions':
|
|
483
|
+
if method != 'GET':
|
|
484
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
485
|
+
return [b'']
|
|
486
|
+
|
|
487
|
+
async def inventory():
|
|
488
|
+
semaphore = asyncio.Semaphore(4)
|
|
489
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
490
|
+
|
|
491
|
+
async def list_one(index, host, port, protocol):
|
|
492
|
+
found = []
|
|
493
|
+
try:
|
|
494
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
495
|
+
async with RacClient(host, port, protocol) as client:
|
|
496
|
+
for record in await client.list_clusters():
|
|
497
|
+
if not isinstance(record, dict):
|
|
498
|
+
continue
|
|
499
|
+
try:
|
|
500
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
501
|
+
except (KeyError, TypeError, ValueError):
|
|
502
|
+
continue
|
|
503
|
+
cluster_name, cluster_host = record.get('name'), record.get('host')
|
|
504
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
505
|
+
or not _search_text(cluster_name)):
|
|
506
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
507
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
508
|
+
else 'Unnamed cluster')
|
|
509
|
+
try:
|
|
510
|
+
for row in await client.list_sessions(UUID(cluster)):
|
|
511
|
+
if not isinstance(row, dict):
|
|
512
|
+
continue
|
|
513
|
+
try:
|
|
514
|
+
session = str(UUID(str(row['session_id'])))
|
|
515
|
+
except (KeyError, TypeError, ValueError):
|
|
516
|
+
continue
|
|
517
|
+
name = row.get('user_name')
|
|
518
|
+
if (not isinstance(name, str) or not name.strip()
|
|
519
|
+
or not _search_text(name)):
|
|
520
|
+
name = 'Unnamed session'
|
|
521
|
+
found.append((index, host, port, protocol, cluster,
|
|
522
|
+
cluster_name, session, name))
|
|
523
|
+
except Exception:
|
|
524
|
+
continue
|
|
525
|
+
except Exception:
|
|
526
|
+
pass # Preserve completed clusters; do not expose private errors.
|
|
527
|
+
return found
|
|
528
|
+
|
|
529
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
530
|
+
for index, endpoint in tuple(servers.items())))
|
|
531
|
+
return [row for group in groups for row in group]
|
|
532
|
+
|
|
533
|
+
rows = asyncio.run(inventory())
|
|
534
|
+
items = ''.join(
|
|
535
|
+
f'<tr data-searchable-row data-search-key="{session}"><td>'
|
|
536
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
537
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
538
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/sessions/{session}">'
|
|
539
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
540
|
+
for index, host, port, protocol, cluster, cluster_name, session, name in rows)
|
|
541
|
+
body = ('<section><h1>Sessions across RAS</h1><p>'
|
|
542
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Sessions') +
|
|
543
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
544
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Session</th>'
|
|
545
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
546
|
+
return page(environ, start_response, body)
|
|
547
|
+
if path == '/ras/connections':
|
|
548
|
+
if method != 'GET':
|
|
549
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
550
|
+
return [b'']
|
|
551
|
+
|
|
552
|
+
async def inventory():
|
|
553
|
+
semaphore = asyncio.Semaphore(4)
|
|
554
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
555
|
+
|
|
556
|
+
async def list_one(index, host, port, protocol):
|
|
557
|
+
found = []
|
|
558
|
+
try:
|
|
559
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
560
|
+
async with RacClient(host, port, protocol) as client:
|
|
561
|
+
for record in await client.list_clusters():
|
|
562
|
+
if not isinstance(record, dict):
|
|
563
|
+
continue
|
|
564
|
+
try:
|
|
565
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
566
|
+
except (KeyError, TypeError, ValueError):
|
|
567
|
+
continue
|
|
568
|
+
cluster_name, cluster_host = record.get('name'), record.get('host')
|
|
569
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
570
|
+
or not _search_text(cluster_name)):
|
|
571
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
572
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
573
|
+
else 'Unnamed cluster')
|
|
574
|
+
try:
|
|
575
|
+
for row in await client.list_connections(UUID(cluster)):
|
|
576
|
+
if not isinstance(row, dict):
|
|
577
|
+
continue
|
|
578
|
+
try:
|
|
579
|
+
connection = str(UUID(str(row['connection'])))
|
|
580
|
+
except (KeyError, TypeError, ValueError):
|
|
581
|
+
continue
|
|
582
|
+
name = row.get('host')
|
|
583
|
+
if (not isinstance(name, str) or not name.strip()
|
|
584
|
+
or not _search_text(name)):
|
|
585
|
+
name = 'Unnamed connection'
|
|
586
|
+
found.append((index, host, port, protocol, cluster,
|
|
587
|
+
cluster_name, connection, name))
|
|
588
|
+
except Exception:
|
|
589
|
+
continue
|
|
590
|
+
except Exception:
|
|
591
|
+
pass # Preserve completed clusters; do not expose private errors.
|
|
592
|
+
return found
|
|
593
|
+
|
|
594
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
595
|
+
for index, endpoint in tuple(servers.items())))
|
|
596
|
+
return [row for group in groups for row in group]
|
|
597
|
+
|
|
598
|
+
rows = asyncio.run(inventory())
|
|
599
|
+
items = ''.join(
|
|
600
|
+
f'<tr data-searchable-row data-search-key="{connection}"><td>'
|
|
601
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
602
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
603
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/connections/{connection}">'
|
|
604
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
605
|
+
for index, host, port, protocol, cluster, cluster_name, connection, name in rows)
|
|
606
|
+
body = ('<section><h1>Connections across RAS</h1><p>'
|
|
607
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Connections') +
|
|
608
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
609
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Connection</th>'
|
|
610
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
611
|
+
return page(environ, start_response, body)
|
|
612
|
+
if path == '/ras/locks':
|
|
613
|
+
if method != 'GET':
|
|
614
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
615
|
+
return [b'']
|
|
616
|
+
|
|
617
|
+
async def inventory():
|
|
618
|
+
semaphore = asyncio.Semaphore(4)
|
|
619
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
620
|
+
|
|
621
|
+
async def list_one(index, host, port, protocol):
|
|
622
|
+
found = []
|
|
623
|
+
try:
|
|
624
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
625
|
+
async with RacClient(host, port, protocol) as client:
|
|
626
|
+
for record in await client.list_clusters():
|
|
627
|
+
if not isinstance(record, dict):
|
|
628
|
+
continue
|
|
629
|
+
try:
|
|
630
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
631
|
+
except (KeyError, TypeError, ValueError):
|
|
632
|
+
continue
|
|
633
|
+
cluster_name = safe_tree_name(record.get('name'),
|
|
634
|
+
safe_tree_name(record.get('host'),
|
|
635
|
+
'Unnamed cluster'))
|
|
636
|
+
try:
|
|
637
|
+
for row in await client.list_locks(UUID(cluster)):
|
|
638
|
+
if not isinstance(row, dict):
|
|
639
|
+
continue
|
|
640
|
+
try:
|
|
641
|
+
connection = str(UUID(str(row['connection'])))
|
|
642
|
+
UUID(str(row['object']))
|
|
643
|
+
except (KeyError, TypeError, ValueError):
|
|
644
|
+
continue
|
|
645
|
+
label = row.get('lock-descr')
|
|
646
|
+
name = (label if isinstance(label, str) and label.strip()
|
|
647
|
+
and _search_text(label) else 'Lock')
|
|
648
|
+
found.append((index, host, port, protocol, cluster,
|
|
649
|
+
cluster_name, connection, name))
|
|
650
|
+
except Exception:
|
|
651
|
+
continue
|
|
652
|
+
except Exception:
|
|
653
|
+
pass # Retain completed clusters; never expose exception text.
|
|
654
|
+
return found
|
|
655
|
+
|
|
656
|
+
batches = await asyncio.gather(*(list_one(index, *endpoint)
|
|
657
|
+
for index, endpoint in tuple(servers.items())))
|
|
658
|
+
return [row for batch in batches for row in batch]
|
|
659
|
+
|
|
660
|
+
rows = asyncio.run(inventory())
|
|
661
|
+
items = ''.join(
|
|
662
|
+
f'<tr data-searchable-row data-search-key="{html.escape(name, quote=True)}">'
|
|
663
|
+
f'<td>{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
664
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
665
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/connections/{connection}/locks">'
|
|
666
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
667
|
+
for index, host, port, protocol, cluster, cluster_name, connection, name in rows)
|
|
668
|
+
body = ('<section><h1>Locks across RAS</h1><p><a href="/ras">All RAS servers</a>'
|
|
669
|
+
'</p>' + inventory_filter('Locks') +
|
|
670
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
671
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Lock</th>'
|
|
672
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
673
|
+
return page(environ, start_response, body)
|
|
674
|
+
if path == '/ras/services':
|
|
675
|
+
if method != 'GET':
|
|
676
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
677
|
+
return [b'']
|
|
678
|
+
|
|
679
|
+
async def inventory():
|
|
680
|
+
semaphore = asyncio.Semaphore(4)
|
|
681
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
682
|
+
|
|
683
|
+
async def list_one(index, host, port, protocol):
|
|
684
|
+
found = []
|
|
685
|
+
try:
|
|
686
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
687
|
+
async with RacClient(host, port, protocol) as client:
|
|
688
|
+
for record in await client.list_clusters():
|
|
689
|
+
if not isinstance(record, dict):
|
|
690
|
+
continue
|
|
691
|
+
try:
|
|
692
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
693
|
+
except (KeyError, TypeError, ValueError):
|
|
694
|
+
continue
|
|
695
|
+
cluster_name, cluster_host = record.get('name'), record.get('host')
|
|
696
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
697
|
+
or not _search_text(cluster_name)):
|
|
698
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
699
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
700
|
+
else 'Unnamed cluster')
|
|
701
|
+
try:
|
|
702
|
+
for row in await client.list_services(UUID(cluster)):
|
|
703
|
+
if not isinstance(row, dict):
|
|
704
|
+
continue
|
|
705
|
+
name = row.get('name')
|
|
706
|
+
if (not isinstance(name, str) or not name.strip()
|
|
707
|
+
or not _search_text(name)):
|
|
708
|
+
continue
|
|
709
|
+
found.append((index, host, port, protocol, cluster,
|
|
710
|
+
cluster_name, name))
|
|
711
|
+
except Exception:
|
|
712
|
+
continue
|
|
713
|
+
except Exception:
|
|
714
|
+
pass # Preserve completed clusters without exposing server errors.
|
|
715
|
+
return found
|
|
716
|
+
|
|
717
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
718
|
+
for index, endpoint in tuple(servers.items())))
|
|
719
|
+
return [row for group in groups for row in group]
|
|
720
|
+
|
|
721
|
+
rows = asyncio.run(inventory())
|
|
722
|
+
items = ''.join(
|
|
723
|
+
f'<tr data-searchable-row><td>{html.escape(f"{host}:{port}")}</td>'
|
|
724
|
+
f'<td>{html.escape(protocol)}</td>'
|
|
725
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
726
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/services">'
|
|
727
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
728
|
+
for index, host, port, protocol, cluster, cluster_name, name in rows)
|
|
729
|
+
body = ('<section><h1>Services across RAS</h1><p>'
|
|
730
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Services') +
|
|
731
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
732
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Service</th>'
|
|
733
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
734
|
+
return page(environ, start_response, body)
|
|
735
|
+
if path == '/ras/server-inventory':
|
|
736
|
+
if method != 'GET':
|
|
737
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
738
|
+
return [b'']
|
|
739
|
+
kind = parse_qs(environ.get('QUERY_STRING', '')).get('kind', [''])[0]
|
|
740
|
+
if kind not in (*SEARCH_SERVER_GROUPS, 'transfer-directories'):
|
|
741
|
+
return page(environ, start_response, '<p>Invalid inventory kind</p>', '400 Bad Request')
|
|
742
|
+
method_name, selector_key, label_key = SEARCH_SERVER_GROUPS.get(
|
|
743
|
+
kind, ('list_service_data_dirs_for_transfer', 'service-name', 'service-name'))
|
|
744
|
+
|
|
745
|
+
async def inventory():
|
|
746
|
+
semaphore = asyncio.Semaphore(4)
|
|
747
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
748
|
+
|
|
749
|
+
async def list_one(index, host, port, protocol):
|
|
750
|
+
found = []
|
|
751
|
+
try:
|
|
752
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
753
|
+
async with RacClient(host, port, protocol) as client:
|
|
754
|
+
for cluster_row in await client.list_clusters():
|
|
755
|
+
if not isinstance(cluster_row, dict):
|
|
756
|
+
continue
|
|
757
|
+
try:
|
|
758
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
759
|
+
except (KeyError, TypeError, ValueError):
|
|
760
|
+
continue
|
|
761
|
+
cluster_name = safe_tree_name(cluster_row.get('name'),
|
|
762
|
+
safe_tree_name(cluster_row.get('host'),
|
|
763
|
+
'Unnamed cluster'))
|
|
764
|
+
try:
|
|
765
|
+
working_servers = await client.list_servers(UUID(cluster))
|
|
766
|
+
except Exception:
|
|
767
|
+
continue
|
|
768
|
+
for server_row in working_servers:
|
|
769
|
+
if not isinstance(server_row, dict):
|
|
770
|
+
continue
|
|
771
|
+
try:
|
|
772
|
+
server_id = str(UUID(str(server_row['server'])))
|
|
773
|
+
except (KeyError, TypeError, ValueError):
|
|
774
|
+
continue
|
|
775
|
+
server_name = safe_tree_name(server_row.get('name'),
|
|
776
|
+
safe_tree_name(server_row.get('host_name'),
|
|
777
|
+
'Unnamed server'))
|
|
778
|
+
try:
|
|
779
|
+
records = await getattr(client, method_name)(
|
|
780
|
+
UUID(cluster), UUID(server_id))
|
|
781
|
+
for row in records:
|
|
782
|
+
if not isinstance(row, dict):
|
|
783
|
+
continue
|
|
784
|
+
if kind == 'transfer-directories':
|
|
785
|
+
selector = row.get('service-name')
|
|
786
|
+
if (not isinstance(selector, str) or not selector.strip()
|
|
787
|
+
or not _search_text(selector)):
|
|
788
|
+
continue
|
|
789
|
+
name = selector
|
|
790
|
+
else:
|
|
791
|
+
try:
|
|
792
|
+
selector = str(UUID(str(row[selector_key])))
|
|
793
|
+
except (KeyError, TypeError, ValueError):
|
|
794
|
+
continue
|
|
795
|
+
name = safe_tree_name(row.get(label_key),
|
|
796
|
+
'Assignment rule' if kind == 'rules'
|
|
797
|
+
else 'Service setting')
|
|
798
|
+
found.append((index, host, port, protocol, cluster,
|
|
799
|
+
cluster_name, server_id, server_name,
|
|
800
|
+
selector, name))
|
|
801
|
+
except Exception:
|
|
802
|
+
continue
|
|
803
|
+
except Exception:
|
|
804
|
+
pass # Preserve completed server reads, not private errors.
|
|
805
|
+
return found
|
|
806
|
+
|
|
807
|
+
batches = await asyncio.gather(*(list_one(index, *endpoint)
|
|
808
|
+
for index, endpoint in tuple(servers.items())))
|
|
809
|
+
return [row for batch in batches for row in batch]
|
|
810
|
+
|
|
811
|
+
rows = asyncio.run(inventory())
|
|
812
|
+
items = ''.join(
|
|
813
|
+
f'<tr data-searchable-row data-search-key="{html.escape(selector, quote=True)}">'
|
|
814
|
+
f'<td>{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
815
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
816
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/servers/{server_id}">'
|
|
817
|
+
f'{html.escape(server_name)}</a></td>'
|
|
818
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/servers/{server_id}/{kind}'
|
|
819
|
+
f'{"?" + urlencode({"service_name": selector}, quote_via=quote) if kind == "transfer-directories" else "/" + selector}">'
|
|
820
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
821
|
+
for index, host, port, protocol, cluster, cluster_name,
|
|
822
|
+
server_id, server_name, selector, name in rows)
|
|
823
|
+
title = kind.replace('-', ' ').title()
|
|
824
|
+
body = (f'<section><h1>{title} across RAS</h1><p><a href="/ras">All RAS servers</a>'
|
|
825
|
+
'</p>' + inventory_filter(title) +
|
|
826
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
827
|
+
f'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Working server</th><th>{title}</th>'
|
|
828
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
829
|
+
return page(environ, start_response, body)
|
|
830
|
+
if path == '/ras/administrators':
|
|
831
|
+
if method != 'GET':
|
|
832
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
833
|
+
return [b'']
|
|
834
|
+
kind = parse_qs(environ.get('QUERY_STRING', '')).get('kind', [''])[0]
|
|
835
|
+
if kind not in ('agent-admins', 'cluster-admins'):
|
|
836
|
+
return page(environ, start_response, '<p>Invalid inventory kind</p>', '400 Bad Request')
|
|
837
|
+
|
|
838
|
+
async def inventory():
|
|
839
|
+
semaphore = asyncio.Semaphore(4)
|
|
840
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
841
|
+
|
|
842
|
+
async def list_one(index, host, port, protocol):
|
|
843
|
+
found = []
|
|
844
|
+
try:
|
|
845
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
846
|
+
async with RacClient(host, port, protocol) as client:
|
|
847
|
+
if kind == 'agent-admins':
|
|
848
|
+
for row in await client.list_agent_admins():
|
|
849
|
+
if not isinstance(row, dict):
|
|
850
|
+
continue
|
|
851
|
+
name = row.get('name')
|
|
852
|
+
if (isinstance(name, str) and name.strip()
|
|
853
|
+
and _search_text(name)):
|
|
854
|
+
found.append((index, host, port, protocol, '', '', name))
|
|
855
|
+
else:
|
|
856
|
+
for record in await client.list_clusters():
|
|
857
|
+
if not isinstance(record, dict):
|
|
858
|
+
continue
|
|
859
|
+
try:
|
|
860
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
861
|
+
except (KeyError, TypeError, ValueError):
|
|
862
|
+
continue
|
|
863
|
+
cluster_name = safe_tree_name(record.get('name'),
|
|
864
|
+
safe_tree_name(record.get('host'),
|
|
865
|
+
'Unnamed cluster'))
|
|
866
|
+
try:
|
|
867
|
+
for row in await client.list_cluster_admins(UUID(cluster)):
|
|
868
|
+
if not isinstance(row, dict):
|
|
869
|
+
continue
|
|
870
|
+
name = row.get('name')
|
|
871
|
+
if (isinstance(name, str) and name.strip()
|
|
872
|
+
and _search_text(name)):
|
|
873
|
+
found.append((index, host, port, protocol,
|
|
874
|
+
cluster, cluster_name, name))
|
|
875
|
+
except Exception:
|
|
876
|
+
continue
|
|
877
|
+
except Exception:
|
|
878
|
+
pass # Keep completed clusters, never expose private exception text.
|
|
879
|
+
return found
|
|
880
|
+
|
|
881
|
+
batches = await asyncio.gather(*(list_one(index, *endpoint)
|
|
882
|
+
for index, endpoint in tuple(servers.items())))
|
|
883
|
+
return [row for batch in batches for row in batch]
|
|
884
|
+
|
|
885
|
+
rows = asyncio.run(inventory())
|
|
886
|
+
items = ''.join(
|
|
887
|
+
f'<tr data-searchable-row data-search-key="{html.escape(name, quote=True)}">'
|
|
888
|
+
f'<td>{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
889
|
+
+ (f'<td><a href="/ras/{index}/clusters/{cluster}">'
|
|
890
|
+
f'{html.escape(cluster_name)}</a></td>' if cluster else '<td>—</td>')
|
|
891
|
+
+ (f'<td><a href="/ras/{index}/clusters/{cluster}/admins">' if cluster else
|
|
892
|
+
f'<td><a href="/ras/{index}/agent/admins">')
|
|
893
|
+
+ html.escape(name) + '</a></td></tr>'
|
|
894
|
+
for index, host, port, protocol, cluster, cluster_name, name in rows)
|
|
895
|
+
title = 'Agent administrators' if kind == 'agent-admins' else 'Cluster administrators'
|
|
896
|
+
body = (f'<section><h1>{title} across RAS</h1><p><a href="/ras">All RAS servers</a>'
|
|
897
|
+
'</p>' + inventory_filter(title) +
|
|
898
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
899
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Administrator</th>'
|
|
900
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
901
|
+
return page(environ, start_response, body)
|
|
902
|
+
if path == '/ras/cluster-inventory':
|
|
903
|
+
if method != 'GET':
|
|
904
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
905
|
+
return [b'']
|
|
906
|
+
kind = parse_qs(environ.get('QUERY_STRING', '')).get('kind', [''])[0]
|
|
907
|
+
if kind not in ('managers', 'counters', 'limits'):
|
|
908
|
+
return page(environ, start_response, '<p>Invalid inventory kind</p>', '400 Bad Request')
|
|
909
|
+
selector_key, is_uuid = SEARCH_CLUSTER_GROUPS[kind]
|
|
910
|
+
|
|
911
|
+
async def inventory():
|
|
912
|
+
semaphore = asyncio.Semaphore(4)
|
|
913
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
914
|
+
|
|
915
|
+
async def list_one(index, host, port, protocol):
|
|
916
|
+
found = []
|
|
917
|
+
try:
|
|
918
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
919
|
+
async with RacClient(host, port, protocol) as client:
|
|
920
|
+
for record in await client.list_clusters():
|
|
921
|
+
if not isinstance(record, dict):
|
|
922
|
+
continue
|
|
923
|
+
try:
|
|
924
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
925
|
+
except (KeyError, TypeError, ValueError):
|
|
926
|
+
continue
|
|
927
|
+
cluster_name = safe_tree_name(record.get('name'),
|
|
928
|
+
safe_tree_name(record.get('host'),
|
|
929
|
+
'Unnamed cluster'))
|
|
930
|
+
try:
|
|
931
|
+
for row in await getattr(client, 'list_' + kind)(UUID(cluster)):
|
|
932
|
+
if not isinstance(row, dict):
|
|
933
|
+
continue
|
|
934
|
+
try:
|
|
935
|
+
selector = (str(UUID(str(row[selector_key]))) if is_uuid
|
|
936
|
+
else row[selector_key])
|
|
937
|
+
except (KeyError, TypeError, ValueError):
|
|
938
|
+
continue
|
|
939
|
+
if not is_uuid and (not isinstance(selector, str)
|
|
940
|
+
or not selector.strip() or not _search_text(selector)):
|
|
941
|
+
continue
|
|
942
|
+
name = (safe_tree_name(row.get('host'), 'Cluster manager')
|
|
943
|
+
if is_uuid else selector)
|
|
944
|
+
found.append((index, host, port, protocol, cluster,
|
|
945
|
+
cluster_name, selector, name))
|
|
946
|
+
except Exception:
|
|
947
|
+
continue
|
|
948
|
+
except Exception:
|
|
949
|
+
pass # Retain completed clusters, not private exception text.
|
|
950
|
+
return found
|
|
951
|
+
|
|
952
|
+
batches = await asyncio.gather(*(list_one(index, *endpoint)
|
|
953
|
+
for index, endpoint in tuple(servers.items())))
|
|
954
|
+
return [row for batch in batches for row in batch]
|
|
955
|
+
|
|
956
|
+
rows = asyncio.run(inventory())
|
|
957
|
+
items = ''.join(
|
|
958
|
+
f'<tr data-searchable-row data-search-key="{html.escape(selector, quote=True)}">'
|
|
959
|
+
f'<td>{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
960
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
961
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/{kind}/{quote(selector, safe="")}">'
|
|
962
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
963
|
+
for index, host, port, protocol, cluster, cluster_name, selector, name in rows)
|
|
964
|
+
title = kind.title()
|
|
965
|
+
body = (f'<section><h1>{title} across RAS</h1><p><a href="/ras">All RAS servers</a>'
|
|
966
|
+
'</p>' + inventory_filter(title) +
|
|
967
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
968
|
+
f'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>{title}</th>'
|
|
969
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
970
|
+
return page(environ, start_response, body)
|
|
971
|
+
if path == '/ras/profiles':
|
|
972
|
+
if method != 'GET':
|
|
973
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
974
|
+
return [b'']
|
|
975
|
+
|
|
976
|
+
async def inventory():
|
|
977
|
+
semaphore = asyncio.Semaphore(4)
|
|
978
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
979
|
+
|
|
980
|
+
async def list_one(index, host, port, protocol):
|
|
981
|
+
found = []
|
|
982
|
+
try:
|
|
983
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
984
|
+
async with RacClient(host, port, protocol) as client:
|
|
985
|
+
for record in await client.list_clusters():
|
|
986
|
+
if not isinstance(record, dict):
|
|
987
|
+
continue
|
|
988
|
+
try:
|
|
989
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
990
|
+
except (KeyError, TypeError, ValueError):
|
|
991
|
+
continue
|
|
992
|
+
cluster_name, cluster_host = record.get('name'), record.get('host')
|
|
993
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
994
|
+
or not _search_text(cluster_name)):
|
|
995
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
996
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
997
|
+
else 'Unnamed cluster')
|
|
998
|
+
try:
|
|
999
|
+
for row in await client.list_profiles(UUID(cluster)):
|
|
1000
|
+
if not isinstance(row, dict):
|
|
1001
|
+
continue
|
|
1002
|
+
name = row.get('name')
|
|
1003
|
+
if (not isinstance(name, str) or not name.strip()
|
|
1004
|
+
or not _search_text(name)):
|
|
1005
|
+
continue
|
|
1006
|
+
found.append((index, host, port, protocol, cluster,
|
|
1007
|
+
cluster_name, name))
|
|
1008
|
+
except Exception:
|
|
1009
|
+
continue
|
|
1010
|
+
except Exception:
|
|
1011
|
+
pass # Preserve completed clusters without exposing server errors.
|
|
1012
|
+
return found
|
|
1013
|
+
|
|
1014
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
1015
|
+
for index, endpoint in tuple(servers.items())))
|
|
1016
|
+
return [row for group in groups for row in group]
|
|
1017
|
+
|
|
1018
|
+
rows = asyncio.run(inventory())
|
|
1019
|
+
items = ''.join(
|
|
1020
|
+
f'<tr data-searchable-row><td>{html.escape(f"{host}:{port}")}</td>'
|
|
1021
|
+
f'<td>{html.escape(protocol)}</td>'
|
|
1022
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
1023
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/profiles/overview?'
|
|
1024
|
+
f'{urlencode({"name": name}, quote_via=quote)}">{html.escape(name)}</a></td></tr>'
|
|
1025
|
+
for index, host, port, protocol, cluster, cluster_name, name in rows)
|
|
1026
|
+
body = ('<section><h1>Security profiles across RAS</h1><p>'
|
|
1027
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Security profiles') +
|
|
1028
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
1029
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Profile</th>'
|
|
1030
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
1031
|
+
return page(environ, start_response, body)
|
|
1032
|
+
if path == '/ras/infobases':
|
|
1033
|
+
if method != 'GET':
|
|
1034
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
1035
|
+
return [b'']
|
|
1036
|
+
|
|
1037
|
+
async def inventory():
|
|
1038
|
+
semaphore = asyncio.Semaphore(4)
|
|
1039
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
1040
|
+
|
|
1041
|
+
async def list_one(index, host, port, protocol):
|
|
1042
|
+
found = []
|
|
1043
|
+
try:
|
|
1044
|
+
async with asyncio.timeout_at(deadline), semaphore:
|
|
1045
|
+
async with RacClient(host, port, protocol) as client:
|
|
1046
|
+
for record in await client.list_clusters():
|
|
1047
|
+
if not isinstance(record, dict):
|
|
1048
|
+
continue
|
|
1049
|
+
try:
|
|
1050
|
+
cluster = str(UUID(str(record['cluster'])))
|
|
1051
|
+
except (KeyError, TypeError, ValueError):
|
|
1052
|
+
continue
|
|
1053
|
+
cluster_name = record.get('name')
|
|
1054
|
+
cluster_host = record.get('host')
|
|
1055
|
+
if (not isinstance(cluster_name, str) or not cluster_name.strip()
|
|
1056
|
+
or not _search_text(cluster_name)):
|
|
1057
|
+
cluster_name = (cluster_host if isinstance(cluster_host, str)
|
|
1058
|
+
and cluster_host.strip() and _search_text(cluster_host)
|
|
1059
|
+
else 'Unnamed cluster')
|
|
1060
|
+
try:
|
|
1061
|
+
for row in await client.list_infobases(UUID(cluster)):
|
|
1062
|
+
if not isinstance(row, dict):
|
|
1063
|
+
continue
|
|
1064
|
+
try:
|
|
1065
|
+
infobase = str(UUID(str(row['infobase'])))
|
|
1066
|
+
except (KeyError, TypeError, ValueError):
|
|
1067
|
+
continue
|
|
1068
|
+
name = row.get('name')
|
|
1069
|
+
if (not isinstance(name, str) or not name.strip()
|
|
1070
|
+
or not _search_text(name)):
|
|
1071
|
+
name = 'Unnamed infobase'
|
|
1072
|
+
found.append((index, host, port, protocol, cluster,
|
|
1073
|
+
cluster_name, infobase, name))
|
|
1074
|
+
except Exception:
|
|
1075
|
+
continue
|
|
1076
|
+
except Exception:
|
|
1077
|
+
pass # Keep completed clusters, never expose RAS errors.
|
|
1078
|
+
return found
|
|
1079
|
+
|
|
1080
|
+
groups = await asyncio.gather(*(list_one(index, *endpoint)
|
|
1081
|
+
for index, endpoint in tuple(servers.items())))
|
|
1082
|
+
return [row for group in groups for row in group]
|
|
1083
|
+
|
|
1084
|
+
rows = asyncio.run(inventory())
|
|
1085
|
+
items = ''.join(
|
|
1086
|
+
f'<tr data-searchable-row data-search-key="{base}"><td>'
|
|
1087
|
+
f'{html.escape(f"{host}:{port}")}</td><td>{html.escape(protocol)}</td>'
|
|
1088
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}">{html.escape(cluster_name)}</a></td>'
|
|
1089
|
+
f'<td><a href="/ras/{index}/clusters/{cluster}/infobases/{base}/summary">'
|
|
1090
|
+
f'{html.escape(name)}</a></td></tr>'
|
|
1091
|
+
for index, host, port, protocol, cluster, cluster_name, base, name in rows)
|
|
1092
|
+
body = ('<section><h1>Infobases across RAS</h1><p>'
|
|
1093
|
+
'<a href="/ras">All RAS servers</a></p>' + inventory_filter('Infobases') +
|
|
1094
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
1095
|
+
'<th>RAS</th><th>Protocol</th><th>Cluster</th><th>Infobase</th>'
|
|
1096
|
+
'</tr></thead><tbody>' + items + '</tbody></table></div></section>')
|
|
1097
|
+
return page(environ, start_response, body)
|
|
1098
|
+
if path == '/ras/search':
|
|
1099
|
+
if method != 'GET':
|
|
1100
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
1101
|
+
return [b'']
|
|
1102
|
+
query = environ.get('QUERY_STRING', '')
|
|
1103
|
+
if len(query) > 512:
|
|
1104
|
+
return page(environ, start_response, '<p>Invalid search</p>', '400 Bad Request')
|
|
1105
|
+
try:
|
|
1106
|
+
if re.search(r'%(?![0-9A-Fa-f]{2})', query):
|
|
1107
|
+
raise ValueError('escape')
|
|
1108
|
+
unquote_to_bytes(query).decode('utf-8')
|
|
1109
|
+
fields = parse_qs(query, strict_parsing=bool(query))
|
|
1110
|
+
if set(fields) - {'q', 'scope'} or any(len(values) != 1 for values in fields.values()):
|
|
1111
|
+
raise ValueError('query')
|
|
1112
|
+
scope = fields.get('scope', ['inventory'])[0]
|
|
1113
|
+
if scope not in ('inventory', 'clusters', 'infobases', 'agent-admins', 'cluster-admins',
|
|
1114
|
+
'services', 'locks', 'transfer-directories',
|
|
1115
|
+
'servers', 'processes', 'sessions', 'profiles', *SEARCH_PROFILE_ACL,
|
|
1116
|
+
*SEARCH_CLUSTER_GROUPS, *SEARCH_SERVER_GROUPS):
|
|
1117
|
+
raise ValueError('scope')
|
|
1118
|
+
term = fields.get('q', [''])[0].strip()
|
|
1119
|
+
if len(term) > 100 or '\x00' in term:
|
|
1120
|
+
raise ValueError('query')
|
|
1121
|
+
except (ValueError, UnicodeError):
|
|
1122
|
+
return page(environ, start_response, '<p>Invalid search</p>', '400 Bad Request')
|
|
1123
|
+
|
|
1124
|
+
async def inventory():
|
|
1125
|
+
semaphore = asyncio.Semaphore(4)
|
|
1126
|
+
deadline = asyncio.get_running_loop().time() + SEARCH_ENDPOINT_TIMEOUT
|
|
1127
|
+
|
|
1128
|
+
async def query_server(index, host, port, protocol):
|
|
1129
|
+
found = []
|
|
1130
|
+
try:
|
|
1131
|
+
async with asyncio.timeout_at(deadline):
|
|
1132
|
+
async with semaphore:
|
|
1133
|
+
async with RacClient(host, port, protocol) as client:
|
|
1134
|
+
if scope == 'agent-admins':
|
|
1135
|
+
for admin in await client.list_agent_admins():
|
|
1136
|
+
if not isinstance(admin, dict):
|
|
1137
|
+
continue
|
|
1138
|
+
name = admin.get('name')
|
|
1139
|
+
os_user = admin.get('os-user', '')
|
|
1140
|
+
if (not isinstance(name, str) or not name or not _search_text(name)):
|
|
1141
|
+
continue
|
|
1142
|
+
if term.casefold() in name.casefold() or (
|
|
1143
|
+
isinstance(os_user, str) and _search_text(os_user)
|
|
1144
|
+
and term.casefold() in os_user.casefold()):
|
|
1145
|
+
found.append((index, host, port, protocol, scope, '', '', name))
|
|
1146
|
+
return found
|
|
1147
|
+
clusters = await client.list_clusters()
|
|
1148
|
+
for cluster in clusters:
|
|
1149
|
+
try:
|
|
1150
|
+
ident = str(UUID(str(cluster['cluster'])))
|
|
1151
|
+
except (KeyError, TypeError, ValueError):
|
|
1152
|
+
continue
|
|
1153
|
+
cluster_name = cluster.get('name', '')
|
|
1154
|
+
valid_name = _search_text(cluster_name) and bool(cluster_name.strip())
|
|
1155
|
+
cluster_host = cluster.get('host', '')
|
|
1156
|
+
valid_host = _search_text(cluster_host) and bool(cluster_host.strip())
|
|
1157
|
+
if scope in ('inventory', 'clusters') and (
|
|
1158
|
+
term.casefold() in ident.casefold()
|
|
1159
|
+
or valid_name and term.casefold() in cluster_name.casefold()
|
|
1160
|
+
or valid_host and term.casefold() in cluster_host.casefold()):
|
|
1161
|
+
found.append((index, host, port, protocol, 'cluster', ident,
|
|
1162
|
+
'', cluster_name if valid_name else
|
|
1163
|
+
cluster_host if valid_host and cluster_host else 'Unnamed cluster'))
|
|
1164
|
+
try:
|
|
1165
|
+
if scope == 'cluster-admins':
|
|
1166
|
+
for admin in await client.list_cluster_admins(UUID(ident)):
|
|
1167
|
+
if not isinstance(admin, dict):
|
|
1168
|
+
continue
|
|
1169
|
+
name = admin.get('name')
|
|
1170
|
+
os_user = admin.get('os-user', '')
|
|
1171
|
+
if (not isinstance(name, str) or not name or not _search_text(name)):
|
|
1172
|
+
continue
|
|
1173
|
+
if term.casefold() in name.casefold() or (
|
|
1174
|
+
isinstance(os_user, str) and _search_text(os_user)
|
|
1175
|
+
and term.casefold() in os_user.casefold()):
|
|
1176
|
+
found.append((index, host, port, protocol, scope,
|
|
1177
|
+
ident, '', name))
|
|
1178
|
+
elif scope in (*SEARCH_SERVER_GROUPS, 'transfer-directories'):
|
|
1179
|
+
for server in await client.list_servers(UUID(ident)):
|
|
1180
|
+
try:
|
|
1181
|
+
server_id = str(UUID(str(server['server'])))
|
|
1182
|
+
except (KeyError, TypeError, ValueError):
|
|
1183
|
+
continue
|
|
1184
|
+
try:
|
|
1185
|
+
if scope == 'transfer-directories':
|
|
1186
|
+
records = await client.list_service_data_dirs_for_transfer(
|
|
1187
|
+
UUID(ident), UUID(server_id))
|
|
1188
|
+
for record in records:
|
|
1189
|
+
if not isinstance(record, dict):
|
|
1190
|
+
continue
|
|
1191
|
+
name = record.get('service-name')
|
|
1192
|
+
infobase = record.get('infobase-name', '')
|
|
1193
|
+
if (not isinstance(name, str) or not name
|
|
1194
|
+
or not _search_text(name)):
|
|
1195
|
+
continue
|
|
1196
|
+
valid_infobase = (isinstance(infobase, str)
|
|
1197
|
+
and _search_text(infobase))
|
|
1198
|
+
if (term.casefold() in name.casefold()
|
|
1199
|
+
or isinstance(infobase, str)
|
|
1200
|
+
and valid_infobase
|
|
1201
|
+
and term.casefold() in infobase.casefold()):
|
|
1202
|
+
found.append((index, host, port, protocol, scope,
|
|
1203
|
+
ident, server_id, name))
|
|
1204
|
+
else:
|
|
1205
|
+
method, key, label_key = SEARCH_SERVER_GROUPS[scope]
|
|
1206
|
+
for record in await getattr(client, method)(UUID(ident), UUID(server_id)):
|
|
1207
|
+
try:
|
|
1208
|
+
record_id = str(UUID(str(record[key])))
|
|
1209
|
+
except (KeyError, TypeError, ValueError):
|
|
1210
|
+
continue
|
|
1211
|
+
name = record.get(label_key, '')
|
|
1212
|
+
valid_name = _search_text(name) and isinstance(name, str) and bool(name.strip())
|
|
1213
|
+
if scope == 'rules':
|
|
1214
|
+
secondary = (record.get('infobase-name', ''),
|
|
1215
|
+
record.get('application-ext', ''))
|
|
1216
|
+
else:
|
|
1217
|
+
secondary = (record.get('infobase-name', ''),)
|
|
1218
|
+
fields = (name, *secondary, record_id)
|
|
1219
|
+
if any(isinstance(field, str) and _search_text(field)
|
|
1220
|
+
and term.casefold() in field.casefold()
|
|
1221
|
+
for field in fields):
|
|
1222
|
+
label = (' · '.join(field for field in fields[:-1]
|
|
1223
|
+
if isinstance(field, str) and field.strip()
|
|
1224
|
+
and _search_text(field))
|
|
1225
|
+
if scope == 'rules' else
|
|
1226
|
+
name if valid_name else '')
|
|
1227
|
+
found.append((index, host, port, protocol, scope,
|
|
1228
|
+
ident, f'{server_id}/{record_id}',
|
|
1229
|
+
label or ('Assignment rule' if scope == 'rules'
|
|
1230
|
+
else 'Service setting')))
|
|
1231
|
+
except Exception:
|
|
1232
|
+
found.append((index, host, port, protocol, 'error', '', '', 'Unavailable'))
|
|
1233
|
+
elif scope == 'services':
|
|
1234
|
+
for record in await client.list_services(UUID(ident)):
|
|
1235
|
+
if not isinstance(record, dict):
|
|
1236
|
+
continue
|
|
1237
|
+
name = record.get('name')
|
|
1238
|
+
description = record.get('descr', '')
|
|
1239
|
+
if not isinstance(name, str) or not name or not _search_text(name):
|
|
1240
|
+
continue
|
|
1241
|
+
manager = record.get('manager')
|
|
1242
|
+
if (term.casefold() in name.casefold()
|
|
1243
|
+
or _search_text(description)
|
|
1244
|
+
and term.casefold() in description.casefold()
|
|
1245
|
+
or isinstance(manager, str) and _search_text(manager)
|
|
1246
|
+
and term.casefold() in manager.casefold()):
|
|
1247
|
+
found.append((index, host, port, protocol, scope,
|
|
1248
|
+
ident, '', name))
|
|
1249
|
+
elif scope == 'locks':
|
|
1250
|
+
for record in await client.list_locks(UUID(ident)):
|
|
1251
|
+
if not isinstance(record, dict):
|
|
1252
|
+
continue
|
|
1253
|
+
try:
|
|
1254
|
+
connection_id = str(UUID(str(record['connection'])))
|
|
1255
|
+
object_id = str(UUID(str(record['object'])))
|
|
1256
|
+
except (KeyError, TypeError, ValueError):
|
|
1257
|
+
continue
|
|
1258
|
+
description = record.get('lock-descr', '')
|
|
1259
|
+
valid_description = _search_text(description)
|
|
1260
|
+
if (term.casefold() in object_id.casefold()
|
|
1261
|
+
or term.casefold() in connection_id.casefold()
|
|
1262
|
+
or valid_description and term.casefold() in description.casefold()):
|
|
1263
|
+
label = description if valid_description and description.strip() else 'Lock'
|
|
1264
|
+
found.append((index, host, port, protocol, scope,
|
|
1265
|
+
ident, connection_id,
|
|
1266
|
+
label))
|
|
1267
|
+
elif scope == 'servers':
|
|
1268
|
+
for server in await client.list_servers(UUID(ident)):
|
|
1269
|
+
try:
|
|
1270
|
+
server_id = str(UUID(str(server['server'])))
|
|
1271
|
+
except (KeyError, TypeError, ValueError):
|
|
1272
|
+
continue
|
|
1273
|
+
server_name = server.get('name', '')
|
|
1274
|
+
valid_name = _search_text(server_name) and bool(server_name.strip())
|
|
1275
|
+
server_host = server.get('host_name', '')
|
|
1276
|
+
valid_host = _search_text(server_host) and bool(server_host.strip())
|
|
1277
|
+
if (term.casefold() in server_id.casefold()
|
|
1278
|
+
or valid_name and term.casefold() in server_name.casefold()
|
|
1279
|
+
or valid_host and term.casefold() in server_host.casefold()):
|
|
1280
|
+
found.append((index, host, port, protocol, 'server', ident,
|
|
1281
|
+
server_id, server_name if valid_name else
|
|
1282
|
+
server_host if valid_host and server_host else 'Unnamed server'))
|
|
1283
|
+
elif scope == 'processes':
|
|
1284
|
+
for process in await client.list_processes(UUID(ident)):
|
|
1285
|
+
try:
|
|
1286
|
+
process_id = str(UUID(str(process['process'])))
|
|
1287
|
+
except (KeyError, TypeError, ValueError):
|
|
1288
|
+
continue
|
|
1289
|
+
pid = process.get('pid')
|
|
1290
|
+
valid_pid = (type(pid) is str and pid.isascii()
|
|
1291
|
+
and pid.isdecimal() and len(pid) <= 20)
|
|
1292
|
+
process_host = process.get('host', '')
|
|
1293
|
+
if (isinstance(pid, str) and valid_pid
|
|
1294
|
+
and term.casefold() in pid.casefold()
|
|
1295
|
+
or term.casefold() in process_id.casefold()
|
|
1296
|
+
or _search_text(process_host)
|
|
1297
|
+
and term.casefold() in process_host.casefold()):
|
|
1298
|
+
label = f'PID {pid}' if valid_pid else 'Working process'
|
|
1299
|
+
found.append((index, host, port, protocol, 'process', ident,
|
|
1300
|
+
process_id, label))
|
|
1301
|
+
elif scope == 'sessions':
|
|
1302
|
+
for session in await client.list_sessions(UUID(ident)):
|
|
1303
|
+
try:
|
|
1304
|
+
session_id = str(UUID(str(session['session_id'])))
|
|
1305
|
+
except (KeyError, TypeError, ValueError):
|
|
1306
|
+
continue
|
|
1307
|
+
user = session.get('user_name', '')
|
|
1308
|
+
valid_user = _search_text(user) and bool(user.strip())
|
|
1309
|
+
number = session.get('id')
|
|
1310
|
+
number_matches = (type(number) is int and number >= 0
|
|
1311
|
+
and term == str(number))
|
|
1312
|
+
if (number_matches or any(isinstance(field, str)
|
|
1313
|
+
and _search_text(field)
|
|
1314
|
+
and term.casefold() in field.casefold()
|
|
1315
|
+
for field in (user, session.get('host'),
|
|
1316
|
+
session.get('app_id'), session_id))):
|
|
1317
|
+
display = (user if valid_user else
|
|
1318
|
+
f'Session {number}' if type(number) is int and number >= 0
|
|
1319
|
+
else 'Session')
|
|
1320
|
+
found.append((index, host, port, protocol, 'session', ident,
|
|
1321
|
+
session_id, display))
|
|
1322
|
+
elif scope == 'profiles':
|
|
1323
|
+
for profile in await client.list_profiles(UUID(ident)):
|
|
1324
|
+
if not isinstance(profile, dict):
|
|
1325
|
+
continue
|
|
1326
|
+
name = profile.get('name')
|
|
1327
|
+
if (not isinstance(name, str) or not name or not _search_text(name)):
|
|
1328
|
+
continue
|
|
1329
|
+
if term.casefold() in name.casefold():
|
|
1330
|
+
found.append((index, host, port, protocol, 'profile', ident,
|
|
1331
|
+
'', name))
|
|
1332
|
+
elif scope in SEARCH_PROFILE_ACL:
|
|
1333
|
+
acl_type, field = SEARCH_PROFILE_ACL[scope]
|
|
1334
|
+
for profile in await client.list_profiles(UUID(ident)):
|
|
1335
|
+
if not isinstance(profile, dict):
|
|
1336
|
+
continue
|
|
1337
|
+
name = profile.get('name')
|
|
1338
|
+
if (not isinstance(name, str) or not name.strip() or not _search_text(name)):
|
|
1339
|
+
continue
|
|
1340
|
+
try:
|
|
1341
|
+
records = await getattr(client, PROFILE_ACL_LISTS[acl_type])(
|
|
1342
|
+
UUID(ident), name)
|
|
1343
|
+
for record in records:
|
|
1344
|
+
if not isinstance(record, dict):
|
|
1345
|
+
continue
|
|
1346
|
+
alias = record.get(field)
|
|
1347
|
+
if (isinstance(alias, str) and alias and _search_text(alias)
|
|
1348
|
+
and term.casefold() in alias.casefold()):
|
|
1349
|
+
found.append((index, host, port, protocol, scope,
|
|
1350
|
+
ident, name, alias))
|
|
1351
|
+
except Exception:
|
|
1352
|
+
found.append((index, host, port, protocol, 'error', '', '',
|
|
1353
|
+
'Unavailable'))
|
|
1354
|
+
elif scope in SEARCH_CLUSTER_GROUPS:
|
|
1355
|
+
field, is_uuid = SEARCH_CLUSTER_GROUPS[scope]
|
|
1356
|
+
for record in await getattr(client, 'list_' + scope)(UUID(ident)):
|
|
1357
|
+
try:
|
|
1358
|
+
value = record[field]
|
|
1359
|
+
if is_uuid:
|
|
1360
|
+
value = str(UUID(str(value)))
|
|
1361
|
+
elif (not isinstance(value, str) or not value or
|
|
1362
|
+
'\x00' in value or len(value.encode('utf-8')) > 1024):
|
|
1363
|
+
continue
|
|
1364
|
+
except (KeyError, TypeError, ValueError):
|
|
1365
|
+
continue
|
|
1366
|
+
if scope == 'managers':
|
|
1367
|
+
manager_host = record.get('host', '')
|
|
1368
|
+
valid_host = _search_text(manager_host) and bool(manager_host.strip())
|
|
1369
|
+
fields = (value, manager_host, record.get('descr', ''),
|
|
1370
|
+
record.get('pid', ''))
|
|
1371
|
+
label = manager_host if valid_host and manager_host else 'Cluster manager'
|
|
1372
|
+
elif scope == 'connections':
|
|
1373
|
+
connection_host = record.get('host', '')
|
|
1374
|
+
valid_host = _search_text(connection_host) and bool(connection_host.strip())
|
|
1375
|
+
fields = (value, connection_host, record.get('application'))
|
|
1376
|
+
number = record.get('conn-id')
|
|
1377
|
+
if type(number) is int and number >= 0 and term == str(number):
|
|
1378
|
+
fields += (str(number),)
|
|
1379
|
+
label = (connection_host if valid_host and connection_host else
|
|
1380
|
+
f'Connection {number}' if type(number) is int and number >= 0
|
|
1381
|
+
else 'Connection')
|
|
1382
|
+
else:
|
|
1383
|
+
fields = (value, record.get('descr'))
|
|
1384
|
+
label = value
|
|
1385
|
+
if any(isinstance(field, str) and _search_text(field)
|
|
1386
|
+
and term.casefold() in field.casefold()
|
|
1387
|
+
for field in fields):
|
|
1388
|
+
found.append((index, host, port, protocol, scope, ident,
|
|
1389
|
+
value, label or value))
|
|
1390
|
+
elif scope in ('inventory', 'infobases'):
|
|
1391
|
+
for base in await client.list_infobases(UUID(ident)):
|
|
1392
|
+
try:
|
|
1393
|
+
base_id = str(UUID(str(base['infobase'])))
|
|
1394
|
+
except (KeyError, TypeError, ValueError):
|
|
1395
|
+
continue
|
|
1396
|
+
base_name = base.get('name', '')
|
|
1397
|
+
valid_name = _search_text(base_name) and bool(base_name.strip())
|
|
1398
|
+
if (term.casefold() in base_id.casefold()
|
|
1399
|
+
or valid_name and term.casefold() in base_name.casefold()):
|
|
1400
|
+
found.append((index, host, port, protocol, 'infobase', ident,
|
|
1401
|
+
base_id, base_name if valid_name else 'Unnamed infobase'))
|
|
1402
|
+
except Exception:
|
|
1403
|
+
found.append((index, host, port, protocol, 'error', '', '', 'Unavailable'))
|
|
1404
|
+
except Exception:
|
|
1405
|
+
# No server exception text in HTML (may contain private connection data).
|
|
1406
|
+
found.append((index, host, port, protocol, 'error', '', '', 'Unavailable'))
|
|
1407
|
+
return found
|
|
1408
|
+
|
|
1409
|
+
groups = await asyncio.gather(*(query_server(index, *endpoint)
|
|
1410
|
+
for index, endpoint in tuple(servers.items())))
|
|
1411
|
+
return [row for group in groups for row in group]
|
|
1412
|
+
|
|
1413
|
+
rows = asyncio.run(inventory()) if term else []
|
|
1414
|
+
items = []
|
|
1415
|
+
for index, host, port, protocol, kind, cluster, base, name in rows:
|
|
1416
|
+
address = html.escape(f'{host}:{port}')
|
|
1417
|
+
label = html.escape(name)
|
|
1418
|
+
if kind == 'error':
|
|
1419
|
+
items.append(f'<tr><td>{address}</td><td>{html.escape(protocol)}</td>'
|
|
1420
|
+
'<td>Unavailable</td><td>—</td></tr>')
|
|
1421
|
+
continue
|
|
1422
|
+
target = f'/ras/{index}/clusters/{cluster}'
|
|
1423
|
+
if kind == 'agent-admins':
|
|
1424
|
+
target = f'/ras/{index}/agent/admins'
|
|
1425
|
+
elif kind == 'cluster-admins':
|
|
1426
|
+
target += '/admins'
|
|
1427
|
+
elif kind == 'services':
|
|
1428
|
+
target += '/services'
|
|
1429
|
+
elif kind == 'locks':
|
|
1430
|
+
target += f'/connections/{base}/locks'
|
|
1431
|
+
elif kind == 'infobase':
|
|
1432
|
+
target += f'/infobases/{base}/summary'
|
|
1433
|
+
elif kind == 'server':
|
|
1434
|
+
target += f'/servers/{base}'
|
|
1435
|
+
elif kind == 'process':
|
|
1436
|
+
target += f'/processes/{base}'
|
|
1437
|
+
elif kind == 'session':
|
|
1438
|
+
target += f'/sessions/{base}'
|
|
1439
|
+
elif kind == 'profile':
|
|
1440
|
+
target += '/profiles/overview?' + urlencode({'name': name})
|
|
1441
|
+
elif kind in SEARCH_PROFILE_ACL:
|
|
1442
|
+
target += (f'/profiles/{quote(base, safe="")}/acl/{SEARCH_PROFILE_ACL[kind][0]}'
|
|
1443
|
+
'?' + urlencode({'name': name}))
|
|
1444
|
+
elif kind in SEARCH_CLUSTER_GROUPS:
|
|
1445
|
+
target += f'/{kind}/{quote(base, safe="")}'
|
|
1446
|
+
elif kind == 'transfer-directories':
|
|
1447
|
+
target += f'/servers/{base}/transfer-directories?' + urlencode({'service_name': name})
|
|
1448
|
+
elif kind in SEARCH_SERVER_GROUPS:
|
|
1449
|
+
server_id, record_id = base.split('/')
|
|
1450
|
+
target += f'/servers/{server_id}/{kind}/{record_id}'
|
|
1451
|
+
attributes = {'ras': f'{host}:{port}', 'protocol': protocol, 'type': kind,
|
|
1452
|
+
'object': name, 'cluster': cluster or '—'}
|
|
1453
|
+
inspection = ''.join(f' data-inspect-{key}="{html.escape(value, quote=True)}"'
|
|
1454
|
+
for key, value in attributes.items())
|
|
1455
|
+
items.append(f'<tr class="selectable-row" tabindex="0" aria-selected="false"{inspection}>'
|
|
1456
|
+
f'<td>{address}</td><td>{html.escape(protocol)}</td>'
|
|
1457
|
+
f'<td>{html.escape(SEARCH_KIND_NAMES.get(kind, SECTION_NAMES.get(kind, kind)))}</td><td><a href="{html.escape(target, quote=True)}">'
|
|
1458
|
+
f'{label}</a></td></tr>')
|
|
1459
|
+
body = ('<section><h1>Поиск по серверам RAS</h1><p><a href="/ras">Серверы RAS</a></p>'
|
|
1460
|
+
'<form method="get" action="/ras/search"><label>Имя, пользователь, PID или UUID '
|
|
1461
|
+
f'<input name="q" value="{html.escape(term, quote=True)}" maxlength="100"></label>'
|
|
1462
|
+
'<label>Область поиска <select name="scope"><option value="inventory"' +
|
|
1463
|
+
(' selected' if scope == 'inventory' else '') + '>Кластеры и базы</option>'
|
|
1464
|
+
'<option value="clusters"' + (' selected' if scope == 'clusters' else '') +
|
|
1465
|
+
'>Только кластеры</option>'
|
|
1466
|
+
'<option value="infobases"' + (' selected' if scope == 'infobases' else '') +
|
|
1467
|
+
'>Только базы</option>'
|
|
1468
|
+
'<option value="agent-admins"' + (' selected' if scope == 'agent-admins' else '') +
|
|
1469
|
+
'>Администраторы агента</option>'
|
|
1470
|
+
'<option value="cluster-admins"' + (' selected' if scope == 'cluster-admins' else '') +
|
|
1471
|
+
'>Администраторы кластера</option>'
|
|
1472
|
+
'<option value="services"' + (' selected' if scope == 'services' else '') +
|
|
1473
|
+
'>Сервисы кластера</option>'
|
|
1474
|
+
'<option value="locks"' + (' selected' if scope == 'locks' else '') +
|
|
1475
|
+
'>Блокировки кластера</option>'
|
|
1476
|
+
'<option value="servers"' + (' selected' if scope == 'servers' else '') +
|
|
1477
|
+
'>Кластеры и рабочие серверы</option>'
|
|
1478
|
+
'<option value="processes"' + (' selected' if scope == 'processes' else '') +
|
|
1479
|
+
'>Кластеры и процессы</option>'
|
|
1480
|
+
'<option value="sessions"' + (' selected' if scope == 'sessions' else '') +
|
|
1481
|
+
'>Кластеры и сеансы</option>'
|
|
1482
|
+
'<option value="profiles"' + (' selected' if scope == 'profiles' else '') +
|
|
1483
|
+
'>Кластеры и профили безопасности</option>' +
|
|
1484
|
+
''.join(f'<option value="{kind}"' + (' selected' if scope == kind else '') +
|
|
1485
|
+
f'>{SEARCH_KIND_NAMES.get(kind, SECTION_NAMES.get(kind, kind))}</option>'
|
|
1486
|
+
for kind in SEARCH_PROFILE_ACL) +
|
|
1487
|
+
''.join(f'<option value="{kind}"' + (' selected' if scope == kind else '') +
|
|
1488
|
+
f'>Кластеры и {SECTION_NAMES.get(kind, kind)}</option>' for kind in SEARCH_CLUSTER_GROUPS) +
|
|
1489
|
+
''.join(f'<option value="{kind}"' + (' selected' if scope == kind else '') +
|
|
1490
|
+
f'>Серверы и {SECTION_NAMES.get(kind, kind)}</option>' for kind in SEARCH_SERVER_GROUPS) +
|
|
1491
|
+
'<option value="transfer-directories"' +
|
|
1492
|
+
(' selected' if scope == 'transfer-directories' else '') +
|
|
1493
|
+
'>Серверы и каталоги переноса</option>' +
|
|
1494
|
+
'</select></label>'
|
|
1495
|
+
'<button>Найти</button></form><div class="table-scroll">'
|
|
1496
|
+
'<table class="inventory-grid"><thead><tr><th scope="col">RAS</th>'
|
|
1497
|
+
'<th scope="col">Протокол</th><th scope="col">Тип</th>'
|
|
1498
|
+
'<th scope="col">Объект</th></tr></thead><tbody>' + ''.join(items) + '</tbody></table></div>'
|
|
1499
|
+
f'<p>Найдено объектов: {len(items)}</p></section>')
|
|
1500
|
+
detail = ('<h2>Выбор</h2><p>Выберите объект в таблице для просмотра подробностей.</p>'
|
|
1501
|
+
'<h2>Область поиска</h2><p>' +
|
|
1502
|
+
html.escape(SEARCH_KIND_NAMES.get(scope, SECTION_NAMES.get(scope, scope))) + '</p>')
|
|
1503
|
+
return page(environ, start_response, body, inspector=detail)
|
|
1504
|
+
if path in ('/ras', '/ras/'):
|
|
1505
|
+
if method == 'POST':
|
|
1506
|
+
host_header = environ.get('HTTP_HOST', '')
|
|
1507
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1508
|
+
if (environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1') or
|
|
1509
|
+
not _LOOPBACK.fullmatch(host_header) or origin.scheme != 'http' or
|
|
1510
|
+
origin.netloc != host_header or origin.path or origin.query or origin.fragment or
|
|
1511
|
+
environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded'):
|
|
1512
|
+
return page(environ, start_response, '<p>Invalid origin</p>', '403 Forbidden')
|
|
1513
|
+
try:
|
|
1514
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1515
|
+
if not 0 <= length <= 2048:
|
|
1516
|
+
raise ValueError('length')
|
|
1517
|
+
raw = environ['wsgi.input'].read(length)
|
|
1518
|
+
if len(raw) != length:
|
|
1519
|
+
raise ValueError('truncated')
|
|
1520
|
+
fields = parse_qs(raw.decode('utf-8'), strict_parsing=True)
|
|
1521
|
+
if set(fields) != {'csrf_token', 'host', 'port', 'protocol'} or any(
|
|
1522
|
+
len(values) != 1 for values in fields.values()):
|
|
1523
|
+
raise ValueError('fields')
|
|
1524
|
+
if not hmac.compare_digest(fields['csrf_token'][0], csrf_token):
|
|
1525
|
+
raise ValueError('token')
|
|
1526
|
+
host, port, protocol = (fields[name][0] for name in ('host', 'port', 'protocol'))
|
|
1527
|
+
if (not _HOST.fullmatch(host) or not port.isascii() or not port.isdecimal() or
|
|
1528
|
+
not 1 <= int(port) <= 65535 or protocol not in ('16.0', '10.0', 'auto') or
|
|
1529
|
+
len(servers) >= 64):
|
|
1530
|
+
raise ValueError('endpoint')
|
|
1531
|
+
endpoint = (host, int(port), protocol)
|
|
1532
|
+
if endpoint not in servers.values():
|
|
1533
|
+
servers[next_index] = endpoint
|
|
1534
|
+
next_index += 1
|
|
1535
|
+
except (ValueError, UnicodeError, KeyError):
|
|
1536
|
+
return page(environ, start_response, '<p>Invalid RAS endpoint</p>', '403 Forbidden')
|
|
1537
|
+
elif method != 'GET':
|
|
1538
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST')])
|
|
1539
|
+
return [b'']
|
|
1540
|
+
cards = ''.join(f'<tr><td><a href="/ras/{index}/clusters">{html.escape(host)}:{port}'
|
|
1541
|
+
f'</a></td><td>{html.escape(protocol)}</td><td>Локальное подключение</td><td>'
|
|
1542
|
+
'<form method="post" action="/ras/remove">'
|
|
1543
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1544
|
+
f'<input type="hidden" name="index" value="{index}">'
|
|
1545
|
+
f'<input type="hidden" name="confirm" value="{html.escape(f"{host}:{port}/{protocol}", quote=True)}">'
|
|
1546
|
+
'<button type="submit" class="danger-button">Удалить</button></form></td></tr>'
|
|
1547
|
+
for index, (host, port, protocol) in servers.items())
|
|
1548
|
+
body = ('<section><h1>Серверы RAS</h1><p>Выберите подключение для просмотра кластеров и баз. '
|
|
1549
|
+
'<a href="/ras/search">Поиск по серверам</a> · '
|
|
1550
|
+
'<a href="/ras/clusters">Все кластеры</a> · '
|
|
1551
|
+
'<a href="/ras/infobases">Все базы</a> · '
|
|
1552
|
+
'<a href="/ras/servers">Рабочие серверы</a> · '
|
|
1553
|
+
'<a href="/ras/processes">Рабочие процессы</a> · '
|
|
1554
|
+
'<a href="/ras/sessions">Сеансы</a> · '
|
|
1555
|
+
'<a href="/ras/connections">Соединения</a> · '
|
|
1556
|
+
'<a href="/ras/locks">Блокировки</a> · '
|
|
1557
|
+
'<a href="/ras/services">Сервисы</a> · '
|
|
1558
|
+
'<a href="/ras/profiles">Профили безопасности</a> · '
|
|
1559
|
+
'<a href="/ras/administrators?kind=agent-admins">Администраторы агента</a> · '
|
|
1560
|
+
'<a href="/ras/administrators?kind=cluster-admins">Администраторы кластера</a> · '
|
|
1561
|
+
'<a href="/ras/cluster-inventory?kind=managers">Менеджеры</a> · '
|
|
1562
|
+
'<a href="/ras/cluster-inventory?kind=counters">Счётчики</a> · '
|
|
1563
|
+
'<a href="/ras/cluster-inventory?kind=limits">Ограничения</a> · '
|
|
1564
|
+
'<a href="/ras/server-inventory?kind=rules">Правила</a> · '
|
|
1565
|
+
'<a href="/ras/server-inventory?kind=service-settings">Настройки сервисов</a> · '
|
|
1566
|
+
'<a href="/ras/server-inventory?kind=transfer-directories">Каталоги переноса</a></p>'
|
|
1567
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
1568
|
+
'<th>Сервер RAS</th><th>Протокол</th><th>Режим</th><th>Действия</th>'
|
|
1569
|
+
'</tr></thead><tbody>' + cards + '</tbody></table></div><form method="post" action="/ras">'
|
|
1570
|
+
'<h2><span class="ui-icon" aria-hidden="true">+</span> Добавить сервер RAS</h2><input type="hidden" name="csrf_token" value="' +
|
|
1571
|
+
html.escape(csrf_token, quote=True) + '"><label>Адрес <input name="host" required></label>'
|
|
1572
|
+
'<label>Порт <input name="port" type="number" min="1" max="65535" value="1545" required></label>'
|
|
1573
|
+
'<label>Протокол <select name="protocol"><option>16.0</option><option>10.0</option><option>auto</option>'
|
|
1574
|
+
'</select></label><button>Добавить сервер</button></form></section>')
|
|
1575
|
+
return page(environ, start_response, body,
|
|
1576
|
+
inspector='<h2>Рабочая область</h2><dl><dt>Режим</dt><dd>Только чтение</dd>'
|
|
1577
|
+
f'<dt>Серверов</dt><dd>{len(servers)}</dd></dl>'
|
|
1578
|
+
'<p>Подключения хранятся только в памяти. Учётные данные не сохраняются.</p>')
|
|
1579
|
+
match = _ENDPOINT.fullmatch(path)
|
|
1580
|
+
if match:
|
|
1581
|
+
if method != 'GET':
|
|
1582
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET')])
|
|
1583
|
+
return [b'']
|
|
1584
|
+
if len(match.group(1)) > 16:
|
|
1585
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1586
|
+
return [b'Unknown RAS server']
|
|
1587
|
+
index = int(match.group(1))
|
|
1588
|
+
if index not in servers:
|
|
1589
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1590
|
+
return [b'Unknown RAS server']
|
|
1591
|
+
host, port, protocol = servers[index]
|
|
1592
|
+
server_tree = re.fullmatch(
|
|
1593
|
+
r'clusters/([0-9a-fA-F-]{36})/servers/tree', match.group(2))
|
|
1594
|
+
profile_tree = re.fullmatch(
|
|
1595
|
+
r'clusters/([0-9a-fA-F-]{36})/profiles/tree', match.group(2))
|
|
1596
|
+
process_tree = re.fullmatch(
|
|
1597
|
+
r'clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/processes/tree',
|
|
1598
|
+
match.group(2))
|
|
1599
|
+
connection_tree = re.fullmatch(
|
|
1600
|
+
r'clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/processes/'
|
|
1601
|
+
r'([0-9a-fA-F-]{36})/connections/tree', match.group(2))
|
|
1602
|
+
if match.group(2) == 'tree' or server_tree or profile_tree or process_tree or connection_tree:
|
|
1603
|
+
def has_uuid(rows, field, selected):
|
|
1604
|
+
for row in rows:
|
|
1605
|
+
try:
|
|
1606
|
+
if str(UUID(str(row[field]))) == selected:
|
|
1607
|
+
return True
|
|
1608
|
+
except (KeyError, TypeError, ValueError):
|
|
1609
|
+
continue
|
|
1610
|
+
return False
|
|
1611
|
+
|
|
1612
|
+
selected_cluster = None
|
|
1613
|
+
selected_server = None
|
|
1614
|
+
selected_process = None
|
|
1615
|
+
tree_match = server_tree or profile_tree or process_tree or connection_tree
|
|
1616
|
+
if tree_match is not None:
|
|
1617
|
+
try:
|
|
1618
|
+
selected_cluster = str(UUID(tree_match.group(1)))
|
|
1619
|
+
if process_tree or connection_tree:
|
|
1620
|
+
selected_server = str(UUID(tree_match.group(2)))
|
|
1621
|
+
if connection_tree:
|
|
1622
|
+
selected_process = str(UUID(connection_tree.group(3)))
|
|
1623
|
+
except ValueError:
|
|
1624
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1625
|
+
return [b'Unknown cluster']
|
|
1626
|
+
async def load_tree():
|
|
1627
|
+
result = []
|
|
1628
|
+
async with asyncio.timeout(SEARCH_ENDPOINT_TIMEOUT):
|
|
1629
|
+
async with RacClient(host, port, protocol) as client:
|
|
1630
|
+
for cluster in await client.list_clusters():
|
|
1631
|
+
try:
|
|
1632
|
+
cluster_id = str(UUID(str(cluster['cluster'])))
|
|
1633
|
+
except (ValueError, KeyError, TypeError):
|
|
1634
|
+
continue
|
|
1635
|
+
bases = []
|
|
1636
|
+
if selected_cluster is None:
|
|
1637
|
+
try:
|
|
1638
|
+
for base in await client.list_infobases(UUID(cluster_id)):
|
|
1639
|
+
try:
|
|
1640
|
+
base_id = str(UUID(str(base['infobase'])))
|
|
1641
|
+
except (KeyError, TypeError, ValueError):
|
|
1642
|
+
continue
|
|
1643
|
+
base_name = safe_tree_name(base.get('name'), 'Unnamed infobase')
|
|
1644
|
+
bases.append((base_id, base_name))
|
|
1645
|
+
except Exception:
|
|
1646
|
+
bases.append((None, 'Unavailable'))
|
|
1647
|
+
cluster_name = safe_tree_name(cluster.get('name'), 'Unnamed cluster')
|
|
1648
|
+
result.append((cluster_id, cluster_name, bases))
|
|
1649
|
+
working_servers = []
|
|
1650
|
+
profiles = []
|
|
1651
|
+
profile_error = False
|
|
1652
|
+
if profile_tree and selected_cluster is not None and any(
|
|
1653
|
+
row[0] == selected_cluster for row in result):
|
|
1654
|
+
try:
|
|
1655
|
+
for profile in await client.list_profiles(UUID(selected_cluster)):
|
|
1656
|
+
if not isinstance(profile, dict):
|
|
1657
|
+
continue
|
|
1658
|
+
name = profile.get('name')
|
|
1659
|
+
if (isinstance(name, str) and name.strip() and '\x00' not in name
|
|
1660
|
+
and safe_tree_name(name, '') == name):
|
|
1661
|
+
profiles.append(name)
|
|
1662
|
+
except Exception:
|
|
1663
|
+
profiles = []
|
|
1664
|
+
profile_error = True
|
|
1665
|
+
if selected_cluster is not None and not profile_tree and any(
|
|
1666
|
+
row[0] == selected_cluster for row in result):
|
|
1667
|
+
working_servers = await client.list_servers(UUID(selected_cluster))
|
|
1668
|
+
processes = []
|
|
1669
|
+
if selected_server is not None and has_uuid(working_servers, 'server', selected_server):
|
|
1670
|
+
for process in await client.list_processes(
|
|
1671
|
+
UUID(selected_cluster), UUID(selected_server)):
|
|
1672
|
+
if not isinstance(process, dict):
|
|
1673
|
+
continue
|
|
1674
|
+
try:
|
|
1675
|
+
UUID(str(process['process']))
|
|
1676
|
+
except (KeyError, TypeError, ValueError):
|
|
1677
|
+
continue
|
|
1678
|
+
pid = process.get('pid')
|
|
1679
|
+
if (type(pid) is not str or not pid.isascii() or not pid.isdecimal()
|
|
1680
|
+
or len(pid) > 20):
|
|
1681
|
+
process = dict(process, pid='Working process')
|
|
1682
|
+
processes.append(process)
|
|
1683
|
+
connections = []
|
|
1684
|
+
if selected_process is not None and has_uuid(processes, 'process', selected_process):
|
|
1685
|
+
for connection in await client.list_connections(
|
|
1686
|
+
UUID(selected_cluster), process_id=UUID(selected_process)):
|
|
1687
|
+
if not isinstance(connection, dict):
|
|
1688
|
+
continue
|
|
1689
|
+
try:
|
|
1690
|
+
UUID(str(connection['connection']))
|
|
1691
|
+
except (KeyError, TypeError, ValueError):
|
|
1692
|
+
continue
|
|
1693
|
+
connection_host = safe_tree_name(connection.get('host'), 'Connection')
|
|
1694
|
+
connections.append(dict(connection, host=connection_host))
|
|
1695
|
+
return result, working_servers, processes, connections, profiles, profile_error
|
|
1696
|
+
|
|
1697
|
+
try:
|
|
1698
|
+
clusters, working_servers, processes, connections, profiles, profile_error = asyncio.run(load_tree())
|
|
1699
|
+
except Exception:
|
|
1700
|
+
clusters = ()
|
|
1701
|
+
working_servers = ()
|
|
1702
|
+
processes = ()
|
|
1703
|
+
connections = ()
|
|
1704
|
+
profiles = ()
|
|
1705
|
+
profile_error = False
|
|
1706
|
+
status = 'Unavailable'
|
|
1707
|
+
else:
|
|
1708
|
+
status = f'{len(clusters)} clusters'
|
|
1709
|
+
if selected_cluster is not None:
|
|
1710
|
+
if status == 'Unavailable':
|
|
1711
|
+
return page(environ, start_response, '<p>RAS unavailable</p>',
|
|
1712
|
+
'503 Service Unavailable')
|
|
1713
|
+
if not any(row[0] == selected_cluster for row in clusters):
|
|
1714
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1715
|
+
return [b'Unknown cluster']
|
|
1716
|
+
if selected_server is not None and not has_uuid(working_servers, 'server', selected_server):
|
|
1717
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1718
|
+
return [b'Unknown server']
|
|
1719
|
+
if selected_process is not None and not has_uuid(processes, 'process', selected_process):
|
|
1720
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1721
|
+
return [b'Unknown process']
|
|
1722
|
+
from ..read_service import SERVER_LISTS
|
|
1723
|
+
rows = []
|
|
1724
|
+
server_nodes = []
|
|
1725
|
+
for server in working_servers:
|
|
1726
|
+
try:
|
|
1727
|
+
server_id = str(UUID(str(server['server'])))
|
|
1728
|
+
except (KeyError, ValueError, TypeError):
|
|
1729
|
+
continue
|
|
1730
|
+
base = f'/ras/{index}/clusters/{selected_cluster}/servers/{server_id}'
|
|
1731
|
+
server_name = (safe_tree_name(server.get('name'), '') or
|
|
1732
|
+
safe_tree_name(server.get('host_name'), '') or 'Unnamed server')
|
|
1733
|
+
server_nodes.append((server_id, server_name))
|
|
1734
|
+
label = html.escape(server_name)
|
|
1735
|
+
links = (f'<a href="{base}">info</a> ' + ' '.join(
|
|
1736
|
+
f'<a href="{base}/{kind}">{html.escape(kind.replace("-", " "))}</a>'
|
|
1737
|
+
for kind in ('processes', *SERVER_LISTS)))
|
|
1738
|
+
attributes = (f'data-inspect-ras="{html.escape(f"{host}:{port}", quote=True)}" '
|
|
1739
|
+
f'data-inspect-protocol="{html.escape(protocol, quote=True)}" '
|
|
1740
|
+
f'data-inspect-cluster="{selected_cluster}" '
|
|
1741
|
+
'data-inspect-type="server" '
|
|
1742
|
+
f'data-inspect-object="{html.escape(server_name, quote=True)}"')
|
|
1743
|
+
rows.append(f'<tr data-searchable-row data-search-key="{server_id}" class="selectable-row" tabindex="0" aria-selected="false" '
|
|
1744
|
+
f'{attributes}><td>{label}</td>'
|
|
1745
|
+
f'<td>{links}</td></tr>')
|
|
1746
|
+
body = ('<section><h1>Working servers</h1><p>Cluster ' +
|
|
1747
|
+
html.escape(next((name for ident, name, _ in clusters if ident == selected_cluster),
|
|
1748
|
+
'Unnamed cluster')) + '</p>' + inventory_filter('Working servers') +
|
|
1749
|
+
'<div class="table-scroll">'
|
|
1750
|
+
'<table class="inventory-grid"><thead><tr><th>Name</th>'
|
|
1751
|
+
'<th>Sections</th></tr></thead><tbody>' +
|
|
1752
|
+
''.join(rows) + '</tbody></table></div></section>')
|
|
1753
|
+
if profile_tree:
|
|
1754
|
+
profile_rows = []
|
|
1755
|
+
for name in profiles:
|
|
1756
|
+
profile_url = (f'/ras/{index}/clusters/{selected_cluster}/profiles/'
|
|
1757
|
+
f'{quote(name, safe="")}')
|
|
1758
|
+
overview = (f'/ras/{index}/clusters/{selected_cluster}/profiles/overview?'
|
|
1759
|
+
+ urlencode({'name': name}))
|
|
1760
|
+
links = ' '.join(f'<a href="{profile_url}/acl/{kind}">'
|
|
1761
|
+
f'{html.escape(kind.title())}</a>' for kind in PROFILE_ACL_LISTS)
|
|
1762
|
+
attributes = (f'data-inspect-ras="{html.escape(f"{host}:{port}", quote=True)}" '
|
|
1763
|
+
f'data-inspect-protocol="{html.escape(protocol, quote=True)}" '
|
|
1764
|
+
f'data-inspect-cluster="{selected_cluster}" '
|
|
1765
|
+
'data-inspect-type="profile" '
|
|
1766
|
+
f'data-inspect-object="{html.escape(name, quote=True)}"')
|
|
1767
|
+
profile_rows.append(
|
|
1768
|
+
'<tr data-searchable-row class="selectable-row" tabindex="0" '
|
|
1769
|
+
f'aria-selected="false" {attributes}><td><a href="{overview}">'
|
|
1770
|
+
f'{html.escape(name)}</a></td><td>{links}</td></tr>')
|
|
1771
|
+
body = ('<section><h1>Security profiles</h1><p>Cluster ' +
|
|
1772
|
+
html.escape(next((name for ident, name, _ in clusters if ident == selected_cluster),
|
|
1773
|
+
'Unnamed cluster')) + '</p>' + inventory_filter('Security profiles') +
|
|
1774
|
+
'<div class="table-scroll"><table class="inventory-grid">'
|
|
1775
|
+
'<thead><tr><th>Name</th><th>ACL sections</th></tr></thead><tbody>' +
|
|
1776
|
+
(('<tr><td>Unavailable</td><td>—</td></tr>') if profile_error else
|
|
1777
|
+
''.join(profile_rows)) + '</tbody></table></div></section>')
|
|
1778
|
+
process_nodes = []
|
|
1779
|
+
connection_nodes = []
|
|
1780
|
+
if selected_server is not None:
|
|
1781
|
+
process_rows = []
|
|
1782
|
+
for process in processes:
|
|
1783
|
+
try:
|
|
1784
|
+
process_id = str(UUID(str(process['process'])))
|
|
1785
|
+
except (KeyError, ValueError, TypeError):
|
|
1786
|
+
continue
|
|
1787
|
+
pid = str(process.get('pid', ''))
|
|
1788
|
+
process_nodes.append((process_id, pid))
|
|
1789
|
+
url = f'/ras/{index}/clusters/{selected_cluster}/processes/{process_id}'
|
|
1790
|
+
attributes = (f'data-inspect-ras="{html.escape(f"{host}:{port}", quote=True)}" '
|
|
1791
|
+
f'data-inspect-protocol="{html.escape(protocol, quote=True)}" '
|
|
1792
|
+
f'data-inspect-cluster="{selected_cluster}" '
|
|
1793
|
+
'data-inspect-type="process" '
|
|
1794
|
+
f'data-inspect-object="{html.escape(pid, quote=True)}"')
|
|
1795
|
+
process_rows.append(
|
|
1796
|
+
f'<tr data-searchable-row data-search-key="{process_id}" class="selectable-row" tabindex="0" aria-selected="false" '
|
|
1797
|
+
f'{attributes}><td><a href="{url}">{html.escape(pid)}</a></td>'
|
|
1798
|
+
f'<td><a href="{url}/connections">'
|
|
1799
|
+
'connections</a></td></tr>')
|
|
1800
|
+
body += ('<section><h2>Processes</h2>' + inventory_filter('Processes') +
|
|
1801
|
+
'<div class="table-scroll">'
|
|
1802
|
+
'<table class="inventory-grid"><thead><tr><th>PID</th>'
|
|
1803
|
+
'<th>Sections</th></tr></thead><tbody>' + ''.join(process_rows) +
|
|
1804
|
+
'</tbody></table></div></section>')
|
|
1805
|
+
if selected_process is not None:
|
|
1806
|
+
connection_rows = []
|
|
1807
|
+
for connection in connections:
|
|
1808
|
+
try:
|
|
1809
|
+
connection_id = str(UUID(str(connection['connection'])))
|
|
1810
|
+
except (KeyError, ValueError, TypeError):
|
|
1811
|
+
continue
|
|
1812
|
+
connection_host = str(connection.get('host', ''))
|
|
1813
|
+
connection_nodes.append((connection_id, connection_host))
|
|
1814
|
+
url = f'/ras/{index}/clusters/{selected_cluster}/connections/{connection_id}'
|
|
1815
|
+
attributes = (f'data-inspect-ras="{html.escape(f"{host}:{port}", quote=True)}" '
|
|
1816
|
+
f'data-inspect-protocol="{html.escape(protocol, quote=True)}" '
|
|
1817
|
+
f'data-inspect-cluster="{selected_cluster}" '
|
|
1818
|
+
'data-inspect-type="connection" '
|
|
1819
|
+
f'data-inspect-object="{html.escape(connection_host, quote=True)}"')
|
|
1820
|
+
connection_rows.append(
|
|
1821
|
+
f'<tr data-searchable-row data-search-key="{connection_id}" class="selectable-row" tabindex="0" '
|
|
1822
|
+
f'aria-selected="false" {attributes}><td><a href="{url}">'
|
|
1823
|
+
f'{html.escape(connection_host)}</a></td>'
|
|
1824
|
+
f'<td><a href="{url}/locks">locks</a></td></tr>')
|
|
1825
|
+
body += ('<section><h2>Connections</h2>' + inventory_filter('Connections') +
|
|
1826
|
+
'<div class="table-scroll"><table class="inventory-grid">'
|
|
1827
|
+
'<thead><tr><th>Host</th><th>Sections</th></tr></thead>'
|
|
1828
|
+
'<tbody>' + ''.join(connection_rows) + '</tbody></table></div></section>')
|
|
1829
|
+
return page(environ, start_response, body,
|
|
1830
|
+
expanded_index=index, clusters=clusters,
|
|
1831
|
+
expanded_cluster=selected_cluster, servers_for_cluster=server_nodes,
|
|
1832
|
+
expanded_server=selected_server, processes_for_server=process_nodes,
|
|
1833
|
+
expanded_process=selected_process, connections_for_process=connection_nodes,
|
|
1834
|
+
profiles_for_cluster=profiles)
|
|
1835
|
+
rows = []
|
|
1836
|
+
for cluster_id, name, bases in clusters:
|
|
1837
|
+
cluster_url = f'/ras/{index}/clusters/{cluster_id}'
|
|
1838
|
+
cluster_label = html.escape(name)
|
|
1839
|
+
attributes = (f'data-inspect-ras="{html.escape(f"{host}:{port}", quote=True)}" '
|
|
1840
|
+
f'data-inspect-protocol="{html.escape(protocol, quote=True)}" '
|
|
1841
|
+
f'data-inspect-cluster="{cluster_id}"')
|
|
1842
|
+
rows.append(f'<tr data-searchable-row data-search-key="{cluster_id}" class="selectable-row" tabindex="0" aria-selected="false" '
|
|
1843
|
+
f'{attributes} data-inspect-type="cluster" '
|
|
1844
|
+
f'data-inspect-object="{html.escape(name, quote=True)}">'
|
|
1845
|
+
f'<td><a href="{cluster_url}">{cluster_label}</a></td>'
|
|
1846
|
+
'<td>Cluster</td></tr>')
|
|
1847
|
+
for base_id, base_name in bases:
|
|
1848
|
+
if base_id is None:
|
|
1849
|
+
rows.append('<tr><td>Unavailable</td><td>Infobases</td></tr>')
|
|
1850
|
+
continue
|
|
1851
|
+
base_url = f'{cluster_url}/infobases/{base_id}/summary'
|
|
1852
|
+
rows.append(f'<tr data-searchable-row data-search-key="{base_id}" class="selectable-row" tabindex="0" aria-selected="false" '
|
|
1853
|
+
f'{attributes} data-inspect-type="infobase" '
|
|
1854
|
+
f'data-inspect-object="{html.escape(base_name, quote=True)}">'
|
|
1855
|
+
f'<td>↳ <a href="{base_url}">{html.escape(base_name)}</a></td>'
|
|
1856
|
+
'<td>Infobase</td></tr>')
|
|
1857
|
+
body = ('<section><h1>RAS navigation</h1><p>' + html.escape(f'{host}:{port}') +
|
|
1858
|
+
' · ' + html.escape(protocol) + '</p><p>' + status +
|
|
1859
|
+
'</p><p>Select a cluster or infobase in the tree, or '
|
|
1860
|
+
f'<a href="/ras/{index}/clusters">open the cluster table</a>.</p>' +
|
|
1861
|
+
inventory_filter('Clusters and infobases') +
|
|
1862
|
+
'<div class="table-scroll"><table class="inventory-grid"><thead><tr>'
|
|
1863
|
+
'<th>Name</th><th>Type</th></tr></thead><tbody>' +
|
|
1864
|
+
''.join(rows) + '</tbody></table></div></section>')
|
|
1865
|
+
return page(environ, start_response, body, expanded_index=index, clusters=clusters,
|
|
1866
|
+
inspector='<h2>Selected RAS</h2><dl><dt>Endpoint</dt><dd>' +
|
|
1867
|
+
html.escape(f'{host}:{port}') + '</dd><dt>Protocol</dt><dd>' +
|
|
1868
|
+
html.escape(protocol) + '</dd></dl>')
|
|
1869
|
+
prefix = f'/ras/{index}'
|
|
1870
|
+
scoped = dict(environ, PATH_INFO='/' + match.group(2))
|
|
1871
|
+
# Prefix only local absolute links/forms. External links are never rewritten.
|
|
1872
|
+
def respond(status, headers, exc_info=None):
|
|
1873
|
+
scoped['_response_status'] = status
|
|
1874
|
+
scoped['_response_headers'] = headers
|
|
1875
|
+
return start_response(status, headers, exc_info)
|
|
1876
|
+
chunks = create_app(lambda: RacClient(host, port, protocol),
|
|
1877
|
+
read_timeout=SEARCH_ENDPOINT_TIMEOUT)(scoped, respond)
|
|
1878
|
+
content_type = next((value for name, value in scoped.get('_response_headers', [])
|
|
1879
|
+
if name.lower() == 'content-type'), '')
|
|
1880
|
+
if 'text/html' not in content_type:
|
|
1881
|
+
return chunks
|
|
1882
|
+
scoped_html = b''.join(re.sub(
|
|
1883
|
+
rb'\b(href|action)="/(?!ras(?:/|"))',
|
|
1884
|
+
lambda hit: hit.group(1) + b'="' + prefix.encode() + b'/', chunk)
|
|
1885
|
+
for chunk in chunks)
|
|
1886
|
+
if scoped.get('_response_status') != '200 OK':
|
|
1887
|
+
return [scoped_html]
|
|
1888
|
+
# The read-only dashboard returns a complete document. Reuse only its
|
|
1889
|
+
# generated main content, not its standalone navigation or scripts.
|
|
1890
|
+
_head, marker, rest = scoped_html.partition(b'<main>')
|
|
1891
|
+
content, end, after = rest.rpartition(b'</main>')
|
|
1892
|
+
if not marker or not end or b'</body>' not in after:
|
|
1893
|
+
return [scoped_html]
|
|
1894
|
+
return [render_document('RAS workspace', content.decode('utf-8'),
|
|
1895
|
+
workspace_tree=navigation(),
|
|
1896
|
+
inspector='<h2>Selected RAS</h2><dl><dt>Endpoint</dt><dd>' +
|
|
1897
|
+
html.escape(f'{host}:{port}') + '</dd><dt>Protocol</dt><dd>' +
|
|
1898
|
+
html.escape(protocol) + '</dd></dl>').encode('utf-8')]
|
|
1899
|
+
if path in ('/', '/clusters'):
|
|
1900
|
+
return page(environ, start_response,
|
|
1901
|
+
'<section><h1>RAS workspace</h1><p><a href="/ras">Open servers →</a></p></section>')
|
|
1902
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
1903
|
+
return [b'Not found']
|
|
1904
|
+
|
|
1905
|
+
return workspace
|