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/client.py ADDED
@@ -0,0 +1,2382 @@
1
+ """Versioned Python RAC library: 1C 8.3.11 (10.0) and 8.5.1 (16.0)."""
2
+ import ast
3
+ import asyncio
4
+ import contextlib
5
+ import io
6
+ import re
7
+ import time
8
+ import uuid
9
+ from argparse import Namespace
10
+ from . import v8311
11
+ from .protocol.v851_client import V851Client, EndpointVersionRejected
12
+
13
+
14
+ class RacError(Exception):
15
+ pass
16
+
17
+
18
+ class RacTransportError(RacError):
19
+ pass
20
+
21
+
22
+ class UnsupportedFeatureError(RacError):
23
+ pass
24
+
25
+
26
+ def parse_records(text):
27
+ """Parse historical rac text output for downstream compatibility only."""
28
+ rows, current = [], {}
29
+ for line in text.splitlines():
30
+ if not line.strip():
31
+ if current:
32
+ rows.append(current)
33
+ current = {}
34
+ continue
35
+ if ':' not in line:
36
+ raise RacError('Malformed rac output')
37
+ key, value = (part.strip() for part in line.split(':', 1))
38
+ if not re.fullmatch(r'[a-z][a-z0-9-]*', key):
39
+ raise RacError('Malformed rac output key')
40
+ if value.startswith('"') and value.endswith('"'):
41
+ value = value[1:-1].replace('""', '"')
42
+ current[key] = value
43
+ if current:
44
+ rows.append(current)
45
+ return rows
46
+
47
+
48
+ class RacClient:
49
+ def __init__(self, host='localhost', port=1545, protocol='16.0', timeout=10.0,
50
+ cluster_user=None, cluster_password=None, agent_user=None, agent_password=None):
51
+ if protocol not in ('10.0', '16.0', 'auto'):
52
+ raise ValueError('Select protocol auto, 10.0 or 16.0')
53
+ if type(port) is not int or not 1 <= port <= 65535:
54
+ raise ValueError('Invalid RAS port')
55
+ if protocol in ('16.0', 'auto'):
56
+ for name, password in ((cluster_user, cluster_password), (agent_user, agent_password)):
57
+ if password is not None and (not isinstance(name, str) or not name
58
+ or not isinstance(password, str) or not password
59
+ or '\x00' in password or len(password.encode('utf-8')) > 1024):
60
+ raise ValueError('Invalid administrator credentials')
61
+ if protocol in ('16.0', 'auto'):
62
+ for label, name in (('agent', agent_user), ('cluster', cluster_user)):
63
+ if name is not None and (not isinstance(name, str) or not name
64
+ or '\x00' in name or len(name.encode('utf-8')) > 1024):
65
+ raise ValueError('Invalid ' + label + ' administrator name')
66
+ self.host, self.port, self.protocol, self.timeout = host, port, protocol, timeout
67
+ self._selected_protocol = protocol
68
+ self.cluster_user, self.cluster_password = cluster_user, cluster_password
69
+ self.agent_user, self.agent_password = agent_user, agent_password
70
+ self._native = None
71
+
72
+ async def __aenter__(self):
73
+ if self._selected_protocol == 'auto':
74
+ self.protocol = 'auto'
75
+ if self.protocol in ('16.0', 'auto'):
76
+ native = V851Client(self.host, self.port, self.timeout)
77
+ try:
78
+ await native.__aenter__()
79
+ except EndpointVersionRejected:
80
+ if self.protocol != 'auto':
81
+ raise
82
+ self.protocol = '10.0'
83
+ else:
84
+ self._native = native
85
+ self.protocol = '16.0'
86
+ return self
87
+
88
+ async def __aexit__(self, *args):
89
+ if self._native is not None:
90
+ try:
91
+ await self._native.__aexit__(*args)
92
+ finally:
93
+ self._native = None
94
+ if self._selected_protocol == 'auto':
95
+ self.protocol = 'auto'
96
+ return False
97
+
98
+ def _direct(self, *, allow_password=False):
99
+ if not allow_password and (self.cluster_password is not None or self.agent_password is not None):
100
+ raise UnsupportedFeatureError('Password authentication is only available for administrator list probes')
101
+ if self._native is None:
102
+ raise RacTransportError('16.0 client must be used inside async with')
103
+ return self._native
104
+
105
+ async def _v8311(self, command, subcommand, cluster=None, subcommand2=None):
106
+ args = Namespace(ras_host=self.host, ras_port=self.port, command=command,
107
+ subcommand1=subcommand, subcommand2=subcommand2,
108
+ cluster=str(cluster) if cluster else None,
109
+ cluster_user=self.cluster_user, cluster_pwd=self.cluster_password,
110
+ agent_user=self.agent_user, agent_pwd=self.agent_password)
111
+ stream = io.StringIO()
112
+ try:
113
+ with contextlib.redirect_stdout(stream):
114
+ async with asyncio.timeout(self.timeout):
115
+ await v8311.ras_command(args)
116
+ except Exception as exc:
117
+ raise RacTransportError('1C 8.3.11 RAC request failed') from exc
118
+ try:
119
+ tree = ast.parse(stream.getvalue(), mode='eval')
120
+ class UUIDLiteral(ast.NodeTransformer):
121
+ def visit_Call(self, node):
122
+ if (not isinstance(node.func, ast.Name) or node.func.id != 'UUID'
123
+ or len(node.args) != 1 or node.keywords):
124
+ raise ValueError('Unexpected 1C 8.3.11 object')
125
+ return ast.copy_location(ast.Constant(value=str(uuid.UUID(ast.literal_eval(node.args[0])))), node)
126
+ return ast.literal_eval(UUIDLiteral().visit(tree))
127
+ except (ValueError, SyntaxError, TypeError) as exc:
128
+ raise RacError('Unexpected 1C 8.3.11 response') from exc
129
+
130
+ async def agent_version(self):
131
+ if self.protocol == '10.0':
132
+ raise UnsupportedFeatureError('1C 8.3.11 agent version is not exposed')
133
+ return await self._direct().agent_version()
134
+
135
+ async def list_cluster_admins(self, cluster_id):
136
+ cluster = uuid.UUID(str(cluster_id))
137
+ if self.protocol == '10.0':
138
+ return await self._v8311('cluster', 'admin', cluster, subcommand2='list')
139
+ if self.cluster_password is not None:
140
+ if self.agent_password is not None:
141
+ raise UnsupportedFeatureError('Use one password scope per probe')
142
+ return await self._direct(allow_password=True).list_cluster_admins(
143
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
144
+ return await self._direct().list_cluster_admins(cluster, cluster_os_user=self.cluster_user)
145
+
146
+ async def register_cluster_admin(self, cluster_id, name, os_user, *, descr=''):
147
+ cluster = uuid.UUID(str(cluster_id))
148
+ if self.protocol == '10.0':
149
+ raise UnsupportedFeatureError('Cluster administrator registration requires protocol 16.0')
150
+ for label, value in (('name', name), ('OS user', os_user), ('description', descr)):
151
+ if (not isinstance(value, str) or (label != 'description' and not value)
152
+ or '\x00' in value or len(value.encode('utf-8')) > 1024):
153
+ raise ValueError('Invalid cluster administrator ' + label)
154
+ before = await self.list_cluster_admins(cluster)
155
+ if any(row['name'] == name or row.get('os-user') == os_user for row in before):
156
+ raise ValueError('Cluster administrator name or OS user already registered')
157
+ if self.cluster_user:
158
+ await self._direct().register_cluster_admin(cluster, name, os_user, descr,
159
+ cluster_os_user=self.cluster_user)
160
+ after = await self.list_cluster_admins(cluster)
161
+ else:
162
+ await self._direct().register_cluster_admin(cluster, name, os_user, descr)
163
+ after = await self._direct().list_cluster_admins(cluster, cluster_os_user=name)
164
+ expected = {'name': name, 'descr': descr, 'password-auth-allowed': True,
165
+ 'os-auth-allowed': True, 'os-user': os_user}
166
+ if after != before + [expected]:
167
+ raise RacError('Cluster administrators did not match after registration')
168
+ return expected
169
+
170
+ async def remove_cluster_admin(self, cluster_id, name):
171
+ cluster = uuid.UUID(str(cluster_id))
172
+ if self.protocol == '10.0':
173
+ raise UnsupportedFeatureError('Cluster administrator removal requires protocol 16.0')
174
+ if (not isinstance(name, str) or not name or '\x00' in name
175
+ or len(name.encode('utf-8')) > 1024):
176
+ raise ValueError('Invalid cluster administrator name')
177
+ if not self.cluster_user:
178
+ raise ValueError('Removing an administrator requires an authenticated administrator')
179
+ before = await self.list_cluster_admins(cluster)
180
+ if len([row for row in before if row['name'] == name]) != 1:
181
+ raise ValueError('Cluster administrator not found or not unique')
182
+ if name == self.cluster_user and len(before) != 1:
183
+ raise ValueError('Cannot verify removal of current administrator while others remain')
184
+ await self._direct().remove_cluster_admin(cluster, name, self.cluster_user)
185
+ if name == self.cluster_user:
186
+ after = await self._direct().list_cluster_admins(cluster)
187
+ else:
188
+ after = await self.list_cluster_admins(cluster)
189
+ if after != [row for row in before if row['name'] != name]:
190
+ raise RacError('Cluster administrators did not match after removal')
191
+ return after
192
+
193
+ async def list_agent_admins(self):
194
+ if self.protocol == '10.0':
195
+ return await self._v8311('agent', 'admin', subcommand2='list')
196
+ if self.agent_password is not None:
197
+ if self.cluster_password is not None:
198
+ raise UnsupportedFeatureError('Use one password scope per probe')
199
+ return await self._direct(allow_password=True).list_agent_admins(
200
+ agent_os_user=self.agent_user, agent_password=self.agent_password)
201
+ return await self._direct().list_agent_admins(agent_os_user=self.agent_user)
202
+
203
+ async def register_agent_admin(self, name, os_user, *, descr=''):
204
+ if self.protocol == '10.0':
205
+ raise UnsupportedFeatureError('Agent administrator registration requires protocol 16.0')
206
+ for label, value in (('name', name), ('OS user', os_user), ('description', descr)):
207
+ if (not isinstance(value, str) or (label != 'description' and not value)
208
+ or '\x00' in value or len(value.encode('utf-8')) > 1024):
209
+ raise ValueError('Invalid agent administrator ' + label)
210
+ before = await self.list_agent_admins()
211
+ if any(row['name'] == name or row.get('os-user') == os_user for row in before):
212
+ raise ValueError('Agent administrator name or OS user already registered')
213
+ if self.agent_user:
214
+ await self._direct().register_agent_admin(name, os_user, descr,
215
+ agent_os_user=self.agent_user)
216
+ after = await self.list_agent_admins()
217
+ else:
218
+ await self._direct().register_agent_admin(name, os_user, descr)
219
+ after = await self._direct().list_agent_admins(agent_os_user=name)
220
+ expected = {'name': name, 'descr': descr, 'password-auth-allowed': True,
221
+ 'os-auth-allowed': True, 'os-user': os_user}
222
+ if after != before + [expected]:
223
+ raise RacError('Agent administrators did not match after registration')
224
+ return expected
225
+
226
+ async def remove_agent_admin(self, name):
227
+ if self.protocol == '10.0':
228
+ raise UnsupportedFeatureError('Agent administrator removal requires protocol 16.0')
229
+ if (not isinstance(name, str) or not name or '\x00' in name
230
+ or len(name.encode('utf-8')) > 1024):
231
+ raise ValueError('Invalid agent administrator name')
232
+ if not self.agent_user:
233
+ raise ValueError('Removing an administrator requires an authenticated administrator')
234
+ before = await self.list_agent_admins()
235
+ if len([row for row in before if row['name'] == name]) != 1:
236
+ raise ValueError('Agent administrator not found or not unique')
237
+ if name == self.agent_user and len(before) != 1:
238
+ raise ValueError('Cannot verify removal of current administrator while others remain')
239
+ await self._direct().remove_agent_admin(name, self.agent_user)
240
+ if name == self.agent_user:
241
+ after = await self._direct().list_agent_admins()
242
+ else:
243
+ after = await self.list_agent_admins()
244
+ if after != [row for row in before if row['name'] != name]:
245
+ raise RacError('Agent administrators did not match after removal')
246
+ return after
247
+
248
+ async def list_sessions(self, cluster_id, infobase_id=None):
249
+ cluster = uuid.UUID(str(cluster_id))
250
+ infobase = uuid.UUID(str(infobase_id)) if infobase_id is not None else None
251
+ if self.protocol == '10.0':
252
+ if infobase is not None:
253
+ raise UnsupportedFeatureError('1C 8.3.11 filtered sessions not exposed')
254
+ return await self._v8311('session', 'list', cluster)
255
+ if self.cluster_password is not None:
256
+ return await self._direct(allow_password=True).list_sessions(
257
+ cluster, infobase, cluster_os_user=self.cluster_user,
258
+ cluster_password=self.cluster_password)
259
+ if self.cluster_user:
260
+ return await self._direct().list_sessions(cluster, infobase, cluster_os_user=self.cluster_user)
261
+ return await self._direct().list_sessions(cluster, infobase)
262
+
263
+ async def get_session(self, cluster_id, session_id):
264
+ cluster, session = uuid.UUID(str(cluster_id)), uuid.UUID(str(session_id))
265
+ if self.protocol == '10.0':
266
+ raise UnsupportedFeatureError('1C 8.3.11 session info not exposed')
267
+ if self.cluster_password is not None:
268
+ return await self._direct(allow_password=True).get_session(
269
+ cluster, session, cluster_os_user=self.cluster_user,
270
+ cluster_password=self.cluster_password)
271
+ if self.cluster_user:
272
+ return await self._direct().get_session(cluster, session, cluster_os_user=self.cluster_user)
273
+ return await self._direct().get_session(cluster, session)
274
+
275
+ async def list_profiles(self, cluster_id):
276
+ cluster = uuid.UUID(str(cluster_id))
277
+ if self.protocol == '10.0':
278
+ raise UnsupportedFeatureError('1C 8.3.11 profiles not exposed')
279
+ if self.cluster_password is not None:
280
+ return await self._direct(allow_password=True).list_profiles(
281
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
282
+ if self.cluster_user:
283
+ return await self._direct().list_profiles(cluster, cluster_os_user=self.cluster_user)
284
+ return await self._direct().list_profiles(cluster)
285
+
286
+ async def update_profile(self, cluster_id, name, *, descr=None, config=None, priv=None,
287
+ full_privileged_mode=None, crypto=None,
288
+ right_extension=None, all_modules_extension=None,
289
+ right_extension_definition_roles=None, privileged_mode_roles=None,
290
+ modules_available_for_extension=None,
291
+ modules_not_available_for_extension=None, directory_access=None,
292
+ com_access=None, addin_access=None, module_access=None,
293
+ app_access=None, inet_access=None):
294
+ cluster = uuid.UUID(str(cluster_id))
295
+ if self.protocol == '10.0':
296
+ raise UnsupportedFeatureError('Profile update requires protocol 16.0')
297
+ if (not isinstance(name, str) or not name.strip()
298
+ or '\x00' in name or len(name.encode('utf-8')) > 1024):
299
+ raise ValueError('Invalid security profile name')
300
+ if descr is not None and (not isinstance(descr, str) or '\x00' in descr
301
+ or len(descr.encode('utf-8')) > 1024):
302
+ raise ValueError('Invalid profile description')
303
+ if config is not None and type(config) is not bool:
304
+ raise ValueError('Invalid config flag')
305
+ if priv is not None and full_privileged_mode is not None:
306
+ raise ValueError('Ambiguous privileged mode aliases')
307
+ if full_privileged_mode is not None:
308
+ if type(full_privileged_mode) is not bool:
309
+ raise ValueError('Invalid full-privileged-mode flag')
310
+ priv = full_privileged_mode
311
+ if priv is not None and type(priv) is not bool:
312
+ raise ValueError('Invalid priv flag')
313
+ if crypto is not None and type(crypto) is not bool:
314
+ raise ValueError('Invalid crypto flag')
315
+ if directory_access is not None and type(directory_access) is not bool:
316
+ raise ValueError('Invalid directory access flag')
317
+ if com_access is not None and type(com_access) is not bool:
318
+ raise ValueError('Invalid com access flag')
319
+ if addin_access is not None and type(addin_access) is not bool:
320
+ raise ValueError('Invalid addin access flag')
321
+ if module_access is not None and type(module_access) is not bool:
322
+ raise ValueError('Invalid module access flag')
323
+ if app_access is not None and type(app_access) is not bool:
324
+ raise ValueError('Invalid app access flag')
325
+ if inet_access is not None and type(inet_access) is not bool:
326
+ raise ValueError('Invalid inet access flag')
327
+ if right_extension is not None and type(right_extension) is not bool:
328
+ raise ValueError('Invalid right-extension flag')
329
+ if all_modules_extension is not None and type(all_modules_extension) is not bool:
330
+ raise ValueError('Invalid all-modules-extension flag')
331
+ if (right_extension_definition_roles is not None and
332
+ (not isinstance(right_extension_definition_roles, str)
333
+ or '\x00' in right_extension_definition_roles
334
+ or len(right_extension_definition_roles.encode('utf-8')) > 1024)):
335
+ raise ValueError('Invalid right-extension-definition-roles')
336
+ if (privileged_mode_roles is not None and
337
+ (not isinstance(privileged_mode_roles, str)
338
+ or '\x00' in privileged_mode_roles
339
+ or len(privileged_mode_roles.encode('utf-8')) > 1024)):
340
+ raise ValueError('Invalid privileged-mode-roles')
341
+ if (modules_available_for_extension is not None and
342
+ (not isinstance(modules_available_for_extension, str)
343
+ or '\x00' in modules_available_for_extension
344
+ or len(modules_available_for_extension.encode('utf-8')) > 1024)):
345
+ raise ValueError('Invalid modules-available-for-extension')
346
+ if (modules_not_available_for_extension is not None and
347
+ (not isinstance(modules_not_available_for_extension, str)
348
+ or '\x00' in modules_not_available_for_extension
349
+ or len(modules_not_available_for_extension.encode('utf-8')) > 1024)):
350
+ raise ValueError('Invalid modules-not-available-for-extension')
351
+ has_fields = any(value is not None for value in (
352
+ descr, config, priv, crypto, right_extension, all_modules_extension,
353
+ right_extension_definition_roles, privileged_mode_roles,
354
+ modules_available_for_extension, modules_not_available_for_extension,
355
+ directory_access, com_access, addin_access, module_access, app_access,
356
+ inet_access))
357
+ before = await self.list_profiles(cluster)
358
+ matches = [row for row in before if row['name'] == name]
359
+ if len(matches) > 1:
360
+ raise ValueError('Security profile not unique')
361
+ if matches:
362
+ if not has_fields:
363
+ raise ValueError('Specify a profile field to update')
364
+ expected = dict(matches[0])
365
+ else:
366
+ # The 16.0 profile update request also creates. Match the locally
367
+ # observed default record so full read-back catches silent failures.
368
+ expected = {'name': name, 'descr': '',
369
+ **{key: False for key in ('directory', 'com', 'addin', 'app', 'inet',
370
+ 'config', 'priv', 'raw-external-module-access',
371
+ 'crypto', 'right-extension', 'all-modules-extension')},
372
+ **{key: '' for key in ('right-extension-definition-roles',
373
+ 'modules-available-for-extension',
374
+ 'modules-not-available-for-extension',
375
+ 'privileged-mode-roles')}}
376
+ if descr is not None:
377
+ expected['descr'] = descr
378
+ if config is not None:
379
+ expected['config'] = config
380
+ if priv is not None:
381
+ expected['priv'] = priv
382
+ if crypto is not None:
383
+ expected['crypto'] = crypto
384
+ if directory_access is not None:
385
+ expected['directory'] = directory_access
386
+ if com_access is not None:
387
+ expected['com'] = com_access
388
+ if addin_access is not None:
389
+ expected['addin'] = addin_access
390
+ if module_access is not None:
391
+ expected['raw-external-module-access'] = module_access
392
+ if app_access is not None:
393
+ expected['app'] = app_access
394
+ if inet_access is not None:
395
+ expected['inet'] = inet_access
396
+ if right_extension is not None:
397
+ expected['right-extension'] = right_extension
398
+ if all_modules_extension is not None:
399
+ expected['all-modules-extension'] = all_modules_extension
400
+ if right_extension_definition_roles is not None:
401
+ expected['right-extension-definition-roles'] = right_extension_definition_roles
402
+ if privileged_mode_roles is not None:
403
+ expected['privileged-mode-roles'] = privileged_mode_roles
404
+ if modules_available_for_extension is not None:
405
+ expected['modules-available-for-extension'] = modules_available_for_extension
406
+ if modules_not_available_for_extension is not None:
407
+ expected['modules-not-available-for-extension'] = modules_not_available_for_extension
408
+ if self.cluster_user:
409
+ await self._direct().update_profile(cluster, expected, cluster_os_user=self.cluster_user)
410
+ else:
411
+ await self._direct().update_profile(cluster, expected)
412
+ after = await self.list_profiles(cluster)
413
+ current = [row for row in after if row['name'] == name]
414
+ if current != [expected]:
415
+ raise RacError('Security profile did not match after update')
416
+ expected_list = [expected if row['name'] == name else row for row in before]
417
+ if not matches:
418
+ expected_list.append(expected)
419
+ if after != expected_list:
420
+ raise RacError('Security profile list did not match after update')
421
+ return current[0]
422
+
423
+ async def remove_profile(self, cluster_id, profile_name):
424
+ cluster = uuid.UUID(str(cluster_id))
425
+ if self.protocol == '10.0':
426
+ raise UnsupportedFeatureError('Profile removal requires protocol 16.0')
427
+ if (not isinstance(profile_name, str) or not profile_name.strip()
428
+ or '\x00' in profile_name or len(profile_name.encode('utf-8')) > 1024):
429
+ raise ValueError('Invalid security profile name')
430
+ before = await self.list_profiles(cluster)
431
+ matches = [row for row in before if row['name'] == profile_name]
432
+ if len(matches) != 1:
433
+ raise ValueError('Security profile not found or not unique')
434
+ if self.cluster_user:
435
+ await self._direct().remove_profile(cluster, profile_name, cluster_os_user=self.cluster_user)
436
+ else:
437
+ await self._direct().remove_profile(cluster, profile_name)
438
+ after = await self.list_profiles(cluster)
439
+ if after != [row for row in before if row['name'] != profile_name]:
440
+ raise RacError('Security profile list did not match after removal')
441
+
442
+ async def update_profile_directory(self, cluster_id, profile_name, alias, *, physical_path=None,
443
+ allowed_read=None, allowed_write=None, descr=None):
444
+ cluster = uuid.UUID(str(cluster_id))
445
+ if self.protocol == '10.0':
446
+ raise UnsupportedFeatureError('Profile directory update requires protocol 16.0')
447
+ for value in (profile_name, alias):
448
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
449
+ or len(value.encode('utf-8')) > 1024):
450
+ raise ValueError('Invalid profile or directory alias')
451
+ for key, value in (('physicalPath', physical_path), ('descr', descr)):
452
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
453
+ or len(value.encode('utf-8')) > 1024)):
454
+ raise ValueError('Invalid directory ' + key)
455
+ for key, value in (('allowedRead', allowed_read), ('allowedWrite', allowed_write)):
456
+ if value is not None and type(value) is not bool:
457
+ raise ValueError('Invalid directory ' + key)
458
+ before = await self.list_profile_directories(cluster, profile_name)
459
+ matches = [row for row in before if row['alias'] == alias]
460
+ if len(matches) > 1:
461
+ raise ValueError('Profile directory not unique')
462
+ expected = dict(matches[0]) if matches else {
463
+ 'profile': profile_name, 'alias': alias, 'physicalPath': '',
464
+ 'allowedRead': False, 'allowedWrite': False, 'descr': ''}
465
+ for key, value in (('physicalPath', physical_path), ('allowedRead', allowed_read),
466
+ ('allowedWrite', allowed_write), ('descr', descr)):
467
+ if value is not None:
468
+ expected[key] = value
469
+ if self.cluster_user:
470
+ await self._direct().update_profile_directory(cluster, expected,
471
+ cluster_os_user=self.cluster_user)
472
+ else:
473
+ await self._direct().update_profile_directory(cluster, expected)
474
+ after = await self.list_profile_directories(cluster, profile_name)
475
+ current = [row for row in after if row['alias'] == alias]
476
+ if current != [expected]:
477
+ raise RacError('Profile directory did not match after update')
478
+ expected_list = [expected if row['alias'] == alias else row for row in before]
479
+ if not matches:
480
+ expected_list.append(expected)
481
+ if after != expected_list:
482
+ raise RacError('Profile directory list did not match after update')
483
+ return current[0]
484
+
485
+ async def remove_profile_directory(self, cluster_id, profile_name, alias):
486
+ cluster = uuid.UUID(str(cluster_id))
487
+ if self.protocol == '10.0':
488
+ raise UnsupportedFeatureError('Profile directory removal requires protocol 16.0')
489
+ if (not isinstance(profile_name, str) or not isinstance(alias, str)
490
+ or not profile_name.strip() or not alias.strip()
491
+ or '\x00' in profile_name or '\x00' in alias
492
+ or len(profile_name.encode('utf-8')) > 1024
493
+ or len(alias.encode('utf-8')) > 1024):
494
+ raise ValueError('Invalid profile or directory alias')
495
+ before = await self.list_profile_directories(cluster, profile_name)
496
+ matches = [row for row in before if row['alias'] == alias]
497
+ if len(matches) != 1:
498
+ raise ValueError('Profile directory not found or not unique')
499
+ if self.cluster_user:
500
+ await self._direct().remove_profile_directory(cluster, profile_name, alias,
501
+ cluster_os_user=self.cluster_user)
502
+ else:
503
+ await self._direct().remove_profile_directory(cluster, profile_name, alias)
504
+ after = await self.list_profile_directories(cluster, profile_name)
505
+ if after != [row for row in before if row['alias'] != alias]:
506
+ raise RacError('Profile directory list did not match after removal')
507
+
508
+ async def list_profile_directories(self, cluster_id, profile_name):
509
+ cluster = uuid.UUID(str(cluster_id))
510
+ if self.protocol == '10.0':
511
+ raise UnsupportedFeatureError('1C 8.3.11 profile directories not exposed')
512
+ if self.cluster_password is not None:
513
+ return await self._direct(allow_password=True).list_profile_directories(
514
+ cluster, profile_name, cluster_os_user=self.cluster_user,
515
+ cluster_password=self.cluster_password)
516
+ if self.cluster_user:
517
+ return await self._direct().list_profile_directories(
518
+ cluster, profile_name, cluster_os_user=self.cluster_user)
519
+ return await self._direct().list_profile_directories(cluster, profile_name)
520
+
521
+ async def list_profile_com_classes(self, cluster_id, profile_name):
522
+ cluster = uuid.UUID(str(cluster_id))
523
+ if self.protocol == '10.0':
524
+ raise UnsupportedFeatureError('1C 8.3.11 profile COM ACL not exposed')
525
+ if self.cluster_password is not None:
526
+ return await self._direct(allow_password=True).list_profile_com_classes(
527
+ cluster, profile_name, cluster_os_user=self.cluster_user,
528
+ cluster_password=self.cluster_password)
529
+ if self.cluster_user:
530
+ return await self._direct().list_profile_com_classes(
531
+ cluster, profile_name, cluster_os_user=self.cluster_user)
532
+ return await self._direct().list_profile_com_classes(cluster, profile_name)
533
+
534
+ async def update_profile_com_class(self, cluster_id, profile_name, class_name, *, file_name=None,
535
+ class_id=None, host=None, descr=None):
536
+ cluster = uuid.UUID(str(cluster_id))
537
+ if self.protocol == '10.0':
538
+ raise UnsupportedFeatureError('Profile COM update requires protocol 16.0')
539
+ for value in (profile_name, class_name):
540
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
541
+ or len(value.encode('utf-8')) > 1024):
542
+ raise ValueError('Invalid profile or COM class name')
543
+ for key, value in (('fileName', file_name), ('id', class_id), ('host', host), ('descr', descr)):
544
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
545
+ or len(value.encode('utf-8')) > 1024)):
546
+ raise ValueError('Invalid COM ' + key)
547
+ before = await self.list_profile_com_classes(cluster, profile_name)
548
+ matches = [row for row in before if row['name'] == class_name]
549
+ if len(matches) > 1:
550
+ raise ValueError('Profile COM class not unique')
551
+ expected = dict(matches[0]) if matches else {
552
+ 'profile': profile_name, 'name': class_name,
553
+ 'fileName': '', 'id': '', 'host': '', 'descr': ''}
554
+ for key, value in (('fileName', file_name), ('id', class_id), ('host', host), ('descr', descr)):
555
+ if value is not None:
556
+ expected[key] = value
557
+ if self.cluster_user:
558
+ await self._direct().update_profile_com_class(cluster, expected,
559
+ cluster_os_user=self.cluster_user)
560
+ else:
561
+ await self._direct().update_profile_com_class(cluster, expected)
562
+ after = await self.list_profile_com_classes(cluster, profile_name)
563
+ current = [row for row in after if row['name'] == class_name]
564
+ if current != [expected]:
565
+ raise RacError('Profile COM class did not match after update')
566
+ expected_list = [expected if row['name'] == class_name else row for row in before]
567
+ if not matches:
568
+ expected_list.append(expected)
569
+ if after != expected_list:
570
+ raise RacError('Profile COM class list did not match after update')
571
+ return current[0]
572
+
573
+ async def remove_profile_com_class(self, cluster_id, profile_name, class_name):
574
+ cluster = uuid.UUID(str(cluster_id))
575
+ if self.protocol == '10.0':
576
+ raise UnsupportedFeatureError('Profile COM removal requires protocol 16.0')
577
+ if (not isinstance(profile_name, str) or not isinstance(class_name, str)
578
+ or not profile_name.strip() or not class_name.strip()
579
+ or '\x00' in profile_name or '\x00' in class_name
580
+ or len(profile_name.encode('utf-8')) > 1024
581
+ or len(class_name.encode('utf-8')) > 1024):
582
+ raise ValueError('Invalid profile or COM class name')
583
+ before = await self.list_profile_com_classes(cluster, profile_name)
584
+ matches = [row for row in before if row['name'] == class_name]
585
+ if len(matches) != 1:
586
+ raise ValueError('Profile COM class not found or not unique')
587
+ if self.cluster_user:
588
+ await self._direct().remove_profile_com_class(
589
+ cluster, profile_name, class_name, cluster_os_user=self.cluster_user)
590
+ else:
591
+ await self._direct().remove_profile_com_class(cluster, profile_name, class_name)
592
+ after = await self.list_profile_com_classes(cluster, profile_name)
593
+ if after != [row for row in before if row['name'] != class_name]:
594
+ raise RacError('Profile COM class list did not match after removal')
595
+
596
+ async def list_profile_addins(self, cluster_id, profile_name):
597
+ cluster = uuid.UUID(str(cluster_id))
598
+ if self.protocol == '10.0':
599
+ raise UnsupportedFeatureError('1C 8.3.11 profile addin ACL not exposed')
600
+ if self.cluster_password is not None:
601
+ return await self._direct(allow_password=True).list_profile_addins(
602
+ cluster, profile_name, cluster_os_user=self.cluster_user,
603
+ cluster_password=self.cluster_password)
604
+ if self.cluster_user:
605
+ return await self._direct().list_profile_addins(
606
+ cluster, profile_name, cluster_os_user=self.cluster_user)
607
+ return await self._direct().list_profile_addins(cluster, profile_name)
608
+
609
+ async def update_profile_addin(self, cluster_id, profile_name, addin_name, *, hash_value=None,
610
+ descr=None):
611
+ cluster = uuid.UUID(str(cluster_id))
612
+ if self.protocol == '10.0':
613
+ raise UnsupportedFeatureError('Profile addin update requires protocol 16.0')
614
+ for value in (profile_name, addin_name):
615
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
616
+ or len(value.encode('utf-8')) > 1024):
617
+ raise ValueError('Invalid profile or addin name')
618
+ for key, value in (('hash', hash_value), ('descr', descr)):
619
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
620
+ or len(value.encode('utf-8')) > 1024)):
621
+ raise ValueError('Invalid addin ' + key)
622
+ before = await self.list_profile_addins(cluster, profile_name)
623
+ matches = [row for row in before if row['name'] == addin_name]
624
+ if len(matches) > 1:
625
+ raise ValueError('Profile addin not unique')
626
+ expected = dict(matches[0]) if matches else {
627
+ 'profile': profile_name, 'name': addin_name, 'hash': '', 'descr': ''}
628
+ if hash_value is not None:
629
+ expected['hash'] = hash_value
630
+ if descr is not None:
631
+ expected['descr'] = descr
632
+ if self.cluster_user:
633
+ await self._direct().update_profile_addin(cluster, expected,
634
+ cluster_os_user=self.cluster_user)
635
+ else:
636
+ await self._direct().update_profile_addin(cluster, expected)
637
+ after = await self.list_profile_addins(cluster, profile_name)
638
+ current = [row for row in after if row['name'] == addin_name]
639
+ if current != [expected]:
640
+ raise RacError('Profile addin did not match after update')
641
+ expected_list = [expected if row['name'] == addin_name else row for row in before]
642
+ if not matches:
643
+ expected_list.append(expected)
644
+ if after != expected_list:
645
+ raise RacError('Profile addin list did not match after update')
646
+ return current[0]
647
+
648
+ async def remove_profile_addin(self, cluster_id, profile_name, addin_name):
649
+ cluster = uuid.UUID(str(cluster_id))
650
+ if self.protocol == '10.0':
651
+ raise UnsupportedFeatureError('Profile addin removal requires protocol 16.0')
652
+ if (not isinstance(profile_name, str) or not isinstance(addin_name, str)
653
+ or not profile_name.strip() or not addin_name.strip()
654
+ or '\x00' in profile_name or '\x00' in addin_name
655
+ or len(profile_name.encode('utf-8')) > 1024
656
+ or len(addin_name.encode('utf-8')) > 1024):
657
+ raise ValueError('Invalid profile or addin name')
658
+ before = await self.list_profile_addins(cluster, profile_name)
659
+ matches = [row for row in before if row['name'] == addin_name]
660
+ if len(matches) != 1:
661
+ raise ValueError('Profile addin not found or not unique')
662
+ if self.cluster_user:
663
+ await self._direct().remove_profile_addin(
664
+ cluster, profile_name, addin_name, cluster_os_user=self.cluster_user)
665
+ else:
666
+ await self._direct().remove_profile_addin(cluster, profile_name, addin_name)
667
+ after = await self.list_profile_addins(cluster, profile_name)
668
+ if after != [row for row in before if row['name'] != addin_name]:
669
+ raise RacError('Profile addin list did not match after removal')
670
+
671
+ async def list_profile_modules(self, cluster_id, profile_name):
672
+ cluster = uuid.UUID(str(cluster_id))
673
+ if self.protocol == '10.0':
674
+ raise UnsupportedFeatureError('1C 8.3.11 profile module ACL not exposed')
675
+ if self.cluster_password is not None:
676
+ return await self._direct(allow_password=True).list_profile_modules(
677
+ cluster, profile_name, cluster_os_user=self.cluster_user,
678
+ cluster_password=self.cluster_password)
679
+ if self.cluster_user:
680
+ return await self._direct().list_profile_modules(
681
+ cluster, profile_name, cluster_os_user=self.cluster_user)
682
+ return await self._direct().list_profile_modules(cluster, profile_name)
683
+
684
+ async def update_profile_module(self, cluster_id, profile_name, module_name, *, hash_value=None,
685
+ descr=None):
686
+ cluster = uuid.UUID(str(cluster_id))
687
+ if self.protocol == '10.0':
688
+ raise UnsupportedFeatureError('Profile module update requires protocol 16.0')
689
+ for value in (profile_name, module_name):
690
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
691
+ or len(value.encode('utf-8')) > 1024):
692
+ raise ValueError('Invalid profile or module name')
693
+ for key, value in (('hash', hash_value), ('descr', descr)):
694
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
695
+ or len(value.encode('utf-8')) > 1024)):
696
+ raise ValueError('Invalid module ' + key)
697
+ before = await self.list_profile_modules(cluster, profile_name)
698
+ matches = [row for row in before if row['name'] == module_name]
699
+ if len(matches) > 1:
700
+ raise ValueError('Profile module not unique')
701
+ expected = dict(matches[0]) if matches else {
702
+ 'profile': profile_name, 'name': module_name, 'hash': '', 'descr': ''}
703
+ if hash_value is not None:
704
+ expected['hash'] = hash_value
705
+ if descr is not None:
706
+ expected['descr'] = descr
707
+ if self.cluster_user:
708
+ await self._direct().update_profile_module(cluster, expected,
709
+ cluster_os_user=self.cluster_user)
710
+ else:
711
+ await self._direct().update_profile_module(cluster, expected)
712
+ after = await self.list_profile_modules(cluster, profile_name)
713
+ current = [row for row in after if row['name'] == module_name]
714
+ if current != [expected]:
715
+ raise RacError('Profile module did not match after update')
716
+ expected_list = [expected if row['name'] == module_name else row for row in before]
717
+ if not matches:
718
+ expected_list.append(expected)
719
+ if after != expected_list:
720
+ raise RacError('Profile module list did not match after update')
721
+ return current[0]
722
+
723
+ async def remove_profile_module(self, cluster_id, profile_name, module_name):
724
+ cluster = uuid.UUID(str(cluster_id))
725
+ if self.protocol == '10.0':
726
+ raise UnsupportedFeatureError('Profile module removal requires protocol 16.0')
727
+ if (not isinstance(profile_name, str) or not isinstance(module_name, str)
728
+ or not profile_name.strip() or not module_name.strip()
729
+ or '\x00' in profile_name or '\x00' in module_name
730
+ or len(profile_name.encode('utf-8')) > 1024
731
+ or len(module_name.encode('utf-8')) > 1024):
732
+ raise ValueError('Invalid profile or module name')
733
+ before = await self.list_profile_modules(cluster, profile_name)
734
+ matches = [row for row in before if row['name'] == module_name]
735
+ if len(matches) != 1:
736
+ raise ValueError('Profile module not found or not unique')
737
+ if self.cluster_user:
738
+ await self._direct().remove_profile_module(
739
+ cluster, profile_name, module_name, cluster_os_user=self.cluster_user)
740
+ else:
741
+ await self._direct().remove_profile_module(cluster, profile_name, module_name)
742
+ after = await self.list_profile_modules(cluster, profile_name)
743
+ if after != [row for row in before if row['name'] != module_name]:
744
+ raise RacError('Profile module list did not match after removal')
745
+
746
+ async def list_profile_apps(self, cluster_id, profile_name):
747
+ cluster = uuid.UUID(str(cluster_id))
748
+ if self.protocol == '10.0':
749
+ raise UnsupportedFeatureError('1C 8.3.11 profile app ACL not exposed')
750
+ if self.cluster_password is not None:
751
+ return await self._direct(allow_password=True).list_profile_apps(
752
+ cluster, profile_name, cluster_os_user=self.cluster_user,
753
+ cluster_password=self.cluster_password)
754
+ if self.cluster_user:
755
+ return await self._direct().list_profile_apps(
756
+ cluster, profile_name, cluster_os_user=self.cluster_user)
757
+ return await self._direct().list_profile_apps(cluster, profile_name)
758
+
759
+ async def update_profile_app(self, cluster_id, profile_name, app_name, *, wild=None, descr=None):
760
+ cluster = uuid.UUID(str(cluster_id))
761
+ if self.protocol == '10.0':
762
+ raise UnsupportedFeatureError('Profile application update requires protocol 16.0')
763
+ for value in (profile_name, app_name):
764
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
765
+ or len(value.encode('utf-8')) > 1024):
766
+ raise ValueError('Invalid profile or application name')
767
+ for key, value in (('wild', wild), ('descr', descr)):
768
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
769
+ or len(value.encode('utf-8')) > 1024)):
770
+ raise ValueError('Invalid application ' + key)
771
+ before = await self.list_profile_apps(cluster, profile_name)
772
+ matches = [row for row in before if row['name'] == app_name]
773
+ if len(matches) > 1:
774
+ raise ValueError('Profile application not unique')
775
+ expected = dict(matches[0]) if matches else {
776
+ 'profile': profile_name, 'name': app_name, 'wild': '', 'descr': ''}
777
+ if wild is not None:
778
+ expected['wild'] = wild
779
+ if descr is not None:
780
+ expected['descr'] = descr
781
+ if self.cluster_user:
782
+ await self._direct().update_profile_app(cluster, expected,
783
+ cluster_os_user=self.cluster_user)
784
+ else:
785
+ await self._direct().update_profile_app(cluster, expected)
786
+ after = await self.list_profile_apps(cluster, profile_name)
787
+ current = [row for row in after if row['name'] == app_name]
788
+ if current != [expected]:
789
+ raise RacError('Profile application did not match after update')
790
+ expected_list = [expected if row['name'] == app_name else row for row in before]
791
+ if not matches:
792
+ expected_list.append(expected)
793
+ if after != expected_list:
794
+ raise RacError('Profile application list did not match after update')
795
+ return current[0]
796
+
797
+ async def remove_profile_app(self, cluster_id, profile_name, app_name):
798
+ cluster = uuid.UUID(str(cluster_id))
799
+ if self.protocol == '10.0':
800
+ raise UnsupportedFeatureError('Profile app removal requires protocol 16.0')
801
+ if (not isinstance(profile_name, str) or not isinstance(app_name, str)
802
+ or not profile_name.strip() or not app_name.strip()
803
+ or '\x00' in profile_name or '\x00' in app_name
804
+ or len(profile_name.encode('utf-8')) > 1024
805
+ or len(app_name.encode('utf-8')) > 1024):
806
+ raise ValueError('Invalid profile or application name')
807
+ before = await self.list_profile_apps(cluster, profile_name)
808
+ matches = [row for row in before if row['name'] == app_name]
809
+ if len(matches) != 1:
810
+ raise ValueError('Profile application not found or not unique')
811
+ if self.cluster_user:
812
+ await self._direct().remove_profile_app(
813
+ cluster, profile_name, app_name, cluster_os_user=self.cluster_user)
814
+ else:
815
+ await self._direct().remove_profile_app(cluster, profile_name, app_name)
816
+ after = await self.list_profile_apps(cluster, profile_name)
817
+ if after != [row for row in before if row['name'] != app_name]:
818
+ raise RacError('Profile application list did not match after removal')
819
+
820
+ async def update_profile_inet(self, cluster_id, profile_name, inet_name, *, url=None,
821
+ port=None, protocol=None, descr=None):
822
+ cluster = uuid.UUID(str(cluster_id))
823
+ if self.protocol == '10.0':
824
+ raise UnsupportedFeatureError('Profile inet update requires protocol 16.0')
825
+ for value in (profile_name, inet_name):
826
+ if (not isinstance(value, str) or not value.strip() or '\x00' in value
827
+ or len(value.encode('utf-8')) > 1024):
828
+ raise ValueError('Invalid profile or inet name')
829
+ for key, value in (('url', url), ('protocol', protocol), ('descr', descr)):
830
+ if (value is not None and (not isinstance(value, str) or '\x00' in value
831
+ or len(value.encode('utf-8')) > 1024)):
832
+ raise ValueError('Invalid inet ' + key)
833
+ if port is not None and (type(port) is not int or not 0 <= port <= 65535):
834
+ raise ValueError('Invalid inet port')
835
+ before = await self.list_profile_inet(cluster, profile_name)
836
+ matches = [row for row in before if row['name'] == inet_name]
837
+ if len(matches) > 1:
838
+ raise ValueError('Profile inet not unique')
839
+ expected = dict(matches[0]) if matches else {
840
+ 'profile': profile_name, 'name': inet_name,
841
+ 'url': '', 'port': 0, 'protocol': '', 'descr': ''}
842
+ for key, value in (('url', url), ('port', port), ('protocol', protocol), ('descr', descr)):
843
+ if value is not None:
844
+ expected[key] = value
845
+ if self.cluster_user:
846
+ await self._direct().update_profile_inet(cluster, expected,
847
+ cluster_os_user=self.cluster_user)
848
+ else:
849
+ await self._direct().update_profile_inet(cluster, expected)
850
+ after = await self.list_profile_inet(cluster, profile_name)
851
+ current = [row for row in after if row['name'] == inet_name]
852
+ if current != [expected]:
853
+ raise RacError('Profile inet did not match after update')
854
+ expected_list = [expected if row['name'] == inet_name else row for row in before]
855
+ if not matches:
856
+ expected_list.append(expected)
857
+ if after != expected_list:
858
+ raise RacError('Profile inet list did not match after update')
859
+ return current[0]
860
+
861
+ async def remove_profile_inet(self, cluster_id, profile_name, inet_name):
862
+ cluster = uuid.UUID(str(cluster_id))
863
+ if self.protocol == '10.0':
864
+ raise UnsupportedFeatureError('Profile inet removal requires protocol 16.0')
865
+ if (not isinstance(profile_name, str) or not isinstance(inet_name, str)
866
+ or not profile_name.strip() or not inet_name.strip()
867
+ or '\x00' in profile_name or '\x00' in inet_name
868
+ or len(profile_name.encode('utf-8')) > 1024
869
+ or len(inet_name.encode('utf-8')) > 1024):
870
+ raise ValueError('Invalid profile or inet resource name')
871
+ before = await self.list_profile_inet(cluster, profile_name)
872
+ matches = [row for row in before if row['name'] == inet_name]
873
+ if len(matches) != 1:
874
+ raise ValueError('Profile inet resource not found or not unique')
875
+ if self.cluster_user:
876
+ await self._direct().remove_profile_inet(
877
+ cluster, profile_name, inet_name, cluster_os_user=self.cluster_user)
878
+ else:
879
+ await self._direct().remove_profile_inet(cluster, profile_name, inet_name)
880
+ after = await self.list_profile_inet(cluster, profile_name)
881
+ if after != [row for row in before if row['name'] != inet_name]:
882
+ raise RacError('Profile inet resource list did not match after removal')
883
+
884
+ async def list_profile_inet(self, cluster_id, profile_name):
885
+ cluster = uuid.UUID(str(cluster_id))
886
+ if self.protocol == '10.0':
887
+ raise UnsupportedFeatureError('1C 8.3.11 profile inet ACL not exposed')
888
+ if self.cluster_password is not None:
889
+ return await self._direct(allow_password=True).list_profile_inet(
890
+ cluster, profile_name, cluster_os_user=self.cluster_user,
891
+ cluster_password=self.cluster_password)
892
+ if self.cluster_user:
893
+ return await self._direct().list_profile_inet(
894
+ cluster, profile_name, cluster_os_user=self.cluster_user)
895
+ return await self._direct().list_profile_inet(cluster, profile_name)
896
+
897
+ async def insert_service_setting(self, cluster_id, server_id, service_name, *,
898
+ infobase_name='', service_data_dir=''):
899
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
900
+ if self.protocol == '10.0':
901
+ raise UnsupportedFeatureError('Service setting insert requires protocol 16.0')
902
+ for key, value in (('service name', service_name), ('infobase name', infobase_name),
903
+ ('service data dir', service_data_dir)):
904
+ if (not isinstance(value, str) or '\x00' in value
905
+ or len(value.encode('utf-8')) > 1024
906
+ or key == 'service name' and not value.strip()):
907
+ raise ValueError('Invalid ' + key)
908
+ before = await self.list_service_settings(cluster, server)
909
+ if any(row['service-name'] == service_name and row['infobase-name'] == infobase_name
910
+ for row in before):
911
+ raise ValueError('Service setting already exists for service and infobase')
912
+ if self.cluster_user:
913
+ created_id = await self._direct().insert_service_setting(
914
+ cluster, server, service_name, infobase_name, service_data_dir,
915
+ cluster_os_user=self.cluster_user)
916
+ else:
917
+ created_id = await self._direct().insert_service_setting(
918
+ cluster, server, service_name, infobase_name, service_data_dir)
919
+ after = await self.list_service_settings(cluster, server)
920
+ existing_ids = {row['setting'] for row in before}
921
+ new = [row for row in after if row['setting'] not in existing_ids]
922
+ # RAS normalizes directory paths; compare the actual row with the requested
923
+ # path after trimming only the trailing slash introduced by RAS.
924
+ if (len(new) != 1 or len(after) != len(before) + 1
925
+ or [row for row in after if row['setting'] in existing_ids] != before
926
+ or new[0]['setting'] != str(created_id)
927
+ or new[0]['service-name'] != service_name
928
+ or new[0]['infobase-name'] != infobase_name
929
+ or new[0]['service-data-dir'].rstrip('/') != service_data_dir.rstrip('/')
930
+ or new[0]['active'] is not False):
931
+ raise RacError('Service setting did not match after insert')
932
+ return new[0]
933
+
934
+ async def update_service_setting(self, cluster_id, server_id, setting_id, service_data_dir):
935
+ cluster, server, setting = (uuid.UUID(str(value)) for value in
936
+ (cluster_id, server_id, setting_id))
937
+ if self.protocol == '10.0':
938
+ raise UnsupportedFeatureError('Service setting update requires protocol 16.0')
939
+ if (not isinstance(service_data_dir, str) or '\x00' in service_data_dir
940
+ or len(service_data_dir.encode('utf-8')) > 1024):
941
+ raise ValueError('Invalid service data dir')
942
+ before = await self.list_service_settings(cluster, server)
943
+ matches = [row for row in before if row['setting'] == str(setting)]
944
+ if len(matches) != 1:
945
+ raise ValueError('Service setting not found or not unique')
946
+ desired = {**matches[0], 'service-data-dir': service_data_dir}
947
+ if self.cluster_user:
948
+ await self._direct().update_service_setting(cluster, server, desired,
949
+ cluster_os_user=self.cluster_user)
950
+ else:
951
+ await self._direct().update_service_setting(cluster, server, desired)
952
+ after = await self.list_service_settings(cluster, server)
953
+ expected = {**matches[0], 'service-data-dir': service_data_dir.rstrip('/') + '/'}
954
+ expected_list = [expected if row['setting'] == str(setting) else row
955
+ for row in before]
956
+ if after != expected_list:
957
+ raise RacError('Service setting did not match after update')
958
+ return expected
959
+
960
+ async def remove_service_setting(self, cluster_id, server_id, setting_id):
961
+ cluster, server, setting = (uuid.UUID(str(value)) for value in
962
+ (cluster_id, server_id, setting_id))
963
+ if self.protocol == '10.0':
964
+ raise UnsupportedFeatureError('Service setting remove requires protocol 16.0')
965
+ before = await self.list_service_settings(cluster, server)
966
+ matches = [row for row in before if row['setting'] == str(setting)]
967
+ if len(matches) != 1:
968
+ raise ValueError('Service setting not found or not unique')
969
+ if self.cluster_user:
970
+ await self._direct().remove_service_setting(cluster, server, setting,
971
+ cluster_os_user=self.cluster_user)
972
+ else:
973
+ await self._direct().remove_service_setting(cluster, server, setting)
974
+ after = await self.list_service_settings(cluster, server)
975
+ if after != [row for row in before if row['setting'] != str(setting)]:
976
+ raise RacError('Service setting did not match after remove')
977
+
978
+ async def apply_service_settings(self, cluster_id, server_id):
979
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
980
+ if self.protocol == '10.0':
981
+ raise UnsupportedFeatureError('Service setting apply requires protocol 16.0')
982
+ before = await self.list_service_settings(cluster, server)
983
+ if not any(not row['active'] for row in before):
984
+ raise ValueError('No pending service settings to apply')
985
+ if self.cluster_user:
986
+ await self._direct().apply_service_settings(cluster, server,
987
+ cluster_os_user=self.cluster_user)
988
+ else:
989
+ await self._direct().apply_service_settings(cluster, server)
990
+ after = await self.list_service_settings(cluster, server)
991
+ if after != [{**row, 'active': True} for row in before]:
992
+ raise RacError('Service settings did not match after apply')
993
+ return after
994
+
995
+ async def list_service_settings(self, cluster_id, server_id):
996
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
997
+ if self.protocol == '10.0':
998
+ raise UnsupportedFeatureError('1C 8.3.11 service settings not exposed')
999
+ if self.cluster_user:
1000
+ return await self._direct().list_service_settings(cluster, server, cluster_os_user=self.cluster_user)
1001
+ return await self._direct().list_service_settings(cluster, server)
1002
+
1003
+ async def get_service_setting(self, cluster_id, server_id, setting_id):
1004
+ cluster, server, setting = (uuid.UUID(str(value)) for value in
1005
+ (cluster_id, server_id, setting_id))
1006
+ if self.protocol == '10.0':
1007
+ raise UnsupportedFeatureError('1C 8.3.11 service setting info not exposed')
1008
+ if self.cluster_user:
1009
+ return await self._direct().get_service_setting(cluster, server, setting,
1010
+ cluster_os_user=self.cluster_user)
1011
+ return await self._direct().get_service_setting(cluster, server, setting)
1012
+
1013
+ async def list_service_data_dirs_for_transfer(self, cluster_id, server_id, service_name=''):
1014
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
1015
+ if self.protocol == '10.0':
1016
+ raise UnsupportedFeatureError('1C 8.3.11 service transfer directories not exposed')
1017
+ if self.cluster_user:
1018
+ return await self._direct().list_service_data_dirs_for_transfer(
1019
+ cluster, server, service_name, cluster_os_user=self.cluster_user)
1020
+ return await self._direct().list_service_data_dirs_for_transfer(cluster, server, service_name)
1021
+
1022
+ async def update_counter(self, cluster_id, name, *, collection_time=None, group=None,
1023
+ filter_type=None, filter=None, descr=None, **flags):
1024
+ cluster = uuid.UUID(str(cluster_id))
1025
+ if self.protocol == '10.0':
1026
+ raise UnsupportedFeatureError('Counter update requires protocol 16.0')
1027
+ flag_names = ('duration', 'cpu_time', 'memory', 'read', 'write',
1028
+ 'duration_dbms', 'dbms_bytes', 'service', 'call',
1029
+ 'number_of_active_sessions', 'number_of_sessions')
1030
+ if (not isinstance(name, str) or not name.strip() or '\x00' in name
1031
+ or len(name.encode('utf-8')) > 1024):
1032
+ raise ValueError('Invalid counter name')
1033
+ if collection_time is not None and (type(collection_time) is not int
1034
+ or not 0 <= collection_time < 2**63):
1035
+ raise ValueError('Invalid collection time')
1036
+ if group is not None and group not in ('users', 'data-separation'):
1037
+ raise ValueError('Invalid group')
1038
+ if filter_type is not None and filter_type not in ('all-selected', 'all-but-selected', 'all'):
1039
+ raise ValueError('Invalid filter type')
1040
+ for key, value in (('filter', filter), ('descr', descr)):
1041
+ if value is not None and (not isinstance(value, str) or '\x00' in value
1042
+ or len(value.encode('utf-8')) > 1024):
1043
+ raise ValueError('Invalid ' + key)
1044
+ if any(key not in flag_names or type(value) is not bool for key, value in flags.items()):
1045
+ raise ValueError('Invalid counter flag')
1046
+ before = await self.list_counters(cluster)
1047
+ matches = [row for row in before if row['name'] == name]
1048
+ if len(matches) > 1:
1049
+ raise ValueError('Counter not unique')
1050
+ if matches:
1051
+ expected = {key: value for key, value in matches[0].items()
1052
+ if key not in ('raw-group', 'raw-filter-type')}
1053
+ if all(value is None for value in (collection_time, group, filter_type, filter, descr)) and not flags:
1054
+ raise ValueError('Specify a counter field to update')
1055
+ else:
1056
+ if collection_time is None or group is None or filter_type is None or filter is None:
1057
+ raise ValueError('Counter creation requires collection time, group, filter type and filter')
1058
+ expected = {'name': name, 'collection-time': collection_time,
1059
+ 'group': group, 'filter-type': filter_type, 'filter': filter,
1060
+ 'descr': '', **{key.replace('_', '-'): False for key in flag_names}}
1061
+ for key, value in (('collection-time', collection_time), ('group', group),
1062
+ ('filter-type', filter_type), ('filter', filter), ('descr', descr)):
1063
+ if value is not None:
1064
+ expected[key] = value
1065
+ expected.update({key.replace('_', '-'): value for key, value in flags.items()})
1066
+ if self.cluster_user:
1067
+ await self._direct().update_counter(cluster, expected, cluster_os_user=self.cluster_user)
1068
+ else:
1069
+ await self._direct().update_counter(cluster, expected)
1070
+ after = await self.list_counters(cluster)
1071
+ current = [row for row in after if row['name'] == name]
1072
+ if len(current) != 1 or any(current[0].get(key) != value for key, value in expected.items()):
1073
+ raise RacError('Counter did not match after update')
1074
+ expected_list = [dict(row) for row in before]
1075
+ if matches:
1076
+ expected_list[before.index(matches[0])] = current[0]
1077
+ else:
1078
+ expected_list.append(current[0])
1079
+ if after != expected_list:
1080
+ raise RacError('Counter list did not match after update')
1081
+ return current[0]
1082
+
1083
+ async def remove_counter(self, cluster_id, name):
1084
+ cluster = uuid.UUID(str(cluster_id))
1085
+ if self.protocol == '10.0':
1086
+ raise UnsupportedFeatureError('Counter removal requires protocol 16.0')
1087
+ if (not isinstance(name, str) or not name.strip() or '\x00' in name
1088
+ or len(name.encode('utf-8')) > 1024):
1089
+ raise ValueError('Invalid counter name')
1090
+ before = await self.list_counters(cluster)
1091
+ matches = [row for row in before if row['name'] == name]
1092
+ if len(matches) != 1:
1093
+ raise ValueError('Counter not found or not unique')
1094
+ if self.cluster_user:
1095
+ await self._direct().remove_counter(cluster, name, cluster_os_user=self.cluster_user)
1096
+ else:
1097
+ await self._direct().remove_counter(cluster, name)
1098
+ after = await self.list_counters(cluster)
1099
+ if after != [row for row in before if row['name'] != name]:
1100
+ raise RacError('Counter list did not match after removal')
1101
+
1102
+ async def clear_counter(self, cluster_id, name, object_filter=''):
1103
+ cluster = uuid.UUID(str(cluster_id))
1104
+ if self.protocol == '10.0':
1105
+ raise UnsupportedFeatureError('Counter clear requires protocol 16.0')
1106
+ if (not isinstance(name, str) or not name.strip() or '\x00' in name
1107
+ or len(name.encode('utf-8')) > 1024):
1108
+ raise ValueError('Invalid counter name')
1109
+ if (not isinstance(object_filter, str) or '\x00' in object_filter
1110
+ or len(object_filter.encode('utf-8')) > 4096):
1111
+ raise ValueError('Invalid counter object filter')
1112
+ before = await self.list_counters(cluster)
1113
+ matches = [row for row in before if row['name'] == name]
1114
+ if len(matches) != 1:
1115
+ raise ValueError('Counter not found or not unique')
1116
+ if self.cluster_user:
1117
+ await self._direct().clear_counter(cluster, name, object_filter,
1118
+ cluster_os_user=self.cluster_user)
1119
+ else:
1120
+ await self._direct().clear_counter(cluster, name, object_filter)
1121
+ after = await self.list_counters(cluster)
1122
+ if after != before:
1123
+ raise RacError('Counter configuration changed after clear')
1124
+ # Readings are not guaranteed to exist and the populated 16.0 values
1125
+ # decoder is not live-verified; do not claim clearing them via read-back.
1126
+ return matches[0]
1127
+
1128
+ async def list_counters(self, cluster_id):
1129
+ cluster = uuid.UUID(str(cluster_id))
1130
+ if self.protocol == '10.0':
1131
+ raise UnsupportedFeatureError('1C 8.3.11 counters not exposed')
1132
+ if self.cluster_password is not None:
1133
+ return await self._direct(allow_password=True).list_counters(
1134
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1135
+ if self.cluster_user:
1136
+ return await self._direct().list_counters(cluster, cluster_os_user=self.cluster_user)
1137
+ return await self._direct().list_counters(cluster)
1138
+
1139
+ async def get_counter(self, cluster_id, name):
1140
+ cluster = uuid.UUID(str(cluster_id))
1141
+ if self.protocol == '10.0':
1142
+ raise UnsupportedFeatureError('1C 8.3.11 counter info not exposed')
1143
+ if self.cluster_user:
1144
+ return await self._direct().get_counter(cluster, name, cluster_os_user=self.cluster_user)
1145
+ return await self._direct().get_counter(cluster, name)
1146
+
1147
+ async def list_counter_values(self, cluster_id, counter, object_filter=''):
1148
+ cluster = uuid.UUID(str(cluster_id))
1149
+ if self.protocol == '10.0':
1150
+ raise UnsupportedFeatureError('1C 8.3.11 counter values not exposed')
1151
+ if self.cluster_user:
1152
+ return await self._direct().list_counter_values(
1153
+ cluster, counter, object_filter, cluster_os_user=self.cluster_user)
1154
+ return await self._direct().list_counter_values(cluster, counter, object_filter)
1155
+
1156
+ async def list_accumulated_counter_values(self, cluster_id, counter, object_filter=''):
1157
+ cluster = uuid.UUID(str(cluster_id))
1158
+ if self.protocol == '10.0':
1159
+ raise UnsupportedFeatureError('1C 8.3.11 accumulated counter values not exposed')
1160
+ if self.cluster_user:
1161
+ return await self._direct().list_accumulated_counter_values(
1162
+ cluster, counter, object_filter, cluster_os_user=self.cluster_user)
1163
+ return await self._direct().list_accumulated_counter_values(cluster, counter, object_filter)
1164
+
1165
+ async def update_limit(self, cluster_id, name, *, action=None, counter=None,
1166
+ error_message=None, descr=None, **thresholds):
1167
+ cluster = uuid.UUID(str(cluster_id))
1168
+ if self.protocol == '10.0':
1169
+ raise UnsupportedFeatureError('Limit update requires protocol 16.0')
1170
+ actions = ('none', 'set-low-priority-thread', 'interrupt-current-call',
1171
+ 'interrupt-session')
1172
+ names = ('duration', 'cpu_time', 'duration_dbms', 'duration_service',
1173
+ 'memory', 'read', 'write', 'dbms_bytes', 'calls',
1174
+ 'active_sessions', 'sessions')
1175
+ if (not isinstance(name, str) or not name.strip() or '\x00' in name
1176
+ or len(name.encode('utf-8')) > 1024):
1177
+ raise ValueError('Invalid limit name')
1178
+ if action is not None and action not in actions:
1179
+ raise ValueError('Invalid limit action')
1180
+ for key, value in (('counter', counter), ('error-message', error_message), ('descr', descr)):
1181
+ if value is not None and (not isinstance(value, str) or '\x00' in value
1182
+ or len(value.encode('utf-8')) > 1024):
1183
+ raise ValueError('Invalid limit ' + key)
1184
+ if any(key not in names or type(value) is not int or not 0 <= value < 2**63
1185
+ for key, value in thresholds.items()):
1186
+ raise ValueError('Invalid limit threshold')
1187
+ before = await self.list_limits(cluster)
1188
+ matches = [row for row in before if row['name'] == name]
1189
+ if len(matches) > 1:
1190
+ raise ValueError('Limit not unique')
1191
+ if matches:
1192
+ if all(value is None for value in (action, counter, error_message, descr)) and not thresholds:
1193
+ raise ValueError('Specify a limit field to update')
1194
+ expected = {key: value for key, value in matches[0].items() if key != 'raw-action'}
1195
+ else:
1196
+ if action is None or counter is None:
1197
+ raise ValueError('Limit creation requires action and counter')
1198
+ expected = {'name': name, 'action': action, 'counter': counter,
1199
+ 'error-message': '', 'descr': '',
1200
+ **{key.replace('_', '-'): 0 for key in names}}
1201
+ for key, value in (('action', action), ('counter', counter),
1202
+ ('error-message', error_message), ('descr', descr)):
1203
+ if value is not None:
1204
+ expected[key] = value
1205
+ expected.update({key.replace('_', '-'): value for key, value in thresholds.items()})
1206
+ if self.cluster_user:
1207
+ await self._direct().update_limit(cluster, expected, cluster_os_user=self.cluster_user)
1208
+ else:
1209
+ await self._direct().update_limit(cluster, expected)
1210
+ after = await self.list_limits(cluster)
1211
+ current = [row for row in after if row['name'] == name]
1212
+ if len(current) != 1 or any(current[0].get(key) != value for key, value in expected.items()):
1213
+ raise RacError('Limit did not match after update')
1214
+ expected_list = [dict(row) for row in before]
1215
+ if matches:
1216
+ expected_list[before.index(matches[0])] = current[0]
1217
+ else:
1218
+ expected_list.append(current[0])
1219
+ if after != expected_list:
1220
+ raise RacError('Limit list did not match after update')
1221
+ return current[0]
1222
+
1223
+ async def remove_limit(self, cluster_id, name):
1224
+ cluster = uuid.UUID(str(cluster_id))
1225
+ if self.protocol == '10.0':
1226
+ raise UnsupportedFeatureError('Limit removal requires protocol 16.0')
1227
+ if (not isinstance(name, str) or not name.strip() or '\x00' in name
1228
+ or len(name.encode('utf-8')) > 1024):
1229
+ raise ValueError('Invalid limit name')
1230
+ before = await self.list_limits(cluster)
1231
+ matches = [row for row in before if row['name'] == name]
1232
+ if len(matches) != 1:
1233
+ raise ValueError('Limit not found or not unique')
1234
+ if self.cluster_user:
1235
+ await self._direct().remove_limit(cluster, name, cluster_os_user=self.cluster_user)
1236
+ else:
1237
+ await self._direct().remove_limit(cluster, name)
1238
+ after = await self.list_limits(cluster)
1239
+ if after != [row for row in before if row['name'] != name]:
1240
+ raise RacError('Limit list did not match after removal')
1241
+
1242
+ async def list_limits(self, cluster_id):
1243
+ cluster = uuid.UUID(str(cluster_id))
1244
+ if self.protocol == '10.0':
1245
+ raise UnsupportedFeatureError('1C 8.3.11 limits not exposed')
1246
+ if self.cluster_password is not None:
1247
+ return await self._direct(allow_password=True).list_limits(
1248
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1249
+ if self.cluster_user:
1250
+ return await self._direct().list_limits(cluster, cluster_os_user=self.cluster_user)
1251
+ return await self._direct().list_limits(cluster)
1252
+
1253
+ async def get_limit(self, cluster_id, name):
1254
+ cluster = uuid.UUID(str(cluster_id))
1255
+ if self.protocol == '10.0':
1256
+ raise UnsupportedFeatureError('1C 8.3.11 limit info not exposed')
1257
+ if self.cluster_user:
1258
+ return await self._direct().get_limit(cluster, name, cluster_os_user=self.cluster_user)
1259
+ return await self._direct().get_limit(cluster, name)
1260
+
1261
+ async def list_managers(self, cluster_id):
1262
+ cluster = uuid.UUID(str(cluster_id))
1263
+ if self.protocol == '10.0':
1264
+ raise UnsupportedFeatureError('1C 8.3.11 manager list is not exposed')
1265
+ if self.cluster_password is not None:
1266
+ return await self._direct(allow_password=True).list_managers(
1267
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1268
+ if self.cluster_user:
1269
+ return await self._direct().list_managers(cluster, cluster_os_user=self.cluster_user)
1270
+ return await self._direct().list_managers(cluster)
1271
+
1272
+ async def get_manager(self, cluster_id, manager_id):
1273
+ cluster = uuid.UUID(str(cluster_id))
1274
+ manager = uuid.UUID(str(manager_id))
1275
+ if self.protocol == '10.0':
1276
+ raise UnsupportedFeatureError('1C 8.3.11 manager info is not exposed')
1277
+ if self.cluster_password is not None:
1278
+ return await self._direct(allow_password=True).get_manager(
1279
+ cluster, manager, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1280
+ if self.cluster_user:
1281
+ return await self._direct().get_manager(cluster, manager, cluster_os_user=self.cluster_user)
1282
+ return await self._direct().get_manager(cluster, manager)
1283
+
1284
+ async def list_services(self, cluster_id):
1285
+ cluster = uuid.UUID(str(cluster_id))
1286
+ if self.protocol == '10.0':
1287
+ raise UnsupportedFeatureError('1C 8.3.11 service list is not exposed')
1288
+ if self.cluster_password is not None:
1289
+ return await self._direct(allow_password=True).list_services(
1290
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1291
+ if self.cluster_user:
1292
+ return await self._direct().list_services(cluster, cluster_os_user=self.cluster_user)
1293
+ return await self._direct().list_services(cluster)
1294
+
1295
+ async def list_clusters(self):
1296
+ if self.protocol == '10.0':
1297
+ return await self._v8311('cluster', 'list')
1298
+ return await self._direct().list_clusters()
1299
+
1300
+ async def get_cluster(self, cluster_id):
1301
+ cluster = uuid.UUID(str(cluster_id))
1302
+ if self.protocol == '10.0':
1303
+ rows = await self._v8311('cluster', 'info', cluster)
1304
+ return rows[0]
1305
+ return await self._direct().cluster_info(cluster)
1306
+
1307
+ async def list_servers(self, cluster_id):
1308
+ cluster = uuid.UUID(str(cluster_id))
1309
+ if self.protocol == '10.0':
1310
+ raise UnsupportedFeatureError('1C 8.3.11 server list is not exposed by the existing client')
1311
+ if self.cluster_password is not None:
1312
+ return await self._direct(allow_password=True).list_servers(
1313
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1314
+ if self.cluster_user:
1315
+ return await self._direct().list_servers(cluster, cluster_os_user=self.cluster_user)
1316
+ return await self._direct().list_servers(cluster)
1317
+
1318
+ async def get_server(self, cluster_id, server_id):
1319
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
1320
+ if self.protocol == '10.0':
1321
+ raise UnsupportedFeatureError('1C 8.3.11 server info is not exposed by the library')
1322
+ if self.cluster_password is not None:
1323
+ return await self._direct(allow_password=True).get_server(
1324
+ cluster, server, cluster_os_user=self.cluster_user,
1325
+ cluster_password=self.cluster_password)
1326
+ if self.cluster_user:
1327
+ return await self._direct().get_server(cluster, server, cluster_os_user=self.cluster_user)
1328
+ return await self._direct().get_server(cluster, server)
1329
+
1330
+ async def list_processes(self, cluster_id, server_id=None):
1331
+ cluster = uuid.UUID(str(cluster_id))
1332
+ if self.protocol == '10.0':
1333
+ raise UnsupportedFeatureError('1C 8.3.11 process decoding is not implemented')
1334
+ server = uuid.UUID(str(server_id)) if server_id is not None else None
1335
+ if self.cluster_password is not None:
1336
+ return await self._direct(allow_password=True).list_processes(
1337
+ cluster, server, cluster_os_user=self.cluster_user,
1338
+ cluster_password=self.cluster_password)
1339
+ if self.cluster_user:
1340
+ return await self._direct().list_processes(cluster, server, cluster_os_user=self.cluster_user)
1341
+ return await self._direct().list_processes(cluster, server)
1342
+
1343
+ async def turn_off_process(self, cluster_id, process_id, *, verify_timeout=10.0,
1344
+ poll_interval=0.2):
1345
+ """Request shutdown of an existing process and verify its disappearance.
1346
+
1347
+ A server ACK alone may also acknowledge an absent target. A timeout
1348
+ means the result is unknown; it must not be reported as success.
1349
+ """
1350
+ cluster, process = uuid.UUID(str(cluster_id)), uuid.UUID(str(process_id))
1351
+ if self.protocol != '16.0':
1352
+ raise UnsupportedFeatureError('Process turn-off requires protocol 16.0')
1353
+ for value in (verify_timeout, poll_interval):
1354
+ if type(value) not in (int, float) or not 0 <= value <= 60:
1355
+ raise ValueError('Invalid process verification budget')
1356
+ direct = self._direct()
1357
+ if self.cluster_user:
1358
+ raise UnsupportedFeatureError('OS-authenticated process turn-off is not verified')
1359
+ before = await self.list_processes(cluster)
1360
+ if sum(row['process'] == str(process) for row in before) != 1:
1361
+ raise ValueError('Process not found or not unique')
1362
+ await direct.turn_off_process(cluster, process)
1363
+ deadline = time.monotonic() + verify_timeout
1364
+ while True:
1365
+ after = await self.list_processes(cluster)
1366
+ if all(row['process'] != str(process) for row in after):
1367
+ return after
1368
+ if time.monotonic() >= deadline:
1369
+ raise RacError('Process turn-off not verified before timeout')
1370
+ await asyncio.sleep(min(poll_interval, max(0, deadline - time.monotonic())))
1371
+
1372
+ async def get_process(self, cluster_id, process_id):
1373
+ cluster, process = uuid.UUID(str(cluster_id)), uuid.UUID(str(process_id))
1374
+ if self.protocol == '10.0':
1375
+ raise UnsupportedFeatureError('1C 8.3.11 process info is not exposed')
1376
+ if self.cluster_password is not None:
1377
+ return await self._direct(allow_password=True).get_process(
1378
+ cluster, process, cluster_os_user=self.cluster_user,
1379
+ cluster_password=self.cluster_password)
1380
+ if self.cluster_user:
1381
+ return await self._direct().get_process(cluster, process, cluster_os_user=self.cluster_user)
1382
+ return await self._direct().get_process(cluster, process)
1383
+
1384
+ async def list_connections(self, cluster_id, infobase_id=None, *, process_id=None):
1385
+ cluster = uuid.UUID(str(cluster_id))
1386
+ infobase = uuid.UUID(str(infobase_id)) if infobase_id is not None else None
1387
+ process = uuid.UUID(str(process_id)) if process_id is not None else None
1388
+ if self.protocol == '10.0':
1389
+ raise UnsupportedFeatureError('1C 8.3.11 connections not exposed')
1390
+ if self.cluster_password is not None:
1391
+ rows = await self._direct(allow_password=True).list_connections(
1392
+ cluster, infobase, cluster_os_user=self.cluster_user,
1393
+ cluster_password=self.cluster_password)
1394
+ elif self.cluster_user:
1395
+ rows = await self._direct().list_connections(cluster, infobase, cluster_os_user=self.cluster_user)
1396
+ else:
1397
+ rows = await self._direct().list_connections(cluster, infobase)
1398
+ # No verified 16.0 process-scoped request without an infobase ID.
1399
+ # Filter the decoded cluster/infobase list locally rather than guess a wire ID.
1400
+ if process is None:
1401
+ return rows
1402
+ matching = []
1403
+ for row in rows:
1404
+ try:
1405
+ if uuid.UUID(str(row['process'])) == process:
1406
+ matching.append(row)
1407
+ except (KeyError, TypeError, ValueError):
1408
+ continue
1409
+ return matching
1410
+
1411
+ async def get_connection(self, cluster_id, connection_id):
1412
+ cluster, connection = uuid.UUID(str(cluster_id)), uuid.UUID(str(connection_id))
1413
+ if self.protocol == '10.0':
1414
+ raise UnsupportedFeatureError('1C 8.3.11 connection info not exposed')
1415
+ if self.cluster_password is not None:
1416
+ return await self._direct(allow_password=True).get_connection(
1417
+ cluster, connection, cluster_os_user=self.cluster_user,
1418
+ cluster_password=self.cluster_password)
1419
+ if self.cluster_user:
1420
+ return await self._direct().get_connection(cluster, connection, cluster_os_user=self.cluster_user)
1421
+ return await self._direct().get_connection(cluster, connection)
1422
+
1423
+ async def list_locks(self, cluster_id, *, infobase_id=None, connection_id=None, session_id=None):
1424
+ cluster = uuid.UUID(str(cluster_id))
1425
+ if self.protocol == '10.0':
1426
+ raise UnsupportedFeatureError('1C 8.3.11 locks not exposed')
1427
+ if self.cluster_password is not None:
1428
+ return await self._direct(allow_password=True).list_locks(
1429
+ cluster, infobase_id, connection_id, session_id,
1430
+ cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1431
+ if self.cluster_user:
1432
+ return await self._direct().list_locks(cluster, infobase_id, connection_id, session_id,
1433
+ cluster_os_user=self.cluster_user)
1434
+ return await self._direct().list_locks(cluster, infobase_id, connection_id, session_id)
1435
+
1436
+ async def insert_rule(self, cluster_id, server_id, position, *, object_type='',
1437
+ infobase_name='', rule_type='auto', application_ext='', priority=0):
1438
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
1439
+ if self.protocol == '10.0':
1440
+ raise UnsupportedFeatureError('Assignment rule insert requires protocol 16.0')
1441
+ if type(position) is not int or not 0 <= position < 2**31:
1442
+ raise ValueError('Invalid rule position')
1443
+ if type(priority) is not int or not -(2**31) <= priority < 2**31:
1444
+ raise ValueError('Invalid rule priority')
1445
+ if rule_type not in ('never', 'auto', 'always'):
1446
+ raise ValueError('Invalid rule type')
1447
+ for value in (object_type, infobase_name, application_ext):
1448
+ if not isinstance(value, str) or '\x00' in value or len(value.encode('utf-8')) > 1024:
1449
+ raise ValueError('Invalid rule field')
1450
+ before = await self.list_rules(cluster, server)
1451
+ if position > len(before):
1452
+ raise ValueError('Rule position outside current list')
1453
+ if self.cluster_user:
1454
+ ident = await self._direct().insert_rule(
1455
+ cluster, server, position, object_type=object_type, infobase_name=infobase_name,
1456
+ rule_type=rule_type, application_ext=application_ext, priority=priority,
1457
+ cluster_os_user=self.cluster_user)
1458
+ else:
1459
+ ident = await self._direct().insert_rule(
1460
+ cluster, server, position, object_type=object_type, infobase_name=infobase_name,
1461
+ rule_type=rule_type, application_ext=application_ext, priority=priority)
1462
+ after = await self.list_rules(cluster, server)
1463
+ expected = {'rule': str(ident), 'object-type': object_type,
1464
+ 'infobase-name': infobase_name,
1465
+ 'raw-rule-type': {'never': 0, 'auto': 1, 'always': 2}[rule_type],
1466
+ 'rule-type': rule_type, 'application-ext': application_ext,
1467
+ 'priority': priority}
1468
+ if (len(after) != len(before) + 1 or after[position] != expected
1469
+ or after[:position] + after[position+1:] != before):
1470
+ raise RacError('Assignment rule did not match after insert')
1471
+ return expected
1472
+
1473
+ async def update_rule(self, cluster_id, server_id, rule_id, position, *,
1474
+ object_type=None, infobase_name=None, rule_type=None,
1475
+ application_ext=None, priority=None):
1476
+ cluster, server, rule = (uuid.UUID(str(value)) for value in
1477
+ (cluster_id, server_id, rule_id))
1478
+ if self.protocol == '10.0':
1479
+ raise UnsupportedFeatureError('Assignment rule update requires protocol 16.0')
1480
+ if type(position) is not int or not 0 <= position < 2**31:
1481
+ raise ValueError('Invalid rule position')
1482
+ if priority is not None and (type(priority) is not int or not -(2**31) <= priority < 2**31):
1483
+ raise ValueError('Invalid rule priority')
1484
+ if rule_type is not None and rule_type not in ('never', 'auto', 'always'):
1485
+ raise ValueError('Invalid rule type')
1486
+ for value in (object_type, infobase_name, application_ext):
1487
+ if value is not None and (not isinstance(value, str) or '\x00' in value
1488
+ or len(value.encode('utf-8')) > 1024):
1489
+ raise ValueError('Invalid rule field')
1490
+ before = await self.list_rules(cluster, server)
1491
+ matches = [row for row in before if row['rule'] == str(rule)]
1492
+ if len(matches) != 1 or position >= len(before):
1493
+ raise ValueError('Assignment rule not found, not unique or invalid position')
1494
+ desired = dict(matches[0])
1495
+ for key, value in (('object-type', object_type), ('infobase-name', infobase_name),
1496
+ ('rule-type', rule_type), ('application-ext', application_ext),
1497
+ ('priority', priority)):
1498
+ if value is not None:
1499
+ desired[key] = value
1500
+ desired['raw-rule-type'] = {'never': 0, 'auto': 1, 'always': 2}[desired['rule-type']]
1501
+ if self.cluster_user:
1502
+ await self._direct().update_rule(cluster, server, position, desired,
1503
+ cluster_os_user=self.cluster_user)
1504
+ else:
1505
+ await self._direct().update_rule(cluster, server, position, desired)
1506
+ after = await self.list_rules(cluster, server)
1507
+ remaining = [row for row in before if row['rule'] != str(rule)]
1508
+ if (len(after) != len(before) or after[position] != desired
1509
+ or after[:position] + after[position+1:] != remaining):
1510
+ raise RacError('Assignment rule did not match after update')
1511
+ return desired
1512
+
1513
+ async def apply_rules(self, cluster_id, *, full=True):
1514
+ cluster = uuid.UUID(str(cluster_id))
1515
+ if self.protocol == '10.0':
1516
+ raise UnsupportedFeatureError('Assignment rule apply requires protocol 16.0')
1517
+ if type(full) is not bool:
1518
+ raise ValueError('Invalid assignment rule apply mode')
1519
+ servers = await self.list_servers(cluster)
1520
+ before = {row['server']: await self.list_rules(cluster, row['server']) for row in servers}
1521
+ if not any(before.values()):
1522
+ raise ValueError('No assignment rules to apply')
1523
+ if self.cluster_user:
1524
+ await self._direct().apply_rules(cluster, full=full, cluster_os_user=self.cluster_user)
1525
+ else:
1526
+ await self._direct().apply_rules(cluster, full=full)
1527
+ after = {row['server']: await self.list_rules(cluster, row['server']) for row in servers}
1528
+ if after != before:
1529
+ raise RacError('Assignment rules changed after apply')
1530
+ # The active allocation policy is not exposed by list/info: this checks
1531
+ # the acknowledgement and unchanged rules, not effective placement.
1532
+ return after
1533
+
1534
+ async def remove_rule(self, cluster_id, server_id, rule_id):
1535
+ cluster, server, rule = (uuid.UUID(str(value)) for value in
1536
+ (cluster_id, server_id, rule_id))
1537
+ if self.protocol == '10.0':
1538
+ raise UnsupportedFeatureError('Assignment rule remove requires protocol 16.0')
1539
+ before = await self.list_rules(cluster, server)
1540
+ matches = [row for row in before if row['rule'] == str(rule)]
1541
+ if len(matches) != 1:
1542
+ raise ValueError('Assignment rule not found or not unique')
1543
+ if self.cluster_user:
1544
+ await self._direct().remove_rule(cluster, server, rule, cluster_os_user=self.cluster_user)
1545
+ else:
1546
+ await self._direct().remove_rule(cluster, server, rule)
1547
+ after = await self.list_rules(cluster, server)
1548
+ remaining = [row for row in before if row['rule'] != str(rule)]
1549
+ if after != remaining:
1550
+ raise RacError('Assignment rule did not match after remove')
1551
+
1552
+ async def list_rules(self, cluster_id, server_id):
1553
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
1554
+ if self.protocol == '10.0':
1555
+ raise UnsupportedFeatureError('1C 8.3.11 assignment rules not exposed')
1556
+ if self.cluster_user:
1557
+ return await self._direct().list_rules(cluster, server, cluster_os_user=self.cluster_user)
1558
+ return await self._direct().list_rules(cluster, server)
1559
+
1560
+ async def get_rule(self, cluster_id, server_id, rule_id):
1561
+ cluster, server, rule = (uuid.UUID(str(item)) for item in (cluster_id, server_id, rule_id))
1562
+ if self.protocol == '10.0':
1563
+ raise UnsupportedFeatureError('1C 8.3.11 assignment rule info not exposed')
1564
+ if self.cluster_user:
1565
+ return await self._direct().get_rule(cluster, server, rule, cluster_os_user=self.cluster_user)
1566
+ return await self._direct().get_rule(cluster, server, rule)
1567
+
1568
+ async def list_binary_storages(self, cluster_id, infobase_id):
1569
+ cluster = uuid.UUID(str(cluster_id))
1570
+ infobase = uuid.UUID(str(infobase_id))
1571
+ if self.protocol == '10.0':
1572
+ raise UnsupportedFeatureError('Binary data storage list requires protocol 16.0')
1573
+ direct = self._direct() # Reject unverified password authentication before wire I/O.
1574
+ if self.cluster_user:
1575
+ return await direct.list_binary_storages(
1576
+ cluster, infobase, cluster_os_user=self.cluster_user)
1577
+ return await direct.list_binary_storages(cluster, infobase)
1578
+
1579
+ async def create_infobase(self, cluster_id, *, name, db_name, db_server, db_user, locale,
1580
+ create_database, descr='', scheduled_jobs_denied=False,
1581
+ license_distribution='deny', date_offset=0, security_level=0):
1582
+ if type(security_level) is not int or security_level not in (0, 1):
1583
+ raise ValueError('Invalid security_level')
1584
+ if type(date_offset) is not int or date_offset not in (0, 2000):
1585
+ raise ValueError('Invalid date_offset')
1586
+ if license_distribution not in ('allow', 'deny') or type(license_distribution) is not str:
1587
+ raise ValueError('Invalid license_distribution')
1588
+ if type(scheduled_jobs_denied) is not bool:
1589
+ raise ValueError('Invalid scheduled_jobs_denied')
1590
+ if self.protocol == '10.0':
1591
+ raise UnsupportedFeatureError('Infobase create requires protocol 16.0')
1592
+ if type(create_database) is not bool:
1593
+ raise ValueError('Explicit boolean create_database policy required')
1594
+ for label, value in (('name', name), ('db_name', db_name), ('db_server', db_server),
1595
+ ('db_user', db_user), ('locale', locale)):
1596
+ if (not isinstance(value, str) or not value or '\x00' in value
1597
+ or len(value.encode('utf-8')) > 255):
1598
+ raise ValueError('Invalid infobase ' + label)
1599
+ if not isinstance(descr, str) or '\x00' in descr or len(descr.encode('utf-8')) > 1024:
1600
+ raise ValueError('Invalid infobase description')
1601
+ cluster = uuid.UUID(str(cluster_id))
1602
+ direct = self._direct()
1603
+ before = await self.list_infobases(cluster)
1604
+ if any(row['name'] == name for row in before):
1605
+ raise ValueError('Infobase name already exists')
1606
+ create_options = {}
1607
+ if scheduled_jobs_denied:
1608
+ create_options['scheduled_jobs_denied'] = True
1609
+ if license_distribution != 'deny':
1610
+ create_options['license_distribution'] = license_distribution
1611
+ if date_offset:
1612
+ create_options['date_offset'] = date_offset
1613
+ if security_level:
1614
+ create_options['security_level'] = security_level
1615
+ ident = await direct.create_infobase(cluster, name=name, db_name=db_name,
1616
+ db_server=db_server, db_user=db_user, locale=locale,
1617
+ create_database=create_database, descr=descr, **create_options)
1618
+ after = await self.list_infobases(cluster)
1619
+ expected = {'infobase': str(ident), 'name': name, 'descr': descr}
1620
+ if after != before + [expected]:
1621
+ raise RacError('Infobase list did not match after create')
1622
+ full = await self.get_infobase_info(cluster, ident)
1623
+ verified = (('infobase', str(ident)), ('name', name), ('descr', descr),
1624
+ ('db_name', db_name), ('db_server_name', db_server),
1625
+ ('db_user', db_user), ('dbms', 'PostgreSQL'), ('locale', locale))
1626
+ if (any(full.get(key) != value for key, value in verified)
1627
+ or full.get('scheduled_jobs_denied') is not scheduled_jobs_denied
1628
+ or full.get('license_distribution') != {'allow': 1, 'deny': 0}[license_distribution]
1629
+ or full.get('date_offset') != date_offset
1630
+ or full.get('security_level') != security_level):
1631
+ raise RacError('Infobase info did not match after create')
1632
+ return full
1633
+
1634
+ async def drop_infobase(self, cluster_id, infobase_id, *, drop_database,
1635
+ clear_database=False):
1636
+ if self.protocol == '10.0':
1637
+ raise UnsupportedFeatureError('Infobase drop requires protocol 16.0')
1638
+ if type(drop_database) is not bool or type(clear_database) is not bool:
1639
+ raise ValueError('Explicit boolean drop_database/clear_database policy required')
1640
+ if drop_database and clear_database:
1641
+ raise ValueError('Conflicting infobase database policy')
1642
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1643
+ direct = self._direct()
1644
+ before = await self.list_infobases(cluster)
1645
+ matches = [row for row in before if row['infobase'] == str(ident)]
1646
+ if len(matches) != 1:
1647
+ raise ValueError('Infobase not found or not unique')
1648
+ drop_options = {'clear_database': True} if clear_database else {}
1649
+ if self.cluster_user:
1650
+ await direct.drop_infobase(cluster, ident, drop_database=drop_database,
1651
+ cluster_os_user=self.cluster_user, **drop_options)
1652
+ else:
1653
+ await direct.drop_infobase(cluster, ident, drop_database=drop_database,
1654
+ **drop_options)
1655
+ after = await self.list_infobases(cluster)
1656
+ if after != [row for row in before if row['infobase'] != str(ident)]:
1657
+ raise RacError('Infobase list did not match after drop')
1658
+ return after
1659
+
1660
+ async def update_infobase_safe_mode_security_profile(self, cluster_id, infobase_id, name):
1661
+ if self.protocol == '10.0':
1662
+ raise UnsupportedFeatureError('Infobase safe mode security profile update requires protocol 16.0')
1663
+ if (not isinstance(name, str) or '\x00' in name or len(name.encode('utf-8')) > 1024):
1664
+ raise ValueError('Invalid security profile name')
1665
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1666
+ direct = self._direct()
1667
+ before_list = await self.list_infobases(cluster)
1668
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1669
+ raise ValueError('Infobase not found or not unique')
1670
+ before = await self.get_infobase_info(cluster, ident)
1671
+ await direct.update_infobase_safe_mode_security_profile(cluster, ident, name)
1672
+ after = await self.get_infobase_info(cluster, ident)
1673
+ after_list = await self.list_infobases(cluster)
1674
+ if after != before | {'safe_mode_securirty_profile': name} or after_list != before_list:
1675
+ raise RacError('Infobase safe mode security profile did not match after update')
1676
+ return after
1677
+
1678
+ async def update_infobase_security_profile(self, cluster_id, infobase_id, name):
1679
+ if self.protocol == '10.0':
1680
+ raise UnsupportedFeatureError('Infobase security profile update requires protocol 16.0')
1681
+ if (not isinstance(name, str) or '\x00' in name or len(name.encode('utf-8')) > 1024):
1682
+ raise ValueError('Invalid security profile name')
1683
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1684
+ direct = self._direct()
1685
+ before_list = await self.list_infobases(cluster)
1686
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1687
+ raise ValueError('Infobase not found or not unique')
1688
+ before = await self.get_infobase_info(cluster, ident)
1689
+ await direct.update_infobase_security_profile(cluster, ident, name)
1690
+ after = await self.get_infobase_info(cluster, ident)
1691
+ after_list = await self.list_infobases(cluster)
1692
+ if after != before | {'securirty_profile': name} or after_list != before_list:
1693
+ raise RacError('Infobase security profile did not match after update')
1694
+ return after
1695
+
1696
+ async def update_infobase_maximum_scheduled_jobs_start_shift(self, cluster_id, infobase_id, shift):
1697
+ if self.protocol == '10.0':
1698
+ raise UnsupportedFeatureError('Maximum scheduled jobs shift update requires protocol 16.0')
1699
+ if type(shift) is not int or not 0 <= shift <= 255:
1700
+ raise ValueError('Invalid maximum scheduled jobs start shift')
1701
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1702
+ direct = self._direct()
1703
+ before_list = await self.list_infobases(cluster)
1704
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1705
+ raise ValueError('Infobase not found or not unique')
1706
+ before = await self.get_infobase_info(cluster, ident)
1707
+ await direct.update_infobase_maximum_scheduled_jobs_start_shift(cluster, ident, shift)
1708
+ after = await self.get_infobase_info(cluster, ident)
1709
+ after_list = await self.list_infobases(cluster)
1710
+ key = 'maximum_scheduled_jobs_start_shift_without_active_users'
1711
+ if after != before | {key: shift} or after_list != before_list:
1712
+ raise RacError('Maximum scheduled jobs shift did not match after update')
1713
+ return after
1714
+
1715
+ async def update_infobase_minimum_scheduled_jobs_start_period(self, cluster_id, infobase_id, period):
1716
+ if self.protocol == '10.0':
1717
+ raise UnsupportedFeatureError('Minimum scheduled jobs period update requires protocol 16.0')
1718
+ if type(period) is not int or not 0 <= period <= 0xffffffff:
1719
+ raise ValueError('Invalid minimum scheduled jobs start period')
1720
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1721
+ direct = self._direct()
1722
+ before_list = await self.list_infobases(cluster)
1723
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1724
+ raise ValueError('Infobase not found or not unique')
1725
+ before = await self.get_infobase_info(cluster, ident)
1726
+ await direct.update_infobase_minimum_scheduled_jobs_start_period(cluster, ident, period)
1727
+ after = await self.get_infobase_info(cluster, ident)
1728
+ after_list = await self.list_infobases(cluster)
1729
+ key = 'minimum_scheduled_jobs_start_period_without_active_users'
1730
+ if after != before | {key: period} or after_list != before_list:
1731
+ raise RacError('Minimum scheduled jobs period did not match after update')
1732
+ return after
1733
+
1734
+ async def update_infobase_configuration_unload_delay(self, cluster_id, infobase_id, delay):
1735
+ if self.protocol == '10.0':
1736
+ raise UnsupportedFeatureError('Configuration unload delay update requires protocol 16.0')
1737
+ if type(delay) is not int or not 0 <= delay <= 0xffffffff:
1738
+ raise ValueError('Invalid configuration unload delay')
1739
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1740
+ direct = self._direct()
1741
+ before_list = await self.list_infobases(cluster)
1742
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1743
+ raise ValueError('Infobase not found or not unique')
1744
+ before = await self.get_infobase_info(cluster, ident)
1745
+ await direct.update_infobase_configuration_unload_delay(cluster, ident, delay)
1746
+ after = await self.get_infobase_info(cluster, ident)
1747
+ after_list = await self.list_infobases(cluster)
1748
+ key = 'configuration_unload_delay_by_working_process_without_active_users'
1749
+ if after != before | {key: delay} or after_list != before_list:
1750
+ raise RacError('Configuration unload delay did not match after update')
1751
+ return after
1752
+
1753
+ async def update_infobase_disable_local_speech_to_text(self, cluster_id, infobase_id, disabled):
1754
+ if self.protocol == '10.0':
1755
+ raise UnsupportedFeatureError('Local speech-to-text update requires protocol 16.0')
1756
+ if type(disabled) is not bool:
1757
+ raise ValueError('Disable local speech to text must be boolean')
1758
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1759
+ direct = self._direct()
1760
+ before_list = await self.list_infobases(cluster)
1761
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1762
+ raise ValueError('Infobase not found or not unique')
1763
+ before = await self.get_infobase_info(cluster, ident)
1764
+ await direct.update_infobase_disable_local_speech_to_text(cluster, ident, disabled)
1765
+ after = await self.get_infobase_info(cluster, ident)
1766
+ after_list = await self.list_infobases(cluster)
1767
+ if after != before | {'disable_local_speech_to_text': disabled} or after_list != before_list:
1768
+ raise RacError('Local speech-to-text flag did not match after update')
1769
+ return after
1770
+
1771
+ async def update_infobase_external_session_manager_required(self, cluster_id, infobase_id, required):
1772
+ if self.protocol == '10.0':
1773
+ raise UnsupportedFeatureError('External session manager update requires protocol 16.0')
1774
+ if type(required) is not bool:
1775
+ raise ValueError('External session manager required must be boolean')
1776
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1777
+ direct = self._direct()
1778
+ before_list = await self.list_infobases(cluster)
1779
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1780
+ raise ValueError('Infobase not found or not unique')
1781
+ before = await self.get_infobase_info(cluster, ident)
1782
+ await direct.update_infobase_external_session_manager_required(cluster, ident, required)
1783
+ after = await self.get_infobase_info(cluster, ident)
1784
+ after_list = await self.list_infobases(cluster)
1785
+ if after != before | {'external_session_manager_required': required} or after_list != before_list:
1786
+ raise RacError('External session manager required did not match after update')
1787
+ return after
1788
+
1789
+ async def update_infobase_reserve_working_processes(self, cluster_id, infobase_id, reserve):
1790
+ if self.protocol == '10.0':
1791
+ raise UnsupportedFeatureError('Infobase reserve working processes update requires protocol 16.0')
1792
+ if type(reserve) is not bool:
1793
+ raise ValueError('Reserve working processes must be boolean')
1794
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1795
+ direct = self._direct()
1796
+ before_list = await self.list_infobases(cluster)
1797
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1798
+ raise ValueError('Infobase not found or not unique')
1799
+ before = await self.get_infobase_info(cluster, ident)
1800
+ await direct.update_infobase_reserve_working_processes(cluster, ident, reserve)
1801
+ after = await self.get_infobase_info(cluster, ident)
1802
+ after_list = await self.list_infobases(cluster)
1803
+ if after != before | {'reserve_working_processes': reserve} or after_list != before_list:
1804
+ raise RacError('Infobase reserve working processes did not match after update')
1805
+ return after
1806
+
1807
+ async def update_infobase_denied_message(self, cluster_id, infobase_id, message):
1808
+ if self.protocol == '10.0':
1809
+ raise UnsupportedFeatureError('Infobase denied message update requires protocol 16.0')
1810
+ if not isinstance(message, str) or '\x00' in message:
1811
+ raise ValueError('Invalid denied message')
1812
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1813
+ direct = self._direct()
1814
+ before_list = await self.list_infobases(cluster)
1815
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1816
+ raise ValueError('Infobase not found or not unique')
1817
+ before = await self.get_infobase_info(cluster, ident)
1818
+ await direct.update_infobase_denied_message(cluster, ident, message)
1819
+ after = await self.get_infobase_info(cluster, ident)
1820
+ after_list = await self.list_infobases(cluster)
1821
+ if after != before | {'denied_message': message} or after_list != before_list:
1822
+ raise RacError('Infobase denied message did not match after update')
1823
+ return after
1824
+
1825
+ async def update_infobase_license_distribution(self, cluster_id, infobase_id, distribution):
1826
+ if self.protocol == '10.0':
1827
+ raise UnsupportedFeatureError('Infobase license distribution update requires protocol 16.0')
1828
+ if distribution not in ('allow', 'deny'):
1829
+ raise ValueError('Invalid license distribution')
1830
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1831
+ direct = self._direct()
1832
+ before_list = await self.list_infobases(cluster)
1833
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1834
+ raise ValueError('Infobase not found or not unique')
1835
+ before = await self.get_infobase_info(cluster, ident)
1836
+ await direct.update_infobase_license_distribution(cluster, ident, distribution)
1837
+ after = await self.get_infobase_info(cluster, ident)
1838
+ after_list = await self.list_infobases(cluster)
1839
+ expected = 1 if distribution == 'allow' else 0
1840
+ if after != before | {'license_distribution': expected} or after_list != before_list:
1841
+ raise RacError('Infobase license distribution did not match after update')
1842
+ return after
1843
+
1844
+ async def update_infobase_sessions_deny(self, cluster_id, infobase_id, denied):
1845
+ if self.protocol == '10.0':
1846
+ raise UnsupportedFeatureError('Infobase sessions deny update requires protocol 16.0')
1847
+ if type(denied) is not bool:
1848
+ raise ValueError('Sessions deny must be boolean')
1849
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1850
+ direct = self._direct()
1851
+ before_list = await self.list_infobases(cluster)
1852
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1853
+ raise ValueError('Infobase not found or not unique')
1854
+ before = await self.get_infobase_info(cluster, ident)
1855
+ await direct.update_infobase_sessions_deny(cluster, ident, denied)
1856
+ after = await self.get_infobase_info(cluster, ident)
1857
+ after_list = await self.list_infobases(cluster)
1858
+ if after != before | {'sessions_denied': denied} or after_list != before_list:
1859
+ raise RacError('Infobase sessions deny did not match after update')
1860
+ return after
1861
+
1862
+ async def update_infobase_scheduled_jobs_deny(self, cluster_id, infobase_id, denied):
1863
+ if self.protocol == '10.0':
1864
+ raise UnsupportedFeatureError('Infobase scheduled jobs update requires protocol 16.0')
1865
+ if type(denied) is not bool:
1866
+ raise ValueError('Scheduled jobs deny must be boolean')
1867
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1868
+ direct = self._direct()
1869
+ before_list = await self.list_infobases(cluster)
1870
+ if len([row for row in before_list if row['infobase'] == str(ident)]) != 1:
1871
+ raise ValueError('Infobase not found or not unique')
1872
+ before = await self.get_infobase_info(cluster, ident)
1873
+ await direct.update_infobase_scheduled_jobs_deny(cluster, ident, denied)
1874
+ after = await self.get_infobase_info(cluster, ident)
1875
+ after_list = await self.list_infobases(cluster)
1876
+ if after != before | {'scheduled_jobs_denied': denied} or after_list != before_list:
1877
+ raise RacError('Infobase scheduled jobs deny did not match after update')
1878
+ return after
1879
+
1880
+ async def update_infobase_description(self, cluster_id, infobase_id, description):
1881
+ if self.protocol == '10.0':
1882
+ raise UnsupportedFeatureError('Infobase full update requires protocol 16.0')
1883
+ if (not isinstance(description, str) or '\x00' in description
1884
+ or len(description.encode('utf-8')) > 1024):
1885
+ raise ValueError('Invalid infobase description')
1886
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1887
+ direct = self._direct()
1888
+ before_list = await self.list_infobases(cluster)
1889
+ matches = [row for row in before_list if row['infobase'] == str(ident)]
1890
+ if len(matches) != 1:
1891
+ raise ValueError('Infobase not found or not unique')
1892
+ before = await self.get_infobase_info(cluster, ident)
1893
+ await direct.update_infobase_description(cluster, ident, description)
1894
+ after = await self.get_infobase_info(cluster, ident)
1895
+ after_list = await self.list_infobases(cluster)
1896
+ expected_list = [row | {'descr': description} if row['infobase'] == str(ident) else row
1897
+ for row in before_list]
1898
+ if after != before | {'descr': description} or after_list != expected_list:
1899
+ raise RacError('Infobase information did not match after update')
1900
+ return after
1901
+
1902
+ async def update_infobase_summary_description(self, cluster_id, infobase_id, description):
1903
+ if self.protocol == '10.0':
1904
+ raise UnsupportedFeatureError('Infobase summary update requires protocol 16.0')
1905
+ cluster, ident = uuid.UUID(str(cluster_id)), uuid.UUID(str(infobase_id))
1906
+ if (not isinstance(description, str) or '\x00' in description
1907
+ or len(description.encode('utf-8')) > 1024):
1908
+ raise ValueError('Invalid infobase summary description')
1909
+ direct = self._direct()
1910
+ before = await self.list_infobases(cluster)
1911
+ matches = [row for row in before if row['infobase'] == str(ident)]
1912
+ if len(matches) != 1:
1913
+ raise ValueError('Infobase not found or not unique')
1914
+ if self.cluster_user:
1915
+ await direct.update_infobase_summary_description(
1916
+ cluster, ident, description, cluster_os_user=self.cluster_user)
1917
+ else:
1918
+ await direct.update_infobase_summary_description(cluster, ident, description)
1919
+ after = await self.list_infobases(cluster)
1920
+ expected = [row | {'descr': description} if row['infobase'] == str(ident) else row
1921
+ for row in before]
1922
+ if after != expected:
1923
+ raise RacError('Infobase summary description did not change as expected')
1924
+ return next(row for row in after if row['infobase'] == str(ident))
1925
+
1926
+ async def list_infobases(self, cluster_id):
1927
+ cluster = uuid.UUID(str(cluster_id))
1928
+ if self.protocol == '10.0':
1929
+ return await self._v8311('infobase', 'summary', cluster, subcommand2='list')
1930
+ if self.cluster_password is not None:
1931
+ return await self._direct(allow_password=True).list_infobases(
1932
+ cluster, cluster_os_user=self.cluster_user, cluster_password=self.cluster_password)
1933
+ if self.cluster_user:
1934
+ return await self._direct().list_infobases(cluster, cluster_os_user=self.cluster_user)
1935
+ return await self._direct().list_infobases(cluster)
1936
+
1937
+ async def get_infobase_info(self, cluster_id, infobase_id=None, *, name=None):
1938
+ cluster = uuid.UUID(str(cluster_id))
1939
+ if (infobase_id is None) == (name is None):
1940
+ raise ValueError('Specify exactly one of infobase_id or name')
1941
+ if name is not None and (not isinstance(name, str) or not name.strip() or '\x00' in name or len(name.encode('utf-8')) > 1024):
1942
+ raise ValueError('Invalid infobase name')
1943
+ if self.protocol == '10.0':
1944
+ raise UnsupportedFeatureError('Infobase full info requires protocol 16.0')
1945
+ if name is not None:
1946
+ matches = [row for row in await self.list_infobases(cluster) if row['name'] == name]
1947
+ if len(matches) != 1:
1948
+ raise ValueError('Infobase name is not unique or not found')
1949
+ infobase_id = matches[0]['infobase']
1950
+ ident = uuid.UUID(str(infobase_id))
1951
+ direct = self._direct()
1952
+ if self.cluster_user:
1953
+ row = await direct.get_infobase_info(cluster, ident, cluster_os_user=self.cluster_user)
1954
+ else:
1955
+ row = await direct.get_infobase_info(cluster, ident)
1956
+ if name is not None and (row['infobase'] != str(ident) or row['name'] != name):
1957
+ raise ValueError('Infobase name changed during lookup')
1958
+ return row
1959
+
1960
+ async def get_infobase_summary(self, cluster_id, infobase_id=None, *, name=None):
1961
+ cluster = uuid.UUID(str(cluster_id))
1962
+ if (infobase_id is None) == (name is None):
1963
+ raise ValueError('Specify exactly one of infobase_id or name')
1964
+ if self.protocol == '10.0':
1965
+ raise UnsupportedFeatureError('1C 8.3.11 infobase summary info not exposed')
1966
+ if name is not None:
1967
+ if not isinstance(name, str) or not name.strip() or '\x00' in name or len(name.encode('utf-8')) > 1024:
1968
+ raise ValueError('Invalid infobase name')
1969
+ matches = [row for row in await self.list_infobases(cluster) if row['name'] == name]
1970
+ if len(matches) != 1:
1971
+ raise ValueError('Infobase name is not unique or not found')
1972
+ infobase_id = matches[0]['infobase']
1973
+ ident = uuid.UUID(str(infobase_id))
1974
+ if self.cluster_password is not None:
1975
+ row = await self._direct(allow_password=True).get_infobase_summary(
1976
+ cluster, ident, cluster_os_user=self.cluster_user,
1977
+ cluster_password=self.cluster_password)
1978
+ elif self.cluster_user:
1979
+ row = await self._direct().get_infobase_summary(
1980
+ cluster, ident, cluster_os_user=self.cluster_user)
1981
+ else:
1982
+ row = await self._direct().get_infobase_summary(cluster, ident)
1983
+ if name is not None and (row['infobase'] != str(ident) or row['name'] != name):
1984
+ raise ValueError('Infobase name changed during lookup')
1985
+ return row
1986
+
1987
+ @staticmethod
1988
+ def _validate_schedule(schedule):
1989
+ if not isinstance(schedule, str) or len(schedule) > 256 or not re.fullmatch(r'[0-9 ,/\-]*', schedule):
1990
+ raise ValueError('Invalid restart schedule')
1991
+
1992
+ async def remove_cluster(self, cluster_id):
1993
+ cluster = uuid.UUID(str(cluster_id))
1994
+ if self.protocol == '10.0':
1995
+ raise UnsupportedFeatureError('cluster remove requires protocol 16.0')
1996
+ before = await self.list_clusters()
1997
+ matches = [row for row in before if row['cluster'] == str(cluster)]
1998
+ if len(matches) != 1:
1999
+ raise ValueError('Cluster not found or not unique')
2000
+ if self.cluster_user:
2001
+ await self._direct().remove_cluster(cluster, cluster_os_user=self.cluster_user)
2002
+ else:
2003
+ await self._direct().remove_cluster(cluster)
2004
+ after = await self.list_clusters()
2005
+ if after != [row for row in before if row['cluster'] != str(cluster)]:
2006
+ raise RacError('Cluster list did not match after remove')
2007
+ return after
2008
+
2009
+ async def insert_cluster(self, host, port, name='', *, expiration_timeout=60, security_level=0,
2010
+ session_fault_tolerance_level=0, load_balancing_mode='performance',
2011
+ kill_problem_processes=True, kill_by_memory_with_dump=False,
2012
+ restart_schedule='', ping_period=0, ping_timeout=0, audit=False):
2013
+ if self.protocol == '10.0':
2014
+ raise UnsupportedFeatureError('cluster insert requires protocol 16.0')
2015
+ if (not isinstance(host, str) or not host or '\x00' in host
2016
+ or len(host.encode('utf-8')) > 255 or type(port) is not int or not 1 <= port <= 65535
2017
+ or not isinstance(name, str) or '\x00' in name
2018
+ or len(name.encode('utf-8')) > 256):
2019
+ raise ValueError('Invalid cluster insert parameters')
2020
+ if type(expiration_timeout) is not int or not 0 <= expiration_timeout < 2**31:
2021
+ raise ValueError('Invalid cluster expiration timeout')
2022
+ if type(security_level) is not int or not 0 <= security_level < 2**31:
2023
+ raise ValueError('Invalid cluster security level')
2024
+ if (type(session_fault_tolerance_level) is not int
2025
+ or not 0 <= session_fault_tolerance_level < 2**31):
2026
+ raise ValueError('Invalid cluster session fault tolerance level')
2027
+ if load_balancing_mode not in ('performance', 'memory') or type(load_balancing_mode) is not str:
2028
+ raise ValueError('Invalid cluster load balancing mode')
2029
+ if type(kill_problem_processes) is not bool:
2030
+ raise ValueError('Invalid cluster kill problem processes setting')
2031
+ if type(kill_by_memory_with_dump) is not bool:
2032
+ raise ValueError('Invalid cluster kill by memory with dump setting')
2033
+ if type(audit) is not bool:
2034
+ raise ValueError('Invalid cluster audit flag')
2035
+ if (not isinstance(restart_schedule, str)
2036
+ or (restart_schedule and (len(restart_schedule.encode('utf-8')) > 256
2037
+ or len(restart_schedule.split()) != 6
2038
+ or any(not part.isascii() or not part.isdecimal()
2039
+ for part in restart_schedule.split())))
2040
+ or '\x00' in restart_schedule):
2041
+ raise ValueError('Invalid cluster restart schedule')
2042
+ if (type(ping_period) is not int or type(ping_timeout) is not int
2043
+ or not ((ping_period == ping_timeout == 0)
2044
+ or (0 < ping_period < ping_timeout < 2**31))):
2045
+ raise ValueError('Invalid cluster ping period/timeout pair')
2046
+ before = await self.list_clusters()
2047
+ direct_insert = self._direct().insert_cluster
2048
+ async def insert(*args, **kwargs):
2049
+ if self.agent_user:
2050
+ kwargs['agent_os_user'] = self.agent_user
2051
+ if audit:
2052
+ kwargs['audit'] = audit
2053
+ return await direct_insert(*args, **kwargs)
2054
+ defaults = {'expiration_timeout': 60, 'security_level': 0,
2055
+ 'session_fault_tolerance_level': 0, 'load_balancing_mode': 'performance',
2056
+ 'kill_problem_processes': True, 'kill_by_memory_with_dump': False,
2057
+ 'restart_schedule': '', 'ping_period': 0, 'ping_timeout': 0}
2058
+ requested = {'expiration_timeout': expiration_timeout, 'security_level': security_level,
2059
+ 'session_fault_tolerance_level': session_fault_tolerance_level,
2060
+ 'load_balancing_mode': load_balancing_mode,
2061
+ 'kill_problem_processes': kill_problem_processes,
2062
+ 'kill_by_memory_with_dump': kill_by_memory_with_dump,
2063
+ 'restart_schedule': restart_schedule, 'ping_period': ping_period,
2064
+ 'ping_timeout': ping_timeout}
2065
+ options = {key: value for key, value in requested.items() if value != defaults[key]}
2066
+ ident = await insert(host, port, name, **options)
2067
+ after = await self.list_clusters()
2068
+ matches = [row for row in after if row['cluster'] == str(ident)]
2069
+ if (len(after) != len(before) + 1 or len(matches) != 1
2070
+ or [row for row in after if row['cluster'] != str(ident)] != before
2071
+ or matches[0] != await self.get_cluster(ident)
2072
+ or any(matches[0][key] != value for key, value in
2073
+ (('host', host), ('port', port), ('name', name),
2074
+ ('expiration-timeout', expiration_timeout), ('security-level', security_level),
2075
+ ('session-fault-tolerance-level', session_fault_tolerance_level),
2076
+ ('load-balancing-mode', load_balancing_mode),
2077
+ ('kill-problem-processes', int(kill_problem_processes)),
2078
+ ('kill-by-memory-with-dump', int(kill_by_memory_with_dump)),
2079
+ ('restart-schedule', restart_schedule),
2080
+ ('ping-period', ping_period), ('ping-timeout', ping_timeout)))
2081
+ or matches[0].get('allow-access-right-audit-events-recording') != int(audit)):
2082
+ raise RacError('Cluster did not match after insert')
2083
+ return matches[0]
2084
+
2085
+ async def update_cluster_audit(self, cluster_id, enabled):
2086
+ cluster = uuid.UUID(str(cluster_id))
2087
+ if self.protocol == '10.0':
2088
+ raise UnsupportedFeatureError('cluster audit update requires protocol 16.0')
2089
+ if type(enabled) is not bool:
2090
+ raise ValueError('Invalid cluster audit flag')
2091
+ before = await self.get_cluster(cluster)
2092
+ if self.agent_user:
2093
+ await self._direct().update_cluster_audit(cluster, enabled, agent_os_user=self.agent_user)
2094
+ else:
2095
+ await self._direct().update_cluster_audit(cluster, enabled)
2096
+ result = await self.get_cluster(cluster)
2097
+ if result != {**before, 'allow-access-right-audit-events-recording': int(enabled)}:
2098
+ raise RacError('Cluster audit flag did not match after update')
2099
+ return result
2100
+
2101
+ async def update_cluster_ping(self, cluster_id, period, timeout):
2102
+ cluster = uuid.UUID(str(cluster_id))
2103
+ if self.protocol == '10.0':
2104
+ raise UnsupportedFeatureError('cluster ping update requires protocol 16.0')
2105
+ if (type(period) is not int or type(timeout) is not int or
2106
+ not ((period == 0 and timeout == 0) or (0 < period < timeout <= 2_147_483_647))):
2107
+ raise ValueError('Invalid cluster ping period/timeout pair')
2108
+ before = await self.get_cluster(cluster)
2109
+ if self.agent_user:
2110
+ await self._direct().update_cluster_ping(cluster, period, timeout, agent_os_user=self.agent_user)
2111
+ else:
2112
+ await self._direct().update_cluster_ping(cluster, period, timeout)
2113
+ result = await self.get_cluster(cluster)
2114
+ if result != {**before, 'ping-period': period, 'ping-timeout': timeout}:
2115
+ raise RacError('Cluster ping period/timeout did not match after update')
2116
+ return result
2117
+
2118
+ async def update_cluster_kill_by_memory_with_dump(self, cluster_id, enabled):
2119
+ cluster = uuid.UUID(str(cluster_id))
2120
+ if self.protocol == '10.0':
2121
+ raise UnsupportedFeatureError('cluster kill by memory dump update requires protocol 16.0')
2122
+ if type(enabled) is not bool:
2123
+ raise ValueError('Invalid kill by memory dump flag')
2124
+ before = await self.get_cluster(cluster)
2125
+ if self.agent_user:
2126
+ await self._direct().update_cluster_kill_by_memory_with_dump(cluster, enabled,
2127
+ agent_os_user=self.agent_user)
2128
+ else:
2129
+ await self._direct().update_cluster_kill_by_memory_with_dump(cluster, enabled)
2130
+ result = await self.get_cluster(cluster)
2131
+ if result != {**before, 'kill-by-memory-with-dump': int(enabled)}:
2132
+ raise RacError('Cluster kill by memory dump did not match after update')
2133
+ return result
2134
+
2135
+ async def update_cluster_kill_problem_processes(self, cluster_id, enabled):
2136
+ cluster = uuid.UUID(str(cluster_id))
2137
+ if self.protocol == '10.0':
2138
+ raise UnsupportedFeatureError('cluster kill problem processes update requires protocol 16.0')
2139
+ if type(enabled) is not bool:
2140
+ raise ValueError('Invalid kill problem processes flag')
2141
+ before = await self.get_cluster(cluster)
2142
+ if self.agent_user:
2143
+ await self._direct().update_cluster_kill_problem_processes(cluster, enabled,
2144
+ agent_os_user=self.agent_user)
2145
+ else:
2146
+ await self._direct().update_cluster_kill_problem_processes(cluster, enabled)
2147
+ result = await self.get_cluster(cluster)
2148
+ if result != {**before, 'kill-problem-processes': int(enabled)}:
2149
+ raise RacError('Cluster kill problem processes did not match after update')
2150
+ return result
2151
+
2152
+ async def update_cluster_load_balancing_mode(self, cluster_id, mode):
2153
+ cluster = uuid.UUID(str(cluster_id))
2154
+ if self.protocol == '10.0':
2155
+ raise UnsupportedFeatureError('cluster load balancing update requires protocol 16.0')
2156
+ if mode not in ('performance', 'memory'):
2157
+ raise ValueError('Invalid cluster load balancing mode')
2158
+ before = await self.get_cluster(cluster)
2159
+ if self.agent_user:
2160
+ await self._direct().update_cluster_load_balancing_mode(cluster, mode, agent_os_user=self.agent_user)
2161
+ else:
2162
+ await self._direct().update_cluster_load_balancing_mode(cluster, mode)
2163
+ result = await self.get_cluster(cluster)
2164
+ if result != {**before, 'load-balancing-mode': mode}:
2165
+ raise RacError('Cluster load balancing mode did not match after update')
2166
+ return result
2167
+
2168
+ async def update_cluster_session_fault_tolerance_level(self, cluster_id, level):
2169
+ cluster = uuid.UUID(str(cluster_id))
2170
+ if self.protocol == '10.0':
2171
+ raise UnsupportedFeatureError('cluster session fault tolerance update requires protocol 16.0')
2172
+ if type(level) is not int or not 0 <= level <= 2_147_483_647:
2173
+ raise ValueError('Invalid cluster session fault tolerance level')
2174
+ before = await self.get_cluster(cluster)
2175
+ if self.agent_user:
2176
+ await self._direct().update_cluster_session_fault_tolerance_level(
2177
+ cluster, level, agent_os_user=self.agent_user)
2178
+ else:
2179
+ await self._direct().update_cluster_session_fault_tolerance_level(cluster, level)
2180
+ result = await self.get_cluster(cluster)
2181
+ if result != {**before, 'session-fault-tolerance-level': level}:
2182
+ raise RacError('Cluster session fault tolerance level did not match after update')
2183
+ return result
2184
+
2185
+ async def update_cluster_security_level(self, cluster_id, level):
2186
+ cluster = uuid.UUID(str(cluster_id))
2187
+ if self.protocol == '10.0':
2188
+ raise UnsupportedFeatureError('cluster security level update requires protocol 16.0')
2189
+ if type(level) is not int or not 0 <= level <= 2_147_483_647:
2190
+ raise ValueError('Invalid cluster security level')
2191
+ before = await self.get_cluster(cluster)
2192
+ if self.agent_user:
2193
+ await self._direct().update_cluster_security_level(cluster, level, agent_os_user=self.agent_user)
2194
+ else:
2195
+ await self._direct().update_cluster_security_level(cluster, level)
2196
+ result = await self.get_cluster(cluster)
2197
+ if result != {**before, 'security-level': level}:
2198
+ raise RacError('Cluster security level did not match after update')
2199
+ return result
2200
+
2201
+ async def update_cluster_expiration_timeout(self, cluster_id, seconds):
2202
+ cluster = uuid.UUID(str(cluster_id))
2203
+ if self.protocol == '10.0':
2204
+ raise UnsupportedFeatureError('cluster timeout update requires protocol 16.0')
2205
+ if isinstance(seconds, bool) or not isinstance(seconds, int) or not 0 <= seconds <= 2_147_483_647:
2206
+ raise ValueError('Invalid cluster expiration timeout')
2207
+ before = await self.get_cluster(cluster)
2208
+ if self.agent_user:
2209
+ await self._direct().update_cluster_expiration_timeout(cluster, seconds,
2210
+ agent_os_user=self.agent_user)
2211
+ else:
2212
+ await self._direct().update_cluster_expiration_timeout(cluster, seconds)
2213
+ result = await self.get_cluster(cluster)
2214
+ if result != {**before, 'expiration-timeout': seconds}:
2215
+ raise RacError('Cluster expiration timeout did not match after update')
2216
+ return result
2217
+
2218
+ async def update_cluster_name(self, cluster_id, name):
2219
+ cluster = uuid.UUID(str(cluster_id))
2220
+ if self.protocol == '10.0':
2221
+ raise UnsupportedFeatureError('cluster name update requires protocol 16.0')
2222
+ if not isinstance(name, str) or (name and not name.strip()) or len(name.encode('utf-8')) > 256 or '\x00' in name:
2223
+ raise ValueError('Invalid cluster name')
2224
+ before = await self.get_cluster(cluster)
2225
+ if self.agent_user:
2226
+ await self._direct().update_cluster_name(cluster, name, agent_os_user=self.agent_user)
2227
+ else:
2228
+ await self._direct().update_cluster_name(cluster, name)
2229
+ result = await self.get_cluster(cluster)
2230
+ if result != {**before, 'name': name}:
2231
+ raise RacError('Cluster name did not match after update')
2232
+ return result
2233
+
2234
+ async def update_restart_schedule(self, cluster_id, schedule):
2235
+ cluster = uuid.UUID(str(cluster_id))
2236
+ if self.protocol == '10.0':
2237
+ raise UnsupportedFeatureError('restart schedule requires protocol 16.0')
2238
+ self._validate_schedule(schedule)
2239
+ before = await self.get_cluster(cluster)
2240
+ if self.agent_user:
2241
+ await self._direct().update_cluster_schedule(cluster, schedule, agent_os_user=self.agent_user)
2242
+ else:
2243
+ await self._direct().update_cluster_schedule(cluster, schedule)
2244
+ result = await self.get_cluster(cluster)
2245
+ if result != {**before, 'restart-schedule': schedule}:
2246
+ raise RacError('Restart schedule did not match after update')
2247
+ return result
2248
+
2249
+ async def update_server_port_ranges(self, cluster_id, server_id, ranges):
2250
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2251
+ if self.protocol == '10.0':
2252
+ raise UnsupportedFeatureError('server port range update requires protocol 16.0')
2253
+ if (type(ranges) is not list or not ranges or len(ranges) > 64
2254
+ or any(type(pair) not in (tuple, list) or len(pair) != 2
2255
+ or any(type(port) is not int or not 1 <= port <= 65535 for port in pair)
2256
+ or pair[0] > pair[1] for pair in ranges)):
2257
+ raise ValueError('Invalid server port ranges')
2258
+ sorted_ranges = sorted(ranges)
2259
+ if any(previous[1] >= following[0] for previous, following in zip(sorted_ranges, sorted_ranges[1:])):
2260
+ raise ValueError('Overlapping server port ranges')
2261
+ before = await self.get_server(cluster, server)
2262
+ await self._direct().update_server_port_ranges(cluster, server, ranges)
2263
+ result = await self.get_server(cluster, server)
2264
+ if result != {**before, 'port_ranges': [tuple(pair) for pair in ranges]}:
2265
+ raise RacError('Server port ranges did not match after update')
2266
+ return result
2267
+
2268
+ async def update_server_dedicate_managers(self, cluster_id, server_id, choice):
2269
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2270
+ if self.protocol == '10.0':
2271
+ raise UnsupportedFeatureError('server dedicate managers update requires protocol 16.0')
2272
+ if choice not in ('all', 'none') or type(choice) is not str:
2273
+ raise ValueError('Invalid server dedicate managers choice')
2274
+ before = await self.get_server(cluster, server)
2275
+ await self._direct().update_server_dedicate_managers(cluster, server, choice)
2276
+ result = await self.get_server(cluster, server)
2277
+ if result != {**before, 'dedicated_managers': choice == 'all'}:
2278
+ raise RacError('Server dedicate managers did not match after update')
2279
+ return result
2280
+
2281
+ async def update_server_temporary_allowed_total_memory_time_limit(self, cluster_id, server_id, limit):
2282
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2283
+ if self.protocol == '10.0':
2284
+ raise UnsupportedFeatureError('server temporary memory time limit update requires protocol 16.0')
2285
+ if type(limit) is not int or not 0 <= limit < 2**63:
2286
+ raise ValueError('Invalid server temporary memory time limit')
2287
+ before = await self.get_server(cluster, server)
2288
+ await self._direct().update_server_temporary_allowed_total_memory_time_limit(cluster, server, limit)
2289
+ result = await self.get_server(cluster, server)
2290
+ if result != {**before, 'temporary_allowed_processes_total_memory_time_limit': limit}:
2291
+ raise RacError('Server temporary memory time limit did not match after update')
2292
+ return result
2293
+
2294
+ async def update_server_temporary_allowed_total_memory(self, cluster_id, server_id, limit):
2295
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2296
+ if self.protocol == '10.0':
2297
+ raise UnsupportedFeatureError('server temporary allowed total memory update requires protocol 16.0')
2298
+ if type(limit) is not int or not 0 <= limit < 2**63:
2299
+ raise ValueError('Invalid server temporary allowed total memory')
2300
+ before = await self.get_server(cluster, server)
2301
+ await self._direct().update_server_temporary_allowed_total_memory(cluster, server, limit)
2302
+ result = await self.get_server(cluster, server)
2303
+ if result != {**before, 'temporary_allowed_processes_total_memory': limit}:
2304
+ raise RacError('Server temporary allowed total memory did not match after update')
2305
+ return result
2306
+
2307
+ async def update_server_critical_total_memory(self, cluster_id, server_id, limit):
2308
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2309
+ if self.protocol == '10.0':
2310
+ raise UnsupportedFeatureError('server critical total memory update requires protocol 16.0')
2311
+ if type(limit) is not int or not 0 <= limit < 2**63:
2312
+ raise ValueError('Invalid server critical total memory')
2313
+ before = await self.get_server(cluster, server)
2314
+ await self._direct().update_server_critical_total_memory(cluster, server, limit)
2315
+ result = await self.get_server(cluster, server)
2316
+ if result != {**before, 'critical_processes_total_memory': limit}:
2317
+ raise RacError('Server critical total memory did not match after update')
2318
+ return result
2319
+
2320
+ async def update_server_service_principal_name(self, cluster_id, server_id, name):
2321
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2322
+ if self.protocol == '10.0':
2323
+ raise UnsupportedFeatureError('server SPN update requires protocol 16.0')
2324
+ if not isinstance(name, str) or '\x00' in name or len(name.encode('utf-8')) > 256:
2325
+ raise ValueError('Invalid server service principal name')
2326
+ before = await self.get_server(cluster, server)
2327
+ await self._direct().update_server_service_principal_name(cluster, server, name)
2328
+ result = await self.get_server(cluster, server)
2329
+ if result != {**before, 'service-principal-name': name}:
2330
+ raise RacError('Server service principal name did not match after update')
2331
+ return result
2332
+
2333
+ async def update_server_safe_call_memory_limit(self, cluster_id, server_id, limit):
2334
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2335
+ if self.protocol == '10.0':
2336
+ raise UnsupportedFeatureError('server safe call memory limit update requires protocol 16.0')
2337
+ if type(limit) is not int or not 0 <= limit < 2**63:
2338
+ raise ValueError('Invalid server safe call memory limit')
2339
+ before = await self.get_server(cluster, server)
2340
+ await self._direct().update_server_safe_call_memory_limit(cluster, server, limit)
2341
+ result = await self.get_server(cluster, server)
2342
+ if result != {**before, 'safe_call_memory_limit': limit}:
2343
+ raise RacError('Server safe call memory limit did not match after update')
2344
+ return result
2345
+
2346
+ async def update_server_connections_limit(self, cluster_id, server_id, limit):
2347
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2348
+ if self.protocol == '10.0':
2349
+ raise UnsupportedFeatureError('server connections limit update requires protocol 16.0')
2350
+ if type(limit) is not int or not 0 <= limit <= 2_147_483_647:
2351
+ raise ValueError('Invalid server connections limit')
2352
+ before = await self.get_server(cluster, server)
2353
+ await self._direct().update_server_connections_limit(cluster, server, limit)
2354
+ result = await self.get_server(cluster, server)
2355
+ if result != {**before, 'connections_per_working_process_limit': limit}:
2356
+ raise RacError('Server connections limit did not match after update')
2357
+ return result
2358
+
2359
+ async def update_server_infobases_limit(self, cluster_id, server_id, limit):
2360
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2361
+ if self.protocol == '10.0':
2362
+ raise UnsupportedFeatureError('server infobases limit update requires protocol 16.0')
2363
+ if type(limit) is not int or not 0 <= limit <= 2_147_483_647:
2364
+ raise ValueError('Invalid server infobases limit')
2365
+ before = await self.get_server(cluster, server)
2366
+ await self._direct().update_server_infobases_limit(cluster, server, limit)
2367
+ result = await self.get_server(cluster, server)
2368
+ if result != {**before, 'infobases_per_working_process_limit': limit}:
2369
+ raise RacError('Server infobases limit did not match after update')
2370
+ return result
2371
+
2372
+ async def update_server_restart_schedule(self, cluster_id, server_id, schedule):
2373
+ cluster, server = uuid.UUID(str(cluster_id)), uuid.UUID(str(server_id))
2374
+ if self.protocol == '10.0':
2375
+ raise UnsupportedFeatureError('restart schedule requires protocol 16.0')
2376
+ self._validate_schedule(schedule)
2377
+ before = await self.get_server(cluster, server)
2378
+ await self._direct().update_server_schedule(cluster, server, schedule)
2379
+ result = await self.get_server(cluster, server)
2380
+ if result != {**before, 'restart-schedule': schedule}:
2381
+ raise RacError('Restart schedule did not match after server update')
2382
+ return result