-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpeer_relation.py
More file actions
500 lines (415 loc) · 16.7 KB
/
Copy pathpeer_relation.py
File metadata and controls
500 lines (415 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/usr/bin/env python3
# Copyright 2026 Canonical Ltd.
# See LICENSE file for licensing details.
"""State objects for database-peers relation."""
import json
from collections.abc import MutableMapping
from functools import cached_property
from ops import Application, BlockedStatus, Relation, Unit
from single_kernel_postgresql.config.enums import Substrates
from single_kernel_postgresql.config.literals import (
MONITORING_PASSWORD_KEY,
PATRONI_PASSWORD_KEY,
RAFT_PASSWORD_KEY,
REPLICATION_PASSWORD_KEY,
REWIND_PASSWORD_KEY,
USER_PASSWORD_KEY,
)
from single_kernel_postgresql.core.relation_state import RelationState
from single_kernel_postgresql.lib.charms.data_platform_libs.v0.data_interfaces import (
DataPeerData,
DataPeerUnitData,
)
class PostgreSQLPeer(RelationState):
"""State/Relation data collection for a PostgreSQL unit."""
data_interface: DataPeerUnitData
unit: Unit
def __init__(
self,
relation: Relation | None,
data_interface: DataPeerUnitData,
component: Unit,
):
"""Initialize the PostgreSQLPeer object."""
super().__init__(relation, data_interface, component)
self.data_interface = data_interface
self.unit = component
def get_secret(self, key: str) -> str | None:
"""Get the secret value for 'key' from the peer relation data."""
if not self.relation:
return None
return self.data_interface.get_secret(self.relation.id, key)
def set_secret(self, key: str, value: str) -> None:
"""Set the secret value for 'key' in the peer relation data."""
if not self.relation:
return
self.data_interface.set_secret(self.relation.id, key, value)
def remove_secret(self, key: str) -> None:
"""Remove the secret value for 'key' from the peer relation data."""
if not self.relation:
return
self.data_interface.delete_relation_data(self.relation.id, [key])
@property
def is_app_leader(self) -> bool:
"""Check if the current unit is the leader of the application."""
return self.unit.is_leader()
@property
def is_blocked(self) -> bool:
"""Returns whether the unit is in a blocked state."""
return isinstance(self.unit.status, BlockedStatus)
@property
def internal_cert(self) -> str | None:
"""Get internal certificate.
Returns:
The internal certificate from the peer relation or None if it has not yet been set by the leader.
"""
return self.get_secret("internal-cert")
@property
def internal_key(self) -> str | None:
"""Get internal private key.
Returns:
The internal private key from the peer relation or None if it has not yet been set by the leader.
"""
return self.get_secret("internal-key")
@internal_cert.setter
def internal_cert(self, value: str) -> None:
"""Set internal certificate in the peer relation."""
self.set_secret("internal-cert", value)
@internal_key.setter
def internal_key(self, value: str) -> None:
"""Set internal private key in the peer relation."""
self.set_secret("internal-key", value)
@property
def current_ca(self) -> str | None:
"""Current peer CA (unit secret); part of the peer CA bundle."""
return self.get_secret("current-ca")
@current_ca.setter
def current_ca(self, value: str) -> None:
self.set_secret("current-ca", value)
@property
def old_ca(self) -> str | None:
"""Previous peer CA (unit secret); retained for the rotation window."""
return self.get_secret("old-ca")
@old_ca.setter
def old_ca(self, value: str) -> None:
self.set_secret("old-ca", value)
@property
def ip(self) -> str | None:
"""Get the unit's IP address from the peer relation data."""
if not self.relation:
return None
return self.relation.data[self.unit].get("ip", "")
@ip.setter
def ip(self, value: str | None) -> None:
"""Set the unit's IP address in the peer relation data."""
if not self.relation:
return
if value:
self.relation.data[self.unit]["ip"] = value
@property
def member_name(self) -> str:
"""Get the member name for this unit."""
return self.unit.name.replace("/", "-")
@property
def unit_name(self) -> str:
"""Get the unit name."""
return self.unit.name
@property
def unit_id(self) -> str:
"""Get the unit id."""
return self.unit.name.split("/")[1]
@property
def patroni_on_failure_condition_override(self) -> str | None:
"""Get the on-failure condition override for patroni from the peer relation data."""
if not self.relation:
return None
return self.relation.data[self.unit].get("patroni-on-failure-condition-override", None)
@property
def database_peers_address(self) -> str | None:
"""Get the address to be used for database peers communication."""
if not self.relation:
return None
return self.relation.data[self.unit].get("database-peers-address", None)
@property
def database_address(self) -> str | None:
"""Get the client-facing database endpoint address."""
if not self.relation:
return None
return self.relation.data[self.unit].get("database-address", None)
@property
def replication_address(self) -> str | None:
"""Get the address to be used for replication communication."""
if not self.relation:
return None
return self.relation.data[self.unit].get("replication-address", None)
@property
def replication_offer_address(self) -> str | None:
"""Get the address to be used for replication communication in case of replication offer."""
if not self.relation:
return None
return self.relation.data[self.unit].get("replication-offer-address", None)
@property
def private_address(self) -> str | None:
"""Get the private address of the unit."""
if not self.relation:
return None
return self.relation.data[self.unit].get("private-address", None)
@property
def peer_addresses(self) -> set[str]:
"""Set of peer unit addresses (database, replication, and replication-offer)."""
peer_addrs = set()
if addr := self.database_peers_address:
peer_addrs.add(addr)
if addr := self.replication_address:
peer_addrs.add(addr)
if addr := self.replication_offer_address:
peer_addrs.add(addr)
if addr := (self.ip or self.private_address):
peer_addrs.add(addr)
return peer_addrs
@property
def is_unit_departing(self) -> bool:
"""Returns whether the unit is departing."""
if not self.relation:
return False
return "departing" in self.relation.data[self.unit]
@property
def is_unit_stopped(self) -> bool:
"""Returns whether the unit is stopped."""
if not self.relation:
return False
return "stopped" in self.relation.data[self.unit]
@property
def is_connectivity_enabled(self) -> bool:
"""Return whether this unit can be connected externally."""
if not self.relation:
return True
return self.relation.data[self.unit].get("connectivity", "on") == "on"
@property
def config_hash(self) -> str | None:
"""Get the last-applied PostgreSQL config hash from the peer relation data."""
if not self.relation:
return None
return self.relation.data[self.unit].get("config_hash")
@config_hash.setter
def config_hash(self, value: str) -> None:
"""Set the last-applied PostgreSQL config hash in the peer relation data."""
if not self.relation:
return
self.relation.data[self.unit]["config_hash"] = value
@property
def user_hash(self) -> str | None:
"""Get the last-applied users hash from the peer relation data."""
if not self.relation:
return None
return self.relation.data[self.unit].get("user_hash")
@user_hash.setter
def user_hash(self, value: str) -> None:
"""Set the last-applied users hash in the peer relation data."""
if not self.relation:
return
self.relation.data[self.unit]["user_hash"] = value
@property
def tls(self) -> bool:
"""Get the last-rendered TLS flag from the peer relation data."""
if not self.relation:
return False
return self.relation.data[self.unit].get("tls") == "enabled"
@tls.setter
def tls(self, value: bool) -> None:
"""Set the last-rendered TLS flag in the peer relation data."""
if not self.relation:
return
self.relation.data[self.unit]["tls"] = "enabled" if value else ""
@cached_property
def data(self) -> MutableMapping[str, str]:
"""Escape hatch method to access the peer data directly."""
if not self.relation:
return {}
return self.relation.data[self.unit]
@property
def peer_addresses_no_ip(self) -> set[str]:
"""Peer addresses excluding the ``ip`` databag key (original K8s charm behavior).
The K8s charm never wrote ``ip`` into the operator peer-cert SANs; it relied on
``database-peers-address`` + ``replication-address`` + ``replication-offer-address``
+ ``private-address``. The VM charm additionally included ``ip``. This property
exposes the K8s-shaped set so :class:`CharmState` can pick the right one per
substrate without the peer object needing to know the substrate.
"""
peer_addrs: set[str] = set()
if addr := self.database_peers_address:
peer_addrs.add(addr)
if addr := self.replication_address:
peer_addrs.add(addr)
if addr := self.replication_offer_address:
peer_addrs.add(addr)
if addr := self.private_address:
peer_addrs.add(addr)
return peer_addrs
class PostgreSQLApplication(RelationState):
"""An PostgreSQL Application is the peer application state.
This class defines state/relation data for a single PostgreSQL application.
"""
data_interface: DataPeerData
app: Application
def __init__(
self,
relation: Relation | None,
data_interface: DataPeerData,
component: Application,
substrate: Substrates,
):
"""Initialize the PostgreSQLApplication object."""
super().__init__(relation, data_interface, component)
self.app = component
self.data_interface = data_interface
self.substrate = substrate
@property
def replication_password(self) -> str | None:
"""Get replication user password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(REPLICATION_PASSWORD_KEY)
@property
def monitoring_password(self) -> str | None:
"""Get monitoring user password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(MONITORING_PASSWORD_KEY)
@property
def user_password(self) -> str | None:
"""Get operator user password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(USER_PASSWORD_KEY)
@property
def patroni_password(self) -> str | None:
"""Get Patroni REST API password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(PATRONI_PASSWORD_KEY)
# rewind-password
@property
def rewind_password(self) -> str | None:
"""Get rewind user password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(REWIND_PASSWORD_KEY)
@property
def raft_password(self) -> str | None:
"""Get raft user password.
Returns:
The password from the peer relation or None if the
password has not yet been set by the leader.
"""
return self.get_secret(RAFT_PASSWORD_KEY)
@property
def internal_ca(self) -> str | None:
"""Get internal CA.
Returns:
The internal CA from the peer relation or None if it has not yet been set by the leader.
"""
return self.get_secret("internal-ca")
@property
def internal_ca_key(self) -> str | None:
"""Get internal CA private key.
Returns:
The internal CA private key from the peer relation or None if it has not yet been set by the leader.
"""
return self.get_secret("internal-ca-key")
@property
def cluster_name(self) -> str:
"""Get cluster name.
Returns:
The cluster name, which is the same as the application name.
"""
if self.substrate == Substrates.K8S:
return f"patroni-{self.app.name}"
return self.app.name
@cached_property
def planned_units(self) -> int:
"""Get the number of planned units for the application."""
return self.app.planned_units()
@property
def members_ips(self) -> set[str]:
"""Returns the list of IPs addresses of the current members of the cluster."""
if not self.relation:
return set()
return set(json.loads(self.relation.data[self.app].get("members_ips", "[]")))
@property
def endpoints(self) -> set[str]:
"""Returns the list of endpoints of the current members of the cluster."""
if not self.relation:
return set()
return set(json.loads(self.relation.data[self.app].get("endpoints", "[]")))
@property
def is_cluster_initialised(self) -> bool:
"""Returns whether the cluster is already initialised."""
if not self.relation:
return False
return "cluster_initialised" in self.relation.data[self.app]
@property
def is_cluster_restoring_backup(self) -> bool:
"""Returns whether the cluster is restoring a backup."""
if not self.relation:
return False
return "restoring-backup" in self.relation.data[self.app]
@property
def is_cluster_restoring_to_time(self) -> bool:
"""Returns whether the cluster is restoring a backup to a specific time."""
if not self.relation:
return False
return "restore-to-time" in self.relation.data[self.app]
@property
def is_ldap_charm_related(self) -> bool:
"""Return whether this unit has an LDAP charm related."""
if not self.relation:
return False
return self.relation.data[self.app].get("ldap_enabled", "False") == "True"
@property
def is_ldap_enabled(self) -> bool:
"""Return whether this unit has LDAP enabled."""
return self.is_ldap_charm_related and self.is_cluster_initialised
@property
def user_hash(self) -> str | None:
"""Get the last-applied users hash from the peer relation data."""
if not self.relation:
return None
return self.relation.data[self.app].get("user_hash")
@user_hash.setter
def user_hash(self, value: str) -> None:
"""Set the last-applied users hash in the peer relation data."""
if not self.relation:
return
self.relation.data[self.app]["user_hash"] = value
def get_secret(self, key: str) -> str | None:
"""Get the secret value for 'key' from the peer relation data."""
if not self.relation:
return None
return self.data_interface.get_secret(self.relation.id, key)
def set_secret(self, key: str, value: str) -> None:
"""Set the secret value for 'key' in the peer relation data."""
if not self.relation:
return
self.data_interface.set_secret(self.relation.id, key, value)
def remove_secret(self, key: str) -> None:
"""Remove the secret value for 'key' from the peer relation data."""
if not self.relation:
return
self.data_interface.delete_relation_data(self.relation.id, [key])
@cached_property
def data(self) -> MutableMapping[str, str]:
"""Escape hatch method to access the peer data directly."""
if not self.relation:
return {}
return self.relation.data[self.app]