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/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