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/read_service.py
ADDED
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
"""Shared, bounded read-only views over the versioned RAC client."""
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from .client import RacClient
|
|
5
|
+
|
|
6
|
+
# Only verified, cluster-scoped reads. No caller-supplied Python method names.
|
|
7
|
+
CLUSTER_LISTS = {
|
|
8
|
+
'servers': 'list_servers',
|
|
9
|
+
'processes': 'list_processes',
|
|
10
|
+
'infobases': 'list_infobases',
|
|
11
|
+
'sessions': 'list_sessions',
|
|
12
|
+
'connections': 'list_connections',
|
|
13
|
+
'profiles': 'list_profiles',
|
|
14
|
+
'counters': 'list_counters',
|
|
15
|
+
'limits': 'list_limits',
|
|
16
|
+
'managers': 'list_managers',
|
|
17
|
+
'services': 'list_services',
|
|
18
|
+
'locks': 'list_locks',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
SERVER_LISTS = {
|
|
22
|
+
'rules': 'list_rules',
|
|
23
|
+
'service-settings': 'list_service_settings',
|
|
24
|
+
'transfer-directories': 'list_service_data_dirs_for_transfer',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
PROFILE_ACL_LISTS = {
|
|
28
|
+
'directory': 'list_profile_directories',
|
|
29
|
+
'com': 'list_profile_com_classes',
|
|
30
|
+
'addin': 'list_profile_addins',
|
|
31
|
+
'module': 'list_profile_modules',
|
|
32
|
+
'app': 'list_profile_apps',
|
|
33
|
+
'inet': 'list_profile_inet',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ReadService:
|
|
38
|
+
PRIVATE_FIELDS = frozenset({'password', 'db_password', 'permission_code',
|
|
39
|
+
'denied_parameter', 'external_connection_string'})
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def public_row(row):
|
|
43
|
+
return {key: value for key, value in row.items()
|
|
44
|
+
if key not in ReadService.PRIVATE_FIELDS}
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def public_data(value):
|
|
48
|
+
if isinstance(value, dict):
|
|
49
|
+
return {key: ReadService.public_data(item) for key, item in value.items()
|
|
50
|
+
if key not in ReadService.PRIVATE_FIELDS}
|
|
51
|
+
if isinstance(value, list):
|
|
52
|
+
return [ReadService.public_data(item) for item in value]
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
def __init__(self, factory=None):
|
|
56
|
+
self.factory = factory or RacClient
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _search_needle(query):
|
|
60
|
+
if (not isinstance(query, str) or not query.strip() or '\x00' in query
|
|
61
|
+
or len(query) > 100 or not ReadService._valid_search_text(query)):
|
|
62
|
+
raise ValueError('Invalid search query')
|
|
63
|
+
return query.strip().casefold()
|
|
64
|
+
|
|
65
|
+
async def clusters(self):
|
|
66
|
+
async with self.factory() as client:
|
|
67
|
+
return self.public_data(await client.list_clusters())
|
|
68
|
+
|
|
69
|
+
async def search_clusters(self, query):
|
|
70
|
+
needle = self._search_needle(query)
|
|
71
|
+
matches = []
|
|
72
|
+
named = set()
|
|
73
|
+
async with self.factory() as client:
|
|
74
|
+
for row in await client.list_clusters():
|
|
75
|
+
if not isinstance(row, dict):
|
|
76
|
+
continue
|
|
77
|
+
try:
|
|
78
|
+
cluster = str(UUID(str(row['cluster'])))
|
|
79
|
+
except (KeyError, TypeError, ValueError):
|
|
80
|
+
continue
|
|
81
|
+
name = row.get('name', '')
|
|
82
|
+
valid_name = isinstance(name, str) and self._valid_search_text(name) and bool(name.strip())
|
|
83
|
+
host = row.get('host', '')
|
|
84
|
+
valid_host = isinstance(host, str) and self._valid_search_text(host) and bool(host.strip())
|
|
85
|
+
if (needle in cluster.casefold()
|
|
86
|
+
or valid_name and needle in name.casefold()
|
|
87
|
+
or valid_host and needle in host.casefold()):
|
|
88
|
+
entry = {'cluster': cluster, 'name': name if valid_name else
|
|
89
|
+
host if valid_host and host else 'Unnamed cluster'}
|
|
90
|
+
previous = next((i for i, item in enumerate(matches)
|
|
91
|
+
if item['cluster'] == cluster), None)
|
|
92
|
+
if previous is None:
|
|
93
|
+
matches.append(entry)
|
|
94
|
+
if valid_name:
|
|
95
|
+
named.add(cluster)
|
|
96
|
+
elif valid_name and cluster not in named:
|
|
97
|
+
matches[previous] = entry
|
|
98
|
+
named.add(cluster)
|
|
99
|
+
return matches
|
|
100
|
+
|
|
101
|
+
async def search_infobases(self, query):
|
|
102
|
+
needle = self._search_needle(query)
|
|
103
|
+
matches = []
|
|
104
|
+
async with self.factory() as client:
|
|
105
|
+
for cluster_row in await client.list_clusters():
|
|
106
|
+
if not isinstance(cluster_row, dict):
|
|
107
|
+
continue
|
|
108
|
+
try:
|
|
109
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
110
|
+
except (KeyError, TypeError, ValueError):
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
for base in await client.list_infobases(UUID(cluster)):
|
|
114
|
+
if not isinstance(base, dict):
|
|
115
|
+
continue
|
|
116
|
+
try:
|
|
117
|
+
infobase = str(UUID(str(base['infobase'])))
|
|
118
|
+
except (KeyError, TypeError, ValueError):
|
|
119
|
+
continue
|
|
120
|
+
name = base.get('name', '')
|
|
121
|
+
valid_name = isinstance(name, str) and self._valid_search_text(name) and bool(name.strip())
|
|
122
|
+
if needle in infobase.casefold() or valid_name and needle in name.casefold():
|
|
123
|
+
matches.append({'cluster': cluster, 'infobase': infobase,
|
|
124
|
+
'name': name if valid_name else 'Unnamed infobase'})
|
|
125
|
+
except Exception:
|
|
126
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
127
|
+
continue
|
|
128
|
+
return matches
|
|
129
|
+
|
|
130
|
+
async def search_profiles(self, query):
|
|
131
|
+
needle = self._search_needle(query)
|
|
132
|
+
matches = []
|
|
133
|
+
async with self.factory() as client:
|
|
134
|
+
for cluster_row in await client.list_clusters():
|
|
135
|
+
if not isinstance(cluster_row, dict):
|
|
136
|
+
continue
|
|
137
|
+
try:
|
|
138
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
139
|
+
except (KeyError, TypeError, ValueError):
|
|
140
|
+
continue
|
|
141
|
+
try:
|
|
142
|
+
for profile in await client.list_profiles(UUID(cluster)):
|
|
143
|
+
if not isinstance(profile, dict):
|
|
144
|
+
continue
|
|
145
|
+
name = profile.get('name')
|
|
146
|
+
if (isinstance(name, str) and self._valid_search_text(name)
|
|
147
|
+
and name.strip() and needle in name.casefold()):
|
|
148
|
+
matches.append({'cluster': cluster, 'name': name})
|
|
149
|
+
except Exception:
|
|
150
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
151
|
+
continue
|
|
152
|
+
return matches
|
|
153
|
+
|
|
154
|
+
async def search_working_servers(self, query):
|
|
155
|
+
needle = self._search_needle(query)
|
|
156
|
+
matches = []
|
|
157
|
+
async with self.factory() as client:
|
|
158
|
+
for cluster_row in await client.list_clusters():
|
|
159
|
+
if not isinstance(cluster_row, dict):
|
|
160
|
+
continue
|
|
161
|
+
try:
|
|
162
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
163
|
+
except (KeyError, TypeError, ValueError):
|
|
164
|
+
continue
|
|
165
|
+
try:
|
|
166
|
+
for row in await client.list_servers(UUID(cluster)):
|
|
167
|
+
if not isinstance(row, dict):
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
server = str(UUID(str(row['server'])))
|
|
171
|
+
except (KeyError, TypeError, ValueError):
|
|
172
|
+
continue
|
|
173
|
+
name = row.get('name', '')
|
|
174
|
+
valid_name = isinstance(name, str) and self._valid_search_text(name) and bool(name.strip())
|
|
175
|
+
host = row.get('host_name', '')
|
|
176
|
+
valid_host = isinstance(host, str) and self._valid_search_text(host) and bool(host.strip())
|
|
177
|
+
if (needle in server.casefold()
|
|
178
|
+
or valid_name and needle in name.casefold()
|
|
179
|
+
or valid_host and needle in host.casefold()):
|
|
180
|
+
matches.append({'cluster': cluster, 'server': server,
|
|
181
|
+
'name': name if valid_name else
|
|
182
|
+
host if valid_host and host else 'Unnamed server'})
|
|
183
|
+
except Exception:
|
|
184
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
185
|
+
continue
|
|
186
|
+
return matches
|
|
187
|
+
|
|
188
|
+
async def search_working_processes(self, query):
|
|
189
|
+
needle = self._search_needle(query)
|
|
190
|
+
matches = []
|
|
191
|
+
async with self.factory() as client:
|
|
192
|
+
for cluster_row in await client.list_clusters():
|
|
193
|
+
if not isinstance(cluster_row, dict):
|
|
194
|
+
continue
|
|
195
|
+
try:
|
|
196
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
197
|
+
except (KeyError, TypeError, ValueError):
|
|
198
|
+
continue
|
|
199
|
+
try:
|
|
200
|
+
for row in await client.list_processes(UUID(cluster)):
|
|
201
|
+
if not isinstance(row, dict):
|
|
202
|
+
continue
|
|
203
|
+
try:
|
|
204
|
+
process = str(UUID(str(row['process'])))
|
|
205
|
+
except (KeyError, TypeError, ValueError):
|
|
206
|
+
continue
|
|
207
|
+
pid = row.get('pid')
|
|
208
|
+
valid_pid = (type(pid) is str and pid.isascii()
|
|
209
|
+
and pid.isdecimal() and len(pid) <= 20)
|
|
210
|
+
host = row.get('host', '')
|
|
211
|
+
valid_host = self._valid_search_text(host)
|
|
212
|
+
if (isinstance(pid, str) and valid_pid and needle in pid.casefold()
|
|
213
|
+
or needle in process.casefold()
|
|
214
|
+
or valid_host and needle in host.casefold()):
|
|
215
|
+
address = {'cluster': cluster, 'process': process}
|
|
216
|
+
if isinstance(pid, str) and valid_pid:
|
|
217
|
+
address['pid'] = pid
|
|
218
|
+
if valid_host:
|
|
219
|
+
address['host'] = host
|
|
220
|
+
matches.append(address)
|
|
221
|
+
except Exception:
|
|
222
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
223
|
+
continue
|
|
224
|
+
return matches
|
|
225
|
+
|
|
226
|
+
async def search_sessions(self, query):
|
|
227
|
+
return await self._search_cluster_records(query, 'list_sessions', 'session_id',
|
|
228
|
+
'user_name', ('host', 'app_id'), 'id')
|
|
229
|
+
|
|
230
|
+
async def search_connections(self, query):
|
|
231
|
+
return await self._search_cluster_records(query, 'list_connections', 'connection',
|
|
232
|
+
'host', ('application',), 'conn-id')
|
|
233
|
+
|
|
234
|
+
async def search_managers(self, query):
|
|
235
|
+
return await self._search_cluster_records(query, 'list_managers', 'manager',
|
|
236
|
+
'host', ('descr', 'pid'), None)
|
|
237
|
+
|
|
238
|
+
async def search_services(self, query):
|
|
239
|
+
needle = self._search_needle(query)
|
|
240
|
+
matches = []
|
|
241
|
+
async with self.factory() as client:
|
|
242
|
+
for cluster_row in await client.list_clusters():
|
|
243
|
+
if not isinstance(cluster_row, dict):
|
|
244
|
+
continue
|
|
245
|
+
try:
|
|
246
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
247
|
+
except (KeyError, TypeError, ValueError):
|
|
248
|
+
continue
|
|
249
|
+
try:
|
|
250
|
+
for row in await client.list_services(UUID(cluster)):
|
|
251
|
+
if not isinstance(row, dict):
|
|
252
|
+
continue
|
|
253
|
+
name = row.get('name')
|
|
254
|
+
if (not isinstance(name, str) or not name
|
|
255
|
+
or not self._valid_search_text(name)):
|
|
256
|
+
continue
|
|
257
|
+
description = row.get('descr')
|
|
258
|
+
manager = row.get('manager')
|
|
259
|
+
fields = (name, description, manager)
|
|
260
|
+
if any(isinstance(value, str) and self._valid_search_text(value)
|
|
261
|
+
and needle in value.casefold() for value in fields):
|
|
262
|
+
matches.append({'cluster': cluster, 'name': name})
|
|
263
|
+
except Exception:
|
|
264
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
265
|
+
continue
|
|
266
|
+
return matches
|
|
267
|
+
|
|
268
|
+
async def search_counters(self, query):
|
|
269
|
+
return await self._search_named_records(query, 'counters')
|
|
270
|
+
|
|
271
|
+
async def search_limits(self, query):
|
|
272
|
+
return await self._search_named_records(query, 'limits')
|
|
273
|
+
|
|
274
|
+
async def search_locks(self, query):
|
|
275
|
+
needle = self._search_needle(query)
|
|
276
|
+
matches = []
|
|
277
|
+
async with self.factory() as client:
|
|
278
|
+
for cluster_row in await client.list_clusters():
|
|
279
|
+
if not isinstance(cluster_row, dict):
|
|
280
|
+
continue
|
|
281
|
+
try:
|
|
282
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
283
|
+
except (KeyError, TypeError, ValueError):
|
|
284
|
+
continue
|
|
285
|
+
try:
|
|
286
|
+
for row in await client.list_locks(UUID(cluster)):
|
|
287
|
+
if not isinstance(row, dict):
|
|
288
|
+
continue
|
|
289
|
+
try:
|
|
290
|
+
connection = str(UUID(str(row['connection'])))
|
|
291
|
+
obj = str(UUID(str(row['object'])))
|
|
292
|
+
except (KeyError, TypeError, ValueError):
|
|
293
|
+
continue
|
|
294
|
+
description = row.get('lock-descr')
|
|
295
|
+
if (needle in connection.casefold() or needle in obj.casefold()
|
|
296
|
+
or isinstance(description, str)
|
|
297
|
+
and self._valid_search_text(description)
|
|
298
|
+
and needle in description.casefold()):
|
|
299
|
+
matches.append({'cluster': cluster, 'connection': connection,
|
|
300
|
+
'object': obj})
|
|
301
|
+
except Exception:
|
|
302
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
303
|
+
continue
|
|
304
|
+
return matches
|
|
305
|
+
|
|
306
|
+
async def search_rules(self, query):
|
|
307
|
+
return await self._search_server_records(query, 'rules')
|
|
308
|
+
|
|
309
|
+
async def search_service_settings(self, query):
|
|
310
|
+
return await self._search_server_records(query, 'service-settings')
|
|
311
|
+
|
|
312
|
+
async def search_transfer_directories(self, query):
|
|
313
|
+
needle = self._search_needle(query)
|
|
314
|
+
matches = []
|
|
315
|
+
async with self.factory() as client:
|
|
316
|
+
for cluster_row in await client.list_clusters():
|
|
317
|
+
if not isinstance(cluster_row, dict):
|
|
318
|
+
continue
|
|
319
|
+
try:
|
|
320
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
321
|
+
except (KeyError, TypeError, ValueError):
|
|
322
|
+
continue
|
|
323
|
+
try:
|
|
324
|
+
servers = await client.list_servers(UUID(cluster))
|
|
325
|
+
except Exception:
|
|
326
|
+
continue
|
|
327
|
+
for server_row in servers:
|
|
328
|
+
if not isinstance(server_row, dict):
|
|
329
|
+
continue
|
|
330
|
+
try:
|
|
331
|
+
server = str(UUID(str(server_row['server'])))
|
|
332
|
+
except (KeyError, TypeError, ValueError):
|
|
333
|
+
continue
|
|
334
|
+
try:
|
|
335
|
+
rows = await client.list_service_data_dirs_for_transfer(
|
|
336
|
+
UUID(cluster), UUID(server))
|
|
337
|
+
for row in rows:
|
|
338
|
+
if not isinstance(row, dict):
|
|
339
|
+
continue
|
|
340
|
+
service = row.get('service-name')
|
|
341
|
+
infobase = row.get('infobase-name')
|
|
342
|
+
if (not isinstance(service, str) or not service
|
|
343
|
+
or not self._valid_search_text(service)):
|
|
344
|
+
continue
|
|
345
|
+
valid_infobase = (isinstance(infobase, str)
|
|
346
|
+
and self._valid_search_text(infobase))
|
|
347
|
+
if (needle in service.casefold() or isinstance(infobase, str)
|
|
348
|
+
and valid_infobase and needle in infobase.casefold()):
|
|
349
|
+
address = {'cluster': cluster, 'server': server,
|
|
350
|
+
'service-name': service}
|
|
351
|
+
if isinstance(infobase, str) and valid_infobase:
|
|
352
|
+
address['infobase-name'] = infobase
|
|
353
|
+
matches.append(address)
|
|
354
|
+
except Exception:
|
|
355
|
+
# An inaccessible server must not hide other servers.
|
|
356
|
+
continue
|
|
357
|
+
return matches
|
|
358
|
+
|
|
359
|
+
async def _search_server_records(self, query, group):
|
|
360
|
+
needle = self._search_needle(query)
|
|
361
|
+
method, id_key, label_key = {
|
|
362
|
+
'rules': ('list_rules', 'rule', 'object-type'),
|
|
363
|
+
'service-settings': ('list_service_settings', 'setting', 'service-name'),
|
|
364
|
+
}[group]
|
|
365
|
+
secondary = ('infobase-name', 'application-ext') if group == 'rules' else ('infobase-name',)
|
|
366
|
+
matches = []
|
|
367
|
+
async with self.factory() as client:
|
|
368
|
+
for cluster_row in await client.list_clusters():
|
|
369
|
+
if not isinstance(cluster_row, dict):
|
|
370
|
+
continue
|
|
371
|
+
try:
|
|
372
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
373
|
+
except (KeyError, TypeError, ValueError):
|
|
374
|
+
continue
|
|
375
|
+
try:
|
|
376
|
+
servers = await client.list_servers(UUID(cluster))
|
|
377
|
+
except Exception:
|
|
378
|
+
continue
|
|
379
|
+
for server_row in servers:
|
|
380
|
+
if not isinstance(server_row, dict):
|
|
381
|
+
continue
|
|
382
|
+
try:
|
|
383
|
+
server = str(UUID(str(server_row['server'])))
|
|
384
|
+
except (KeyError, TypeError, ValueError):
|
|
385
|
+
continue
|
|
386
|
+
try:
|
|
387
|
+
for row in await getattr(client, method)(UUID(cluster), UUID(server)):
|
|
388
|
+
if not isinstance(row, dict):
|
|
389
|
+
continue
|
|
390
|
+
try:
|
|
391
|
+
ident = str(UUID(str(row[id_key])))
|
|
392
|
+
except (KeyError, TypeError, ValueError):
|
|
393
|
+
continue
|
|
394
|
+
label = row.get(label_key)
|
|
395
|
+
valid_label = (isinstance(label, str)
|
|
396
|
+
and self._valid_search_text(label) and bool(label.strip()))
|
|
397
|
+
fields = (label, ident) + tuple(row.get(key) for key in secondary)
|
|
398
|
+
if any(isinstance(field, str) and self._valid_search_text(field)
|
|
399
|
+
and needle in field.casefold() for field in fields):
|
|
400
|
+
matches.append({'cluster': cluster, 'server': server,
|
|
401
|
+
id_key: ident,
|
|
402
|
+
'name': label if valid_label else
|
|
403
|
+
'Assignment rule' if group == 'rules' else 'Service setting'})
|
|
404
|
+
except Exception:
|
|
405
|
+
# A broken server inventory must not hide its neighbors.
|
|
406
|
+
continue
|
|
407
|
+
return matches
|
|
408
|
+
|
|
409
|
+
async def _search_named_records(self, query, kind):
|
|
410
|
+
needle = self._search_needle(query)
|
|
411
|
+
methods = {'counters': 'list_counters', 'limits': 'list_limits'}
|
|
412
|
+
method = methods[kind]
|
|
413
|
+
matches = []
|
|
414
|
+
async with self.factory() as client:
|
|
415
|
+
for cluster_row in await client.list_clusters():
|
|
416
|
+
if not isinstance(cluster_row, dict):
|
|
417
|
+
continue
|
|
418
|
+
try:
|
|
419
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
420
|
+
except (KeyError, TypeError, ValueError):
|
|
421
|
+
continue
|
|
422
|
+
try:
|
|
423
|
+
for row in await getattr(client, method)(UUID(cluster)):
|
|
424
|
+
if not isinstance(row, dict):
|
|
425
|
+
continue
|
|
426
|
+
name = row.get('name')
|
|
427
|
+
if (not isinstance(name, str) or not name.strip()
|
|
428
|
+
or not self._valid_search_text(name)):
|
|
429
|
+
continue
|
|
430
|
+
description = row.get('descr')
|
|
431
|
+
if (needle in name.casefold() or isinstance(description, str)
|
|
432
|
+
and self._valid_search_text(description)
|
|
433
|
+
and needle in description.casefold()):
|
|
434
|
+
matches.append({'cluster': cluster, 'kind': kind[:-1], 'name': name})
|
|
435
|
+
except Exception:
|
|
436
|
+
# One inaccessible cluster must not conceal other clusters.
|
|
437
|
+
continue
|
|
438
|
+
return matches
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _valid_search_text(value):
|
|
442
|
+
if not isinstance(value, str) or '\x00' in value:
|
|
443
|
+
return False
|
|
444
|
+
try:
|
|
445
|
+
return len(value.encode('utf-8')) <= 1024
|
|
446
|
+
except UnicodeError:
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
async def _search_cluster_records(self, query, method, ident_key, label_key,
|
|
450
|
+
secondary_keys, number_key):
|
|
451
|
+
needle = self._search_needle(query)
|
|
452
|
+
matches = []
|
|
453
|
+
async with self.factory() as client:
|
|
454
|
+
for cluster_row in await client.list_clusters():
|
|
455
|
+
if not isinstance(cluster_row, dict):
|
|
456
|
+
continue
|
|
457
|
+
try:
|
|
458
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
459
|
+
except (KeyError, TypeError, ValueError):
|
|
460
|
+
continue
|
|
461
|
+
try:
|
|
462
|
+
for row in await getattr(client, method)(UUID(cluster)):
|
|
463
|
+
if not isinstance(row, dict):
|
|
464
|
+
continue
|
|
465
|
+
try:
|
|
466
|
+
ident = str(UUID(str(row[ident_key])))
|
|
467
|
+
except (KeyError, TypeError, ValueError):
|
|
468
|
+
continue
|
|
469
|
+
label = row.get(label_key)
|
|
470
|
+
valid_label = isinstance(label, str) and self._valid_search_text(label) and bool(label.strip())
|
|
471
|
+
fields = (label, ident) + tuple(row.get(key) for key in secondary_keys)
|
|
472
|
+
number = row.get(number_key) if number_key is not None else None
|
|
473
|
+
if (any(isinstance(value, str) and self._valid_search_text(value)
|
|
474
|
+
and needle in value.casefold() for value in fields)
|
|
475
|
+
or type(number) is int and number >= 0
|
|
476
|
+
and needle == str(number)):
|
|
477
|
+
fallback = {'session_id': 'Session', 'connection': 'Connection',
|
|
478
|
+
'manager': 'Cluster manager'}[ident_key]
|
|
479
|
+
if not valid_label and number_key is not None and type(number) is int and number >= 0:
|
|
480
|
+
fallback = str(number)
|
|
481
|
+
matches.append({'cluster': cluster, ident_key: ident,
|
|
482
|
+
label_key: label if valid_label else fallback})
|
|
483
|
+
except Exception:
|
|
484
|
+
# A broken cluster cannot conceal valid results from its neighbors.
|
|
485
|
+
continue
|
|
486
|
+
return matches
|
|
487
|
+
|
|
488
|
+
async def agent_version(self):
|
|
489
|
+
async with self.factory() as client:
|
|
490
|
+
return await client.agent_version()
|
|
491
|
+
|
|
492
|
+
async def agent_admins(self):
|
|
493
|
+
async with self.factory() as client:
|
|
494
|
+
return self.public_data(await client.list_agent_admins())
|
|
495
|
+
|
|
496
|
+
@classmethod
|
|
497
|
+
def _matching_admins(cls, rows, needle, cluster=None):
|
|
498
|
+
matches = []
|
|
499
|
+
for row in rows:
|
|
500
|
+
if not isinstance(row, dict):
|
|
501
|
+
continue
|
|
502
|
+
name = row.get('name')
|
|
503
|
+
if not isinstance(name, str) or not name or not cls._valid_search_text(name):
|
|
504
|
+
continue
|
|
505
|
+
os_user = row.get('os-user')
|
|
506
|
+
if (needle in name.casefold() or isinstance(os_user, str)
|
|
507
|
+
and cls._valid_search_text(os_user) and needle in os_user.casefold()):
|
|
508
|
+
address = {'name': name}
|
|
509
|
+
if cluster is not None:
|
|
510
|
+
address['cluster'] = cluster
|
|
511
|
+
matches.append(address)
|
|
512
|
+
return matches
|
|
513
|
+
|
|
514
|
+
async def search_agent_admins(self, query):
|
|
515
|
+
needle = self._search_needle(query)
|
|
516
|
+
async with self.factory() as client:
|
|
517
|
+
return self._matching_admins(await client.list_agent_admins(), needle)
|
|
518
|
+
|
|
519
|
+
async def search_cluster_admins(self, query):
|
|
520
|
+
needle = self._search_needle(query)
|
|
521
|
+
matches = []
|
|
522
|
+
async with self.factory() as client:
|
|
523
|
+
for row in await client.list_clusters():
|
|
524
|
+
if not isinstance(row, dict):
|
|
525
|
+
continue
|
|
526
|
+
try:
|
|
527
|
+
cluster = str(UUID(str(row['cluster'])))
|
|
528
|
+
except (KeyError, TypeError, ValueError):
|
|
529
|
+
continue
|
|
530
|
+
try:
|
|
531
|
+
matches.extend(self._matching_admins(
|
|
532
|
+
await client.list_cluster_admins(UUID(cluster)), needle, cluster))
|
|
533
|
+
except Exception:
|
|
534
|
+
# An inaccessible cluster cannot suppress neighboring results.
|
|
535
|
+
continue
|
|
536
|
+
return matches
|
|
537
|
+
|
|
538
|
+
async def cluster_admins(self, cluster_id):
|
|
539
|
+
cluster = UUID(str(cluster_id))
|
|
540
|
+
async with self.factory() as client:
|
|
541
|
+
return self.public_data(await client.list_cluster_admins(cluster))
|
|
542
|
+
|
|
543
|
+
async def cluster(self, cluster_id, include_info=True):
|
|
544
|
+
ident = UUID(str(cluster_id))
|
|
545
|
+
async with self.factory() as client:
|
|
546
|
+
result = {
|
|
547
|
+
'servers': await client.list_servers(ident),
|
|
548
|
+
'processes': await client.list_processes(ident),
|
|
549
|
+
'infobases': await client.list_infobases(ident),
|
|
550
|
+
}
|
|
551
|
+
if include_info:
|
|
552
|
+
result['cluster'] = await client.get_cluster(ident)
|
|
553
|
+
return self.public_data(result)
|
|
554
|
+
|
|
555
|
+
async def one(self, cluster_id, method):
|
|
556
|
+
if method not in ('get_cluster', 'list_servers', 'list_processes', 'list_infobases'):
|
|
557
|
+
raise ValueError('Unsupported read operation')
|
|
558
|
+
ident = UUID(str(cluster_id))
|
|
559
|
+
async with self.factory() as client:
|
|
560
|
+
return self.public_data(await getattr(client, method)(ident))
|
|
561
|
+
|
|
562
|
+
async def list_group(self, cluster_id, group):
|
|
563
|
+
if group not in CLUSTER_LISTS:
|
|
564
|
+
raise ValueError('Unsupported read group')
|
|
565
|
+
ident = UUID(str(cluster_id))
|
|
566
|
+
async with self.factory() as client:
|
|
567
|
+
return self.public_data(await getattr(client, CLUSTER_LISTS[group])(ident))
|
|
568
|
+
|
|
569
|
+
async def server(self, cluster_id, server_id):
|
|
570
|
+
cluster, server = UUID(str(cluster_id)), UUID(str(server_id))
|
|
571
|
+
async with self.factory() as client:
|
|
572
|
+
return self.public_data(await client.get_server(cluster, server))
|
|
573
|
+
|
|
574
|
+
async def process(self, cluster_id, process_id):
|
|
575
|
+
cluster, process = UUID(str(cluster_id)), UUID(str(process_id))
|
|
576
|
+
async with self.factory() as client:
|
|
577
|
+
return self.public_data(await client.get_process(cluster, process))
|
|
578
|
+
|
|
579
|
+
async def processes_on_server(self, cluster_id, server_id):
|
|
580
|
+
cluster, server = UUID(str(cluster_id)), UUID(str(server_id))
|
|
581
|
+
async with self.factory() as client:
|
|
582
|
+
return self.public_data(await client.list_processes(cluster, server))
|
|
583
|
+
|
|
584
|
+
async def session(self, cluster_id, session_id):
|
|
585
|
+
cluster, session = UUID(str(cluster_id)), UUID(str(session_id))
|
|
586
|
+
async with self.factory() as client:
|
|
587
|
+
return self.public_data(await client.get_session(cluster, session))
|
|
588
|
+
|
|
589
|
+
async def sessions_in_infobase(self, cluster_id, infobase_id):
|
|
590
|
+
cluster, infobase = UUID(str(cluster_id)), UUID(str(infobase_id))
|
|
591
|
+
async with self.factory() as client:
|
|
592
|
+
return self.public_data(await client.list_sessions(cluster, infobase))
|
|
593
|
+
|
|
594
|
+
async def infobase_summary(self, cluster_id, infobase_id):
|
|
595
|
+
cluster, infobase = UUID(str(cluster_id)), UUID(str(infobase_id))
|
|
596
|
+
async with self.factory() as client:
|
|
597
|
+
return self.public_data(await client.get_infobase_summary(cluster, infobase))
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _validated_infobase_name(name):
|
|
601
|
+
if (not isinstance(name, str) or not name.strip() or '\x00' in name
|
|
602
|
+
or len(name.encode('utf-8')) > 1024):
|
|
603
|
+
raise ValueError('Invalid infobase name')
|
|
604
|
+
return name
|
|
605
|
+
|
|
606
|
+
async def infobase_summary_by_name(self, cluster_id, name):
|
|
607
|
+
self._validated_infobase_name(name)
|
|
608
|
+
cluster = UUID(str(cluster_id))
|
|
609
|
+
async with self.factory() as client:
|
|
610
|
+
return self.public_data(await client.get_infobase_summary(cluster, name=name))
|
|
611
|
+
|
|
612
|
+
async def infobase_info_by_name(self, cluster_id, name):
|
|
613
|
+
self._validated_infobase_name(name)
|
|
614
|
+
cluster = UUID(str(cluster_id))
|
|
615
|
+
async with self.factory() as client:
|
|
616
|
+
row = await client.get_infobase_info(cluster, name=name)
|
|
617
|
+
return self._public_infobase_info(row)
|
|
618
|
+
|
|
619
|
+
@staticmethod
|
|
620
|
+
def _public_infobase_info(row):
|
|
621
|
+
# Do not leak authentication material even if another adapter returns it.
|
|
622
|
+
return ReadService.public_data(row)
|
|
623
|
+
|
|
624
|
+
async def infobase_info(self, cluster_id, infobase_id):
|
|
625
|
+
cluster, infobase = UUID(str(cluster_id)), UUID(str(infobase_id))
|
|
626
|
+
async with self.factory() as client:
|
|
627
|
+
row = await client.get_infobase_info(cluster, infobase)
|
|
628
|
+
return self._public_infobase_info(row)
|
|
629
|
+
|
|
630
|
+
async def binary_storages(self, cluster_id, infobase_id):
|
|
631
|
+
cluster, infobase = UUID(str(cluster_id)), UUID(str(infobase_id))
|
|
632
|
+
async with self.factory() as client:
|
|
633
|
+
rows = await client.list_binary_storages(cluster, infobase)
|
|
634
|
+
return self.public_data(rows)
|
|
635
|
+
|
|
636
|
+
async def connection(self, cluster_id, connection_id):
|
|
637
|
+
cluster, connection = UUID(str(cluster_id)), UUID(str(connection_id))
|
|
638
|
+
async with self.factory() as client:
|
|
639
|
+
return self.public_data(await client.get_connection(cluster, connection))
|
|
640
|
+
|
|
641
|
+
async def connections_in_infobase(self, cluster_id, infobase_id):
|
|
642
|
+
cluster, infobase = UUID(str(cluster_id)), UUID(str(infobase_id))
|
|
643
|
+
async with self.factory() as client:
|
|
644
|
+
return self.public_data(await client.list_connections(cluster, infobase))
|
|
645
|
+
|
|
646
|
+
async def connections_in_process(self, cluster_id, process_id):
|
|
647
|
+
cluster, process = UUID(str(cluster_id)), UUID(str(process_id))
|
|
648
|
+
async with self.factory() as client:
|
|
649
|
+
return self.public_data(await client.list_connections(cluster, process_id=process))
|
|
650
|
+
|
|
651
|
+
async def filtered_locks(self, cluster_id, scope, record_id):
|
|
652
|
+
if scope not in ('infobase', 'connection'):
|
|
653
|
+
raise ValueError('Unsupported lock filter')
|
|
654
|
+
cluster, record = UUID(str(cluster_id)), UUID(str(record_id))
|
|
655
|
+
async with self.factory() as client:
|
|
656
|
+
if scope == 'infobase':
|
|
657
|
+
return self.public_data(await client.list_locks(cluster, infobase_id=record))
|
|
658
|
+
return self.public_data(await client.list_locks(cluster, connection_id=record))
|
|
659
|
+
|
|
660
|
+
async def service_setting(self, cluster_id, server_id, setting_id):
|
|
661
|
+
cluster, server, setting = (UUID(str(value)) for value in
|
|
662
|
+
(cluster_id, server_id, setting_id))
|
|
663
|
+
async with self.factory() as client:
|
|
664
|
+
return self.public_data(await client.get_service_setting(cluster, server, setting))
|
|
665
|
+
|
|
666
|
+
async def rule(self, cluster_id, server_id, rule_id):
|
|
667
|
+
cluster, server, rule = (UUID(str(value)) for value in
|
|
668
|
+
(cluster_id, server_id, rule_id))
|
|
669
|
+
async with self.factory() as client:
|
|
670
|
+
return self.public_data(await client.get_rule(cluster, server, rule))
|
|
671
|
+
|
|
672
|
+
async def manager(self, cluster_id, manager_id):
|
|
673
|
+
cluster, manager = UUID(str(cluster_id)), UUID(str(manager_id))
|
|
674
|
+
async with self.factory() as client:
|
|
675
|
+
return self.public_data(await client.get_manager(cluster, manager))
|
|
676
|
+
|
|
677
|
+
async def limit(self, cluster_id, name):
|
|
678
|
+
if (not isinstance(name, str) or not name or '\x00' in name
|
|
679
|
+
or len(name.encode('utf-8')) > 1024):
|
|
680
|
+
raise ValueError('Invalid limit name')
|
|
681
|
+
cluster = UUID(str(cluster_id))
|
|
682
|
+
async with self.factory() as client:
|
|
683
|
+
return self.public_data(await client.get_limit(cluster, name))
|
|
684
|
+
|
|
685
|
+
async def counter(self, cluster_id, name):
|
|
686
|
+
if (not isinstance(name, str) or not name or '\x00' in name
|
|
687
|
+
or len(name.encode('utf-8')) > 1024):
|
|
688
|
+
raise ValueError('Invalid counter name')
|
|
689
|
+
cluster = UUID(str(cluster_id))
|
|
690
|
+
async with self.factory() as client:
|
|
691
|
+
return self.public_data(await client.get_counter(cluster, name))
|
|
692
|
+
|
|
693
|
+
async def counter_values(self, cluster_id, name, object_filter='', *, accumulated=False):
|
|
694
|
+
if (not isinstance(name, str) or not name.strip() or '\x00' in name
|
|
695
|
+
or len(name.encode('utf-8')) > 1024):
|
|
696
|
+
raise ValueError('Invalid counter name')
|
|
697
|
+
if (not isinstance(object_filter, str) or '\x00' in object_filter
|
|
698
|
+
or len(object_filter.encode('utf-8')) > 4096):
|
|
699
|
+
raise ValueError('Invalid counter object filter')
|
|
700
|
+
cluster = UUID(str(cluster_id))
|
|
701
|
+
async with self.factory() as client:
|
|
702
|
+
if accumulated:
|
|
703
|
+
return self.public_data(await client.list_accumulated_counter_values(cluster, name, object_filter))
|
|
704
|
+
return self.public_data(await client.list_counter_values(cluster, name, object_filter))
|
|
705
|
+
|
|
706
|
+
async def list_server_group(self, cluster_id, server_id, group):
|
|
707
|
+
if group not in SERVER_LISTS:
|
|
708
|
+
raise ValueError('Unsupported server read group')
|
|
709
|
+
cluster, server = UUID(str(cluster_id)), UUID(str(server_id))
|
|
710
|
+
async with self.factory() as client:
|
|
711
|
+
return self.public_data(await getattr(client, SERVER_LISTS[group])(cluster, server))
|
|
712
|
+
|
|
713
|
+
async def transfer_directories(self, cluster_id, server_id, service_name=''):
|
|
714
|
+
if (not isinstance(service_name, str) or '\x00' in service_name
|
|
715
|
+
or len(service_name.encode('utf-8')) > 1024):
|
|
716
|
+
raise ValueError('Invalid service name')
|
|
717
|
+
cluster, server = UUID(str(cluster_id)), UUID(str(server_id))
|
|
718
|
+
async with self.factory() as client:
|
|
719
|
+
return self.public_data(await client.list_service_data_dirs_for_transfer(cluster, server, service_name))
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def _validated_profile_name(name):
|
|
723
|
+
if (not isinstance(name, str) or not name.strip() or '\x00' in name
|
|
724
|
+
or len(name.encode('utf-8')) > 1024):
|
|
725
|
+
raise ValueError('Invalid profile name')
|
|
726
|
+
return name
|
|
727
|
+
|
|
728
|
+
async def profile_by_name(self, cluster_id, name):
|
|
729
|
+
self._validated_profile_name(name)
|
|
730
|
+
cluster = UUID(str(cluster_id))
|
|
731
|
+
async with self.factory() as client:
|
|
732
|
+
matches = [row for row in await client.list_profiles(cluster)
|
|
733
|
+
if row['name'] == name]
|
|
734
|
+
if len(matches) != 1:
|
|
735
|
+
raise ValueError('Expected exactly one profile by name')
|
|
736
|
+
return self.public_data(matches[0])
|
|
737
|
+
|
|
738
|
+
async def profile_overview(self, cluster_id, name):
|
|
739
|
+
self._validated_profile_name(name)
|
|
740
|
+
cluster = UUID(str(cluster_id))
|
|
741
|
+
async with self.factory() as client:
|
|
742
|
+
matches = [row for row in await client.list_profiles(cluster)
|
|
743
|
+
if row['name'] == name]
|
|
744
|
+
if len(matches) != 1:
|
|
745
|
+
raise ValueError('Expected exactly one profile by name')
|
|
746
|
+
acl = {kind: await getattr(client, method)(cluster, name)
|
|
747
|
+
for kind, method in PROFILE_ACL_LISTS.items()}
|
|
748
|
+
return self.public_data({'profile': matches[0], 'acl': acl})
|
|
749
|
+
|
|
750
|
+
async def list_profile_acl(self, cluster_id, profile_name, acl_type, name=''):
|
|
751
|
+
if acl_type not in PROFILE_ACL_LISTS:
|
|
752
|
+
raise ValueError('Unsupported profile ACL')
|
|
753
|
+
if (not isinstance(profile_name, str) or not profile_name.strip()
|
|
754
|
+
or '\x00' in profile_name or len(profile_name.encode('utf-8')) > 1024):
|
|
755
|
+
raise ValueError('Invalid profile name')
|
|
756
|
+
if (not isinstance(name, str) or '\x00' in name
|
|
757
|
+
or len(name.encode('utf-8')) > 1024):
|
|
758
|
+
raise ValueError('Invalid ACL name')
|
|
759
|
+
cluster = UUID(str(cluster_id))
|
|
760
|
+
async with self.factory() as client:
|
|
761
|
+
rows = await getattr(client, PROFILE_ACL_LISTS[acl_type])(cluster, profile_name)
|
|
762
|
+
key = 'alias' if acl_type == 'directory' else 'name'
|
|
763
|
+
return self.public_data([row for row in rows if row.get(key) == name] if name else rows)
|
|
764
|
+
|
|
765
|
+
async def search_profile_acl_all(self, acl_type, query):
|
|
766
|
+
if acl_type not in PROFILE_ACL_LISTS:
|
|
767
|
+
raise ValueError('Unsupported profile ACL')
|
|
768
|
+
needle = self._search_needle(query)
|
|
769
|
+
field = 'alias' if acl_type == 'directory' else 'name'
|
|
770
|
+
matches = []
|
|
771
|
+
async with self.factory() as client:
|
|
772
|
+
for cluster_row in await client.list_clusters():
|
|
773
|
+
if not isinstance(cluster_row, dict):
|
|
774
|
+
continue
|
|
775
|
+
try:
|
|
776
|
+
cluster = str(UUID(str(cluster_row['cluster'])))
|
|
777
|
+
profiles = await client.list_profiles(UUID(cluster))
|
|
778
|
+
except Exception:
|
|
779
|
+
continue
|
|
780
|
+
for profile in profiles:
|
|
781
|
+
if not isinstance(profile, dict):
|
|
782
|
+
continue
|
|
783
|
+
name = profile.get('name')
|
|
784
|
+
if (not isinstance(name, str) or not name.strip()
|
|
785
|
+
or not self._valid_search_text(name)):
|
|
786
|
+
continue
|
|
787
|
+
try:
|
|
788
|
+
for entry in await getattr(client, PROFILE_ACL_LISTS[acl_type])(
|
|
789
|
+
UUID(cluster), name):
|
|
790
|
+
if not isinstance(entry, dict):
|
|
791
|
+
continue
|
|
792
|
+
value = entry.get(field)
|
|
793
|
+
if (isinstance(value, str) and value
|
|
794
|
+
and self._valid_search_text(value)
|
|
795
|
+
and needle in value.casefold()):
|
|
796
|
+
matches.append({'cluster': cluster, 'profile': name,
|
|
797
|
+
'acl_type': acl_type, 'name': value})
|
|
798
|
+
except Exception:
|
|
799
|
+
# Other profiles and clusters remain searchable.
|
|
800
|
+
continue
|
|
801
|
+
return matches
|
|
802
|
+
|
|
803
|
+
async def search_profile_acl(self, cluster_id, acl_type, query):
|
|
804
|
+
if acl_type not in PROFILE_ACL_LISTS:
|
|
805
|
+
raise ValueError('Unsupported profile ACL')
|
|
806
|
+
needle = self._search_needle(query)
|
|
807
|
+
cluster = UUID(str(cluster_id))
|
|
808
|
+
field = 'alias' if acl_type == 'directory' else 'name'
|
|
809
|
+
matches = []
|
|
810
|
+
async with self.factory() as client:
|
|
811
|
+
for profile in await client.list_profiles(cluster):
|
|
812
|
+
if not isinstance(profile, dict):
|
|
813
|
+
continue
|
|
814
|
+
name = profile.get('name')
|
|
815
|
+
if (not isinstance(name, str) or not name.strip()
|
|
816
|
+
or not self._valid_search_text(name)):
|
|
817
|
+
continue
|
|
818
|
+
try:
|
|
819
|
+
for entry in await getattr(client, PROFILE_ACL_LISTS[acl_type])(cluster, name):
|
|
820
|
+
if not isinstance(entry, dict):
|
|
821
|
+
continue
|
|
822
|
+
value = entry.get(field)
|
|
823
|
+
if (isinstance(value, str) and value
|
|
824
|
+
and self._valid_search_text(value)
|
|
825
|
+
and needle in value.casefold()):
|
|
826
|
+
matches.append({'cluster': str(cluster), 'profile': name,
|
|
827
|
+
'acl_type': acl_type, 'name': value})
|
|
828
|
+
except Exception:
|
|
829
|
+
# Other profiles remain searchable; never return backend exception text.
|
|
830
|
+
continue
|
|
831
|
+
return matches
|