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/app.py
ADDED
|
@@ -0,0 +1,2835 @@
|
|
|
1
|
+
"""Tiny read-only WSGI dashboard; bind to loopback unless protected by a proxy."""
|
|
2
|
+
import html
|
|
3
|
+
import asyncio
|
|
4
|
+
import hmac
|
|
5
|
+
import re
|
|
6
|
+
from urllib.parse import parse_qs, quote, unquote, urlsplit
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
from ..client import RacClient, RacError
|
|
9
|
+
from ..read_service import CLUSTER_LISTS, PROFILE_ACL_LISTS, SERVER_LISTS, ReadService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidAclQuery(ValueError):
|
|
13
|
+
"""An invalid client-supplied ACL filter, not a RAS failure."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def render_section(title, rows, path, *, allow_cluster_remove=False, allow_admin_remove=False):
|
|
17
|
+
rows = [ReadService.public_data(row) for row in rows]
|
|
18
|
+
searchable = title.lower() in ('clusters', 'infobases', 'sessions', 'infobase sessions',
|
|
19
|
+
'servers', 'working servers', 'processes', 'working processes',
|
|
20
|
+
'server processes',
|
|
21
|
+
'connections', 'process connections', 'infobase connections',
|
|
22
|
+
'managers', 'limits', 'counters', 'profiles', 'rules',
|
|
23
|
+
'service settings', 'agent administrators',
|
|
24
|
+
'cluster administrators', 'locks', 'infobase locks',
|
|
25
|
+
'connection locks', 'services', 'transfer directories',
|
|
26
|
+
'binary data storages', 'counter values',
|
|
27
|
+
'counter accumulated-values') or title.lower() in tuple(
|
|
28
|
+
kind + ' acl' for kind in PROFILE_ACL_LISTS)
|
|
29
|
+
if searchable:
|
|
30
|
+
infobase_cluster = (re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})(?:/infobases)?', path)
|
|
31
|
+
if title.lower() == 'infobases' else None)
|
|
32
|
+
scoped_kind = ('servers' if title.lower().endswith('servers') else 'processes')
|
|
33
|
+
scoped_cluster = (re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})(?:/(servers|processes))?', path)
|
|
34
|
+
if title.lower() in ('servers', 'working servers',
|
|
35
|
+
'processes', 'working processes') else None)
|
|
36
|
+
server_processes = (re.fullmatch(
|
|
37
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/processes', path)
|
|
38
|
+
if title.lower() == 'server processes' else None)
|
|
39
|
+
scoped_connections = (re.fullmatch(
|
|
40
|
+
r'/clusters/([0-9a-fA-F-]{36})/(processes|infobases)/([0-9a-fA-F-]{36})/connections', path)
|
|
41
|
+
if title.lower() in ('process connections', 'infobase connections') else None)
|
|
42
|
+
cluster_inventory = (re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/(connections|managers|limits|counters)', path)
|
|
43
|
+
if title.lower() in ('connections', 'managers', 'limits', 'counters') else None)
|
|
44
|
+
server_inventory = (re.fullmatch(
|
|
45
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/(rules|service-settings)', path)
|
|
46
|
+
if title.lower() in ('rules', 'service settings') else None)
|
|
47
|
+
profile_cluster = (re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/profiles', path)
|
|
48
|
+
if title.lower() == 'profiles' else None)
|
|
49
|
+
admin_list = (path == '/agent/admins' and title.lower() == 'agent administrators'
|
|
50
|
+
or re.fullmatch(r'/clusters/[0-9a-fA-F-]{36}/admins', path)
|
|
51
|
+
and title.lower() == 'cluster administrators')
|
|
52
|
+
session_cluster = (re.fullmatch(
|
|
53
|
+
r'/clusters/([0-9a-fA-F-]{36})(?:/infobases/[0-9a-fA-F-]{36})?/sessions', path)
|
|
54
|
+
if title.lower() in ('sessions', 'infobase sessions') else None)
|
|
55
|
+
columns = list(dict.fromkeys(key for row in rows for key in row))
|
|
56
|
+
hidden_key = None
|
|
57
|
+
if path == '/clusters':
|
|
58
|
+
hidden_key = 'cluster'
|
|
59
|
+
elif infobase_cluster:
|
|
60
|
+
hidden_key = 'infobase'
|
|
61
|
+
elif scoped_cluster:
|
|
62
|
+
hidden_key = 'server' if scoped_kind == 'servers' else 'process'
|
|
63
|
+
elif server_processes:
|
|
64
|
+
hidden_key = 'process'
|
|
65
|
+
elif scoped_connections:
|
|
66
|
+
hidden_key = 'connection'
|
|
67
|
+
elif cluster_inventory:
|
|
68
|
+
hidden_key = {'connections': 'connection', 'managers': 'manager'}.get(
|
|
69
|
+
cluster_inventory.group(2))
|
|
70
|
+
elif server_inventory:
|
|
71
|
+
hidden_key = {'rules': 'rule', 'service-settings': 'setting'}.get(
|
|
72
|
+
server_inventory.group(3))
|
|
73
|
+
elif session_cluster:
|
|
74
|
+
hidden_key = 'session_id'
|
|
75
|
+
if hidden_key is not None:
|
|
76
|
+
columns = [key for key in columns if key != hidden_key]
|
|
77
|
+
if hidden_key in ('rule', 'setting') and 'name' not in columns:
|
|
78
|
+
columns.insert(0, 'name')
|
|
79
|
+
if not columns:
|
|
80
|
+
columns = ['name']
|
|
81
|
+
heading = ''.join('<th scope="col">' + html.escape(str(key)) + '</th>' for key in columns)
|
|
82
|
+
if (path == '/clusters' or infobase_cluster or scoped_cluster or server_processes
|
|
83
|
+
or scoped_connections
|
|
84
|
+
or cluster_inventory or server_inventory or profile_cluster or admin_list):
|
|
85
|
+
heading += '<th scope="col">Actions</th>'
|
|
86
|
+
records = []
|
|
87
|
+
for row in rows:
|
|
88
|
+
cells = ''
|
|
89
|
+
for key in columns:
|
|
90
|
+
raw = row.get(key, '')
|
|
91
|
+
if hidden_key in ('rule', 'setting') and key == 'name':
|
|
92
|
+
if (not isinstance(raw, str) or not ReadService._valid_search_text(raw)
|
|
93
|
+
or not raw.strip()):
|
|
94
|
+
actual_key = 'object-type' if hidden_key == 'rule' else 'service-name'
|
|
95
|
+
actual = row.get(actual_key)
|
|
96
|
+
raw = (actual if isinstance(actual, str) and
|
|
97
|
+
ReadService._valid_search_text(actual) and actual.strip() else
|
|
98
|
+
'Assignment rule' if hidden_key == 'rule' else 'Service setting')
|
|
99
|
+
if key == 'name' and hidden_key in ('cluster', 'infobase', 'server'):
|
|
100
|
+
if (not isinstance(raw, str) or not ReadService._valid_search_text(raw)
|
|
101
|
+
or not raw.strip()):
|
|
102
|
+
host_key = 'host' if hidden_key == 'cluster' else 'host_name'
|
|
103
|
+
host = row.get(host_key)
|
|
104
|
+
if (hidden_key != 'infobase' and isinstance(host, str)
|
|
105
|
+
and ReadService._valid_search_text(host) and host.strip()):
|
|
106
|
+
raw = host
|
|
107
|
+
else:
|
|
108
|
+
raw = {'cluster': 'Unnamed cluster', 'infobase': 'Unnamed infobase',
|
|
109
|
+
'server': 'Unnamed server'}[hidden_key]
|
|
110
|
+
if (session_cluster and key == 'user_name' and
|
|
111
|
+
(not isinstance(raw, str) or not ReadService._valid_search_text(raw)
|
|
112
|
+
or not raw.strip())):
|
|
113
|
+
raw = ''
|
|
114
|
+
if hidden_key == 'process' and key == 'pid':
|
|
115
|
+
text = str(raw)
|
|
116
|
+
if (isinstance(raw, bool) or not isinstance(raw, (str, int))
|
|
117
|
+
or not text.isdecimal() or not ReadService._valid_search_text(text)):
|
|
118
|
+
raw = 'Working process'
|
|
119
|
+
if (hidden_key in ('connection', 'manager') and key == 'host' and
|
|
120
|
+
(not isinstance(raw, str) or not ReadService._valid_search_text(raw)
|
|
121
|
+
or not raw.strip())):
|
|
122
|
+
if hidden_key == 'manager':
|
|
123
|
+
raw = 'Cluster manager'
|
|
124
|
+
else:
|
|
125
|
+
number = row.get('conn-id')
|
|
126
|
+
raw = (f'Connection {number}' if type(number) is int and number >= 0
|
|
127
|
+
else 'Connection')
|
|
128
|
+
value = html.escape(str(raw))
|
|
129
|
+
cells += '<td>' + value + '</td>'
|
|
130
|
+
if session_cluster:
|
|
131
|
+
try:
|
|
132
|
+
session_id = str(UUID(str(row['session_id'])))
|
|
133
|
+
except (KeyError, ValueError, TypeError):
|
|
134
|
+
session_id = None
|
|
135
|
+
if session_id:
|
|
136
|
+
user = row.get('user_name')
|
|
137
|
+
if (isinstance(user, str) and ReadService._valid_search_text(user)
|
|
138
|
+
and user.strip()):
|
|
139
|
+
label = user
|
|
140
|
+
else:
|
|
141
|
+
number = row.get('id')
|
|
142
|
+
label = f'Session {number}' if type(number) is int and number >= 0 else 'Session'
|
|
143
|
+
cells += (f'<td><a href="/clusters/{session_cluster.group(1)}/sessions/'
|
|
144
|
+
f'{session_id}">{html.escape(label)}</a></td>')
|
|
145
|
+
else:
|
|
146
|
+
cells += '<td></td>'
|
|
147
|
+
elif path == '/clusters':
|
|
148
|
+
try:
|
|
149
|
+
cluster_id = str(UUID(str(row['cluster'])))
|
|
150
|
+
except (KeyError, ValueError, TypeError):
|
|
151
|
+
cluster_id = None
|
|
152
|
+
actions = (f'<a href="/clusters/{quote(cluster_id)}">Open cluster</a>'
|
|
153
|
+
if cluster_id else '')
|
|
154
|
+
if cluster_id and allow_cluster_remove:
|
|
155
|
+
actions += (f' <a href="/clusters/{quote(cluster_id)}/remove">'
|
|
156
|
+
'Remove cluster</a>')
|
|
157
|
+
cells += '<td>' + actions + '</td>'
|
|
158
|
+
elif infobase_cluster:
|
|
159
|
+
try:
|
|
160
|
+
infobase_id = str(UUID(str(row['infobase'])))
|
|
161
|
+
except (KeyError, ValueError, TypeError):
|
|
162
|
+
infobase_id = None
|
|
163
|
+
actions = ''
|
|
164
|
+
if infobase_id:
|
|
165
|
+
base = f'/clusters/{infobase_cluster.group(1)}/infobases/{infobase_id}'
|
|
166
|
+
for kind in ('summary', 'info', 'binary-storages',
|
|
167
|
+
'sessions', 'connections', 'locks'):
|
|
168
|
+
actions += (f'<a href="{base}/{kind}">{kind.replace("-", " ")}</a> ')
|
|
169
|
+
cells += '<td>' + actions + '</td>'
|
|
170
|
+
elif scoped_cluster:
|
|
171
|
+
key = 'server' if scoped_kind == 'servers' else 'process'
|
|
172
|
+
try:
|
|
173
|
+
object_id = str(UUID(str(row[key])))
|
|
174
|
+
except (KeyError, ValueError, TypeError):
|
|
175
|
+
object_id = None
|
|
176
|
+
actions = ''
|
|
177
|
+
if object_id:
|
|
178
|
+
base = f'/clusters/{scoped_cluster.group(1)}/{scoped_kind}/{object_id}'
|
|
179
|
+
if scoped_kind == 'servers' and scoped_cluster.group(2):
|
|
180
|
+
kinds = (('', 'info'), *((f'/{kind}', kind) for kind in SERVER_LISTS))
|
|
181
|
+
elif scoped_kind == 'processes' and scoped_cluster.group(2):
|
|
182
|
+
kinds = (('', 'info'), ('/connections', 'connections'))
|
|
183
|
+
else:
|
|
184
|
+
kinds = (('', 'info'),)
|
|
185
|
+
actions = ' '.join(f'<a href="{base}{suffix}">{label}</a>'
|
|
186
|
+
for suffix, label in kinds)
|
|
187
|
+
cells += '<td>' + actions + '</td>'
|
|
188
|
+
elif server_processes:
|
|
189
|
+
try:
|
|
190
|
+
process_id = str(UUID(str(row['process'])))
|
|
191
|
+
except (KeyError, ValueError, TypeError):
|
|
192
|
+
process_id = None
|
|
193
|
+
actions = ''
|
|
194
|
+
if process_id:
|
|
195
|
+
base = f'/clusters/{server_processes.group(1)}/processes/{process_id}'
|
|
196
|
+
actions = f'<a href="{base}">info</a> <a href="{base}/connections">connections</a>'
|
|
197
|
+
cells += '<td>' + actions + '</td>'
|
|
198
|
+
elif scoped_connections:
|
|
199
|
+
try:
|
|
200
|
+
connection_id = str(UUID(str(row['connection'])))
|
|
201
|
+
except (KeyError, ValueError, TypeError):
|
|
202
|
+
connection_id = None
|
|
203
|
+
actions = ''
|
|
204
|
+
if connection_id:
|
|
205
|
+
base = f'/clusters/{scoped_connections.group(1)}/connections/{connection_id}'
|
|
206
|
+
actions = f'<a href="{base}">info</a> <a href="{base}/locks">locks</a>'
|
|
207
|
+
cells += '<td>' + actions + '</td>'
|
|
208
|
+
elif cluster_inventory or server_inventory:
|
|
209
|
+
match = cluster_inventory or server_inventory
|
|
210
|
+
assert match is not None
|
|
211
|
+
group = match.group(2) if cluster_inventory else match.group(3)
|
|
212
|
+
key = {'connections': 'connection', 'managers': 'manager', 'limits': 'name',
|
|
213
|
+
'counters': 'name', 'rules': 'rule', 'service-settings': 'setting'}[group]
|
|
214
|
+
value = row.get(key)
|
|
215
|
+
actions = ''
|
|
216
|
+
if isinstance(value, str):
|
|
217
|
+
if key != 'name':
|
|
218
|
+
try:
|
|
219
|
+
value = str(UUID(value))
|
|
220
|
+
except ValueError:
|
|
221
|
+
value = None
|
|
222
|
+
elif not value or len(value.encode('utf-8')) > 1024:
|
|
223
|
+
value = None
|
|
224
|
+
if value is not None:
|
|
225
|
+
base = f'/clusters/{match.group(1)}'
|
|
226
|
+
if server_inventory:
|
|
227
|
+
base += f'/servers/{match.group(2)}'
|
|
228
|
+
base += f'/{group}/{quote(value, safe="")}'
|
|
229
|
+
actions = f'<a href="{base}">info</a>'
|
|
230
|
+
if group == 'connections':
|
|
231
|
+
actions += f' <a href="{base}/locks">locks</a>'
|
|
232
|
+
cells += '<td>' + actions + '</td>'
|
|
233
|
+
elif profile_cluster:
|
|
234
|
+
name = row.get('name')
|
|
235
|
+
actions = ''
|
|
236
|
+
if (isinstance(name, str) and name and '\x00' not in name
|
|
237
|
+
and len(name.encode('utf-8')) <= 1024):
|
|
238
|
+
encoded = quote(name, safe='')
|
|
239
|
+
base = f'/clusters/{profile_cluster.group(1)}/profiles'
|
|
240
|
+
actions = (f'<a href="{base}/overview?name={encoded}">'
|
|
241
|
+
f'{html.escape(name)}: overview</a> '
|
|
242
|
+
f'<a href="{base}/by-name?name={encoded}">'
|
|
243
|
+
f'{html.escape(name)}: info</a> ' +
|
|
244
|
+
' '.join(f'<a href="{base}/{encoded}/acl/{kind}">{kind}</a>'
|
|
245
|
+
for kind in PROFILE_ACL_LISTS))
|
|
246
|
+
cells += '<td>' + actions + '</td>'
|
|
247
|
+
elif admin_list:
|
|
248
|
+
name = row.get('name')
|
|
249
|
+
actions = ''
|
|
250
|
+
if (allow_admin_remove and isinstance(name, str) and name and
|
|
251
|
+
'/' not in name and '\\' not in name and '\x00' not in name and
|
|
252
|
+
len(name.encode('utf-8')) <= 1024):
|
|
253
|
+
actions = f'<a href="{path}/{quote(name, safe="")}/remove">Remove {html.escape(name)}</a>'
|
|
254
|
+
cells += '<td>' + actions + '</td>'
|
|
255
|
+
search_key = ''
|
|
256
|
+
if hidden_key is not None:
|
|
257
|
+
try:
|
|
258
|
+
search_key = str(UUID(str(row[hidden_key])))
|
|
259
|
+
except (KeyError, TypeError, ValueError):
|
|
260
|
+
pass
|
|
261
|
+
if search_key:
|
|
262
|
+
cells = cells.replace('<td>', f'<td data-search-key="{search_key}">', 1)
|
|
263
|
+
records.append('<tr data-searchable-row>' + cells + '</tr>')
|
|
264
|
+
search = ('<label>Search ' + html.escape(title.lower()) +
|
|
265
|
+
' <input type="search" aria-label="Search ' + html.escape(title.lower()) +
|
|
266
|
+
'" oninput="this.closest(\'section\').querySelectorAll(\'[data-searchable-row]\')'
|
|
267
|
+
'.forEach(row=>row.hidden=!(row.textContent+" "+'
|
|
268
|
+
'(row.querySelector("[data-search-key]")?.dataset.searchKey||""))'
|
|
269
|
+
'.toLocaleLowerCase().includes('
|
|
270
|
+
'this.value.toLocaleLowerCase()))" placeholder="Name or UUID"></label>')
|
|
271
|
+
return ('<section><h1>' + html.escape(title) + '</h1>' + search +
|
|
272
|
+
'<div class="table-scroll"><table><thead><tr>' + heading +
|
|
273
|
+
'</tr></thead><tbody>' + ''.join(records) + '</tbody></table></div></section>')
|
|
274
|
+
links = []
|
|
275
|
+
for row in rows:
|
|
276
|
+
entries = ''.join(f'<dt>{html.escape(str(k))}</dt><dd>{html.escape(str(v))}</dd>'
|
|
277
|
+
for k, v in row.items())
|
|
278
|
+
cluster_id = row.get('cluster') if path == '/clusters' else None
|
|
279
|
+
try:
|
|
280
|
+
cluster_uuid = str(UUID(str(cluster_id))) if cluster_id else None
|
|
281
|
+
except ValueError:
|
|
282
|
+
cluster_uuid = None
|
|
283
|
+
link = (f'<a href="/clusters/{quote(cluster_uuid)}">Open cluster</a>'
|
|
284
|
+
if cluster_uuid else '')
|
|
285
|
+
if cluster_uuid and allow_cluster_remove:
|
|
286
|
+
link += (f' <a href="/clusters/{quote(cluster_uuid)}/remove">'
|
|
287
|
+
'Remove cluster</a>')
|
|
288
|
+
links.append(f'<li>{link}<dl>{entries}</dl></li>')
|
|
289
|
+
return ('<section><h1>' + html.escape(title) + '</h1><ul>' +
|
|
290
|
+
''.join(links) + '</ul></section>')
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def render_page(title, rows, path, *, allow_cluster_remove=False):
|
|
294
|
+
return render_document(title, render_section(title, rows, path,
|
|
295
|
+
allow_cluster_remove=allow_cluster_remove))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def render_document(title, content, *, workspace_tree=None, inspector=None):
|
|
299
|
+
return ('<!doctype html><html lang="ru"><head><meta charset="utf-8">'
|
|
300
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
301
|
+
'<title>' + html.escape(title) + ' · Python RAC</title>'
|
|
302
|
+
'<style>'
|
|
303
|
+
':root{font:15px system-ui,sans-serif;color:#dce5f4;background:#101827}'
|
|
304
|
+
'.ui-icon{display:inline-grid;place-items:center;width:1.4em;height:1.4em;'
|
|
305
|
+
'border-radius:6px;background:#6678ee20;color:#7887ed;font-weight:700;margin-right:5px}'
|
|
306
|
+
'*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;grid-template-columns:220px 1fr}'
|
|
307
|
+
'body>nav{background:#172235;padding:28px 18px;display:flex;flex-direction:column;gap:12px;'
|
|
308
|
+
'border-right:1px solid #334358}body>nav:before{content:"◆ Python RAC";font-weight:700;'
|
|
309
|
+
'color:#8cd9d0;margin-bottom:24px}a{color:#78d9cc;text-decoration:none}a:hover{text-decoration:underline}'
|
|
310
|
+
'main{padding:32px clamp(18px,4vw,60px);min-width:0;max-width:1400px}'
|
|
311
|
+
'h1{font-size:1.7rem;letter-spacing:-.025em}h2{font-size:1.1rem}'
|
|
312
|
+
'section{background:#1b293c;border:1px solid #324258;border-radius:16px;padding:22px;'
|
|
313
|
+
'margin:0 0 22px;box-shadow:0 12px 32px #090e1933}'
|
|
314
|
+
'section>ul{list-style:none;padding:0;display:grid;gap:10px}'
|
|
315
|
+
'section>ul>li{background:#132031;padding:15px;border-radius:10px;overflow-wrap:anywhere}'
|
|
316
|
+
'.table-scroll{overflow:auto;max-height:min(72vh,760px);margin-top:16px;border:1px solid #324258;'
|
|
317
|
+
'border-radius:10px}table{border-collapse:collapse;width:100%;min-width:520px;font-size:14px}'
|
|
318
|
+
'th,td{text-align:left;padding:10px 13px;border-bottom:1px solid #324258;white-space:nowrap}'
|
|
319
|
+
'th{position:sticky;top:0;background:#23354a;color:#bed0e5;z-index:1}'
|
|
320
|
+
'tbody tr:nth-child(even){background:#18283a}tbody tr:hover{background:#285064}'
|
|
321
|
+
'[hidden]{display:none!important}'
|
|
322
|
+
'dl{display:grid;grid-template-columns:minmax(130px,210px) 1fr;gap:6px 16px;margin:12px 0 0}'
|
|
323
|
+
'dt{color:#99aeca}dd{margin:0;overflow-wrap:anywhere}'
|
|
324
|
+
'input,select,button{background:#142338;color:#f0f7ff;border:1px solid #52657f;'
|
|
325
|
+
'border-radius:8px;padding:10px;font:inherit;max-width:100%}'
|
|
326
|
+
'button{background:#177f77;border-color:#21a799;cursor:pointer;font-weight:650}'
|
|
327
|
+
'button:hover{background:#1a988d}form{display:flex;flex-wrap:wrap;align-items:end;gap:12px}'
|
|
328
|
+
'label{display:grid;gap:5px}nav a{display:inline-block;margin:3px 9px 3px 0}'
|
|
329
|
+
'@media(max-width:750px){body{display:block}body>nav{border:0;border-bottom:1px solid #334358;'
|
|
330
|
+
'padding:14px;flex-direction:row;flex-wrap:wrap}body>nav:before{margin:0 10px 0 0}main{padding:18px}}'
|
|
331
|
+
+ ('''body.workspace-console{display:grid;grid-template-columns:240px minmax(0,1fr) 290px;
|
|
332
|
+
grid-template-rows:48px minmax(0,1fr);height:100vh;min-height:0;color:#243449;
|
|
333
|
+
background:#e9edf2;font:13px "Segoe UI",Arial,sans-serif}
|
|
334
|
+
.workspace-console .console-top{grid-column:1/-1;background:#21364d;color:#f2f6fa;
|
|
335
|
+
display:flex;align-items:center;justify-content:space-between;padding:0 18px;
|
|
336
|
+
border-bottom:2px solid #4d9ca6;font-size:14px;font-weight:600}
|
|
337
|
+
.workspace-console .console-top a{color:#e0f4f3}
|
|
338
|
+
.workspace-console .console-tree{background:#f5f7fa;border-right:1px solid #cdd6e0;
|
|
339
|
+
overflow:auto;padding:18px 12px}
|
|
340
|
+
.console-tree h2,.console-inspector h2{font-size:12px;text-transform:uppercase;
|
|
341
|
+
letter-spacing:.07em;color:#63758b;margin:0 0 14px}
|
|
342
|
+
.console-tree a{color:#2a455e;display:block;padding:6px 9px;border-radius:4px}
|
|
343
|
+
.console-tree a:hover,.console-tree a:focus-visible{background:#dcecf0;text-decoration:none}
|
|
344
|
+
.console-tree ul{list-style:none;padding:0 0 0 11px;margin:0}
|
|
345
|
+
.console-tree .tree-node{font-weight:650;overflow-wrap:anywhere}
|
|
346
|
+
.workspace-console .console-main{max-width:none;min-width:0;padding:0;overflow:auto}
|
|
347
|
+
.console-main section{margin:0;background:#fff;border:0;border-radius:0;
|
|
348
|
+
box-shadow:none;min-height:100%;padding:20px 24px}
|
|
349
|
+
.console-main h1{font-size:20px;margin:0 0 12px}
|
|
350
|
+
.console-main form{background:#f3f6fa;border:1px solid #d8e0e8;padding:9px;
|
|
351
|
+
border-radius:5px;margin-bottom:14px}
|
|
352
|
+
.console-main input,.console-main select,.console-main button{background:#fff;color:#26394d;
|
|
353
|
+
border-color:#b7c4d1;border-radius:4px;padding:6px 9px}
|
|
354
|
+
.console-main button{background:#276d79;color:#fff;border-color:#276d79}
|
|
355
|
+
.console-main a{color:#1b6676}
|
|
356
|
+
.console-main .table-scroll{border-color:#cbd6e0;border-radius:4px;margin-top:10px;
|
|
357
|
+
max-height:none}
|
|
358
|
+
.console-main th,.console-main td{padding:7px 10px;border-bottom:1px solid #e0e6ed}
|
|
359
|
+
.console-main th{background:#e7edf3;color:#38516b}
|
|
360
|
+
.console-main tbody tr:nth-child(even){background:#f7f9fb}
|
|
361
|
+
.console-main tbody tr:hover{background:#e5f2f4}
|
|
362
|
+
.console-main .selectable-row{cursor:pointer}
|
|
363
|
+
.console-main .selectable-row:focus-visible{outline:2px solid #276d79;outline-offset:-2px}
|
|
364
|
+
.console-main .selectable-row.is-selected{background:#d3edf0}
|
|
365
|
+
.console-inspector{background:#f7f9fb;border-left:1px solid #cdd6e0;
|
|
366
|
+
overflow:auto;padding:18px 16px}
|
|
367
|
+
.console-inspector dl{grid-template-columns:80px 1fr;gap:8px}
|
|
368
|
+
.console-inspector dt{color:#63758b}
|
|
369
|
+
.console-inspector dd{overflow-wrap:anywhere}
|
|
370
|
+
@media(max-width:1000px){body.workspace-console{grid-template-columns:200px minmax(0,1fr)}
|
|
371
|
+
.console-inspector{display:none}}
|
|
372
|
+
@media(max-width:660px){body.workspace-console{display:block;height:auto}
|
|
373
|
+
.workspace-console .console-tree{max-height:200px}.workspace-console .console-main{overflow:visible}}
|
|
374
|
+
''' if workspace_tree is not None else '') +
|
|
375
|
+
'''body.workspace-console{background:#10141f;color:#e7ebf7;font:14px system-ui,sans-serif;
|
|
376
|
+
grid-template-columns:248px minmax(0,1fr) 270px;grid-template-rows:60px minmax(0,1fr)}
|
|
377
|
+
.workspace-console .console-top{background:#171d2d;border-bottom:1px solid #30394f;
|
|
378
|
+
padding:0 24px;letter-spacing:.01em;box-shadow:0 4px 18px #080b1266}
|
|
379
|
+
.console-top .ui-icon{background:#6d79eb;color:white;width:30px;height:30px;
|
|
380
|
+
font-size:20px;margin-right:10px;vertical-align:middle}
|
|
381
|
+
.workspace-console .console-top a{color:#c9d2ff;background:#ffffff0b;
|
|
382
|
+
border:1px solid #ffffff1a;border-radius:9px;padding:7px 13px}
|
|
383
|
+
.workspace-console .console-top a:hover{background:#ffffff19;text-decoration:none}
|
|
384
|
+
.workspace-console .console-tree{background:#171d2b;border-color:#30394f;padding:22px 13px}
|
|
385
|
+
.console-tree h2,.console-inspector h2{color:#8d9abb;font-size:11px}
|
|
386
|
+
.console-tree a{color:#b6c1da;border-radius:8px;padding:8px 10px}
|
|
387
|
+
.console-tree a:hover,.console-tree a:focus-visible{background:#303958;color:#fff}
|
|
388
|
+
.console-tree .tree-node{color:#eef0ff;font-weight:650}
|
|
389
|
+
.workspace-console .console-main{background:#10141f}
|
|
390
|
+
.console-main section{background:#10141f;color:#e7ebf7;padding:30px 34px}
|
|
391
|
+
.console-main h1{font-size:26px;letter-spacing:-.035em;margin:0 0 16px}
|
|
392
|
+
.console-main h2{color:#e7ebf7}
|
|
393
|
+
.console-main p{color:#acb7d1;line-height:1.85}
|
|
394
|
+
.console-main form{background:#1a2132;border:1px solid #34405a;
|
|
395
|
+
border-radius:12px;padding:16px;margin:18px 0;gap:16px}
|
|
396
|
+
.console-main td form{margin:0;padding:0;background:transparent;border:0}
|
|
397
|
+
.console-main input,.console-main select{background:#121827;color:#edf1ff;
|
|
398
|
+
border:1px solid #42506b;border-radius:8px;padding:9px 12px}
|
|
399
|
+
.console-main input:focus-visible,.console-main select:focus-visible,
|
|
400
|
+
.console-main button:focus-visible,.console-tree a:focus-visible{
|
|
401
|
+
outline:2px solid #98a4ff;outline-offset:2px}
|
|
402
|
+
.console-main button{background:#6674db;color:#fff;border:1px solid #7d89e8;
|
|
403
|
+
border-radius:8px;padding:9px 16px;cursor:pointer}
|
|
404
|
+
.console-main button:hover{background:#7c88ee}
|
|
405
|
+
.console-main button.danger-button{background:#352331;border-color:#69405c;color:#ffc3d6}
|
|
406
|
+
.console-main a{color:#aab8ff}
|
|
407
|
+
.console-main .table-scroll{border:1px solid #34405a;border-radius:12px;
|
|
408
|
+
background:#1a2132;margin-top:20px;overflow:auto}
|
|
409
|
+
.console-main th,.console-main td{padding:13px 17px;border-bottom:1px solid #30394f}
|
|
410
|
+
.console-main th{background:#242d42;color:#bfcafa}
|
|
411
|
+
.console-main tbody tr:nth-child(even){background:#1d2639}
|
|
412
|
+
.console-main tbody tr:hover{background:#303c59}
|
|
413
|
+
.console-main .selectable-row:focus-visible{outline:2px solid #98a4ff}
|
|
414
|
+
.console-main .selectable-row.is-selected{background:#35436b}
|
|
415
|
+
.console-inspector{background:#171d2b;color:#d8e0f1;border-color:#30394f;
|
|
416
|
+
padding:24px 18px}.console-inspector dt{color:#8d9abb}
|
|
417
|
+
@media(max-width:1000px){body.workspace-console{grid-template-columns:200px minmax(0,1fr)}}
|
|
418
|
+
@media(max-width:660px){body.workspace-console{display:block;height:auto}
|
|
419
|
+
.workspace-console .console-tree{max-height:200px}.workspace-console .console-main{overflow:visible}
|
|
420
|
+
.console-main section{padding:22px 17px}}
|
|
421
|
+
''' +
|
|
422
|
+
'</style>' + ('<script src="/ras/inspector.js" defer></script>'
|
|
423
|
+
if workspace_tree is not None else '') +
|
|
424
|
+
'</head><body' + (' class="workspace-console"' if workspace_tree is not None else '') + '>' +
|
|
425
|
+
(('<header class="console-top"><span><span class="ui-icon" aria-hidden="true">◈</span> Python RAC · Консоль кластеров</span>'
|
|
426
|
+
'<a href="/ras/search"><span class="ui-icon" aria-hidden="true">⌕</span> Поиск по серверам</a></header>'
|
|
427
|
+
'<nav class="console-tree" aria-label="Навигация по RAS">' + workspace_tree + '</nav>'
|
|
428
|
+
'<main class="console-main">' + content + '</main>'
|
|
429
|
+
'<aside class="console-inspector" aria-label="Свойства объекта">' +
|
|
430
|
+
(inspector or '<h2>Свойства</h2><p>Выберите объект, чтобы просмотреть свойства.</p>') + '</aside>')
|
|
431
|
+
if workspace_tree is not None else
|
|
432
|
+
('<nav><a href="/ras"><span class="ui-icon" aria-hidden="true">▣</span> Серверы RAS</a> '
|
|
433
|
+
'<a href="/clusters"><span class="ui-icon" aria-hidden="true">◈</span> Кластеры</a> '
|
|
434
|
+
'<a href="/agent/version">Версия агента</a> '
|
|
435
|
+
'<a href="/agent/admins">Администраторы агента</a></nav><main>'
|
|
436
|
+
+ content + '</main>')) + '</body></html>')
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def create_app(client_factory, *, allow_process_turn_off=False,
|
|
440
|
+
allow_cluster_remove=False, allow_cluster_insert=False,
|
|
441
|
+
allow_agent_admin_register=False, allow_cluster_admin_register=False,
|
|
442
|
+
allow_agent_admin_remove=False, allow_cluster_admin_remove=False,
|
|
443
|
+
allow_rule_apply=False, allow_service_settings_apply=False,
|
|
444
|
+
allow_service_setting_remove=False, allow_service_setting_update=False,
|
|
445
|
+
allow_service_setting_insert=False,
|
|
446
|
+
allow_cluster_name_update=False, allow_cluster_expiration_timeout_update=False,
|
|
447
|
+
allow_cluster_security_level_update=False,
|
|
448
|
+
allow_cluster_session_fault_tolerance_level_update=False,
|
|
449
|
+
allow_cluster_load_balancing_mode_update=False,
|
|
450
|
+
allow_cluster_kill_problem_processes_update=False,
|
|
451
|
+
allow_cluster_kill_by_memory_with_dump_update=False,
|
|
452
|
+
allow_cluster_audit_update=False,
|
|
453
|
+
allow_cluster_ping_update=False,
|
|
454
|
+
allow_cluster_restart_schedule_update=False,
|
|
455
|
+
allow_server_restart_schedule_update=False,
|
|
456
|
+
allow_server_infobases_limit_update=False,
|
|
457
|
+
allow_server_connections_limit_update=False,
|
|
458
|
+
allow_server_safe_call_memory_limit_update=False,
|
|
459
|
+
allow_server_critical_total_memory_update=False,
|
|
460
|
+
allow_server_temporary_total_memory_update=False,
|
|
461
|
+
allow_server_temporary_memory_time_limit_update=False,
|
|
462
|
+
allow_server_spn_update=False,
|
|
463
|
+
allow_server_dedicate_managers_update=False,
|
|
464
|
+
allow_server_port_ranges_update=False, csrf_token=None, read_timeout=None):
|
|
465
|
+
if (allow_process_turn_off or allow_cluster_remove or allow_cluster_insert or
|
|
466
|
+
allow_agent_admin_register or allow_cluster_admin_register or
|
|
467
|
+
allow_agent_admin_remove or allow_cluster_admin_remove or allow_rule_apply or
|
|
468
|
+
allow_service_settings_apply or allow_service_setting_remove or
|
|
469
|
+
allow_service_setting_update or allow_service_setting_insert or
|
|
470
|
+
allow_cluster_name_update or
|
|
471
|
+
allow_cluster_expiration_timeout_update or allow_cluster_security_level_update or
|
|
472
|
+
allow_cluster_session_fault_tolerance_level_update or
|
|
473
|
+
allow_cluster_load_balancing_mode_update or
|
|
474
|
+
allow_cluster_kill_problem_processes_update or
|
|
475
|
+
allow_cluster_kill_by_memory_with_dump_update or
|
|
476
|
+
allow_cluster_audit_update or
|
|
477
|
+
allow_cluster_ping_update or allow_cluster_restart_schedule_update or
|
|
478
|
+
allow_server_restart_schedule_update or allow_server_infobases_limit_update or
|
|
479
|
+
allow_server_connections_limit_update or
|
|
480
|
+
allow_server_safe_call_memory_limit_update or
|
|
481
|
+
allow_server_critical_total_memory_update or
|
|
482
|
+
allow_server_temporary_total_memory_update or
|
|
483
|
+
allow_server_temporary_memory_time_limit_update or allow_server_spn_update or
|
|
484
|
+
allow_server_dedicate_managers_update or allow_server_port_ranges_update) and (
|
|
485
|
+
not isinstance(csrf_token, str) or len(csrf_token) < 16):
|
|
486
|
+
raise ValueError('Web mutation requires an unpredictable CSRF token')
|
|
487
|
+
|
|
488
|
+
def application(environ, start_response):
|
|
489
|
+
path = environ.get('PATH_INFO', '/')
|
|
490
|
+
method = environ.get('REQUEST_METHOD', 'GET')
|
|
491
|
+
setting_insert = re.fullmatch(
|
|
492
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/'
|
|
493
|
+
r'service-settings/insert', path)
|
|
494
|
+
if setting_insert and allow_service_setting_insert:
|
|
495
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
496
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
497
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
498
|
+
if method not in ('GET', 'POST'):
|
|
499
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
500
|
+
('Content-Type', 'text/plain')])
|
|
501
|
+
return [b'Method not allowed']
|
|
502
|
+
try:
|
|
503
|
+
cluster, server = (UUID(value) for value in setting_insert.groups())
|
|
504
|
+
if tuple(map(str, (cluster, server))) != setting_insert.groups():
|
|
505
|
+
raise ValueError('Noncanonical UUID')
|
|
506
|
+
host = environ.get('HTTP_HOST', '')
|
|
507
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
508
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
509
|
+
raise ValueError('Invalid mutation host')
|
|
510
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
511
|
+
raise ValueError('Mutation requires loopback peer')
|
|
512
|
+
if method == 'GET':
|
|
513
|
+
content = (f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
514
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
515
|
+
'<label>Service name <input name="service_name" required></label>'
|
|
516
|
+
'<label>Infobase name <input name="infobase_name"></label>'
|
|
517
|
+
'<label>Service data directory <input name="service_data_dir"></label>'
|
|
518
|
+
'<label>Type exact cluster UUID:server UUID:service name:infobase name '
|
|
519
|
+
'<input name="confirm_cluster_server_service_infobase" required></label>'
|
|
520
|
+
'<button type="submit">Insert service setting</button></form>')
|
|
521
|
+
body = render_document('Confirm service setting insert', content)
|
|
522
|
+
else:
|
|
523
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
524
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
525
|
+
origin.path or origin.query or origin.fragment):
|
|
526
|
+
raise ValueError('Invalid mutation origin')
|
|
527
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
528
|
+
raise ValueError('Invalid mutation content type')
|
|
529
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
530
|
+
if not 0 <= length <= 4096:
|
|
531
|
+
raise ValueError('Invalid mutation body length')
|
|
532
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
533
|
+
if len(raw_body) != length:
|
|
534
|
+
raise ValueError('Incomplete mutation body')
|
|
535
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
536
|
+
keep_blank_values=True, strict_parsing=True)
|
|
537
|
+
if (set(fields) != {'csrf_token', 'service_name', 'infobase_name',
|
|
538
|
+
'service_data_dir', 'confirm_cluster_server_service_infobase'} or
|
|
539
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
540
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token)):
|
|
541
|
+
raise ValueError('Invalid mutation form')
|
|
542
|
+
service_name, infobase_name, directory = (
|
|
543
|
+
fields[key][0] for key in ('service_name', 'infobase_name', 'service_data_dir'))
|
|
544
|
+
if (not service_name.strip() or any('\x00' in value or
|
|
545
|
+
len(value.encode('utf-8')) > 1024 for value in
|
|
546
|
+
(service_name, infobase_name, directory)) or
|
|
547
|
+
fields['confirm_cluster_server_service_infobase'][0] !=
|
|
548
|
+
f'{cluster}:{server}:{service_name}:{infobase_name}'):
|
|
549
|
+
raise ValueError('Invalid mutation confirmation')
|
|
550
|
+
async def insert():
|
|
551
|
+
async with client_factory() as client:
|
|
552
|
+
return await client.insert_service_setting(
|
|
553
|
+
cluster, server, service_name, infobase_name=infobase_name,
|
|
554
|
+
service_data_dir=directory)
|
|
555
|
+
row = asyncio.run(insert())
|
|
556
|
+
body = render_document('Service setting insertion verified by full read-back',
|
|
557
|
+
render_section('Service setting', [row], ''))
|
|
558
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
559
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
560
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
561
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
562
|
+
return [b'Service setting insert failed' if method == 'POST'
|
|
563
|
+
else b'RAC unavailable']
|
|
564
|
+
start_response('200 OK', headers)
|
|
565
|
+
return [body.encode('utf-8')]
|
|
566
|
+
setting_update = re.fullmatch(
|
|
567
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/'
|
|
568
|
+
r'service-settings/([0-9a-fA-F-]{36})/update', path)
|
|
569
|
+
if setting_update and allow_service_setting_update:
|
|
570
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
571
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
572
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
573
|
+
if method not in ('GET', 'POST'):
|
|
574
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
575
|
+
('Content-Type', 'text/plain')])
|
|
576
|
+
return [b'Method not allowed']
|
|
577
|
+
try:
|
|
578
|
+
cluster, server, setting = (UUID(value) for value in setting_update.groups())
|
|
579
|
+
if tuple(map(str, (cluster, server, setting))) != setting_update.groups():
|
|
580
|
+
raise ValueError('Noncanonical UUID')
|
|
581
|
+
host = environ.get('HTTP_HOST', '')
|
|
582
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
583
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
584
|
+
raise ValueError('Invalid mutation host')
|
|
585
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
586
|
+
raise ValueError('Mutation requires loopback peer')
|
|
587
|
+
if method == 'GET':
|
|
588
|
+
async def selected_setting():
|
|
589
|
+
async with client_factory() as client:
|
|
590
|
+
return [row for row in await client.list_service_settings(cluster, server)
|
|
591
|
+
if row.get('setting') == str(setting)]
|
|
592
|
+
rows = asyncio.run(selected_setting())
|
|
593
|
+
if len(rows) != 1:
|
|
594
|
+
raise ValueError('Service setting not found or not unique')
|
|
595
|
+
content = (render_section('Service setting', rows, '') +
|
|
596
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
597
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
598
|
+
'<label>New service data directory '
|
|
599
|
+
'<input name="service_data_dir" required></label>'
|
|
600
|
+
'<label>Type exact cluster UUID:server UUID:setting UUID to confirm '
|
|
601
|
+
'<input name="confirm_cluster_server_setting" required></label>'
|
|
602
|
+
'<button type="submit">Update service setting</button></form>')
|
|
603
|
+
body = render_document('Confirm service setting update', content)
|
|
604
|
+
else:
|
|
605
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
606
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
607
|
+
origin.path or origin.query or origin.fragment):
|
|
608
|
+
raise ValueError('Invalid mutation origin')
|
|
609
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
610
|
+
raise ValueError('Invalid mutation content type')
|
|
611
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
612
|
+
if not 0 <= length <= 4096:
|
|
613
|
+
raise ValueError('Invalid mutation body length')
|
|
614
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
615
|
+
if len(raw_body) != length:
|
|
616
|
+
raise ValueError('Incomplete mutation body')
|
|
617
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
618
|
+
keep_blank_values=True, strict_parsing=True)
|
|
619
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_server_setting',
|
|
620
|
+
'service_data_dir'} or
|
|
621
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
622
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
623
|
+
fields['confirm_cluster_server_setting'][0] !=
|
|
624
|
+
f'{cluster}:{server}:{setting}'):
|
|
625
|
+
raise ValueError('Invalid mutation confirmation')
|
|
626
|
+
directory = fields['service_data_dir'][0]
|
|
627
|
+
if '\x00' in directory or len(directory.encode('utf-8')) > 1024:
|
|
628
|
+
raise ValueError('Invalid service data directory')
|
|
629
|
+
async def update():
|
|
630
|
+
async with client_factory() as client:
|
|
631
|
+
return await client.update_service_setting(cluster, server, setting, directory)
|
|
632
|
+
row = asyncio.run(update())
|
|
633
|
+
body = render_document('Service setting path verified by full read-back',
|
|
634
|
+
render_section('Service setting', [row], ''))
|
|
635
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
636
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
637
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
638
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
639
|
+
return [b'Service setting update failed' if method == 'POST'
|
|
640
|
+
else b'RAC unavailable']
|
|
641
|
+
start_response('200 OK', headers)
|
|
642
|
+
return [body.encode('utf-8')]
|
|
643
|
+
setting_remove = re.fullmatch(
|
|
644
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/'
|
|
645
|
+
r'service-settings/([0-9a-fA-F-]{36})/remove', path)
|
|
646
|
+
if setting_remove and allow_service_setting_remove:
|
|
647
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
648
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
649
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
650
|
+
if method not in ('GET', 'POST'):
|
|
651
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
652
|
+
('Content-Type', 'text/plain')])
|
|
653
|
+
return [b'Method not allowed']
|
|
654
|
+
try:
|
|
655
|
+
cluster, server, setting = (UUID(value) for value in setting_remove.groups())
|
|
656
|
+
if tuple(map(str, (cluster, server, setting))) != setting_remove.groups():
|
|
657
|
+
raise ValueError('Noncanonical UUID')
|
|
658
|
+
host = environ.get('HTTP_HOST', '')
|
|
659
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
660
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
661
|
+
raise ValueError('Invalid mutation host')
|
|
662
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
663
|
+
raise ValueError('Mutation requires loopback peer')
|
|
664
|
+
if method == 'GET':
|
|
665
|
+
async def selected_setting():
|
|
666
|
+
async with client_factory() as client:
|
|
667
|
+
return [row for row in await client.list_service_settings(cluster, server)
|
|
668
|
+
if row.get('setting') == str(setting)]
|
|
669
|
+
rows = asyncio.run(selected_setting())
|
|
670
|
+
if len(rows) != 1:
|
|
671
|
+
raise ValueError('Service setting not found or not unique')
|
|
672
|
+
content = (render_section('Service setting', rows, '') +
|
|
673
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
674
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
675
|
+
'<label>Type exact cluster UUID:server UUID:setting UUID to confirm removal '
|
|
676
|
+
'<input name="confirm_cluster_server_setting" required></label>'
|
|
677
|
+
'<button type="submit">Remove service setting</button></form>')
|
|
678
|
+
body = render_document('Confirm service setting removal', content)
|
|
679
|
+
else:
|
|
680
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
681
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
682
|
+
origin.path or origin.query or origin.fragment):
|
|
683
|
+
raise ValueError('Invalid mutation origin')
|
|
684
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
685
|
+
raise ValueError('Invalid mutation content type')
|
|
686
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
687
|
+
if not 0 <= length <= 4096:
|
|
688
|
+
raise ValueError('Invalid mutation body length')
|
|
689
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
690
|
+
if len(raw_body) != length:
|
|
691
|
+
raise ValueError('Incomplete mutation body')
|
|
692
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
693
|
+
keep_blank_values=True, strict_parsing=True)
|
|
694
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_server_setting'} or
|
|
695
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
696
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
697
|
+
fields['confirm_cluster_server_setting'][0] !=
|
|
698
|
+
f'{cluster}:{server}:{setting}'):
|
|
699
|
+
raise ValueError('Invalid mutation confirmation')
|
|
700
|
+
async def remove():
|
|
701
|
+
async with client_factory() as client:
|
|
702
|
+
await client.remove_service_setting(cluster, server, setting)
|
|
703
|
+
asyncio.run(remove())
|
|
704
|
+
body = render_document('Service setting removal verified by full read-back',
|
|
705
|
+
'<p>Service setting removal verified by full read-back.</p>')
|
|
706
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
707
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
708
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
709
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
710
|
+
return [b'Service setting removal failed' if method == 'POST'
|
|
711
|
+
else b'RAC unavailable']
|
|
712
|
+
start_response('200 OK', headers)
|
|
713
|
+
return [body.encode('utf-8')]
|
|
714
|
+
settings_apply = re.fullmatch(
|
|
715
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/service-settings/apply', path)
|
|
716
|
+
if settings_apply and allow_service_settings_apply:
|
|
717
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
718
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
719
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
720
|
+
if method not in ('GET', 'POST'):
|
|
721
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
722
|
+
('Content-Type', 'text/plain')])
|
|
723
|
+
return [b'Method not allowed']
|
|
724
|
+
try:
|
|
725
|
+
cluster, server = (UUID(value) for value in settings_apply.groups())
|
|
726
|
+
host = environ.get('HTTP_HOST', '')
|
|
727
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
728
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
729
|
+
raise ValueError('Invalid mutation host')
|
|
730
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
731
|
+
raise ValueError('Mutation requires loopback peer')
|
|
732
|
+
if method == 'GET':
|
|
733
|
+
content = (f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
734
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
735
|
+
'<label>Type exact cluster UUID:server UUID to confirm '
|
|
736
|
+
'<input name="confirm_cluster_server" required></label>'
|
|
737
|
+
'<button type="submit">Apply service settings</button></form>')
|
|
738
|
+
body = render_document('Confirm service settings apply', content)
|
|
739
|
+
else:
|
|
740
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
741
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
742
|
+
origin.path or origin.query or origin.fragment):
|
|
743
|
+
raise ValueError('Invalid mutation origin')
|
|
744
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
745
|
+
raise ValueError('Invalid mutation content type')
|
|
746
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
747
|
+
if not 0 <= length <= 4096:
|
|
748
|
+
raise ValueError('Invalid mutation body length')
|
|
749
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
750
|
+
if len(raw_body) != length:
|
|
751
|
+
raise ValueError('Incomplete mutation body')
|
|
752
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
753
|
+
keep_blank_values=True, strict_parsing=True)
|
|
754
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_server'} or
|
|
755
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
756
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
757
|
+
fields['confirm_cluster_server'][0] != f'{cluster}:{server}'):
|
|
758
|
+
raise ValueError('Invalid mutation confirmation')
|
|
759
|
+
async def apply():
|
|
760
|
+
async with client_factory() as client:
|
|
761
|
+
return await client.apply_service_settings(cluster, server)
|
|
762
|
+
result = asyncio.run(apply())
|
|
763
|
+
body = render_document(
|
|
764
|
+
'Service settings active flags verified',
|
|
765
|
+
'<p>Service settings active flags verified by full read-back; '
|
|
766
|
+
'runtime service behaviour not independently verified.</p>' +
|
|
767
|
+
render_section('Service settings', result, ''))
|
|
768
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
769
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
770
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
771
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
772
|
+
return [b'Service settings apply not verified' if method == 'POST'
|
|
773
|
+
else b'RAC unavailable']
|
|
774
|
+
start_response('200 OK', headers)
|
|
775
|
+
return [body.encode('utf-8')]
|
|
776
|
+
rule_apply = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/rules/apply', path)
|
|
777
|
+
if rule_apply and allow_rule_apply:
|
|
778
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
779
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
780
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
781
|
+
if method not in ('GET', 'POST'):
|
|
782
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
783
|
+
('Content-Type', 'text/plain')])
|
|
784
|
+
return [b'Method not allowed']
|
|
785
|
+
try:
|
|
786
|
+
cluster = UUID(rule_apply.group(1))
|
|
787
|
+
host = environ.get('HTTP_HOST', '')
|
|
788
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
789
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
790
|
+
raise ValueError('Invalid mutation host')
|
|
791
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
792
|
+
raise ValueError('Mutation requires loopback peer')
|
|
793
|
+
if method == 'GET':
|
|
794
|
+
content = (f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
795
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
796
|
+
'<label>Apply mode <select name="mode"><option value="partial">partial</option>'
|
|
797
|
+
'<option value="full">full</option></select></label>'
|
|
798
|
+
'<label>Type exact cluster UUID to confirm '
|
|
799
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
800
|
+
'<button type="submit">Apply rules</button></form>'
|
|
801
|
+
'<p>Effective placement cannot be verified by rule list/info.</p>')
|
|
802
|
+
body = render_document('Confirm rule apply', content)
|
|
803
|
+
else:
|
|
804
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
805
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
806
|
+
origin.path or origin.query or origin.fragment):
|
|
807
|
+
raise ValueError('Invalid mutation origin')
|
|
808
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
809
|
+
raise ValueError('Invalid mutation content type')
|
|
810
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
811
|
+
if not 0 <= length <= 4096:
|
|
812
|
+
raise ValueError('Invalid mutation body length')
|
|
813
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
814
|
+
if len(raw_body) != length:
|
|
815
|
+
raise ValueError('Incomplete mutation body')
|
|
816
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
817
|
+
keep_blank_values=True, strict_parsing=True)
|
|
818
|
+
if (set(fields) != {'csrf_token', 'mode', 'confirm_cluster_id'} or
|
|
819
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
820
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
821
|
+
fields['mode'][0] not in ('partial', 'full') or
|
|
822
|
+
fields['confirm_cluster_id'][0] != str(cluster)):
|
|
823
|
+
raise ValueError('Invalid mutation confirmation')
|
|
824
|
+
async def apply():
|
|
825
|
+
async with client_factory() as client:
|
|
826
|
+
return await client.apply_rules(cluster, full=fields['mode'][0] == 'full')
|
|
827
|
+
result = asyncio.run(apply())
|
|
828
|
+
body = render_document(
|
|
829
|
+
'Rule apply acknowledged; rules unchanged',
|
|
830
|
+
'<p>Rule apply acknowledged; rules unchanged; effective placement not verified.</p>' +
|
|
831
|
+
''.join(render_section('Rules on server ' + str(server), rules, '')
|
|
832
|
+
for server, rules in result.items()))
|
|
833
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
834
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
835
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
836
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
837
|
+
return [b'Rule apply not verified' if method == 'POST' else b'RAC unavailable']
|
|
838
|
+
start_response('200 OK', headers)
|
|
839
|
+
return [body.encode('utf-8')]
|
|
840
|
+
cluster_admin_remove = re.fullmatch(
|
|
841
|
+
r'/clusters/([0-9a-fA-F-]{36})/admins/([^/]{1,1024})/remove', path)
|
|
842
|
+
if cluster_admin_remove and allow_cluster_admin_remove:
|
|
843
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
844
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
845
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
846
|
+
if method not in ('GET', 'POST'):
|
|
847
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
848
|
+
('Content-Type', 'text/plain')])
|
|
849
|
+
return [b'Method not allowed']
|
|
850
|
+
try:
|
|
851
|
+
cluster = UUID(cluster_admin_remove.group(1))
|
|
852
|
+
name = unquote(cluster_admin_remove.group(2), encoding='utf-8', errors='strict')
|
|
853
|
+
if (not name or '/' in name or '\\' in name or '\x00' in name or
|
|
854
|
+
len(name.encode('utf-8')) > 1024):
|
|
855
|
+
raise ValueError('Invalid administrator name')
|
|
856
|
+
host = environ.get('HTTP_HOST', '')
|
|
857
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
858
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
859
|
+
raise ValueError('Invalid mutation host')
|
|
860
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
861
|
+
raise ValueError('Mutation requires loopback peer')
|
|
862
|
+
if method == 'GET':
|
|
863
|
+
async def admins():
|
|
864
|
+
async with client_factory() as client:
|
|
865
|
+
return await client.list_cluster_admins(cluster)
|
|
866
|
+
rows = [row for row in asyncio.run(admins()) if row.get('name') == name]
|
|
867
|
+
if len(rows) != 1:
|
|
868
|
+
raise ValueError('Administrator not found or not unique')
|
|
869
|
+
content = (render_section('Cluster administrator', rows, '') +
|
|
870
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
871
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
872
|
+
'<label>Type exact cluster UUID:administrator name to confirm removal '
|
|
873
|
+
'<input name="confirm_cluster_name" required></label>'
|
|
874
|
+
'<button type="submit">Remove cluster administrator</button></form>')
|
|
875
|
+
body = render_document('Confirm cluster administrator removal', content)
|
|
876
|
+
else:
|
|
877
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
878
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
879
|
+
origin.path or origin.query or origin.fragment):
|
|
880
|
+
raise ValueError('Invalid mutation origin')
|
|
881
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
882
|
+
raise ValueError('Invalid mutation content type')
|
|
883
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
884
|
+
if not 0 <= length <= 4096:
|
|
885
|
+
raise ValueError('Invalid mutation body length')
|
|
886
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
887
|
+
if len(raw_body) != length:
|
|
888
|
+
raise ValueError('Incomplete mutation body')
|
|
889
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
890
|
+
keep_blank_values=True, strict_parsing=True)
|
|
891
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_name'} or
|
|
892
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
893
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
894
|
+
fields['confirm_cluster_name'][0] != f'{cluster}:{name}'):
|
|
895
|
+
raise ValueError('Invalid mutation confirmation')
|
|
896
|
+
async def remove_admin():
|
|
897
|
+
async with client_factory() as client:
|
|
898
|
+
return await client.remove_cluster_admin(cluster, name)
|
|
899
|
+
result = asyncio.run(remove_admin())
|
|
900
|
+
body = render_page('Verified cluster administrator removal', result, '')
|
|
901
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
902
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
903
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
904
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
905
|
+
return [b'Cluster administrator removal not verified' if method == 'POST'
|
|
906
|
+
else b'RAC unavailable']
|
|
907
|
+
start_response('200 OK', headers)
|
|
908
|
+
return [body.encode('utf-8')]
|
|
909
|
+
agent_admin_remove = re.fullmatch(r'/agent/admins/([^/]{1,1024})/remove', path)
|
|
910
|
+
if agent_admin_remove and allow_agent_admin_remove:
|
|
911
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
912
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
913
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
914
|
+
if method not in ('GET', 'POST'):
|
|
915
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
916
|
+
('Content-Type', 'text/plain')])
|
|
917
|
+
return [b'Method not allowed']
|
|
918
|
+
try:
|
|
919
|
+
name = unquote(agent_admin_remove.group(1), encoding='utf-8', errors='strict')
|
|
920
|
+
if (not name or '/' in name or '\\' in name or '\x00' in name or
|
|
921
|
+
len(name.encode('utf-8')) > 1024):
|
|
922
|
+
raise ValueError('Invalid administrator name')
|
|
923
|
+
host = environ.get('HTTP_HOST', '')
|
|
924
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
925
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
926
|
+
raise ValueError('Invalid mutation host')
|
|
927
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
928
|
+
raise ValueError('Mutation requires loopback peer')
|
|
929
|
+
if method == 'GET':
|
|
930
|
+
async def admins():
|
|
931
|
+
async with client_factory() as client:
|
|
932
|
+
return await client.list_agent_admins()
|
|
933
|
+
rows = [row for row in asyncio.run(admins()) if row.get('name') == name]
|
|
934
|
+
if len(rows) != 1:
|
|
935
|
+
raise ValueError('Administrator not found or not unique')
|
|
936
|
+
content = (render_section('Agent administrator', rows, '') +
|
|
937
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
938
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
939
|
+
'<label>Type exact administrator name to confirm removal '
|
|
940
|
+
'<input name="confirm_name" required></label>'
|
|
941
|
+
'<button type="submit">Remove agent administrator</button></form>')
|
|
942
|
+
body = render_document('Confirm agent administrator removal', content)
|
|
943
|
+
else:
|
|
944
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
945
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
946
|
+
origin.path or origin.query or origin.fragment):
|
|
947
|
+
raise ValueError('Invalid mutation origin')
|
|
948
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
949
|
+
raise ValueError('Invalid mutation content type')
|
|
950
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
951
|
+
if not 0 <= length <= 4096:
|
|
952
|
+
raise ValueError('Invalid mutation body length')
|
|
953
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
954
|
+
if len(raw_body) != length:
|
|
955
|
+
raise ValueError('Incomplete mutation body')
|
|
956
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
957
|
+
keep_blank_values=True, strict_parsing=True)
|
|
958
|
+
if (set(fields) != {'csrf_token', 'confirm_name'} or
|
|
959
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
960
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
961
|
+
fields['confirm_name'][0] != name):
|
|
962
|
+
raise ValueError('Invalid mutation confirmation')
|
|
963
|
+
async def remove_admin():
|
|
964
|
+
async with client_factory() as client:
|
|
965
|
+
return await client.remove_agent_admin(name)
|
|
966
|
+
result = asyncio.run(remove_admin())
|
|
967
|
+
body = render_page('Verified agent administrator removal', result, '')
|
|
968
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
969
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
970
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
971
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
972
|
+
return [b'Agent administrator removal not verified' if method == 'POST'
|
|
973
|
+
else b'RAC unavailable']
|
|
974
|
+
start_response('200 OK', headers)
|
|
975
|
+
return [body.encode('utf-8')]
|
|
976
|
+
cluster_admin_register = re.fullmatch(
|
|
977
|
+
r'/clusters/([0-9a-fA-F-]{36})/admins/register', path)
|
|
978
|
+
if cluster_admin_register and allow_cluster_admin_register:
|
|
979
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
980
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
981
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
982
|
+
if method not in ('GET', 'POST'):
|
|
983
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
984
|
+
('Content-Type', 'text/plain')])
|
|
985
|
+
return [b'Method not allowed']
|
|
986
|
+
try:
|
|
987
|
+
cluster = UUID(cluster_admin_register.group(1))
|
|
988
|
+
host = environ.get('HTTP_HOST', '')
|
|
989
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
990
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
991
|
+
raise ValueError('Invalid mutation host')
|
|
992
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
993
|
+
raise ValueError('Mutation requires loopback peer')
|
|
994
|
+
if method == 'GET':
|
|
995
|
+
content = (f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
996
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
997
|
+
'<label>Administrator name <input name="name" required></label>'
|
|
998
|
+
'<label>OS user <input name="os_user" required></label>'
|
|
999
|
+
'<label>Description <input name="descr"></label>'
|
|
1000
|
+
'<label>Type exact cluster UUID:name:os_user to confirm '
|
|
1001
|
+
'<input name="confirm_cluster_name_os_user" required></label>'
|
|
1002
|
+
'<button type="submit">Register cluster administrator</button></form>')
|
|
1003
|
+
body = render_document('Confirm cluster administrator registration', content)
|
|
1004
|
+
else:
|
|
1005
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1006
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1007
|
+
origin.path or origin.query or origin.fragment):
|
|
1008
|
+
raise ValueError('Invalid mutation origin')
|
|
1009
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1010
|
+
raise ValueError('Invalid mutation content type')
|
|
1011
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1012
|
+
if not 0 <= length <= 4096:
|
|
1013
|
+
raise ValueError('Invalid mutation body length')
|
|
1014
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1015
|
+
if len(raw_body) != length:
|
|
1016
|
+
raise ValueError('Incomplete mutation body')
|
|
1017
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1018
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1019
|
+
if (set(fields) != {'csrf_token', 'name', 'os_user', 'descr',
|
|
1020
|
+
'confirm_cluster_name_os_user'} or
|
|
1021
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1022
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token)):
|
|
1023
|
+
raise ValueError('Invalid mutation fields')
|
|
1024
|
+
name, os_user, descr = (fields[key][0] for key in ('name', 'os_user', 'descr'))
|
|
1025
|
+
if (not name or not os_user or any('\x00' in value or
|
|
1026
|
+
len(value.encode('utf-8')) > 1024 for value in (name, os_user, descr)) or
|
|
1027
|
+
fields['confirm_cluster_name_os_user'][0] != f'{cluster}:{name}:{os_user}'):
|
|
1028
|
+
raise ValueError('Invalid administrator registration or confirmation')
|
|
1029
|
+
async def register_admin():
|
|
1030
|
+
async with client_factory() as client:
|
|
1031
|
+
return await client.register_cluster_admin(cluster, name, os_user, descr=descr)
|
|
1032
|
+
result = asyncio.run(register_admin())
|
|
1033
|
+
body = render_page('Verified cluster administrator registration', [result], '')
|
|
1034
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1035
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1036
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1037
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1038
|
+
return [b'Cluster administrator registration not verified' if method == 'POST'
|
|
1039
|
+
else b'RAC unavailable']
|
|
1040
|
+
start_response('200 OK', headers)
|
|
1041
|
+
return [body.encode('utf-8')]
|
|
1042
|
+
if path == '/agent/admins/register' and allow_agent_admin_register:
|
|
1043
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1044
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1045
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1046
|
+
if method not in ('GET', 'POST'):
|
|
1047
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1048
|
+
('Content-Type', 'text/plain')])
|
|
1049
|
+
return [b'Method not allowed']
|
|
1050
|
+
try:
|
|
1051
|
+
host = environ.get('HTTP_HOST', '')
|
|
1052
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1053
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1054
|
+
raise ValueError('Invalid mutation host')
|
|
1055
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1056
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1057
|
+
if method == 'GET':
|
|
1058
|
+
content = (f'<form method="post" action="/agent/admins/register">'
|
|
1059
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1060
|
+
'<label>Administrator name <input name="name" required></label>'
|
|
1061
|
+
'<label>OS user <input name="os_user" required></label>'
|
|
1062
|
+
'<label>Description <input name="descr"></label>'
|
|
1063
|
+
'<label>Type exact name:os_user to confirm '
|
|
1064
|
+
'<input name="confirm_name_os_user" required></label>'
|
|
1065
|
+
'<button type="submit">Register agent administrator</button></form>')
|
|
1066
|
+
body = render_document('Confirm agent administrator registration', content)
|
|
1067
|
+
else:
|
|
1068
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1069
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1070
|
+
origin.path or origin.query or origin.fragment):
|
|
1071
|
+
raise ValueError('Invalid mutation origin')
|
|
1072
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1073
|
+
raise ValueError('Invalid mutation content type')
|
|
1074
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1075
|
+
if not 0 <= length <= 4096:
|
|
1076
|
+
raise ValueError('Invalid mutation body length')
|
|
1077
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1078
|
+
if len(raw_body) != length:
|
|
1079
|
+
raise ValueError('Incomplete mutation body')
|
|
1080
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1081
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1082
|
+
if (set(fields) != {'csrf_token', 'name', 'os_user', 'descr',
|
|
1083
|
+
'confirm_name_os_user'} or
|
|
1084
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1085
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token)):
|
|
1086
|
+
raise ValueError('Invalid mutation fields')
|
|
1087
|
+
name, os_user, descr = (fields[key][0] for key in ('name', 'os_user', 'descr'))
|
|
1088
|
+
if (not name or not os_user or any('\x00' in value or
|
|
1089
|
+
len(value.encode('utf-8')) > 1024 for value in (name, os_user, descr)) or
|
|
1090
|
+
fields['confirm_name_os_user'][0] != f'{name}:{os_user}'):
|
|
1091
|
+
raise ValueError('Invalid administrator registration or confirmation')
|
|
1092
|
+
async def register_admin():
|
|
1093
|
+
async with client_factory() as client:
|
|
1094
|
+
return await client.register_agent_admin(name, os_user, descr=descr)
|
|
1095
|
+
result = asyncio.run(register_admin())
|
|
1096
|
+
body = render_page('Verified agent administrator registration', [result], '')
|
|
1097
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1098
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1099
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1100
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1101
|
+
return [b'Agent administrator registration not verified' if method == 'POST'
|
|
1102
|
+
else b'RAC unavailable']
|
|
1103
|
+
start_response('200 OK', headers)
|
|
1104
|
+
return [body.encode('utf-8')]
|
|
1105
|
+
if path == '/clusters/insert' and allow_cluster_insert:
|
|
1106
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1107
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1108
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1109
|
+
try:
|
|
1110
|
+
host = environ.get('HTTP_HOST', '')
|
|
1111
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1112
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1113
|
+
raise ValueError('Invalid mutation host')
|
|
1114
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1115
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1116
|
+
if method == 'GET':
|
|
1117
|
+
content = (f'<form method="post" action="/clusters/insert">'
|
|
1118
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1119
|
+
'<label>Cluster host <input name="host" required></label>'
|
|
1120
|
+
'<label>Cluster port <input name="port" required></label>'
|
|
1121
|
+
'<label>Cluster name <input name="name"></label>'
|
|
1122
|
+
'<label>Access-right audit recording <select name="audit">'
|
|
1123
|
+
'<option value="off">off</option><option value="on">on</option></select></label>'
|
|
1124
|
+
'<fieldset><legend>Verified cluster settings (optional)</legend>'
|
|
1125
|
+
'<label>Expiration timeout <input name="expiration_timeout" type="number" min="0"></label>'
|
|
1126
|
+
'<label>Security level <input name="security_level" type="number" min="0"></label>'
|
|
1127
|
+
'<label>Session fault tolerance <input name="session_fault_tolerance_level" type="number" min="0"></label>'
|
|
1128
|
+
'<label>Load balancing <select name="load_balancing_mode">'
|
|
1129
|
+
'<option value="performance">performance</option><option value="memory">memory</option>'
|
|
1130
|
+
'</select></label>'
|
|
1131
|
+
'<label>Kill problem processes <select name="kill_problem_processes">'
|
|
1132
|
+
'<option value="yes">yes</option><option value="no">no</option></select></label>'
|
|
1133
|
+
'<label>Kill by memory with dump <select name="kill_by_memory_with_dump">'
|
|
1134
|
+
'<option value="no">no</option><option value="yes">yes</option></select></label>'
|
|
1135
|
+
'<label>Restart schedule (six integers) <input name="restart_schedule"></label>'
|
|
1136
|
+
'<label>Ping period <input name="ping_period" type="number" min="0"></label>'
|
|
1137
|
+
'<label>Ping timeout <input name="ping_timeout" type="number" min="0"></label>'
|
|
1138
|
+
'</fieldset>'
|
|
1139
|
+
'<label>Type exact host:port to confirm '
|
|
1140
|
+
'<input name="confirm_host_port" required></label>'
|
|
1141
|
+
'<button type="submit">Insert cluster</button></form>')
|
|
1142
|
+
body = render_document('Confirm cluster insertion', content)
|
|
1143
|
+
elif method == 'POST':
|
|
1144
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1145
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1146
|
+
origin.path or origin.query or origin.fragment):
|
|
1147
|
+
raise ValueError('Invalid mutation origin')
|
|
1148
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1149
|
+
raise ValueError('Invalid mutation content type')
|
|
1150
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1151
|
+
if not 0 <= length <= 4096:
|
|
1152
|
+
raise ValueError('Invalid mutation body length')
|
|
1153
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1154
|
+
if len(raw_body) != length:
|
|
1155
|
+
raise ValueError('Incomplete mutation body')
|
|
1156
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1157
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1158
|
+
optional = {'expiration_timeout', 'security_level',
|
|
1159
|
+
'session_fault_tolerance_level', 'load_balancing_mode',
|
|
1160
|
+
'kill_problem_processes', 'kill_by_memory_with_dump',
|
|
1161
|
+
'restart_schedule', 'ping_period', 'ping_timeout'}
|
|
1162
|
+
if (not {'csrf_token', 'host', 'port', 'name', 'audit',
|
|
1163
|
+
'confirm_host_port'} <= set(fields)
|
|
1164
|
+
or set(fields) - {'csrf_token', 'host', 'port', 'name',
|
|
1165
|
+
'audit', 'confirm_host_port'} - optional or
|
|
1166
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1167
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token)):
|
|
1168
|
+
raise ValueError('Invalid mutation fields')
|
|
1169
|
+
defaults = {'expiration_timeout': 60, 'security_level': 0,
|
|
1170
|
+
'session_fault_tolerance_level': 0,
|
|
1171
|
+
'load_balancing_mode': 'performance',
|
|
1172
|
+
'kill_problem_processes': True,
|
|
1173
|
+
'kill_by_memory_with_dump': False, 'restart_schedule': '',
|
|
1174
|
+
'ping_period': 0, 'ping_timeout': 0}
|
|
1175
|
+
options = {}
|
|
1176
|
+
for key in ('expiration_timeout', 'security_level',
|
|
1177
|
+
'session_fault_tolerance_level', 'ping_period', 'ping_timeout'):
|
|
1178
|
+
text = fields.get(key, [''])[0]
|
|
1179
|
+
if text:
|
|
1180
|
+
if not re.fullmatch(r'(0|[1-9][0-9]{0,9})', text) or int(text) >= 2**31:
|
|
1181
|
+
raise ValueError('Invalid cluster numeric setting')
|
|
1182
|
+
options[key] = int(text)
|
|
1183
|
+
for key in ('kill_problem_processes', 'kill_by_memory_with_dump'):
|
|
1184
|
+
text = fields.get(key, [''])[0]
|
|
1185
|
+
if text:
|
|
1186
|
+
if text not in ('yes', 'no'):
|
|
1187
|
+
raise ValueError('Invalid cluster boolean setting')
|
|
1188
|
+
options[key] = text == 'yes'
|
|
1189
|
+
if 'load_balancing_mode' in fields:
|
|
1190
|
+
value = fields['load_balancing_mode'][0]
|
|
1191
|
+
if value not in ('performance', 'memory'):
|
|
1192
|
+
raise ValueError('Invalid cluster load balancing mode')
|
|
1193
|
+
options['load_balancing_mode'] = value
|
|
1194
|
+
if 'restart_schedule' in fields:
|
|
1195
|
+
schedule = fields['restart_schedule'][0]
|
|
1196
|
+
if ('\x00' in schedule or len(schedule.encode('utf-8')) > 256 or
|
|
1197
|
+
(schedule and (len(schedule.split()) != 6 or any(
|
|
1198
|
+
not part.isascii() or not part.isdecimal()
|
|
1199
|
+
for part in schedule.split())))):
|
|
1200
|
+
raise ValueError('Invalid cluster restart schedule')
|
|
1201
|
+
options['restart_schedule'] = schedule
|
|
1202
|
+
period = options.get('ping_period', 0)
|
|
1203
|
+
timeout = options.get('ping_timeout', 0)
|
|
1204
|
+
if not (period == timeout == 0 or 0 < period < timeout):
|
|
1205
|
+
raise ValueError('Invalid cluster ping period/timeout pair')
|
|
1206
|
+
options = {key: value for key, value in options.items() if value != defaults[key]}
|
|
1207
|
+
cluster_host, port_text, name = (fields[key][0] for key in ('host', 'port', 'name'))
|
|
1208
|
+
if (not cluster_host or '\x00' in cluster_host or
|
|
1209
|
+
len(cluster_host.encode('utf-8')) > 255 or
|
|
1210
|
+
not re.fullmatch(r'[1-9][0-9]{0,4}', port_text) or
|
|
1211
|
+
not 1 <= int(port_text) <= 65535 or '\x00' in name or
|
|
1212
|
+
len(name.encode('utf-8')) > 256 or
|
|
1213
|
+
fields['audit'][0] not in ('on', 'off') or
|
|
1214
|
+
fields['confirm_host_port'][0] != f'{cluster_host}:{port_text}'):
|
|
1215
|
+
raise ValueError('Invalid cluster insertion parameters or confirmation')
|
|
1216
|
+
async def insert_cluster():
|
|
1217
|
+
async with client_factory() as client:
|
|
1218
|
+
return await client.insert_cluster(cluster_host, int(port_text), name,
|
|
1219
|
+
audit=fields['audit'][0] == 'on', **options)
|
|
1220
|
+
result = asyncio.run(insert_cluster())
|
|
1221
|
+
body = render_page('Verified cluster insertion', [result], '')
|
|
1222
|
+
else:
|
|
1223
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1224
|
+
('Content-Type', 'text/plain')])
|
|
1225
|
+
return [b'Method not allowed']
|
|
1226
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1227
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1228
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1229
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1230
|
+
return [b'Cluster insertion not verified' if method == 'POST' else b'RAC unavailable']
|
|
1231
|
+
start_response('200 OK', headers)
|
|
1232
|
+
return [body.encode('utf-8')]
|
|
1233
|
+
cluster_remove = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/remove', path)
|
|
1234
|
+
if cluster_remove and allow_cluster_remove:
|
|
1235
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1236
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1237
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1238
|
+
try:
|
|
1239
|
+
cluster = UUID(cluster_remove.group(1))
|
|
1240
|
+
host = environ.get('HTTP_HOST', '')
|
|
1241
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1242
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1243
|
+
raise ValueError('Invalid mutation host')
|
|
1244
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1245
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1246
|
+
if method == 'GET':
|
|
1247
|
+
async def cluster_info():
|
|
1248
|
+
async with client_factory() as client:
|
|
1249
|
+
return await client.get_cluster(cluster)
|
|
1250
|
+
row = asyncio.run(cluster_info())
|
|
1251
|
+
content = (render_section('Cluster', [row], '') +
|
|
1252
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1253
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1254
|
+
'<label>Type exact cluster UUID to confirm removal '
|
|
1255
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1256
|
+
'<button type="submit">Remove cluster</button></form>')
|
|
1257
|
+
body = render_document('Confirm cluster removal', content)
|
|
1258
|
+
elif method == 'POST':
|
|
1259
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1260
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1261
|
+
origin.path or origin.query or origin.fragment):
|
|
1262
|
+
raise ValueError('Invalid mutation origin')
|
|
1263
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1264
|
+
raise ValueError('Invalid mutation content type')
|
|
1265
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1266
|
+
if not 0 <= length <= 4096:
|
|
1267
|
+
raise ValueError('Invalid mutation body length')
|
|
1268
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1269
|
+
if len(raw_body) != length:
|
|
1270
|
+
raise ValueError('Incomplete mutation body')
|
|
1271
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1272
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1273
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id'} or
|
|
1274
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1275
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1276
|
+
fields['confirm_cluster_id'][0] != str(cluster)):
|
|
1277
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1278
|
+
async def remove_cluster():
|
|
1279
|
+
async with client_factory() as client:
|
|
1280
|
+
return await client.remove_cluster(cluster)
|
|
1281
|
+
result = asyncio.run(remove_cluster())
|
|
1282
|
+
body = render_page('Verified cluster removal', result, '/clusters')
|
|
1283
|
+
else:
|
|
1284
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1285
|
+
('Content-Type', 'text/plain')])
|
|
1286
|
+
return [b'Method not allowed']
|
|
1287
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1288
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1289
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1290
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1291
|
+
return [b'Cluster removal not verified' if method == 'POST' else b'RAC unavailable']
|
|
1292
|
+
start_response('200 OK', headers)
|
|
1293
|
+
return [body.encode('utf-8')]
|
|
1294
|
+
server_ranges_update = re.fullmatch(
|
|
1295
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/port-ranges', path)
|
|
1296
|
+
if server_ranges_update and allow_server_port_ranges_update:
|
|
1297
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1298
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1299
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1300
|
+
try:
|
|
1301
|
+
cluster, server = UUID(server_ranges_update.group(1)), UUID(server_ranges_update.group(2))
|
|
1302
|
+
host = environ.get('HTTP_HOST', '')
|
|
1303
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1304
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1305
|
+
raise ValueError('Invalid mutation host')
|
|
1306
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1307
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1308
|
+
if method == 'GET':
|
|
1309
|
+
async def server_info():
|
|
1310
|
+
async with client_factory() as client:
|
|
1311
|
+
return await client.get_server(cluster, server)
|
|
1312
|
+
row = asyncio.run(server_info())
|
|
1313
|
+
content = (render_section('Working server', [row], '') +
|
|
1314
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1315
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1316
|
+
'<label>Port ranges (start:end, comma separated; 1..65535) '
|
|
1317
|
+
'<input name="ranges" required></label>'
|
|
1318
|
+
'<label>Type exact server UUID to confirm '
|
|
1319
|
+
'<input name="confirm_server_id" required></label>'
|
|
1320
|
+
'<button type="submit">Update server port ranges</button></form>')
|
|
1321
|
+
body = render_document('Confirm server port ranges update', content)
|
|
1322
|
+
elif method == 'POST':
|
|
1323
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1324
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1325
|
+
origin.path or origin.query or origin.fragment):
|
|
1326
|
+
raise ValueError('Invalid mutation origin')
|
|
1327
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1328
|
+
raise ValueError('Invalid mutation content type')
|
|
1329
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1330
|
+
if not 0 <= length <= 4096:
|
|
1331
|
+
raise ValueError('Invalid mutation body length')
|
|
1332
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1333
|
+
if len(raw_body) != length:
|
|
1334
|
+
raise ValueError('Incomplete mutation body')
|
|
1335
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1336
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1337
|
+
if (set(fields) != {'csrf_token', 'confirm_server_id', 'ranges'} or
|
|
1338
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1339
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1340
|
+
fields['confirm_server_id'][0] != str(server)):
|
|
1341
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1342
|
+
parts = fields['ranges'][0].split(',')
|
|
1343
|
+
if not 1 <= len(parts) <= 64:
|
|
1344
|
+
raise ValueError('Invalid server port ranges')
|
|
1345
|
+
ranges = []
|
|
1346
|
+
for part in parts:
|
|
1347
|
+
pair = part.strip().split(':')
|
|
1348
|
+
if len(pair) != 2 or any(not value.isascii() or not value.isdecimal() for value in pair):
|
|
1349
|
+
raise ValueError('Invalid server port ranges')
|
|
1350
|
+
first, last = map(int, pair)
|
|
1351
|
+
if not 1 <= first <= last <= 65535:
|
|
1352
|
+
raise ValueError('Invalid server port ranges')
|
|
1353
|
+
ranges.append((first, last))
|
|
1354
|
+
ordered = sorted(ranges)
|
|
1355
|
+
if any(left[1] >= right[0] for left, right in zip(ordered, ordered[1:])):
|
|
1356
|
+
raise ValueError('Overlapping server port ranges')
|
|
1357
|
+
async def update_ranges():
|
|
1358
|
+
async with client_factory() as client:
|
|
1359
|
+
return await client.update_server_port_ranges(cluster, server, ranges)
|
|
1360
|
+
result = asyncio.run(update_ranges())
|
|
1361
|
+
body = render_page('Verified server port ranges update', [result], '')
|
|
1362
|
+
else:
|
|
1363
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1364
|
+
('Content-Type', 'text/plain')])
|
|
1365
|
+
return [b'Method not allowed']
|
|
1366
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1367
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1368
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1369
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1370
|
+
return [b'Server port ranges update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1371
|
+
start_response('200 OK', headers)
|
|
1372
|
+
return [body.encode('utf-8')]
|
|
1373
|
+
server_managers_update = re.fullmatch(
|
|
1374
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/dedicate-managers', path)
|
|
1375
|
+
if server_managers_update and allow_server_dedicate_managers_update:
|
|
1376
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1377
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1378
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1379
|
+
try:
|
|
1380
|
+
cluster, server = UUID(server_managers_update.group(1)), UUID(server_managers_update.group(2))
|
|
1381
|
+
host = environ.get('HTTP_HOST', '')
|
|
1382
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1383
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1384
|
+
raise ValueError('Invalid mutation host')
|
|
1385
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1386
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1387
|
+
if method == 'GET':
|
|
1388
|
+
async def server_info():
|
|
1389
|
+
async with client_factory() as client:
|
|
1390
|
+
return await client.get_server(cluster, server)
|
|
1391
|
+
row = asyncio.run(server_info())
|
|
1392
|
+
content = (render_section('Working server', [row], '') +
|
|
1393
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1394
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1395
|
+
'<label>Dedicated managers <select name="choice">'
|
|
1396
|
+
'<option value="none">none</option><option value="all">all</option>'
|
|
1397
|
+
'</select></label><label>Type exact server UUID to confirm '
|
|
1398
|
+
'<input name="confirm_server_id" required></label>'
|
|
1399
|
+
'<button type="submit">Update server dedicated managers</button></form>')
|
|
1400
|
+
body = render_document('Confirm server dedicated managers update', content)
|
|
1401
|
+
elif method == 'POST':
|
|
1402
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1403
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1404
|
+
origin.path or origin.query or origin.fragment):
|
|
1405
|
+
raise ValueError('Invalid mutation origin')
|
|
1406
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1407
|
+
raise ValueError('Invalid mutation content type')
|
|
1408
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1409
|
+
if not 0 <= length <= 4096:
|
|
1410
|
+
raise ValueError('Invalid mutation body length')
|
|
1411
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1412
|
+
if len(raw_body) != length:
|
|
1413
|
+
raise ValueError('Incomplete mutation body')
|
|
1414
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1415
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1416
|
+
if (set(fields) != {'csrf_token', 'confirm_server_id', 'choice'} or
|
|
1417
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1418
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1419
|
+
fields['confirm_server_id'][0] != str(server) or
|
|
1420
|
+
fields['choice'][0] not in ('all', 'none')):
|
|
1421
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1422
|
+
choice = fields['choice'][0]
|
|
1423
|
+
async def update_managers():
|
|
1424
|
+
async with client_factory() as client:
|
|
1425
|
+
return await client.update_server_dedicate_managers(cluster, server, choice)
|
|
1426
|
+
result = asyncio.run(update_managers())
|
|
1427
|
+
body = render_page('Verified server dedicated managers update', [result], '')
|
|
1428
|
+
else:
|
|
1429
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1430
|
+
('Content-Type', 'text/plain')])
|
|
1431
|
+
return [b'Method not allowed']
|
|
1432
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1433
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1434
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1435
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1436
|
+
return [b'Server dedicated managers update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1437
|
+
start_response('200 OK', headers)
|
|
1438
|
+
return [body.encode('utf-8')]
|
|
1439
|
+
server_spn_update = re.fullmatch(
|
|
1440
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/spn', path)
|
|
1441
|
+
if server_spn_update and allow_server_spn_update:
|
|
1442
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1443
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1444
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1445
|
+
try:
|
|
1446
|
+
cluster, server = UUID(server_spn_update.group(1)), UUID(server_spn_update.group(2))
|
|
1447
|
+
host = environ.get('HTTP_HOST', '')
|
|
1448
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1449
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1450
|
+
raise ValueError('Invalid mutation host')
|
|
1451
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1452
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1453
|
+
if method == 'GET':
|
|
1454
|
+
async def server_info():
|
|
1455
|
+
async with client_factory() as client:
|
|
1456
|
+
return await client.get_server(cluster, server)
|
|
1457
|
+
row = asyncio.run(server_info())
|
|
1458
|
+
content = (render_section('Working server', [row], '') +
|
|
1459
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1460
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1461
|
+
'<label>Service principal name (empty clears; max 256 UTF-8 bytes) '
|
|
1462
|
+
'<input name="name" maxlength="256"></label>'
|
|
1463
|
+
'<label>Type exact server UUID to confirm '
|
|
1464
|
+
'<input name="confirm_server_id" required></label>'
|
|
1465
|
+
'<button type="submit">Update server SPN</button></form>')
|
|
1466
|
+
body = render_document('Confirm server SPN update', content)
|
|
1467
|
+
elif method == 'POST':
|
|
1468
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1469
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1470
|
+
origin.path or origin.query or origin.fragment):
|
|
1471
|
+
raise ValueError('Invalid mutation origin')
|
|
1472
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1473
|
+
raise ValueError('Invalid mutation content type')
|
|
1474
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1475
|
+
if not 0 <= length <= 4096:
|
|
1476
|
+
raise ValueError('Invalid mutation body length')
|
|
1477
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1478
|
+
if len(raw_body) != length:
|
|
1479
|
+
raise ValueError('Incomplete mutation body')
|
|
1480
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1481
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1482
|
+
if (set(fields) != {'csrf_token', 'confirm_server_id', 'name'} or
|
|
1483
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1484
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1485
|
+
fields['confirm_server_id'][0] != str(server)):
|
|
1486
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1487
|
+
name = fields['name'][0]
|
|
1488
|
+
if '\x00' in name or len(name.encode('utf-8')) > 256:
|
|
1489
|
+
raise ValueError('Invalid working-server SPN')
|
|
1490
|
+
async def update_spn():
|
|
1491
|
+
async with client_factory() as client:
|
|
1492
|
+
return await client.update_server_service_principal_name(cluster, server, name)
|
|
1493
|
+
result = asyncio.run(update_spn())
|
|
1494
|
+
body = render_page('Verified server SPN update', [result], '')
|
|
1495
|
+
else:
|
|
1496
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1497
|
+
('Content-Type', 'text/plain')])
|
|
1498
|
+
return [b'Method not allowed']
|
|
1499
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1500
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1501
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1502
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1503
|
+
return [b'Server SPN update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1504
|
+
start_response('200 OK', headers)
|
|
1505
|
+
return [body.encode('utf-8')]
|
|
1506
|
+
server_limit_update = re.fullmatch(
|
|
1507
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/(infobases-limit|connections-limit|safe-call-memory-limit|critical-total-memory|temporary-total-memory|temporary-memory-time-limit)', path)
|
|
1508
|
+
if server_limit_update and ((server_limit_update.group(3) == 'infobases-limit' and
|
|
1509
|
+
allow_server_infobases_limit_update) or
|
|
1510
|
+
(server_limit_update.group(3) == 'connections-limit' and
|
|
1511
|
+
allow_server_connections_limit_update) or
|
|
1512
|
+
(server_limit_update.group(3) == 'safe-call-memory-limit' and
|
|
1513
|
+
allow_server_safe_call_memory_limit_update) or
|
|
1514
|
+
(server_limit_update.group(3) == 'critical-total-memory' and
|
|
1515
|
+
allow_server_critical_total_memory_update) or
|
|
1516
|
+
(server_limit_update.group(3) == 'temporary-total-memory' and
|
|
1517
|
+
allow_server_temporary_total_memory_update) or
|
|
1518
|
+
(server_limit_update.group(3) == 'temporary-memory-time-limit' and
|
|
1519
|
+
allow_server_temporary_memory_time_limit_update)):
|
|
1520
|
+
is_connections = server_limit_update.group(3) == 'connections-limit'
|
|
1521
|
+
is_safe_call = server_limit_update.group(3) == 'safe-call-memory-limit'
|
|
1522
|
+
is_critical = server_limit_update.group(3) == 'critical-total-memory'
|
|
1523
|
+
is_temporary = server_limit_update.group(3) == 'temporary-total-memory'
|
|
1524
|
+
is_temporary_time = server_limit_update.group(3) == 'temporary-memory-time-limit'
|
|
1525
|
+
label = ('temporary memory time limit' if is_temporary_time else 'temporary allowed total memory'
|
|
1526
|
+
if is_temporary else 'critical total memory' if is_critical else 'safe-call memory'
|
|
1527
|
+
if is_safe_call else 'connections' if is_connections else 'infobases')
|
|
1528
|
+
maximum = 2**63 - 1 if is_safe_call or is_critical or is_temporary or is_temporary_time else 2_147_483_647
|
|
1529
|
+
suffix = '' if is_critical or is_temporary or is_temporary_time else ' limit'
|
|
1530
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1531
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1532
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1533
|
+
try:
|
|
1534
|
+
cluster, server = UUID(server_limit_update.group(1)), UUID(server_limit_update.group(2))
|
|
1535
|
+
host = environ.get('HTTP_HOST', '')
|
|
1536
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1537
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1538
|
+
raise ValueError('Invalid mutation host')
|
|
1539
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1540
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1541
|
+
if method == 'GET':
|
|
1542
|
+
async def server_limit_info():
|
|
1543
|
+
async with client_factory() as client:
|
|
1544
|
+
return await client.get_server(cluster, server)
|
|
1545
|
+
row = asyncio.run(server_limit_info())
|
|
1546
|
+
content = (render_section('Working server', [row], '') +
|
|
1547
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1548
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1549
|
+
f'<label>{label.capitalize()}{suffix} '
|
|
1550
|
+
f'<input name="limit" type="number" min="0" max="{maximum}" required></label>'
|
|
1551
|
+
'<label>Type exact server UUID to confirm '
|
|
1552
|
+
'<input name="confirm_server_id" required></label>'
|
|
1553
|
+
f'<button type="submit">Update server {label}{suffix}</button></form>')
|
|
1554
|
+
body = render_document(f'Confirm server {label}{suffix} update', content)
|
|
1555
|
+
elif method == 'POST':
|
|
1556
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1557
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1558
|
+
origin.path or origin.query or origin.fragment):
|
|
1559
|
+
raise ValueError('Invalid mutation origin')
|
|
1560
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1561
|
+
raise ValueError('Invalid mutation content type')
|
|
1562
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1563
|
+
if not 0 <= length <= 4096:
|
|
1564
|
+
raise ValueError('Invalid mutation body length')
|
|
1565
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1566
|
+
if len(raw_body) != length:
|
|
1567
|
+
raise ValueError('Incomplete mutation body')
|
|
1568
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1569
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1570
|
+
if (set(fields) != {'csrf_token', 'confirm_server_id', 'limit'} or
|
|
1571
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1572
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1573
|
+
fields['confirm_server_id'][0] != str(server) or
|
|
1574
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,18})' if is_safe_call or is_critical or is_temporary or is_temporary_time else
|
|
1575
|
+
r'(0|[1-9][0-9]{0,9})', fields['limit'][0])):
|
|
1576
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1577
|
+
limit = int(fields['limit'][0])
|
|
1578
|
+
if limit > maximum:
|
|
1579
|
+
raise ValueError('Invalid working-server limit')
|
|
1580
|
+
async def update_server_limit():
|
|
1581
|
+
async with client_factory() as client:
|
|
1582
|
+
if is_safe_call:
|
|
1583
|
+
return await client.update_server_safe_call_memory_limit(cluster, server, limit)
|
|
1584
|
+
if is_critical:
|
|
1585
|
+
return await client.update_server_critical_total_memory(cluster, server, limit)
|
|
1586
|
+
if is_temporary:
|
|
1587
|
+
return await client.update_server_temporary_allowed_total_memory(cluster, server, limit)
|
|
1588
|
+
if is_temporary_time:
|
|
1589
|
+
return await client.update_server_temporary_allowed_total_memory_time_limit(cluster, server, limit)
|
|
1590
|
+
if is_connections:
|
|
1591
|
+
return await client.update_server_connections_limit(cluster, server, limit)
|
|
1592
|
+
return await client.update_server_infobases_limit(cluster, server, limit)
|
|
1593
|
+
result = asyncio.run(update_server_limit())
|
|
1594
|
+
body = render_page(f'Verified server {label}{suffix} update', [result], '')
|
|
1595
|
+
else:
|
|
1596
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1597
|
+
('Content-Type', 'text/plain')])
|
|
1598
|
+
return [b'Method not allowed']
|
|
1599
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1600
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1601
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1602
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1603
|
+
return [f'Server {label}{suffix} update not verified'.encode() if method == 'POST'
|
|
1604
|
+
else b'RAC unavailable']
|
|
1605
|
+
start_response('200 OK', headers)
|
|
1606
|
+
return [body.encode('utf-8')]
|
|
1607
|
+
server_schedule_update = re.fullmatch(
|
|
1608
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/restart-schedule', path)
|
|
1609
|
+
if server_schedule_update and allow_server_restart_schedule_update:
|
|
1610
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1611
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1612
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1613
|
+
try:
|
|
1614
|
+
cluster, server = UUID(server_schedule_update.group(1)), UUID(server_schedule_update.group(2))
|
|
1615
|
+
host = environ.get('HTTP_HOST', '')
|
|
1616
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1617
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1618
|
+
raise ValueError('Invalid mutation host')
|
|
1619
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1620
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1621
|
+
if method == 'GET':
|
|
1622
|
+
async def server_schedule_info():
|
|
1623
|
+
async with client_factory() as client:
|
|
1624
|
+
return await client.get_server(cluster, server)
|
|
1625
|
+
row = asyncio.run(server_schedule_info())
|
|
1626
|
+
content = (render_section('Working server', [row], '') +
|
|
1627
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1628
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1629
|
+
'<label>Restart schedule (empty clears) '
|
|
1630
|
+
'<input name="schedule" maxlength="256"></label>'
|
|
1631
|
+
'<label>Type exact server UUID to confirm '
|
|
1632
|
+
'<input name="confirm_server_id" required></label>'
|
|
1633
|
+
'<button type="submit">Update server restart schedule</button></form>')
|
|
1634
|
+
body = render_document('Confirm server restart schedule update', content)
|
|
1635
|
+
elif method == 'POST':
|
|
1636
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1637
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1638
|
+
origin.path or origin.query or origin.fragment):
|
|
1639
|
+
raise ValueError('Invalid mutation origin')
|
|
1640
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1641
|
+
raise ValueError('Invalid mutation content type')
|
|
1642
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1643
|
+
if not 0 <= length <= 4096:
|
|
1644
|
+
raise ValueError('Invalid mutation body length')
|
|
1645
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1646
|
+
if len(raw_body) != length:
|
|
1647
|
+
raise ValueError('Incomplete mutation body')
|
|
1648
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1649
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1650
|
+
if (set(fields) != {'csrf_token', 'confirm_server_id', 'schedule'} or
|
|
1651
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1652
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1653
|
+
fields['confirm_server_id'][0] != str(server)):
|
|
1654
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1655
|
+
schedule = fields['schedule'][0]
|
|
1656
|
+
RacClient._validate_schedule(schedule)
|
|
1657
|
+
async def update_server_schedule():
|
|
1658
|
+
async with client_factory() as client:
|
|
1659
|
+
return await client.update_server_restart_schedule(cluster, server, schedule)
|
|
1660
|
+
result = asyncio.run(update_server_schedule())
|
|
1661
|
+
body = render_page('Verified server restart schedule update', [result], '')
|
|
1662
|
+
else:
|
|
1663
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1664
|
+
('Content-Type', 'text/plain')])
|
|
1665
|
+
return [b'Method not allowed']
|
|
1666
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1667
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1668
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1669
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1670
|
+
return [b'Server restart schedule update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1671
|
+
start_response('200 OK', headers)
|
|
1672
|
+
return [body.encode('utf-8')]
|
|
1673
|
+
schedule_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/restart-schedule', path)
|
|
1674
|
+
if schedule_update and allow_cluster_restart_schedule_update:
|
|
1675
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1676
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1677
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1678
|
+
try:
|
|
1679
|
+
cluster = UUID(schedule_update.group(1))
|
|
1680
|
+
host = environ.get('HTTP_HOST', '')
|
|
1681
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1682
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1683
|
+
raise ValueError('Invalid mutation host')
|
|
1684
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1685
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1686
|
+
if method == 'GET':
|
|
1687
|
+
async def schedule_info():
|
|
1688
|
+
async with client_factory() as client:
|
|
1689
|
+
return await client.get_cluster(cluster)
|
|
1690
|
+
row = asyncio.run(schedule_info())
|
|
1691
|
+
content = (render_section('Cluster', [row], '') +
|
|
1692
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1693
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1694
|
+
'<label>Restart schedule (empty clears) '
|
|
1695
|
+
'<input name="schedule" maxlength="256"></label>'
|
|
1696
|
+
'<label>Type exact cluster UUID to confirm '
|
|
1697
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1698
|
+
'<button type="submit">Update cluster restart schedule</button></form>')
|
|
1699
|
+
body = render_document('Confirm cluster restart schedule update', content)
|
|
1700
|
+
elif method == 'POST':
|
|
1701
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1702
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1703
|
+
origin.path or origin.query or origin.fragment):
|
|
1704
|
+
raise ValueError('Invalid mutation origin')
|
|
1705
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1706
|
+
raise ValueError('Invalid mutation content type')
|
|
1707
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1708
|
+
if not 0 <= length <= 4096:
|
|
1709
|
+
raise ValueError('Invalid mutation body length')
|
|
1710
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1711
|
+
if len(raw_body) != length:
|
|
1712
|
+
raise ValueError('Incomplete mutation body')
|
|
1713
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1714
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1715
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'schedule'} or
|
|
1716
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1717
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1718
|
+
fields['confirm_cluster_id'][0] != str(cluster)):
|
|
1719
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1720
|
+
schedule = fields['schedule'][0]
|
|
1721
|
+
RacClient._validate_schedule(schedule)
|
|
1722
|
+
async def update_schedule():
|
|
1723
|
+
async with client_factory() as client:
|
|
1724
|
+
return await client.update_restart_schedule(cluster, schedule)
|
|
1725
|
+
result = asyncio.run(update_schedule())
|
|
1726
|
+
body = render_page('Verified cluster restart schedule update', [result], '')
|
|
1727
|
+
else:
|
|
1728
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1729
|
+
('Content-Type', 'text/plain')])
|
|
1730
|
+
return [b'Method not allowed']
|
|
1731
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1732
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1733
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1734
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1735
|
+
return [b'Cluster restart schedule update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1736
|
+
start_response('200 OK', headers)
|
|
1737
|
+
return [body.encode('utf-8')]
|
|
1738
|
+
ping_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/ping', path)
|
|
1739
|
+
if ping_update and allow_cluster_ping_update:
|
|
1740
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1741
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1742
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1743
|
+
try:
|
|
1744
|
+
cluster = UUID(ping_update.group(1))
|
|
1745
|
+
host = environ.get('HTTP_HOST', '')
|
|
1746
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1747
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1748
|
+
raise ValueError('Invalid mutation host')
|
|
1749
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1750
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1751
|
+
if method == 'GET':
|
|
1752
|
+
async def ping_info():
|
|
1753
|
+
async with client_factory() as client:
|
|
1754
|
+
return await client.get_cluster(cluster)
|
|
1755
|
+
row = asyncio.run(ping_info())
|
|
1756
|
+
content = (render_section('Cluster', [row], '') +
|
|
1757
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1758
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1759
|
+
'<label>Ping period <input name="period" type="number" min="0" '
|
|
1760
|
+
'max="2147483647" required></label>'
|
|
1761
|
+
'<label>Ping timeout <input name="timeout" type="number" min="0" '
|
|
1762
|
+
'max="2147483647" required></label>'
|
|
1763
|
+
'<label>Type exact cluster UUID to confirm '
|
|
1764
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1765
|
+
'<button type="submit">Update cluster ping</button></form>')
|
|
1766
|
+
body = render_document('Confirm cluster ping update', content)
|
|
1767
|
+
elif method == 'POST':
|
|
1768
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1769
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1770
|
+
origin.path or origin.query or origin.fragment):
|
|
1771
|
+
raise ValueError('Invalid mutation origin')
|
|
1772
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1773
|
+
raise ValueError('Invalid mutation content type')
|
|
1774
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1775
|
+
if not 0 <= length <= 4096:
|
|
1776
|
+
raise ValueError('Invalid mutation body length')
|
|
1777
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1778
|
+
if len(raw_body) != length:
|
|
1779
|
+
raise ValueError('Incomplete mutation body')
|
|
1780
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1781
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1782
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'period', 'timeout'} or
|
|
1783
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1784
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1785
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
1786
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,9})', fields['period'][0]) or
|
|
1787
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,9})', fields['timeout'][0])):
|
|
1788
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1789
|
+
period, timeout = int(fields['period'][0]), int(fields['timeout'][0])
|
|
1790
|
+
if not ((period == 0 and timeout == 0) or
|
|
1791
|
+
(0 < period < timeout <= 2_147_483_647)):
|
|
1792
|
+
raise ValueError('Invalid cluster ping period/timeout pair')
|
|
1793
|
+
async def update_ping():
|
|
1794
|
+
async with client_factory() as client:
|
|
1795
|
+
return await client.update_cluster_ping(cluster, period, timeout)
|
|
1796
|
+
result = asyncio.run(update_ping())
|
|
1797
|
+
body = render_page('Verified cluster ping update', [result], '')
|
|
1798
|
+
else:
|
|
1799
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1800
|
+
('Content-Type', 'text/plain')])
|
|
1801
|
+
return [b'Method not allowed']
|
|
1802
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1803
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1804
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1805
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1806
|
+
return [b'Cluster ping update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1807
|
+
start_response('200 OK', headers)
|
|
1808
|
+
return [body.encode('utf-8')]
|
|
1809
|
+
audit_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/audit', path)
|
|
1810
|
+
if audit_update and allow_cluster_audit_update:
|
|
1811
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1812
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1813
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1814
|
+
try:
|
|
1815
|
+
cluster = UUID(audit_update.group(1))
|
|
1816
|
+
host = environ.get('HTTP_HOST', '')
|
|
1817
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1818
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1819
|
+
raise ValueError('Invalid mutation host')
|
|
1820
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1821
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1822
|
+
if method == 'GET':
|
|
1823
|
+
async def audit_info():
|
|
1824
|
+
async with client_factory() as client:
|
|
1825
|
+
return await client.get_cluster(cluster)
|
|
1826
|
+
row = asyncio.run(audit_info())
|
|
1827
|
+
content = (render_section('Cluster', [row], '') +
|
|
1828
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1829
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1830
|
+
'<label>Access-right audit recording <select name="enabled">'
|
|
1831
|
+
'<option value="on">on</option><option value="off">off</option></select></label>'
|
|
1832
|
+
'<label>Type exact cluster UUID to confirm '
|
|
1833
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1834
|
+
'<button type="submit">Update cluster audit</button></form>')
|
|
1835
|
+
body = render_document('Confirm cluster audit update', content)
|
|
1836
|
+
elif method == 'POST':
|
|
1837
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1838
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1839
|
+
origin.path or origin.query or origin.fragment):
|
|
1840
|
+
raise ValueError('Invalid mutation origin')
|
|
1841
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1842
|
+
raise ValueError('Invalid mutation content type')
|
|
1843
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1844
|
+
if not 0 <= length <= 4096:
|
|
1845
|
+
raise ValueError('Invalid mutation body length')
|
|
1846
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1847
|
+
if len(raw_body) != length:
|
|
1848
|
+
raise ValueError('Incomplete mutation body')
|
|
1849
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1850
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1851
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'enabled'} or
|
|
1852
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1853
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1854
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
1855
|
+
fields['enabled'][0] not in ('on', 'off')):
|
|
1856
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1857
|
+
async def update_audit():
|
|
1858
|
+
async with client_factory() as client:
|
|
1859
|
+
return await client.update_cluster_audit(cluster, fields['enabled'][0] == 'on')
|
|
1860
|
+
result = asyncio.run(update_audit())
|
|
1861
|
+
body = render_page('Verified cluster audit update', [result], '')
|
|
1862
|
+
else:
|
|
1863
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1864
|
+
('Content-Type', 'text/plain')])
|
|
1865
|
+
return [b'Method not allowed']
|
|
1866
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1867
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1868
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1869
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1870
|
+
return [b'Cluster audit update not verified' if method == 'POST' else b'RAC unavailable']
|
|
1871
|
+
start_response('200 OK', headers)
|
|
1872
|
+
return [body.encode('utf-8')]
|
|
1873
|
+
memory_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/kill-by-memory-with-dump', path)
|
|
1874
|
+
if memory_update and allow_cluster_kill_by_memory_with_dump_update:
|
|
1875
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1876
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1877
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1878
|
+
try:
|
|
1879
|
+
cluster = UUID(memory_update.group(1))
|
|
1880
|
+
host = environ.get('HTTP_HOST', '')
|
|
1881
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1882
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1883
|
+
raise ValueError('Invalid mutation host')
|
|
1884
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1885
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1886
|
+
if method == 'GET':
|
|
1887
|
+
async def memory_info():
|
|
1888
|
+
async with client_factory() as client:
|
|
1889
|
+
return await client.get_cluster(cluster)
|
|
1890
|
+
row = asyncio.run(memory_info())
|
|
1891
|
+
content = (render_section('Cluster', [row], '') +
|
|
1892
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1893
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1894
|
+
'<label>Kill by memory with dump <select name="enabled">'
|
|
1895
|
+
'<option value="on">on</option><option value="off">off</option></select></label>'
|
|
1896
|
+
'<label>Type exact cluster UUID to confirm '
|
|
1897
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1898
|
+
'<button type="submit">Update cluster kill-by-memory dump</button></form>')
|
|
1899
|
+
body = render_document('Confirm cluster kill-by-memory dump update', content)
|
|
1900
|
+
elif method == 'POST':
|
|
1901
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1902
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1903
|
+
origin.path or origin.query or origin.fragment):
|
|
1904
|
+
raise ValueError('Invalid mutation origin')
|
|
1905
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1906
|
+
raise ValueError('Invalid mutation content type')
|
|
1907
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1908
|
+
if not 0 <= length <= 4096:
|
|
1909
|
+
raise ValueError('Invalid mutation body length')
|
|
1910
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1911
|
+
if len(raw_body) != length:
|
|
1912
|
+
raise ValueError('Incomplete mutation body')
|
|
1913
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1914
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1915
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'enabled'} or
|
|
1916
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1917
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1918
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
1919
|
+
fields['enabled'][0] not in ('on', 'off')):
|
|
1920
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1921
|
+
async def update_memory():
|
|
1922
|
+
async with client_factory() as client:
|
|
1923
|
+
return await client.update_cluster_kill_by_memory_with_dump(
|
|
1924
|
+
cluster, fields['enabled'][0] == 'on')
|
|
1925
|
+
result = asyncio.run(update_memory())
|
|
1926
|
+
body = render_page('Verified cluster kill-by-memory dump update', [result], '')
|
|
1927
|
+
else:
|
|
1928
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1929
|
+
('Content-Type', 'text/plain')])
|
|
1930
|
+
return [b'Method not allowed']
|
|
1931
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1932
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1933
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
1934
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
1935
|
+
return [b'Cluster kill-by-memory dump update not verified'
|
|
1936
|
+
if method == 'POST' else b'RAC unavailable']
|
|
1937
|
+
start_response('200 OK', headers)
|
|
1938
|
+
return [body.encode('utf-8')]
|
|
1939
|
+
kill_policy_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/kill-problem-processes', path)
|
|
1940
|
+
if kill_policy_update and allow_cluster_kill_problem_processes_update:
|
|
1941
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
1942
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
1943
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
1944
|
+
try:
|
|
1945
|
+
cluster = UUID(kill_policy_update.group(1))
|
|
1946
|
+
host = environ.get('HTTP_HOST', '')
|
|
1947
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
1948
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
1949
|
+
raise ValueError('Invalid mutation host')
|
|
1950
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
1951
|
+
raise ValueError('Mutation requires loopback peer')
|
|
1952
|
+
if method == 'GET':
|
|
1953
|
+
async def policy_info():
|
|
1954
|
+
async with client_factory() as client:
|
|
1955
|
+
return await client.get_cluster(cluster)
|
|
1956
|
+
row = asyncio.run(policy_info())
|
|
1957
|
+
content = (render_section('Cluster', [row], '') +
|
|
1958
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
1959
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
1960
|
+
'<label>Terminate problem processes <select name="enabled">'
|
|
1961
|
+
'<option value="on">on</option><option value="off">off</option></select></label>'
|
|
1962
|
+
'<label>Type exact cluster UUID to confirm '
|
|
1963
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
1964
|
+
'<button type="submit">Update cluster problem-process policy</button></form>')
|
|
1965
|
+
body = render_document('Confirm cluster problem-process policy update', content)
|
|
1966
|
+
elif method == 'POST':
|
|
1967
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
1968
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
1969
|
+
origin.path or origin.query or origin.fragment):
|
|
1970
|
+
raise ValueError('Invalid mutation origin')
|
|
1971
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
1972
|
+
raise ValueError('Invalid mutation content type')
|
|
1973
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
1974
|
+
if not 0 <= length <= 4096:
|
|
1975
|
+
raise ValueError('Invalid mutation body length')
|
|
1976
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
1977
|
+
if len(raw_body) != length:
|
|
1978
|
+
raise ValueError('Incomplete mutation body')
|
|
1979
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
1980
|
+
keep_blank_values=True, strict_parsing=True)
|
|
1981
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'enabled'} or
|
|
1982
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
1983
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
1984
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
1985
|
+
fields['enabled'][0] not in ('on', 'off')):
|
|
1986
|
+
raise ValueError('Invalid mutation confirmation')
|
|
1987
|
+
async def update_policy():
|
|
1988
|
+
async with client_factory() as client:
|
|
1989
|
+
return await client.update_cluster_kill_problem_processes(
|
|
1990
|
+
cluster, fields['enabled'][0] == 'on')
|
|
1991
|
+
result = asyncio.run(update_policy())
|
|
1992
|
+
body = render_page('Verified cluster problem-process policy update', [result], '')
|
|
1993
|
+
else:
|
|
1994
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
1995
|
+
('Content-Type', 'text/plain')])
|
|
1996
|
+
return [b'Method not allowed']
|
|
1997
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
1998
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
1999
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2000
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2001
|
+
return [b'Cluster problem-process policy update not verified'
|
|
2002
|
+
if method == 'POST' else b'RAC unavailable']
|
|
2003
|
+
start_response('200 OK', headers)
|
|
2004
|
+
return [body.encode('utf-8')]
|
|
2005
|
+
balancing_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/load-balancing-mode', path)
|
|
2006
|
+
if balancing_update and allow_cluster_load_balancing_mode_update:
|
|
2007
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2008
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2009
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2010
|
+
try:
|
|
2011
|
+
cluster = UUID(balancing_update.group(1))
|
|
2012
|
+
host = environ.get('HTTP_HOST', '')
|
|
2013
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2014
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2015
|
+
raise ValueError('Invalid mutation host')
|
|
2016
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2017
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2018
|
+
if method == 'GET':
|
|
2019
|
+
async def balancing_info():
|
|
2020
|
+
async with client_factory() as client:
|
|
2021
|
+
return await client.get_cluster(cluster)
|
|
2022
|
+
row = asyncio.run(balancing_info())
|
|
2023
|
+
content = (render_section('Cluster', [row], '') +
|
|
2024
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2025
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2026
|
+
'<label>Load balancing mode <select name="mode">'
|
|
2027
|
+
'<option value="performance">performance</option>'
|
|
2028
|
+
'<option value="memory">memory</option></select></label>'
|
|
2029
|
+
'<label>Type exact cluster UUID to confirm '
|
|
2030
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
2031
|
+
'<button type="submit">Update cluster load balancing mode</button></form>')
|
|
2032
|
+
body = render_document('Confirm cluster load balancing mode update', content)
|
|
2033
|
+
elif method == 'POST':
|
|
2034
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2035
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2036
|
+
origin.path or origin.query or origin.fragment):
|
|
2037
|
+
raise ValueError('Invalid mutation origin')
|
|
2038
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2039
|
+
raise ValueError('Invalid mutation content type')
|
|
2040
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2041
|
+
if not 0 <= length <= 4096:
|
|
2042
|
+
raise ValueError('Invalid mutation body length')
|
|
2043
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2044
|
+
if len(raw_body) != length:
|
|
2045
|
+
raise ValueError('Incomplete mutation body')
|
|
2046
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
2047
|
+
keep_blank_values=True, strict_parsing=True)
|
|
2048
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'mode'} or
|
|
2049
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2050
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2051
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
2052
|
+
fields['mode'][0] not in ('performance', 'memory')):
|
|
2053
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2054
|
+
async def update_balancing():
|
|
2055
|
+
async with client_factory() as client:
|
|
2056
|
+
return await client.update_cluster_load_balancing_mode(cluster, fields['mode'][0])
|
|
2057
|
+
result = asyncio.run(update_balancing())
|
|
2058
|
+
body = render_page('Verified cluster load balancing mode update', [result], '')
|
|
2059
|
+
else:
|
|
2060
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2061
|
+
('Content-Type', 'text/plain')])
|
|
2062
|
+
return [b'Method not allowed']
|
|
2063
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2064
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2065
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2066
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2067
|
+
return [b'Cluster load balancing mode update not verified'
|
|
2068
|
+
if method == 'POST' else b'RAC unavailable']
|
|
2069
|
+
start_response('200 OK', headers)
|
|
2070
|
+
return [body.encode('utf-8')]
|
|
2071
|
+
fault_tolerance_update = re.fullmatch(
|
|
2072
|
+
r'/clusters/([0-9a-fA-F-]{36})/session-fault-tolerance-level', path)
|
|
2073
|
+
if fault_tolerance_update and allow_cluster_session_fault_tolerance_level_update:
|
|
2074
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2075
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2076
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2077
|
+
try:
|
|
2078
|
+
cluster = UUID(fault_tolerance_update.group(1))
|
|
2079
|
+
host = environ.get('HTTP_HOST', '')
|
|
2080
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2081
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2082
|
+
raise ValueError('Invalid mutation host')
|
|
2083
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2084
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2085
|
+
if method == 'GET':
|
|
2086
|
+
async def fault_tolerance_info():
|
|
2087
|
+
async with client_factory() as client:
|
|
2088
|
+
return await client.get_cluster(cluster)
|
|
2089
|
+
row = asyncio.run(fault_tolerance_info())
|
|
2090
|
+
content = (render_section('Cluster', [row], '') +
|
|
2091
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2092
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2093
|
+
'<label>Session fault tolerance level '
|
|
2094
|
+
'<input name="level" type="number" min="0" max="2147483647" required></label>'
|
|
2095
|
+
'<label>Type exact cluster UUID to confirm '
|
|
2096
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
2097
|
+
'<button type="submit">Update cluster session fault tolerance level</button></form>')
|
|
2098
|
+
body = render_document('Confirm cluster session fault tolerance level update', content)
|
|
2099
|
+
elif method == 'POST':
|
|
2100
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2101
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2102
|
+
origin.path or origin.query or origin.fragment):
|
|
2103
|
+
raise ValueError('Invalid mutation origin')
|
|
2104
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2105
|
+
raise ValueError('Invalid mutation content type')
|
|
2106
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2107
|
+
if not 0 <= length <= 4096:
|
|
2108
|
+
raise ValueError('Invalid mutation body length')
|
|
2109
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2110
|
+
if len(raw_body) != length:
|
|
2111
|
+
raise ValueError('Incomplete mutation body')
|
|
2112
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
2113
|
+
keep_blank_values=True, strict_parsing=True)
|
|
2114
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'level'} or
|
|
2115
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2116
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2117
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
2118
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,9})', fields['level'][0])):
|
|
2119
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2120
|
+
level = int(fields['level'][0])
|
|
2121
|
+
if level > 2_147_483_647:
|
|
2122
|
+
raise ValueError('Invalid cluster session fault tolerance level')
|
|
2123
|
+
async def update_fault_tolerance():
|
|
2124
|
+
async with client_factory() as client:
|
|
2125
|
+
return await client.update_cluster_session_fault_tolerance_level(cluster, level)
|
|
2126
|
+
result = asyncio.run(update_fault_tolerance())
|
|
2127
|
+
body = render_page('Verified cluster session fault tolerance level update', [result], '')
|
|
2128
|
+
else:
|
|
2129
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2130
|
+
('Content-Type', 'text/plain')])
|
|
2131
|
+
return [b'Method not allowed']
|
|
2132
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2133
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2134
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2135
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2136
|
+
return [b'Cluster session fault tolerance update not verified'
|
|
2137
|
+
if method == 'POST' else b'RAC unavailable']
|
|
2138
|
+
start_response('200 OK', headers)
|
|
2139
|
+
return [body.encode('utf-8')]
|
|
2140
|
+
security_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/security-level', path)
|
|
2141
|
+
if security_update and allow_cluster_security_level_update:
|
|
2142
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2143
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2144
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2145
|
+
try:
|
|
2146
|
+
cluster = UUID(security_update.group(1))
|
|
2147
|
+
host = environ.get('HTTP_HOST', '')
|
|
2148
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2149
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2150
|
+
raise ValueError('Invalid mutation host')
|
|
2151
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2152
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2153
|
+
if method == 'GET':
|
|
2154
|
+
async def security_info():
|
|
2155
|
+
async with client_factory() as client:
|
|
2156
|
+
return await client.get_cluster(cluster)
|
|
2157
|
+
row = asyncio.run(security_info())
|
|
2158
|
+
content = (render_section('Cluster', [row], '') +
|
|
2159
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2160
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2161
|
+
'<label>Security level '
|
|
2162
|
+
'<input name="level" type="number" min="0" max="2147483647" required></label>'
|
|
2163
|
+
'<label>Type exact cluster UUID to confirm '
|
|
2164
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
2165
|
+
'<button type="submit">Update cluster security level</button></form>')
|
|
2166
|
+
body = render_document('Confirm cluster security level update', content)
|
|
2167
|
+
elif method == 'POST':
|
|
2168
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2169
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2170
|
+
origin.path or origin.query or origin.fragment):
|
|
2171
|
+
raise ValueError('Invalid mutation origin')
|
|
2172
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2173
|
+
raise ValueError('Invalid mutation content type')
|
|
2174
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2175
|
+
if not 0 <= length <= 4096:
|
|
2176
|
+
raise ValueError('Invalid mutation body length')
|
|
2177
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2178
|
+
if len(raw_body) != length:
|
|
2179
|
+
raise ValueError('Incomplete mutation body')
|
|
2180
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
2181
|
+
keep_blank_values=True, strict_parsing=True)
|
|
2182
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'level'} or
|
|
2183
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2184
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2185
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
2186
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,9})', fields['level'][0])):
|
|
2187
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2188
|
+
level = int(fields['level'][0])
|
|
2189
|
+
if level > 2_147_483_647:
|
|
2190
|
+
raise ValueError('Invalid cluster security level')
|
|
2191
|
+
async def update_security():
|
|
2192
|
+
async with client_factory() as client:
|
|
2193
|
+
return await client.update_cluster_security_level(cluster, level)
|
|
2194
|
+
result = asyncio.run(update_security())
|
|
2195
|
+
body = render_page('Verified cluster security level update', [result], '')
|
|
2196
|
+
else:
|
|
2197
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2198
|
+
('Content-Type', 'text/plain')])
|
|
2199
|
+
return [b'Method not allowed']
|
|
2200
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2201
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2202
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2203
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2204
|
+
return [b'Cluster security level update not verified'
|
|
2205
|
+
if method == 'POST' else b'RAC unavailable']
|
|
2206
|
+
start_response('200 OK', headers)
|
|
2207
|
+
return [body.encode('utf-8')]
|
|
2208
|
+
timeout_update = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/expiration-timeout', path)
|
|
2209
|
+
if timeout_update and allow_cluster_expiration_timeout_update:
|
|
2210
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2211
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2212
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2213
|
+
try:
|
|
2214
|
+
cluster = UUID(timeout_update.group(1))
|
|
2215
|
+
host = environ.get('HTTP_HOST', '')
|
|
2216
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2217
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2218
|
+
raise ValueError('Invalid mutation host')
|
|
2219
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2220
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2221
|
+
if method == 'GET':
|
|
2222
|
+
async def timeout_info():
|
|
2223
|
+
async with client_factory() as client:
|
|
2224
|
+
return await client.get_cluster(cluster)
|
|
2225
|
+
row = asyncio.run(timeout_info())
|
|
2226
|
+
content = (render_section('Cluster', [row], '') +
|
|
2227
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2228
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2229
|
+
'<label>Expiration timeout (seconds) '
|
|
2230
|
+
'<input name="seconds" type="number" min="0" max="2147483647" required></label>'
|
|
2231
|
+
'<label>Type exact cluster UUID to confirm '
|
|
2232
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
2233
|
+
'<button type="submit">Update cluster expiration timeout</button></form>')
|
|
2234
|
+
body = render_document('Confirm cluster expiration timeout update', content)
|
|
2235
|
+
elif method == 'POST':
|
|
2236
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2237
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2238
|
+
origin.path or origin.query or origin.fragment):
|
|
2239
|
+
raise ValueError('Invalid mutation origin')
|
|
2240
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2241
|
+
raise ValueError('Invalid mutation content type')
|
|
2242
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2243
|
+
if not 0 <= length <= 4096:
|
|
2244
|
+
raise ValueError('Invalid mutation body length')
|
|
2245
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2246
|
+
if len(raw_body) != length:
|
|
2247
|
+
raise ValueError('Incomplete mutation body')
|
|
2248
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
2249
|
+
keep_blank_values=True, strict_parsing=True)
|
|
2250
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'seconds'} or
|
|
2251
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2252
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2253
|
+
fields['confirm_cluster_id'][0] != str(cluster) or
|
|
2254
|
+
not re.fullmatch(r'(0|[1-9][0-9]{0,9})', fields['seconds'][0])):
|
|
2255
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2256
|
+
seconds = int(fields['seconds'][0])
|
|
2257
|
+
if seconds > 2_147_483_647:
|
|
2258
|
+
raise ValueError('Invalid cluster expiration timeout')
|
|
2259
|
+
async def update_timeout():
|
|
2260
|
+
async with client_factory() as client:
|
|
2261
|
+
return await client.update_cluster_expiration_timeout(cluster, seconds)
|
|
2262
|
+
result = asyncio.run(update_timeout())
|
|
2263
|
+
body = render_page('Verified cluster expiration timeout update', [result], '')
|
|
2264
|
+
else:
|
|
2265
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2266
|
+
('Content-Type', 'text/plain')])
|
|
2267
|
+
return [b'Method not allowed']
|
|
2268
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2269
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2270
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2271
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2272
|
+
return [b'Cluster expiration timeout update not verified'
|
|
2273
|
+
if method == 'POST' else b'RAC unavailable']
|
|
2274
|
+
start_response('200 OK', headers)
|
|
2275
|
+
return [body.encode('utf-8')]
|
|
2276
|
+
rename = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/rename', path)
|
|
2277
|
+
if rename and allow_cluster_name_update:
|
|
2278
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2279
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2280
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2281
|
+
try:
|
|
2282
|
+
cluster = UUID(rename.group(1))
|
|
2283
|
+
host = environ.get('HTTP_HOST', '')
|
|
2284
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2285
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2286
|
+
raise ValueError('Invalid mutation host')
|
|
2287
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2288
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2289
|
+
if method == 'GET':
|
|
2290
|
+
async def info():
|
|
2291
|
+
async with client_factory() as client:
|
|
2292
|
+
return await client.get_cluster(cluster)
|
|
2293
|
+
row = asyncio.run(info())
|
|
2294
|
+
content = (render_section('Cluster', [row], '') +
|
|
2295
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2296
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2297
|
+
'<label>New name <input name="name"></label>'
|
|
2298
|
+
'<label>Type exact cluster UUID to confirm '
|
|
2299
|
+
'<input name="confirm_cluster_id" required></label>'
|
|
2300
|
+
'<button type="submit">Rename cluster</button></form>')
|
|
2301
|
+
body = render_document('Confirm cluster name update', content)
|
|
2302
|
+
elif method == 'POST':
|
|
2303
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2304
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2305
|
+
origin.path or origin.query or origin.fragment):
|
|
2306
|
+
raise ValueError('Invalid mutation origin')
|
|
2307
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2308
|
+
raise ValueError('Invalid mutation content type')
|
|
2309
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2310
|
+
if not 0 <= length <= 4096:
|
|
2311
|
+
raise ValueError('Invalid mutation body length')
|
|
2312
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2313
|
+
if len(raw_body) != length:
|
|
2314
|
+
raise ValueError('Incomplete mutation body')
|
|
2315
|
+
fields = parse_qs(raw_body.decode('utf-8', errors='strict'),
|
|
2316
|
+
keep_blank_values=True, strict_parsing=True)
|
|
2317
|
+
if (set(fields) != {'csrf_token', 'confirm_cluster_id', 'name'} or
|
|
2318
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2319
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2320
|
+
fields['confirm_cluster_id'][0] != str(cluster)):
|
|
2321
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2322
|
+
name = fields['name'][0]
|
|
2323
|
+
if (name and not name.strip()) or '\x00' in name or len(name.encode('utf-8')) > 256:
|
|
2324
|
+
raise ValueError('Invalid cluster name')
|
|
2325
|
+
async def update():
|
|
2326
|
+
async with client_factory() as client:
|
|
2327
|
+
return await client.update_cluster_name(cluster, name)
|
|
2328
|
+
result = asyncio.run(update())
|
|
2329
|
+
body = render_page('Verified cluster name update', [result], '')
|
|
2330
|
+
else:
|
|
2331
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2332
|
+
('Content-Type', 'text/plain')])
|
|
2333
|
+
return [b'Method not allowed']
|
|
2334
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2335
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2336
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2337
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2338
|
+
return [b'Cluster name update not verified' if method == 'POST' else b'RAC unavailable']
|
|
2339
|
+
start_response('200 OK', headers)
|
|
2340
|
+
return [body.encode('utf-8')]
|
|
2341
|
+
mutation = re.fullmatch(
|
|
2342
|
+
r'/clusters/([0-9a-fA-F-]{36})/processes/([0-9a-fA-F-]{36})/turn-off', path)
|
|
2343
|
+
if mutation and allow_process_turn_off:
|
|
2344
|
+
headers = [('Content-Type', 'text/html; charset=utf-8'),
|
|
2345
|
+
('Cache-Control', 'no-store'), ('X-Content-Type-Options', 'nosniff'),
|
|
2346
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')]
|
|
2347
|
+
try:
|
|
2348
|
+
cluster, process = (UUID(part) for part in mutation.groups())
|
|
2349
|
+
# A token-bearing GET must not leak to a nonlocal peer or an
|
|
2350
|
+
# arbitrary Host header (DNS rebinding / cross-origin framing).
|
|
2351
|
+
host = environ.get('HTTP_HOST', '')
|
|
2352
|
+
if (host not in ('localhost', '127.0.0.1', '[::1]') and
|
|
2353
|
+
not re.fullmatch(r'(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}', host)):
|
|
2354
|
+
raise ValueError('Invalid mutation host')
|
|
2355
|
+
if environ.get('REMOTE_ADDR') not in ('127.0.0.1', '::1'):
|
|
2356
|
+
raise ValueError('Mutation requires loopback peer')
|
|
2357
|
+
if method == 'GET':
|
|
2358
|
+
async def info():
|
|
2359
|
+
return await ReadService(client_factory).process(cluster, process)
|
|
2360
|
+
row = asyncio.run(info())
|
|
2361
|
+
content = (render_section('Working process', [row], '') +
|
|
2362
|
+
f'<form method="post" action="{html.escape(path, quote=True)}">'
|
|
2363
|
+
f'<input type="hidden" name="csrf_token" value="{html.escape(csrf_token, quote=True)}">'
|
|
2364
|
+
'<label>Type exact process UUID to confirm '
|
|
2365
|
+
'<input name="confirm_process_id" required></label>'
|
|
2366
|
+
'<button type="submit">Turn off process</button></form>')
|
|
2367
|
+
body = render_document('Confirm process turn-off', content)
|
|
2368
|
+
elif method == 'POST':
|
|
2369
|
+
# Host is untrusted: only known loopback names/ports, an exact
|
|
2370
|
+
# same-origin Origin header and a loopback peer may submit.
|
|
2371
|
+
origin = urlsplit(environ.get('HTTP_ORIGIN', ''))
|
|
2372
|
+
if (origin.scheme != 'http' or origin.netloc != host or
|
|
2373
|
+
origin.path or origin.query or origin.fragment):
|
|
2374
|
+
raise ValueError('Invalid mutation origin')
|
|
2375
|
+
if environ.get('CONTENT_TYPE', '').split(';', 1)[0] != 'application/x-www-form-urlencoded':
|
|
2376
|
+
raise ValueError('Invalid mutation content type')
|
|
2377
|
+
length = int(environ.get('CONTENT_LENGTH', '-1'))
|
|
2378
|
+
if not 0 <= length <= 4096:
|
|
2379
|
+
raise ValueError('Invalid mutation body length')
|
|
2380
|
+
raw_body = environ['wsgi.input'].read(length)
|
|
2381
|
+
if len(raw_body) != length:
|
|
2382
|
+
raise ValueError('Incomplete mutation body')
|
|
2383
|
+
payload = raw_body.decode('utf-8', errors='strict')
|
|
2384
|
+
fields = parse_qs(payload, keep_blank_values=True, strict_parsing=True)
|
|
2385
|
+
if (set(fields) != {'csrf_token', 'confirm_process_id'} or
|
|
2386
|
+
any(len(values) != 1 for values in fields.values()) or
|
|
2387
|
+
not hmac.compare_digest(fields['csrf_token'][0], csrf_token) or
|
|
2388
|
+
fields['confirm_process_id'][0] != str(process)):
|
|
2389
|
+
raise ValueError('Invalid mutation confirmation')
|
|
2390
|
+
async def turn_off():
|
|
2391
|
+
async with client_factory() as client:
|
|
2392
|
+
return await client.turn_off_process(cluster, process)
|
|
2393
|
+
rows = asyncio.run(turn_off())
|
|
2394
|
+
body = render_page('Verified process turn-off', rows, '')
|
|
2395
|
+
else:
|
|
2396
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET, POST'),
|
|
2397
|
+
('Content-Type', 'text/plain')])
|
|
2398
|
+
return [b'Method not allowed']
|
|
2399
|
+
except (RacError, ValueError, OSError, TimeoutError, UnicodeError, RuntimeError):
|
|
2400
|
+
start_response('403 Forbidden' if method == 'POST' else '503 Service Unavailable',
|
|
2401
|
+
[('Content-Type', 'text/plain; charset=utf-8'), ('Cache-Control', 'no-store'),
|
|
2402
|
+
('Referrer-Policy', 'no-referrer'), ('X-Frame-Options', 'DENY')])
|
|
2403
|
+
return [b'Process turn-off not verified' if method == 'POST' else b'RAC unavailable']
|
|
2404
|
+
start_response('200 OK', headers)
|
|
2405
|
+
return [body.encode('utf-8')]
|
|
2406
|
+
if method != 'GET':
|
|
2407
|
+
start_response('405 Method Not Allowed', [('Allow', 'GET'), ('Content-Type', 'text/plain')])
|
|
2408
|
+
return [b'Method not allowed']
|
|
2409
|
+
async def collect():
|
|
2410
|
+
service = ReadService(client_factory)
|
|
2411
|
+
if path in ('/', '/clusters'):
|
|
2412
|
+
page = render_section('Clusters', await service.clusters(), '/clusters',
|
|
2413
|
+
allow_cluster_remove=allow_cluster_remove)
|
|
2414
|
+
if allow_cluster_insert:
|
|
2415
|
+
page = '<nav><a href="/clusters/insert">Insert cluster</a></nav>' + page
|
|
2416
|
+
return render_document('Clusters', page)
|
|
2417
|
+
if path == '/agent/version':
|
|
2418
|
+
version = await service.agent_version()
|
|
2419
|
+
return render_document('Agent version',
|
|
2420
|
+
'<section><h1>Agent version</h1><p>' +
|
|
2421
|
+
html.escape(str(version)) + '</p></section>')
|
|
2422
|
+
if path == '/agent/admins':
|
|
2423
|
+
navigation = ('<nav><a href="/agent/admins/register">Register administrator</a></nav>'
|
|
2424
|
+
if allow_agent_admin_register else '')
|
|
2425
|
+
rows = await service.agent_admins()
|
|
2426
|
+
return render_document('Agent administrators', navigation +
|
|
2427
|
+
render_section('Agent administrators', rows, '/agent/admins',
|
|
2428
|
+
allow_admin_remove=allow_agent_admin_remove))
|
|
2429
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/infobases/info-by-name', path)
|
|
2430
|
+
if match:
|
|
2431
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2432
|
+
if len(raw_query) > 4096:
|
|
2433
|
+
raise ValueError('Infobase lookup query too long')
|
|
2434
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2435
|
+
if set(parameters) != {'name'} or len(parameters['name']) != 1:
|
|
2436
|
+
raise ValueError('Invalid infobase lookup query')
|
|
2437
|
+
row = await service.infobase_info_by_name(match.group(1), parameters['name'][0])
|
|
2438
|
+
cluster, infobase = UUID(match.group(1)), UUID(str(row['infobase']))
|
|
2439
|
+
navigation = (f'<nav><a href="/clusters/{cluster}/infobases/{infobase}/info">'
|
|
2440
|
+
'Infobase full info</a></nav>')
|
|
2441
|
+
return render_document('Infobase full info by name', navigation +
|
|
2442
|
+
render_section('Infobase full info by name', [row], ''))
|
|
2443
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/infobases/by-name', path)
|
|
2444
|
+
if match:
|
|
2445
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2446
|
+
if len(raw_query) > 4096:
|
|
2447
|
+
raise ValueError('Infobase lookup query too long')
|
|
2448
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2449
|
+
if set(parameters) != {'name'} or len(parameters['name']) != 1:
|
|
2450
|
+
raise ValueError('Invalid infobase lookup query')
|
|
2451
|
+
row = await service.infobase_summary_by_name(match.group(1), parameters['name'][0])
|
|
2452
|
+
cluster, infobase = UUID(match.group(1)), UUID(str(row['infobase']))
|
|
2453
|
+
navigation = (f'<nav><a href="/clusters/{cluster}/infobases/{infobase}/summary">'
|
|
2454
|
+
'Infobase summary</a></nav>')
|
|
2455
|
+
return render_document('Infobase by name', navigation +
|
|
2456
|
+
render_section('Infobase by name', [row], ''))
|
|
2457
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/info', path)
|
|
2458
|
+
if match:
|
|
2459
|
+
row = await service.one(match.group(1), 'get_cluster')
|
|
2460
|
+
navigation = (f'<nav><a href="/clusters/{UUID(match.group(1))}/rename">'
|
|
2461
|
+
'Rename cluster…</a></nav>' if allow_cluster_name_update else '')
|
|
2462
|
+
if allow_rule_apply:
|
|
2463
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/rules/apply">'
|
|
2464
|
+
'Apply assignment rules…</a></nav>')
|
|
2465
|
+
if allow_cluster_expiration_timeout_update:
|
|
2466
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/expiration-timeout">'
|
|
2467
|
+
'Update cluster expiration timeout…</a></nav>')
|
|
2468
|
+
if allow_cluster_security_level_update:
|
|
2469
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/security-level">'
|
|
2470
|
+
'Update cluster security level…</a></nav>')
|
|
2471
|
+
if allow_cluster_session_fault_tolerance_level_update:
|
|
2472
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/session-fault-tolerance-level">'
|
|
2473
|
+
'Update cluster session fault tolerance level…</a></nav>')
|
|
2474
|
+
if allow_cluster_load_balancing_mode_update:
|
|
2475
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/load-balancing-mode">'
|
|
2476
|
+
'Update cluster load balancing mode…</a></nav>')
|
|
2477
|
+
if allow_cluster_kill_problem_processes_update:
|
|
2478
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/kill-problem-processes">'
|
|
2479
|
+
'Update cluster problem-process policy…</a></nav>')
|
|
2480
|
+
if allow_cluster_kill_by_memory_with_dump_update:
|
|
2481
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/kill-by-memory-with-dump">'
|
|
2482
|
+
'Update cluster kill-by-memory dump…</a></nav>')
|
|
2483
|
+
if allow_cluster_audit_update:
|
|
2484
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/audit">'
|
|
2485
|
+
'Update cluster access-right audit recording…</a></nav>')
|
|
2486
|
+
if allow_cluster_ping_update:
|
|
2487
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/ping">'
|
|
2488
|
+
'Update cluster ping pair…</a></nav>')
|
|
2489
|
+
if allow_cluster_restart_schedule_update:
|
|
2490
|
+
navigation += (f'<nav><a href="/clusters/{UUID(match.group(1))}/restart-schedule">'
|
|
2491
|
+
'Update cluster restart schedule…</a></nav>')
|
|
2492
|
+
return render_document('Cluster info', navigation + render_section('Cluster info', [row], ''))
|
|
2493
|
+
match = re.fullmatch(
|
|
2494
|
+
r'/clusters/([0-9a-fA-F-]{36})/admins', path)
|
|
2495
|
+
if match:
|
|
2496
|
+
navigation = (f'<nav><a href="/clusters/{UUID(match.group(1))}/admins/register">'
|
|
2497
|
+
'Register administrator</a></nav>' if allow_cluster_admin_register else '')
|
|
2498
|
+
rows = await service.cluster_admins(match.group(1))
|
|
2499
|
+
return render_document('Cluster administrators', navigation +
|
|
2500
|
+
render_section('Cluster administrators', rows, path,
|
|
2501
|
+
allow_admin_remove=allow_cluster_admin_remove))
|
|
2502
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/profiles/overview', path)
|
|
2503
|
+
if match:
|
|
2504
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2505
|
+
if len(raw_query) > 4096:
|
|
2506
|
+
raise ValueError('Profile overview query too long')
|
|
2507
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2508
|
+
if set(parameters) != {'name'} or len(parameters['name']) != 1:
|
|
2509
|
+
raise ValueError('Invalid profile overview query')
|
|
2510
|
+
result = await service.profile_overview(match.group(1), parameters['name'][0])
|
|
2511
|
+
content = render_section('Security profile', [result['profile']], '')
|
|
2512
|
+
content += ''.join(render_section(kind.title() + ' ACL', rows, '')
|
|
2513
|
+
for kind, rows in result['acl'].items())
|
|
2514
|
+
return render_document('Security profile overview', content)
|
|
2515
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/profiles/by-name', path)
|
|
2516
|
+
if match:
|
|
2517
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2518
|
+
if len(raw_query) > 4096:
|
|
2519
|
+
raise ValueError('Profile lookup query too long')
|
|
2520
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2521
|
+
if set(parameters) != {'name'} or len(parameters['name']) != 1:
|
|
2522
|
+
raise ValueError('Invalid profile lookup query')
|
|
2523
|
+
row = await service.profile_by_name(match.group(1), parameters['name'][0])
|
|
2524
|
+
cluster = UUID(match.group(1))
|
|
2525
|
+
navigation = '<nav>' + ''.join(
|
|
2526
|
+
f'<a href="/clusters/{cluster}/profiles/{quote(row["name"], safe="")}/acl/{kind}">'
|
|
2527
|
+
f'{html.escape(kind.title())}</a> ' for kind in PROFILE_ACL_LISTS) + '</nav>'
|
|
2528
|
+
return render_document('Security profile', navigation +
|
|
2529
|
+
render_section('Security profile', [row], ''))
|
|
2530
|
+
match = re.fullmatch(
|
|
2531
|
+
r'/clusters/([0-9a-fA-F-]{36})/profiles/([^/]+)/acl/([a-z]+)', path)
|
|
2532
|
+
if match:
|
|
2533
|
+
kind = match.group(3)
|
|
2534
|
+
if kind not in PROFILE_ACL_LISTS:
|
|
2535
|
+
return None
|
|
2536
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2537
|
+
if len(raw_query) > 4096:
|
|
2538
|
+
raise InvalidAclQuery('ACL query too long')
|
|
2539
|
+
try:
|
|
2540
|
+
parameters = (parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2541
|
+
if raw_query else {})
|
|
2542
|
+
except ValueError as exc:
|
|
2543
|
+
raise InvalidAclQuery('Invalid ACL query') from exc
|
|
2544
|
+
if set(parameters) - {'name'} or len(parameters.get('name', [''])) != 1:
|
|
2545
|
+
raise InvalidAclQuery('Invalid ACL query')
|
|
2546
|
+
name = parameters.get('name', [''])[0]
|
|
2547
|
+
if '\x00' in name or len(name.encode('utf-8')) > 1024:
|
|
2548
|
+
raise InvalidAclQuery('Invalid ACL name')
|
|
2549
|
+
rows = await service.list_profile_acl(match.group(1), unquote(match.group(2)),
|
|
2550
|
+
kind, name)
|
|
2551
|
+
form = ('<form method="get"><label>ACL name '
|
|
2552
|
+
f'<input name="name" value="{html.escape(name, quote=True)}">'
|
|
2553
|
+
'</label><button type="submit">Filter</button></form>')
|
|
2554
|
+
return render_document(kind.title() + ' ACL', form +
|
|
2555
|
+
render_section(kind.title() + ' ACL', rows, ''))
|
|
2556
|
+
match = re.fullmatch(
|
|
2557
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/processes', path)
|
|
2558
|
+
if match:
|
|
2559
|
+
cluster, server = (UUID(value) for value in match.groups())
|
|
2560
|
+
rows = await service.processes_on_server(cluster, server)
|
|
2561
|
+
return render_document('Server processes',
|
|
2562
|
+
render_section('Server processes', rows, path))
|
|
2563
|
+
match = re.fullmatch(
|
|
2564
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})', path)
|
|
2565
|
+
if match:
|
|
2566
|
+
row = await service.server(match.group(1), match.group(2))
|
|
2567
|
+
cluster_id, server_id = (UUID(value) for value in match.groups())
|
|
2568
|
+
navigation = (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/processes">'
|
|
2569
|
+
'Processes</a></nav><nav>' + ''.join(
|
|
2570
|
+
f'<a href="/clusters/{cluster_id}/servers/{server_id}/{kind}">'
|
|
2571
|
+
f'{html.escape(kind.title())}</a> '
|
|
2572
|
+
for kind in SERVER_LISTS) + '</nav>')
|
|
2573
|
+
if allow_server_restart_schedule_update:
|
|
2574
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/restart-schedule">'
|
|
2575
|
+
'Update server restart schedule…</a></nav>')
|
|
2576
|
+
if allow_server_infobases_limit_update:
|
|
2577
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/infobases-limit">'
|
|
2578
|
+
'Update server infobases limit…</a></nav>')
|
|
2579
|
+
if allow_server_connections_limit_update:
|
|
2580
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/connections-limit">'
|
|
2581
|
+
'Update server connections limit…</a></nav>')
|
|
2582
|
+
if allow_server_safe_call_memory_limit_update:
|
|
2583
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/safe-call-memory-limit">'
|
|
2584
|
+
'Update server safe-call memory limit…</a></nav>')
|
|
2585
|
+
if allow_server_critical_total_memory_update:
|
|
2586
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/critical-total-memory">'
|
|
2587
|
+
'Update server critical total memory…</a></nav>')
|
|
2588
|
+
if allow_server_temporary_total_memory_update:
|
|
2589
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/temporary-total-memory">'
|
|
2590
|
+
'Update server temporary allowed total memory…</a></nav>')
|
|
2591
|
+
if allow_server_temporary_memory_time_limit_update:
|
|
2592
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/temporary-memory-time-limit">'
|
|
2593
|
+
'Update server temporary memory time limit…</a></nav>')
|
|
2594
|
+
if allow_server_spn_update:
|
|
2595
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/spn">'
|
|
2596
|
+
'Update server SPN…</a></nav>')
|
|
2597
|
+
if allow_server_dedicate_managers_update:
|
|
2598
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/dedicate-managers">'
|
|
2599
|
+
'Update server dedicated managers…</a></nav>')
|
|
2600
|
+
if allow_server_port_ranges_update:
|
|
2601
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/servers/{server_id}/port-ranges">'
|
|
2602
|
+
'Update server port ranges…</a></nav>')
|
|
2603
|
+
return render_document('Working server', navigation +
|
|
2604
|
+
render_section('Working server', [row], ''))
|
|
2605
|
+
match = re.fullmatch(
|
|
2606
|
+
r'/clusters/([0-9a-fA-F-]{36})/processes/([0-9a-fA-F-]{36})', path)
|
|
2607
|
+
if match:
|
|
2608
|
+
row = await service.process(match.group(1), match.group(2))
|
|
2609
|
+
cluster_id, process_id = (UUID(value) for value in match.groups())
|
|
2610
|
+
navigation = (f'<nav><a href="/clusters/{cluster_id}/processes/{process_id}/connections">'
|
|
2611
|
+
'Connections</a></nav>')
|
|
2612
|
+
if allow_process_turn_off:
|
|
2613
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/processes/{process_id}/turn-off">'
|
|
2614
|
+
'Turn off process…</a></nav>')
|
|
2615
|
+
return render_document('Working process', navigation +
|
|
2616
|
+
render_section('Working process', [row], ''))
|
|
2617
|
+
match = re.fullmatch(
|
|
2618
|
+
r'/clusters/([0-9a-fA-F-]{36})/sessions/([0-9a-fA-F-]{36})', path)
|
|
2619
|
+
if match:
|
|
2620
|
+
row = await service.session(match.group(1), match.group(2))
|
|
2621
|
+
return render_page('Session', [row], '')
|
|
2622
|
+
match = re.fullmatch(
|
|
2623
|
+
r'/clusters/([0-9a-fA-F-]{36})/connections/([0-9a-fA-F-]{36})', path)
|
|
2624
|
+
if match:
|
|
2625
|
+
row = await service.connection(match.group(1), match.group(2))
|
|
2626
|
+
cluster_id, connection_id = (UUID(value) for value in match.groups())
|
|
2627
|
+
navigation = (f'<nav><a href="/clusters/{cluster_id}/connections/{connection_id}/locks">'
|
|
2628
|
+
'Locks</a></nav>')
|
|
2629
|
+
return render_document('Connection', navigation +
|
|
2630
|
+
render_section('Connection', [row], ''))
|
|
2631
|
+
match = re.fullmatch(
|
|
2632
|
+
r'/clusters/([0-9a-fA-F-]{36})/managers/([0-9a-fA-F-]{36})', path)
|
|
2633
|
+
if match:
|
|
2634
|
+
row = await service.manager(match.group(1), match.group(2))
|
|
2635
|
+
return render_page('Manager', [row], '')
|
|
2636
|
+
match = re.fullmatch(
|
|
2637
|
+
r'/clusters/([0-9a-fA-F-]{36})/limits/([^/]+)', path)
|
|
2638
|
+
if match:
|
|
2639
|
+
row = await service.limit(match.group(1), unquote(match.group(2)))
|
|
2640
|
+
return render_page('Limit', [row], '')
|
|
2641
|
+
match = re.fullmatch(
|
|
2642
|
+
r'/clusters/([0-9a-fA-F-]{36})/counters/([^/]+)/(values|accumulated-values)', path)
|
|
2643
|
+
if match:
|
|
2644
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2645
|
+
if len(raw_query) > 12288:
|
|
2646
|
+
raise ValueError('Counter query too long')
|
|
2647
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2648
|
+
if set(parameters) - {'object_filter'} or len(parameters.get('object_filter', [''])) != 1:
|
|
2649
|
+
raise ValueError('Invalid counter query')
|
|
2650
|
+
object_filter = parameters.get('object_filter', [''])[0]
|
|
2651
|
+
rows = await service.counter_values(match.group(1), unquote(match.group(2)), object_filter,
|
|
2652
|
+
accumulated=match.group(3) == 'accumulated-values')
|
|
2653
|
+
form = ('<form method="get"><label>Object filter '
|
|
2654
|
+
f'<input name="object_filter" value="{html.escape(object_filter, quote=True)}">'
|
|
2655
|
+
'</label><button type="submit">Filter</button></form>')
|
|
2656
|
+
return render_document('Counter ' + match.group(3), form +
|
|
2657
|
+
render_section('Counter ' + match.group(3), rows, ''))
|
|
2658
|
+
match = re.fullmatch(
|
|
2659
|
+
r'/clusters/([0-9a-fA-F-]{36})/counters/([^/]+)', path)
|
|
2660
|
+
if match:
|
|
2661
|
+
row = await service.counter(match.group(1), unquote(match.group(2)))
|
|
2662
|
+
base = f'/clusters/{UUID(match.group(1))}/counters/{quote(unquote(match.group(2)), safe="")}'
|
|
2663
|
+
navigation = '<nav>' + ''.join(
|
|
2664
|
+
f'<a href="{base}/{kind}">{html.escape(kind.title())}</a> '
|
|
2665
|
+
for kind in ('values', 'accumulated-values')) + '</nav>'
|
|
2666
|
+
return render_document('Counter', navigation + render_section('Counter', [row], ''))
|
|
2667
|
+
match = re.fullmatch(
|
|
2668
|
+
r'/clusters/([0-9a-fA-F-]{36})/(infobases|connections)/([0-9a-fA-F-]{36})/locks', path)
|
|
2669
|
+
if match:
|
|
2670
|
+
scope = {'infobases': 'infobase', 'connections': 'connection'}[match.group(2)]
|
|
2671
|
+
rows = await service.filtered_locks(match.group(1), scope, match.group(3))
|
|
2672
|
+
return render_page(scope.title() + ' locks', rows, '')
|
|
2673
|
+
match = re.fullmatch(
|
|
2674
|
+
r'/clusters/([0-9a-fA-F-]{36})/processes/([0-9a-fA-F-]{36})/connections', path)
|
|
2675
|
+
if match:
|
|
2676
|
+
rows = await service.connections_in_process(*match.groups())
|
|
2677
|
+
return render_document('Process connections',
|
|
2678
|
+
render_section('Process connections', rows, path))
|
|
2679
|
+
match = re.fullmatch(
|
|
2680
|
+
r'/clusters/([0-9a-fA-F-]{36})/infobases/([0-9a-fA-F-]{36})/connections', path)
|
|
2681
|
+
if match:
|
|
2682
|
+
rows = await service.connections_in_infobase(*match.groups())
|
|
2683
|
+
return render_document('Infobase connections',
|
|
2684
|
+
render_section('Infobase connections', rows, path))
|
|
2685
|
+
match = re.fullmatch(
|
|
2686
|
+
r'/clusters/([0-9a-fA-F-]{36})/infobases/([0-9a-fA-F-]{36})/sessions', path)
|
|
2687
|
+
if match:
|
|
2688
|
+
cluster = UUID(match.group(1))
|
|
2689
|
+
rows = await service.sessions_in_infobase(*match.groups())
|
|
2690
|
+
return render_document('Infobase sessions',
|
|
2691
|
+
render_section('Infobase sessions', rows,
|
|
2692
|
+
f'/clusters/{cluster}/sessions'))
|
|
2693
|
+
match = re.fullmatch(
|
|
2694
|
+
r'/clusters/([0-9a-fA-F-]{36})/infobases/([0-9a-fA-F-]{36})/summary', path)
|
|
2695
|
+
if match:
|
|
2696
|
+
row = await service.infobase_summary(match.group(1), match.group(2))
|
|
2697
|
+
cluster_id, infobase_id = (UUID(value) for value in match.groups())
|
|
2698
|
+
base = f'/clusters/{cluster_id}/infobases/{infobase_id}'
|
|
2699
|
+
navigation = '<nav>' + ''.join(
|
|
2700
|
+
f'<a href="{base}/{kind}">{html.escape(kind.title())}</a> '
|
|
2701
|
+
for kind in ('info', 'binary-storages', 'sessions', 'connections', 'locks')) + '</nav>'
|
|
2702
|
+
return render_document('Infobase summary', navigation +
|
|
2703
|
+
render_section('Infobase summary', [row], ''))
|
|
2704
|
+
match = re.fullmatch(
|
|
2705
|
+
r'/clusters/([0-9a-fA-F-]{36})/infobases/([0-9a-fA-F-]{36})/info', path)
|
|
2706
|
+
if match:
|
|
2707
|
+
row = await service.infobase_info(match.group(1), match.group(2))
|
|
2708
|
+
return render_page('Infobase full info', [row], '')
|
|
2709
|
+
match = re.fullmatch(
|
|
2710
|
+
r'/clusters/([0-9a-fA-F-]{36})/infobases/([0-9a-fA-F-]{36})/binary-storages', path)
|
|
2711
|
+
if match:
|
|
2712
|
+
rows = await service.binary_storages(match.group(1), match.group(2))
|
|
2713
|
+
return render_page('Binary data storages', rows, '')
|
|
2714
|
+
match = re.fullmatch(
|
|
2715
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/rules/([0-9a-fA-F-]{36})', path)
|
|
2716
|
+
if match:
|
|
2717
|
+
row = await service.rule(*match.groups())
|
|
2718
|
+
return render_page('Assignment rule', [row], '')
|
|
2719
|
+
match = re.fullmatch(
|
|
2720
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/service-settings/([0-9a-fA-F-]{36})', path)
|
|
2721
|
+
if match:
|
|
2722
|
+
row = await service.service_setting(*match.groups())
|
|
2723
|
+
return render_page('Service setting', [row], '')
|
|
2724
|
+
match = re.fullmatch(
|
|
2725
|
+
r'/clusters/([0-9a-fA-F-]{36})/servers/([0-9a-fA-F-]{36})/([a-z-]+)', path)
|
|
2726
|
+
if match:
|
|
2727
|
+
group = match.group(3)
|
|
2728
|
+
if group not in SERVER_LISTS:
|
|
2729
|
+
return None
|
|
2730
|
+
if group == 'transfer-directories':
|
|
2731
|
+
raw_query = environ.get('QUERY_STRING', '')
|
|
2732
|
+
if len(raw_query) > 4096:
|
|
2733
|
+
raise ValueError('Transfer query too long')
|
|
2734
|
+
parameters = parse_qs(raw_query, keep_blank_values=True, strict_parsing=True)
|
|
2735
|
+
if (set(parameters) - {'service_name'}
|
|
2736
|
+
or len(parameters.get('service_name', [''])) != 1):
|
|
2737
|
+
raise ValueError('Invalid transfer query')
|
|
2738
|
+
service_name = parameters.get('service_name', [''])[0]
|
|
2739
|
+
rows = await service.transfer_directories(match.group(1), match.group(2), service_name)
|
|
2740
|
+
form = ('<form method="get"><label>Service name '
|
|
2741
|
+
f'<input name="service_name" value="{html.escape(service_name, quote=True)}">'
|
|
2742
|
+
'</label><button type="submit">Filter</button></form>')
|
|
2743
|
+
return render_document('Transfer directories', form +
|
|
2744
|
+
render_section('Transfer directories', rows, ''))
|
|
2745
|
+
rows = await service.list_server_group(match.group(1), match.group(2), group)
|
|
2746
|
+
if group == 'rules':
|
|
2747
|
+
return render_page('Rules', rows, path)
|
|
2748
|
+
if group == 'service-settings':
|
|
2749
|
+
return render_page('Service settings', rows, path)
|
|
2750
|
+
return render_page(group.title(), rows, '')
|
|
2751
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})/([a-z-]+)', path)
|
|
2752
|
+
if match:
|
|
2753
|
+
group = match.group(2)
|
|
2754
|
+
if group not in CLUSTER_LISTS:
|
|
2755
|
+
return None
|
|
2756
|
+
ident = UUID(match.group(1))
|
|
2757
|
+
rows = await service.list_group(ident, group)
|
|
2758
|
+
if group == 'profiles':
|
|
2759
|
+
form = (f'<form action="/clusters/{ident}/profiles/by-name" method="get">'
|
|
2760
|
+
'<label>Profile name <input name="name"></label>'
|
|
2761
|
+
'<button type="submit">Find profile</button></form>'
|
|
2762
|
+
f'<form action="/clusters/{ident}/profiles/overview" method="get">'
|
|
2763
|
+
'<label>Profile name <input name="name"></label>'
|
|
2764
|
+
'<button type="submit">Find overview</button></form>')
|
|
2765
|
+
return render_document('Profiles', form +
|
|
2766
|
+
render_section('Profiles', rows, f'/clusters/{ident}/profiles'))
|
|
2767
|
+
if group == 'infobases':
|
|
2768
|
+
form = (f'<form action="/clusters/{ident}/infobases/by-name" method="get">'
|
|
2769
|
+
'<label>Infobase name <input name="name"></label>'
|
|
2770
|
+
'<button type="submit">Find summary</button></form>'
|
|
2771
|
+
f'<form action="/clusters/{ident}/infobases/info-by-name" method="get">'
|
|
2772
|
+
'<label>Infobase name <input name="name"></label>'
|
|
2773
|
+
'<button type="submit">Find full info</button></form>')
|
|
2774
|
+
return render_document('Infobases', form +
|
|
2775
|
+
render_section('Infobases', rows,
|
|
2776
|
+
f'/clusters/{ident}/infobases'))
|
|
2777
|
+
if group == 'servers':
|
|
2778
|
+
return render_document('Servers', render_section('Servers', rows,
|
|
2779
|
+
f'/clusters/{ident}/servers'))
|
|
2780
|
+
if group == 'processes':
|
|
2781
|
+
return render_document('Processes', render_section('Processes', rows,
|
|
2782
|
+
f'/clusters/{ident}/processes'))
|
|
2783
|
+
if group == 'sessions':
|
|
2784
|
+
return render_document('Sessions',
|
|
2785
|
+
render_section('Sessions', rows,
|
|
2786
|
+
f'/clusters/{ident}/sessions'))
|
|
2787
|
+
if group == 'connections':
|
|
2788
|
+
return render_page('Connections', rows, path)
|
|
2789
|
+
if group == 'managers':
|
|
2790
|
+
return render_page('Managers', rows, path)
|
|
2791
|
+
if group == 'limits':
|
|
2792
|
+
return render_page('Limits', rows, path)
|
|
2793
|
+
if group == 'counters':
|
|
2794
|
+
return render_page('Counters', rows, path)
|
|
2795
|
+
return render_page(group.title(), rows, '')
|
|
2796
|
+
match = re.fullmatch(r'/clusters/([0-9a-fA-F-]{36})', path)
|
|
2797
|
+
if not match:
|
|
2798
|
+
return None
|
|
2799
|
+
cluster_id = UUID(match.group(1))
|
|
2800
|
+
view = await service.cluster(cluster_id, include_info=False)
|
|
2801
|
+
sections = [view['servers'], view['processes'], view['infobases']]
|
|
2802
|
+
labels = ('Working servers', 'Working processes', 'Infobases')
|
|
2803
|
+
navigation = '<nav>' + ''.join(
|
|
2804
|
+
f'<a href="/clusters/{cluster_id}/{group}">{html.escape(group.title())}</a> '
|
|
2805
|
+
for group in CLUSTER_LISTS) + '</nav>'
|
|
2806
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/admins">'
|
|
2807
|
+
'Cluster administrators</a></nav>')
|
|
2808
|
+
navigation += (f'<nav><a href="/clusters/{cluster_id}/info">'
|
|
2809
|
+
'Cluster info</a></nav>')
|
|
2810
|
+
navigation += '<nav>' + ''.join(
|
|
2811
|
+
f'<a href="/clusters/{cluster_id}/servers/{quote(str(UUID(row["server"])))}'
|
|
2812
|
+
f'/{group}">{html.escape(group.title())} ({html.escape(str(row.get("name", "")))})</a> '
|
|
2813
|
+
for row in view['servers'] for group in SERVER_LISTS) + '</nav>'
|
|
2814
|
+
return render_document('Cluster ' + str(cluster_id), navigation + ''.join(
|
|
2815
|
+
render_section(title, rows, f'/clusters/{cluster_id}'
|
|
2816
|
+
if title in ('Working servers', 'Working processes', 'Infobases') else '')
|
|
2817
|
+
for title, rows in zip(labels, sections)))
|
|
2818
|
+
async def bounded_collect():
|
|
2819
|
+
async with asyncio.timeout(read_timeout):
|
|
2820
|
+
return await collect()
|
|
2821
|
+
|
|
2822
|
+
try:
|
|
2823
|
+
body = asyncio.run(bounded_collect())
|
|
2824
|
+
if body is None:
|
|
2825
|
+
start_response('404 Not Found', [('Content-Type', 'text/plain')])
|
|
2826
|
+
return [b'Not found']
|
|
2827
|
+
except InvalidAclQuery:
|
|
2828
|
+
start_response('400 Bad Request', [('Content-Type', 'text/plain; charset=utf-8')])
|
|
2829
|
+
return [b'Invalid ACL query']
|
|
2830
|
+
except (RacError, ValueError, OSError, TimeoutError):
|
|
2831
|
+
start_response('503 Service Unavailable', [('Content-Type', 'text/plain; charset=utf-8')])
|
|
2832
|
+
return [b'RAC unavailable']
|
|
2833
|
+
start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
|
|
2834
|
+
return [body.encode('utf-8')]
|
|
2835
|
+
return application
|