mysql-shell-client 0.6.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.
- mysql_shell/__init__.py +7 -0
- mysql_shell/builders/__init__.py +7 -0
- mysql_shell/builders/authorization/__init__.py +5 -0
- mysql_shell/builders/authorization/base.py +23 -0
- mysql_shell/builders/authorization/charm.py +213 -0
- mysql_shell/builders/locking/__init__.py +5 -0
- mysql_shell/builders/locking/base.py +28 -0
- mysql_shell/builders/locking/charm.py +97 -0
- mysql_shell/builders/logging/__init__.py +5 -0
- mysql_shell/builders/logging/base.py +14 -0
- mysql_shell/builders/logging/charm.py +36 -0
- mysql_shell/builders/quoting.py +46 -0
- mysql_shell/clients/__init__.py +5 -0
- mysql_shell/clients/cluster.py +414 -0
- mysql_shell/clients/instance.py +524 -0
- mysql_shell/executors/__init__.py +5 -0
- mysql_shell/executors/base.py +36 -0
- mysql_shell/executors/errors/__init__.py +4 -0
- mysql_shell/executors/errors/runtime.py +15 -0
- mysql_shell/executors/local.py +188 -0
- mysql_shell/models/__init__.py +8 -0
- mysql_shell/models/account.py +49 -0
- mysql_shell/models/cluster.py +57 -0
- mysql_shell/models/connection.py +22 -0
- mysql_shell/models/instance.py +27 -0
- mysql_shell/models/statement.py +30 -0
- mysql_shell_client-0.6.0.dist-info/METADATA +143 -0
- mysql_shell_client-0.6.0.dist-info/RECORD +30 -0
- mysql_shell_client-0.6.0.dist-info/WHEEL +4 -0
- mysql_shell_client-0.6.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from ..builders import StringQueryQuoter
|
|
9
|
+
from ..executors import BaseExecutor
|
|
10
|
+
from ..executors.errors import ExecutionError
|
|
11
|
+
from ..models.account import Role, User
|
|
12
|
+
from ..models.instance import InstanceRole, InstanceState
|
|
13
|
+
from ..models.statement import VariableScope
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger()
|
|
16
|
+
|
|
17
|
+
_Attrs = Mapping[str, str] | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MySQLInstanceClient:
|
|
21
|
+
"""Class to encapsulate all instance operations using MySQL Shell."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, executor: BaseExecutor, quoter: StringQueryQuoter):
|
|
24
|
+
"""Initialize the class."""
|
|
25
|
+
self._executor = executor
|
|
26
|
+
self._quoter = quoter
|
|
27
|
+
|
|
28
|
+
def check_work_ongoing(self, name_pattern: str) -> bool:
|
|
29
|
+
"""Checks whether an instance work is ongoing."""
|
|
30
|
+
query = (
|
|
31
|
+
"SELECT work_completed, work_estimated "
|
|
32
|
+
"FROM performance_schema.events_stages_current "
|
|
33
|
+
"WHERE event_name LIKE {name_pattern}"
|
|
34
|
+
)
|
|
35
|
+
query = query.format(
|
|
36
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
rows = self._executor.execute_sql(query)
|
|
41
|
+
except ExecutionError:
|
|
42
|
+
logger.error(f"Failed to check work for events with {name_pattern=}")
|
|
43
|
+
raise
|
|
44
|
+
else:
|
|
45
|
+
return any(row["work_completed"] < row["work_estimated"] for row in rows)
|
|
46
|
+
|
|
47
|
+
def create_instance_role(self, role: Role, roles: list[str] = None) -> None:
|
|
48
|
+
"""Creates a new instance role."""
|
|
49
|
+
if not roles:
|
|
50
|
+
granting_query = ""
|
|
51
|
+
else:
|
|
52
|
+
granting_query = "GRANT {roles} TO {rolename}@{hostname}"
|
|
53
|
+
granting_query = granting_query.format(
|
|
54
|
+
rolename=self._quoter.quote_value(role.rolename),
|
|
55
|
+
hostname=self._quoter.quote_value(role.hostname),
|
|
56
|
+
roles=", ".join(self._quoter.quote_value(r) for r in roles),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
creation_query = "CREATE ROLE {rolename}@{hostname}"
|
|
60
|
+
creation_query = creation_query.format(
|
|
61
|
+
rolename=self._quoter.quote_value(role.rolename),
|
|
62
|
+
hostname=self._quoter.quote_value(role.hostname),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
queries = ";".join((
|
|
66
|
+
creation_query,
|
|
67
|
+
granting_query,
|
|
68
|
+
))
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
self._executor.execute_sql(queries)
|
|
72
|
+
except ExecutionError:
|
|
73
|
+
logger.error(f"Failed to create instance role {role.rolename}.{role.hostname}")
|
|
74
|
+
raise
|
|
75
|
+
|
|
76
|
+
def create_instance_user(self, user: User, password: str, roles: list[str] = None) -> None:
|
|
77
|
+
"""Creates an instance user with the provided attributes."""
|
|
78
|
+
if not roles:
|
|
79
|
+
granting_query = ""
|
|
80
|
+
else:
|
|
81
|
+
granting_query = "GRANT {roles} TO {username}@{hostname}"
|
|
82
|
+
granting_query = granting_query.format(
|
|
83
|
+
username=self._quoter.quote_value(user.username),
|
|
84
|
+
hostname=self._quoter.quote_value(user.hostname),
|
|
85
|
+
roles=", ".join(self._quoter.quote_value(r) for r in roles),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
creation_query = (
|
|
89
|
+
"CREATE USER {username}@{hostname} IDENTIFIED BY {password} ATTRIBUTE {attrs}"
|
|
90
|
+
)
|
|
91
|
+
creation_query = creation_query.format(
|
|
92
|
+
username=self._quoter.quote_value(user.username),
|
|
93
|
+
hostname=self._quoter.quote_value(user.hostname),
|
|
94
|
+
password=self._quoter.quote_value(password),
|
|
95
|
+
attrs=self._quoter.quote_value(user.serialize_attrs()),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
queries = ";".join((
|
|
99
|
+
creation_query,
|
|
100
|
+
granting_query,
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
self._executor.execute_sql(queries)
|
|
105
|
+
except ExecutionError:
|
|
106
|
+
logger.error(f"Failed to create instance user {user.username}.{user.hostname}")
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
def delete_instance_user(self, user: User) -> None:
|
|
110
|
+
"""Deletes an instance user if it exists."""
|
|
111
|
+
query = "DROP USER IF EXISTS {username}@{hostname}"
|
|
112
|
+
query = query.format(
|
|
113
|
+
username=self._quoter.quote_value(user.username),
|
|
114
|
+
hostname=self._quoter.quote_value(user.hostname),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
self._executor.execute_sql(query)
|
|
119
|
+
except ExecutionError:
|
|
120
|
+
logger.error(f"Failed to delete instance user {user.username}.{user.hostname}")
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def delete_instance_users(self, users: list[User]) -> None:
|
|
124
|
+
"""Deletes the instance users provided."""
|
|
125
|
+
query = "DROP USER IF EXISTS {username}@{hostname}"
|
|
126
|
+
queries = []
|
|
127
|
+
|
|
128
|
+
for user in users:
|
|
129
|
+
queries.append(
|
|
130
|
+
query.format(
|
|
131
|
+
username=self._quoter.quote_value(user.username),
|
|
132
|
+
hostname=self._quoter.quote_value(user.hostname),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
queries = ";".join(queries)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
self._executor.execute_sql(queries)
|
|
140
|
+
except ExecutionError:
|
|
141
|
+
logger.error("Failed to delete instance users")
|
|
142
|
+
raise
|
|
143
|
+
|
|
144
|
+
def update_instance_user(self, user: User, password: str = None, attrs: _Attrs = None) -> None:
|
|
145
|
+
"""Updates an instance user with the provided password and / or attributes."""
|
|
146
|
+
if not password and not attrs:
|
|
147
|
+
raise ValueError("Either password or attrs must be provided")
|
|
148
|
+
|
|
149
|
+
query = "ALTER USER {username}@{hostname}"
|
|
150
|
+
query = query.format(
|
|
151
|
+
username=self._quoter.quote_value(user.username),
|
|
152
|
+
hostname=self._quoter.quote_value(user.hostname),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if password:
|
|
156
|
+
query += f" IDENTIFIED BY {self._quoter.quote_value(password)}"
|
|
157
|
+
if attrs:
|
|
158
|
+
query += f" ATTRIBUTE {self._quoter.quote_value(json.dumps(attrs))}"
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
self._executor.execute_sql(query)
|
|
162
|
+
except ExecutionError:
|
|
163
|
+
logger.error(f"Failed to update instance user {user.username}.{user.hostname}")
|
|
164
|
+
raise
|
|
165
|
+
|
|
166
|
+
def get_cluster_instance_label(self) -> str | None:
|
|
167
|
+
"""Gets the instance label within the cluster."""
|
|
168
|
+
query = (
|
|
169
|
+
"SELECT instance_name "
|
|
170
|
+
"FROM mysql_innodb_cluster_metadata.instances "
|
|
171
|
+
"WHERE mysql_server_uuid = @@server_uuid"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
rows = self._executor.execute_sql(query)
|
|
176
|
+
except ExecutionError:
|
|
177
|
+
logger.error("Failed to get cluster instance label")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
if not rows:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
return rows[0]["instance_name"]
|
|
184
|
+
|
|
185
|
+
def get_cluster_instance_labels(self, cluster_name: str) -> list[str]:
|
|
186
|
+
"""Gets the instance labels within the cluster."""
|
|
187
|
+
query = (
|
|
188
|
+
"SELECT instance_name "
|
|
189
|
+
"FROM mysql_innodb_cluster_metadata.instances "
|
|
190
|
+
"WHERE cluster_id IN ( "
|
|
191
|
+
" SELECT cluster_id "
|
|
192
|
+
" FROM mysql_innodb_cluster_metadata.clusters "
|
|
193
|
+
" WHERE cluster_name = {cluster_name} "
|
|
194
|
+
")"
|
|
195
|
+
)
|
|
196
|
+
query = query.format(
|
|
197
|
+
cluster_name=self._quoter.quote_value(cluster_name),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
rows = self._executor.execute_sql(query)
|
|
202
|
+
except ExecutionError:
|
|
203
|
+
logger.error(f"Failed to get cluster instance labels with {cluster_name=}")
|
|
204
|
+
raise
|
|
205
|
+
else:
|
|
206
|
+
return [row["instance_name"] for row in rows]
|
|
207
|
+
|
|
208
|
+
def get_cluster_labels(self) -> list[str]:
|
|
209
|
+
"""Gets the cluster labels."""
|
|
210
|
+
query = "SELECT cluster_name FROM mysql_innodb_cluster_metadata.clusters"
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
rows = self._executor.execute_sql(query)
|
|
214
|
+
except ExecutionError:
|
|
215
|
+
logger.error("Failed to get cluster labels")
|
|
216
|
+
raise
|
|
217
|
+
else:
|
|
218
|
+
return [row["cluster_name"] for row in rows]
|
|
219
|
+
|
|
220
|
+
def get_instance_replication_state(self) -> InstanceState | None:
|
|
221
|
+
"""Gets the instance replication state."""
|
|
222
|
+
query = (
|
|
223
|
+
"SELECT member_state "
|
|
224
|
+
"FROM performance_schema.replication_group_members "
|
|
225
|
+
"WHERE member_id = @@server_uuid"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
rows = self._executor.execute_sql(query)
|
|
230
|
+
except ExecutionError:
|
|
231
|
+
logger.error("Failed to get instance replication state")
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
if not rows:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
state = rows[0]["member_state"]
|
|
238
|
+
state = InstanceState(state) if state else None
|
|
239
|
+
return state
|
|
240
|
+
|
|
241
|
+
def get_instance_replication_role(self) -> InstanceRole | None:
|
|
242
|
+
"""Gets the instance replication role."""
|
|
243
|
+
query = (
|
|
244
|
+
"SELECT member_role "
|
|
245
|
+
"FROM performance_schema.replication_group_members "
|
|
246
|
+
"WHERE member_id = @@server_uuid"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
rows = self._executor.execute_sql(query)
|
|
251
|
+
except ExecutionError:
|
|
252
|
+
logger.error("Failed to get instance replication role")
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
if not rows:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
role = rows[0]["member_role"]
|
|
259
|
+
role = InstanceRole(role) if role else None
|
|
260
|
+
return role
|
|
261
|
+
|
|
262
|
+
def get_instance_variable(self, scope: VariableScope, name: str) -> Any | None:
|
|
263
|
+
"""Gets an instance variable by scope and name."""
|
|
264
|
+
if scope in (VariableScope.PERSIST, VariableScope.PERSIST_ONLY):
|
|
265
|
+
raise ValueError("Invalid scope")
|
|
266
|
+
|
|
267
|
+
quoted_name = self._quoter.quote_identifier(name)
|
|
268
|
+
|
|
269
|
+
query = "SELECT @@{scope}.{name} AS {alias}"
|
|
270
|
+
query = query.format(
|
|
271
|
+
scope=scope.value,
|
|
272
|
+
name=quoted_name,
|
|
273
|
+
alias=quoted_name,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
rows = self._executor.execute_sql(query)
|
|
278
|
+
except ExecutionError:
|
|
279
|
+
logger.error(f"Failed to get instance variable {scope}.{name}")
|
|
280
|
+
raise
|
|
281
|
+
|
|
282
|
+
if not rows:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
return rows[0][name]
|
|
286
|
+
|
|
287
|
+
def set_instance_variable(self, scope: VariableScope, name: str, value: Any) -> None:
|
|
288
|
+
"""Sets an instance variable by scope and name."""
|
|
289
|
+
quoted_name = self._quoter.quote_identifier(name)
|
|
290
|
+
quoted_value = self._quoter.quote_value(value) if isinstance(value, str) else value
|
|
291
|
+
|
|
292
|
+
query = "SET @@{scope}.{name} = {value}"
|
|
293
|
+
query = query.format(
|
|
294
|
+
scope=scope.value,
|
|
295
|
+
name=quoted_name,
|
|
296
|
+
value=quoted_value,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
self._executor.execute_sql(query)
|
|
301
|
+
except ExecutionError:
|
|
302
|
+
logger.error(f"Failed to set instance variable {scope}.{name}")
|
|
303
|
+
raise
|
|
304
|
+
|
|
305
|
+
def get_instance_version(self) -> str | None:
|
|
306
|
+
"""Gets the instance version value."""
|
|
307
|
+
version = self.get_instance_variable(VariableScope.GLOBAL, "version")
|
|
308
|
+
if not version:
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
return version.split("-")[0]
|
|
312
|
+
|
|
313
|
+
def install_instance_plugin(self, name: str, path: str) -> None:
|
|
314
|
+
"""Installs an instance plugin by name and path."""
|
|
315
|
+
query = "INSTALL PLUGIN {plugin_name} SONAME {plugin_path}"
|
|
316
|
+
query = query.format(
|
|
317
|
+
plugin_name=self._quoter.quote_identifier(name),
|
|
318
|
+
plugin_path=self._quoter.quote_value(path),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
self._executor.execute_sql(query)
|
|
323
|
+
except ExecutionError:
|
|
324
|
+
logger.error(f"Failed to install instance plugin with {name=} and {path=}")
|
|
325
|
+
raise
|
|
326
|
+
|
|
327
|
+
def uninstall_instance_plugin(self, name: str) -> None:
|
|
328
|
+
"""Uninstalls an instance plugin by name."""
|
|
329
|
+
query = "UNINSTALL PLUGIN {plugin_name}"
|
|
330
|
+
query = query.format(
|
|
331
|
+
plugin_name=self._quoter.quote_identifier(name),
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
self._executor.execute_sql(query)
|
|
336
|
+
except ExecutionError:
|
|
337
|
+
logger.error(f"Failed to uninstall instance plugin with {name=}")
|
|
338
|
+
raise
|
|
339
|
+
|
|
340
|
+
def reload_instance_certs(self) -> None:
|
|
341
|
+
"""Reloads TLS certificates."""
|
|
342
|
+
query = "ALTER INSTANCE RELOAD TLS"
|
|
343
|
+
|
|
344
|
+
try:
|
|
345
|
+
self._executor.execute_sql(query)
|
|
346
|
+
except ExecutionError:
|
|
347
|
+
logger.error("Failed to reload instance TLS certificates")
|
|
348
|
+
raise
|
|
349
|
+
|
|
350
|
+
def search_instance_replication_members(
|
|
351
|
+
self,
|
|
352
|
+
roles: Sequence[InstanceRole] | None = None,
|
|
353
|
+
states: Sequence[InstanceState] | None = None,
|
|
354
|
+
) -> list[str]:
|
|
355
|
+
"""Searches the instance replication member IDs by role and/or state."""
|
|
356
|
+
if not roles:
|
|
357
|
+
roles = list(InstanceRole)
|
|
358
|
+
if not states:
|
|
359
|
+
states = list(InstanceState)
|
|
360
|
+
|
|
361
|
+
query = (
|
|
362
|
+
"SELECT member_id "
|
|
363
|
+
"FROM performance_schema.replication_group_members "
|
|
364
|
+
"WHERE member_role IN ({roles}) AND member_state IN ({states})"
|
|
365
|
+
)
|
|
366
|
+
query = query.format(
|
|
367
|
+
roles=", ".join([self._quoter.quote_value(role) for role in roles]),
|
|
368
|
+
states=", ".join([self._quoter.quote_value(state) for state in states]),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
rows = self._executor.execute_sql(query)
|
|
373
|
+
except ExecutionError:
|
|
374
|
+
logger.error("Failed to search instance replication members")
|
|
375
|
+
raise
|
|
376
|
+
else:
|
|
377
|
+
return [row["member_id"] for row in rows]
|
|
378
|
+
|
|
379
|
+
def search_instance_connection_processes(self, name_pattern: str) -> list[int]:
|
|
380
|
+
"""Searches the instance connection process IDs by name pattern."""
|
|
381
|
+
query = (
|
|
382
|
+
"SELECT processlist_id "
|
|
383
|
+
"FROM performance_schema.threads "
|
|
384
|
+
"WHERE "
|
|
385
|
+
" processlist_id != CONNECTION_ID() AND "
|
|
386
|
+
" connection_type IS NOT NULL AND "
|
|
387
|
+
" name LIKE {name_pattern}"
|
|
388
|
+
)
|
|
389
|
+
query = query.format(
|
|
390
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
rows = self._executor.execute_sql(query)
|
|
395
|
+
except ExecutionError:
|
|
396
|
+
logger.error(f"Failed to search instance connections with {name_pattern=}")
|
|
397
|
+
raise
|
|
398
|
+
else:
|
|
399
|
+
return [row["processlist_id"] for row in rows]
|
|
400
|
+
|
|
401
|
+
def search_instance_databases(self, name_pattern: str) -> list[str]:
|
|
402
|
+
"""Searches the instance databases by name pattern."""
|
|
403
|
+
query = (
|
|
404
|
+
"SELECT schema_name "
|
|
405
|
+
"FROM information_schema.schemata "
|
|
406
|
+
"WHERE schema_name LIKE {name_pattern}"
|
|
407
|
+
)
|
|
408
|
+
query = query.format(
|
|
409
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
rows = self._executor.execute_sql(query)
|
|
414
|
+
except ExecutionError:
|
|
415
|
+
logger.error(f"Failed to search instance databases with {name_pattern=}")
|
|
416
|
+
raise
|
|
417
|
+
else:
|
|
418
|
+
return [row["SCHEMA_NAME"] for row in rows]
|
|
419
|
+
|
|
420
|
+
def search_instance_plugins(self, name_pattern: str) -> list[str]:
|
|
421
|
+
"""Searches the instance plugins by name pattern."""
|
|
422
|
+
# fmt: off
|
|
423
|
+
query = (
|
|
424
|
+
"SELECT name "
|
|
425
|
+
"FROM mysql.plugin "
|
|
426
|
+
"WHERE name LIKE {name_pattern}"
|
|
427
|
+
)
|
|
428
|
+
# fmt: on
|
|
429
|
+
|
|
430
|
+
query = query.format(
|
|
431
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
rows = self._executor.execute_sql(query)
|
|
436
|
+
except ExecutionError:
|
|
437
|
+
logger.error(f"Failed to search instance plugins with {name_pattern=}")
|
|
438
|
+
raise
|
|
439
|
+
else:
|
|
440
|
+
return [row["name"] for row in rows]
|
|
441
|
+
|
|
442
|
+
def search_instance_roles(self, name_pattern: str) -> list[Role]:
|
|
443
|
+
"""Searches the instance roles by name pattern."""
|
|
444
|
+
query = (
|
|
445
|
+
"SELECT user, host "
|
|
446
|
+
"FROM mysql.user "
|
|
447
|
+
"WHERE user LIKE {name_pattern} AND authentication_string=''"
|
|
448
|
+
)
|
|
449
|
+
query = query.format(
|
|
450
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
rows = self._executor.execute_sql(query)
|
|
455
|
+
except ExecutionError:
|
|
456
|
+
logger.error(f"Failed to search instance roles with {name_pattern=}")
|
|
457
|
+
raise
|
|
458
|
+
else:
|
|
459
|
+
return [Role.from_row(row["user"], row["host"]) for row in rows]
|
|
460
|
+
|
|
461
|
+
def search_instance_users(self, name_pattern: str, attrs: _Attrs = None) -> list[User]:
|
|
462
|
+
"""Searches the instance users by name pattern and attributes."""
|
|
463
|
+
attr_filter = "attribute LIKE {string}"
|
|
464
|
+
attr_substr = '%"{key}": "{val}"%'
|
|
465
|
+
|
|
466
|
+
if not attrs:
|
|
467
|
+
strings = ["%"]
|
|
468
|
+
filters = [attr_filter.format(string=self._quoter.quote_value(s)) for s in strings]
|
|
469
|
+
else:
|
|
470
|
+
strings = [attr_substr.format(key=key, val=val) for key, val in attrs.items()]
|
|
471
|
+
filters = [attr_filter.format(string=self._quoter.quote_value(s)) for s in strings]
|
|
472
|
+
|
|
473
|
+
query = (
|
|
474
|
+
"SELECT user, host, attribute "
|
|
475
|
+
"FROM information_schema.user_attributes "
|
|
476
|
+
"WHERE user LIKE {name_pattern} AND {attr_filters}"
|
|
477
|
+
)
|
|
478
|
+
query = query.format(
|
|
479
|
+
name_pattern=self._quoter.quote_value(name_pattern),
|
|
480
|
+
attr_filters=" AND ".join(filters),
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
rows = self._executor.execute_sql(query)
|
|
485
|
+
except ExecutionError:
|
|
486
|
+
logger.error(f"Failed to search instance users with {name_pattern=}")
|
|
487
|
+
raise
|
|
488
|
+
else:
|
|
489
|
+
return [User.from_row(row["USER"], row["HOST"], row["ATTRIBUTE"]) for row in rows]
|
|
490
|
+
|
|
491
|
+
def start_instance_replication(self) -> None:
|
|
492
|
+
"""Starts instance group replication."""
|
|
493
|
+
query = "START GROUP_REPLICATION"
|
|
494
|
+
|
|
495
|
+
try:
|
|
496
|
+
self._executor.execute_sql(query)
|
|
497
|
+
except ExecutionError:
|
|
498
|
+
logger.error("Failed to start instance replication")
|
|
499
|
+
raise
|
|
500
|
+
|
|
501
|
+
def stop_instance_replication(self) -> None:
|
|
502
|
+
"""Stops instance group replication."""
|
|
503
|
+
query = "STOP GROUP_REPLICATION"
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
self._executor.execute_sql(query)
|
|
507
|
+
except ExecutionError:
|
|
508
|
+
logger.error("Failed to stop instance replication")
|
|
509
|
+
raise
|
|
510
|
+
|
|
511
|
+
def stop_instance_processes(self, process_ids: Sequence[int]) -> None:
|
|
512
|
+
"""Kills the instances processes by ID."""
|
|
513
|
+
if not process_ids:
|
|
514
|
+
return
|
|
515
|
+
|
|
516
|
+
query = "KILL CONNECTION {id}"
|
|
517
|
+
queries = [query.format(id=self._quoter.quote_value(pid)) for pid in process_ids]
|
|
518
|
+
queries = ";".join(queries)
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
self._executor.execute_sql(queries)
|
|
522
|
+
except ExecutionError:
|
|
523
|
+
logger.error("Failed to kill instance processes")
|
|
524
|
+
raise
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Sequence
|
|
6
|
+
|
|
7
|
+
from ..models import ConnectionDetails
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseExecutor(ABC):
|
|
11
|
+
"""Base class for all MySQL Shell executors."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, conn_details: ConnectionDetails, shell_path: str):
|
|
14
|
+
"""Initialize the executor."""
|
|
15
|
+
self._conn_details = conn_details
|
|
16
|
+
self._shell_path = shell_path
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def connection_details(self) -> ConnectionDetails:
|
|
20
|
+
"""Return the connection details."""
|
|
21
|
+
return self._conn_details
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def check_connection(self) -> None:
|
|
25
|
+
"""Check the connection."""
|
|
26
|
+
raise NotImplementedError()
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def execute_py(self, script: str, *, timeout: int | None = None) -> str:
|
|
30
|
+
"""Execute a Python script."""
|
|
31
|
+
raise NotImplementedError()
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def execute_sql(self, script: str, *, timeout: int | None = None) -> Sequence[dict]:
|
|
35
|
+
"""Execute a SQL script."""
|
|
36
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright 2025 Canonical Ltd.
|
|
2
|
+
# See LICENSE file for licensing details.
|
|
3
|
+
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ExecutionError(RuntimeError):
|
|
8
|
+
"""MySQL shell execution error."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: Any | None = None):
|
|
11
|
+
"""Initialize the error."""
|
|
12
|
+
if isinstance(message, dict):
|
|
13
|
+
message = message.get("message")
|
|
14
|
+
|
|
15
|
+
super().__init__(message)
|