postgresql-charms-single-kernel 16.3.0__tar.gz → 16.3.2__tar.gz
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.
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/PKG-INFO +8 -3
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/pyproject.toml +15 -8
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/abstract_charm.py +4 -12
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/k8s_charm.py +1 -1
- postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/config/exceptions.py +75 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/literals.py +14 -1
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/peer_relation.py +71 -1
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/state.py +83 -7
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/postgresql.py +2 -2
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/base.py +1 -5
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/cluster.py +11 -10
- postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/managers/config.py +295 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/k8s.py +2 -3
- postgresql_charms_single_kernel-16.3.2/single_kernel_postgresql/managers/patroni.py +896 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/tls.py +2 -3
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/base.py +5 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/k8s.py +15 -6
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/base.py +16 -2
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/k8s.py +15 -4
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/vm.py +16 -4
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/vm.py +1 -1
- postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/config/exceptions.py +0 -27
- postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/managers/config.py +0 -194
- postgresql_charms_single_kernel-16.3.0/single_kernel_postgresql/managers/patroni.py +0 -109
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/LICENSE +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/README.md +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charmcraft.yaml +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/charms/vm_charm.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/compat/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/enums.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/locales.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/pyproject.toml +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/config/statuses.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/config.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/core/relation_state.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/events/tls_transfer.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/lib/charms/data_platform_libs/v0/data_interfaces.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/lib/charms/data_platform_libs/v1/data_models.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/managers/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/arch.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/filesystem.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/postgresql.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/secret.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/utils/status.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/__init__.py +0 -0
- {postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/single_kernel_postgresql/workload/paths/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: postgresql-charms-single-kernel
|
|
3
|
-
Version: 16.3.
|
|
3
|
+
Version: 16.3.2
|
|
4
4
|
Summary: Shared and reusable code for PostgreSQL-related charms
|
|
5
5
|
Author: Canonical Data Platform
|
|
6
6
|
Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
|
|
@@ -211,11 +211,10 @@ Classifier: Intended Audience :: Developers
|
|
|
211
211
|
Classifier: Operating System :: POSIX :: Linux
|
|
212
212
|
Requires-Dist: ops>=2.0.0
|
|
213
213
|
Requires-Dist: tenacity>=9.0.0
|
|
214
|
-
Requires-Dist: psycopg2>=2.9.10 ; extra == '
|
|
214
|
+
Requires-Dist: psycopg2>=2.9.10 ; extra == 'db-driver'
|
|
215
215
|
Requires-Dist: httpx ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
216
216
|
Requires-Dist: data-platform-helpers>=0.1.7 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
217
217
|
Requires-Dist: charmlibs-pathops>=1.0.1 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
218
|
-
Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
219
218
|
Requires-Dist: charmlibs-interfaces-tls-certificates>=1.8.3 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
220
219
|
Requires-Dist: charm-refresh ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
221
220
|
Requires-Dist: requests ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
@@ -223,8 +222,14 @@ Requires-Dist: tomli ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
|
223
222
|
Requires-Dist: pydantic>=2.0 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
224
223
|
Requires-Dist: jinja2>=3.1.6 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
225
224
|
Requires-Dist: charmlibs-rollingops>=1.1.1 ; python_full_version >= '3.12' and extra == 'postgresql'
|
|
225
|
+
Requires-Dist: pysyncobj>=0.3.15 ; python_full_version >= '3.12' and extra == 'vm'
|
|
226
|
+
Requires-Dist: psutil>=7.2.2 ; python_full_version >= '3.12' and extra == 'vm'
|
|
227
|
+
Requires-Dist: charmlibs-snap>=1.0.1 ; python_full_version >= '3.12' and extra == 'vm'
|
|
228
|
+
Requires-Dist: charmlibs-systemd>=1.0.0.post0 ; python_full_version >= '3.12' and extra == 'vm'
|
|
226
229
|
Requires-Python: >=3.8, <4.0
|
|
230
|
+
Provides-Extra: db-driver
|
|
227
231
|
Provides-Extra: postgresql
|
|
232
|
+
Provides-Extra: vm
|
|
228
233
|
Description-Content-Type: text/markdown
|
|
229
234
|
|
|
230
235
|
# postgresql-single-kernel-library
|
{postgresql_charms_single_kernel-16.3.0 → postgresql_charms_single_kernel-16.3.2}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "postgresql-charms-single-kernel"
|
|
6
6
|
description = "Shared and reusable code for PostgreSQL-related charms"
|
|
7
|
-
version = "16.3.
|
|
7
|
+
version = "16.3.2"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
license = {file = "LICENSE"}
|
|
10
10
|
authors = [
|
|
@@ -23,11 +23,9 @@ dependencies = [
|
|
|
23
23
|
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
postgresql = [
|
|
26
|
-
"psycopg2>=2.9.10",
|
|
27
26
|
"httpx; python_version >= '3.12'",
|
|
28
27
|
"data-platform-helpers>=0.1.7; python_version >= '3.12'",
|
|
29
28
|
"charmlibs-pathops>=1.0.1; python_version >= '3.12'",
|
|
30
|
-
"charmlibs-snap>=1.0.1; python_version >= '3.12'",
|
|
31
29
|
"charmlibs-interfaces-tls-certificates>=1.8.3; python_version >= '3.12'",
|
|
32
30
|
"charm-refresh; python_version >= '3.12'",
|
|
33
31
|
"requests; python_version >= '3.12'",
|
|
@@ -35,7 +33,16 @@ postgresql = [
|
|
|
35
33
|
"pydantic>=2.0; python_version >= '3.12'",
|
|
36
34
|
"jinja2 >=3.1.6; python_version >= '3.12'",
|
|
37
35
|
"charmlibs-rollingops>=1.1.1; python_version >= '3.12'",
|
|
38
|
-
|
|
36
|
+
]
|
|
37
|
+
vm = [
|
|
38
|
+
"pysyncobj>=0.3.15; python_version >= '3.12'",
|
|
39
|
+
"psutil>=7.2.2; python_version >= '3.12'",
|
|
40
|
+
"charmlibs-snap>=1.0.1; python_version >= '3.12'",
|
|
41
|
+
"charmlibs-systemd>=1.0.0.post0; python_version >= '3.12'"
|
|
42
|
+
]
|
|
43
|
+
db-driver = [
|
|
44
|
+
"psycopg2>=2.9.10",
|
|
45
|
+
]
|
|
39
46
|
|
|
40
47
|
[build-system]
|
|
41
48
|
requires = ["uv_build>=0.11.0,<0.12.0"]
|
|
@@ -47,15 +54,15 @@ module-root = ""
|
|
|
47
54
|
|
|
48
55
|
[dependency-groups]
|
|
49
56
|
format = [
|
|
50
|
-
"ruff==0.15.
|
|
57
|
+
"ruff==0.15.20"
|
|
51
58
|
]
|
|
52
59
|
lint = [
|
|
53
60
|
"codespell==2.4.2",
|
|
54
|
-
"ty==0.0.
|
|
61
|
+
"ty==0.0.55"
|
|
55
62
|
]
|
|
56
63
|
unit = [
|
|
57
|
-
"coverage[toml]==7.14.
|
|
58
|
-
"pytest==9.1.
|
|
64
|
+
"coverage[toml]==7.14.3",
|
|
65
|
+
"pytest==9.1.1"
|
|
59
66
|
]
|
|
60
67
|
|
|
61
68
|
|
|
@@ -29,18 +29,10 @@ class AbstractPostgreSQLCharm(CharmBase, ABC):
|
|
|
29
29
|
self.state = CharmState(charm=self, substrate=self.substrate)
|
|
30
30
|
|
|
31
31
|
# Managers
|
|
32
|
-
self.
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
self.
|
|
36
|
-
state=self.state, workload=self.workload, client=self.postgresql
|
|
37
|
-
)
|
|
38
|
-
self.config_manager = ConfigManager(
|
|
39
|
-
state=self.state, workload=self.workload, client=self.postgresql
|
|
40
|
-
)
|
|
41
|
-
self.patroni_manager = PatroniManager(
|
|
42
|
-
state=self.state, workload=self.workload, client=self.postgresql
|
|
43
|
-
)
|
|
32
|
+
self.tls_manager = TLSManager(state=self.state, workload=self.workload)
|
|
33
|
+
self.patroni_manager = PatroniManager(state=self.state, workload=self.workload)
|
|
34
|
+
self.cluster_manager = ClusterManager(state=self.state, workload=self.workload)
|
|
35
|
+
self.config_manager = ConfigManager(state=self.state, workload=self.workload)
|
|
44
36
|
|
|
45
37
|
# Events Handler
|
|
46
38
|
self.postgresql_events_handler = PostgreSQLEventsHandler(
|
|
@@ -25,7 +25,7 @@ class PostgreSQLK8sCharm(AbstractPostgreSQLCharm):
|
|
|
25
25
|
assert isinstance(self.workload, K8sWorkload), ( # noqa: S101
|
|
26
26
|
"Workload must be an instance of K8sWorkload"
|
|
27
27
|
)
|
|
28
|
-
self.k8s_manager = K8sManager(self.state, self.workload
|
|
28
|
+
self.k8s_manager = K8sManager(self.state, self.workload)
|
|
29
29
|
|
|
30
30
|
@property
|
|
31
31
|
def postgresql(self) -> PostgreSQL:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2025 Canonical Ltd.
|
|
3
|
+
# See LICENSE file for licensing details.
|
|
4
|
+
|
|
5
|
+
"""Charm-specific exceptions."""
|
|
6
|
+
|
|
7
|
+
from single_kernel_postgresql.compat.postgresql import PostgreSQLBaseError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PostgreSQLFileOperationError(PostgreSQLBaseError):
|
|
11
|
+
"""Exception thrown when file operations related to PostgreSQL fail."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StorageUnavailableError(Exception):
|
|
15
|
+
"""Cannot find storage mountpoint."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SettingSystemPasswordError(PostgreSQLBaseError):
|
|
19
|
+
"""Exception thrown when setting the system password fails."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PostgreSQLCannotConnectError(Exception):
|
|
23
|
+
"""Cannot run smoke check on connected Database."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TlsError(Exception):
|
|
27
|
+
"""TLS implementation internal exception."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RaftPostgresqlNotUpError(Exception):
|
|
31
|
+
"""Postgresql not yet started."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RaftPostgresqlStillUpError(Exception):
|
|
35
|
+
"""Postgresql not yet down."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RaftNotPromotedError(Exception):
|
|
39
|
+
"""Leader not yet set when reinitialising raft."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ClusterNotPromotedError(Exception):
|
|
43
|
+
"""Raised when a cluster is not promoted."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NotReadyError(Exception):
|
|
47
|
+
"""Raised when not all cluster members healthy or finished initial sync."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class EndpointNotReadyError(Exception):
|
|
51
|
+
"""Raised when an endpoint is not ready."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class StandbyClusterAlreadyPromotedError(Exception):
|
|
55
|
+
"""Raised when a standby cluster is already promoted."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RemoveRaftMemberFailedError(Exception):
|
|
59
|
+
"""Raised when a remove raft member failed for some reason."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AddRaftMemberFailedError(Exception):
|
|
63
|
+
"""Raised when adding raft member failed for some reason."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SwitchoverFailedError(Exception):
|
|
67
|
+
"""Raised when a switchover failed for some reason."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SwitchoverNotSyncError(SwitchoverFailedError):
|
|
71
|
+
"""Raised when a switchover failed because node is not sync."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class UpdateSyncNodeCountError(Exception):
|
|
75
|
+
"""Raised when updating synchronous_node_count failed for some reason."""
|
|
@@ -31,6 +31,9 @@ SNAP_COMMON = "common"
|
|
|
31
31
|
SNAP = "/snap/charmed-postgresql/current"
|
|
32
32
|
VM_LOGS_PATH = "var/log/postgresql"
|
|
33
33
|
VM_DATA_PATH = "var/lib/postgresql"
|
|
34
|
+
VM_ARCHIVE_PATH = "data/archive"
|
|
35
|
+
VM_DATA_LOGS_PATH = "data/logs"
|
|
36
|
+
VM_TEMP_PATH = "data/temp"
|
|
34
37
|
|
|
35
38
|
## K8s Paths
|
|
36
39
|
K8S_DATA_PATH = "var/lib/pg/data"
|
|
@@ -42,7 +45,9 @@ K8S_DATA_PATH = "var/lib/pg/data"
|
|
|
42
45
|
# e.g. snap_current / "/etc/postgresql" will result in "/etc/postgresql" instead of "/var/snap/postgresql/current/etc/postgresql"
|
|
43
46
|
POSTGRESQL_CONF_PATH = "etc/postgresql"
|
|
44
47
|
POSTGRESQL_CONF_FILE = "postgresql.conf"
|
|
45
|
-
|
|
48
|
+
## Pgbackreest Paths
|
|
49
|
+
PGBACKREST_CONF_PATH = "etc/pgbackrest"
|
|
50
|
+
PGBACKREST_CONF_FILE = "pgbackrest.conf"
|
|
46
51
|
|
|
47
52
|
## TLS Paths
|
|
48
53
|
TLS_CA_BUNDLE_FILE = "peer_ca_bundle.pem"
|
|
@@ -161,3 +166,11 @@ WATCHER_RELATION = "watcher"
|
|
|
161
166
|
WATCHER_USER = "watcher"
|
|
162
167
|
WATCHER_PASSWORD_KEY = "watcher-password" # noqa: S105
|
|
163
168
|
WATCHER_SECRET_LABEL = "watcher-secret" # noqa: S105
|
|
169
|
+
|
|
170
|
+
# Raft
|
|
171
|
+
RAFT_PORT = 2222
|
|
172
|
+
RAFT_PARTNER_PREFIX = "partner_node_status_server_"
|
|
173
|
+
|
|
174
|
+
# VM services
|
|
175
|
+
VM_PATRONI_SERVICE_NAME = "snap.charmed-postgresql.patroni.service"
|
|
176
|
+
VM_PATRONI_SERVICE_DEFAULT_PATH = f"/etc/systemd/system/{VM_PATRONI_SERVICE_NAME}"
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
"""State objects for database-peers relation."""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
from collections.abc import MutableMapping
|
|
10
|
+
from functools import cached_property
|
|
9
11
|
|
|
10
12
|
from ops import Application, BlockedStatus, Relation, Unit
|
|
11
13
|
|
|
@@ -177,6 +179,34 @@ class PostgreSQLPeer(RelationState):
|
|
|
177
179
|
peer_addrs.add(addr)
|
|
178
180
|
return peer_addrs
|
|
179
181
|
|
|
182
|
+
@property
|
|
183
|
+
def is_unit_departing(self) -> bool:
|
|
184
|
+
"""Returns whether the unit is departing."""
|
|
185
|
+
if not self.relation:
|
|
186
|
+
return False
|
|
187
|
+
return "departing" in self.relation.data[self.unit]
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def is_unit_stopped(self) -> bool:
|
|
191
|
+
"""Returns whether the unit is stopped."""
|
|
192
|
+
if not self.relation:
|
|
193
|
+
return False
|
|
194
|
+
return "stopped" in self.relation.data[self.unit]
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def is_connectivity_enabled(self) -> bool:
|
|
198
|
+
"""Return whether this unit can be connected externally."""
|
|
199
|
+
if not self.relation:
|
|
200
|
+
return True
|
|
201
|
+
return self.relation.data[self.unit].get("connectivity", "on") == "on"
|
|
202
|
+
|
|
203
|
+
@cached_property
|
|
204
|
+
def data(self) -> MutableMapping[str, str]:
|
|
205
|
+
"""Escape hatch method to access the peer data directly."""
|
|
206
|
+
if not self.relation:
|
|
207
|
+
return {}
|
|
208
|
+
return self.relation.data[self.unit]
|
|
209
|
+
|
|
180
210
|
|
|
181
211
|
class PostgreSQLApplication(RelationState):
|
|
182
212
|
"""An PostgreSQL Application is the peer application state.
|
|
@@ -290,7 +320,7 @@ class PostgreSQLApplication(RelationState):
|
|
|
290
320
|
return f"patroni-{self.app.name}"
|
|
291
321
|
return self.app.name
|
|
292
322
|
|
|
293
|
-
@
|
|
323
|
+
@cached_property
|
|
294
324
|
def planned_units(self) -> int:
|
|
295
325
|
"""Get the number of planned units for the application."""
|
|
296
326
|
return self.app.planned_units()
|
|
@@ -309,6 +339,39 @@ class PostgreSQLApplication(RelationState):
|
|
|
309
339
|
return set()
|
|
310
340
|
return set(json.loads(self.relation.data[self.app].get("endpoints", "[]")))
|
|
311
341
|
|
|
342
|
+
@property
|
|
343
|
+
def is_cluster_initialised(self) -> bool:
|
|
344
|
+
"""Returns whether the cluster is already initialised."""
|
|
345
|
+
if not self.relation:
|
|
346
|
+
return False
|
|
347
|
+
return "cluster_initialised" in self.relation.data[self.app]
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def is_cluster_restoring_backup(self) -> bool:
|
|
351
|
+
"""Returns whether the cluster is restoring a backup."""
|
|
352
|
+
if not self.relation:
|
|
353
|
+
return False
|
|
354
|
+
return "restoring-backup" in self.relation.data[self.app]
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def is_cluster_restoring_to_time(self) -> bool:
|
|
358
|
+
"""Returns whether the cluster is restoring a backup to a specific time."""
|
|
359
|
+
if not self.relation:
|
|
360
|
+
return False
|
|
361
|
+
return "restore-to-time" in self.relation.data[self.app]
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def is_ldap_charm_related(self) -> bool:
|
|
365
|
+
"""Return whether this unit has an LDAP charm related."""
|
|
366
|
+
if not self.relation:
|
|
367
|
+
return False
|
|
368
|
+
return self.relation.data[self.app].get("ldap_enabled", "False") == "True"
|
|
369
|
+
|
|
370
|
+
@property
|
|
371
|
+
def is_ldap_enabled(self) -> bool:
|
|
372
|
+
"""Return whether this unit has LDAP enabled."""
|
|
373
|
+
return self.is_ldap_charm_related and self.is_cluster_initialised
|
|
374
|
+
|
|
312
375
|
def get_secret(self, key: str) -> str | None:
|
|
313
376
|
"""Get the secret value for 'key' from the peer relation data."""
|
|
314
377
|
if not self.relation:
|
|
@@ -326,3 +389,10 @@ class PostgreSQLApplication(RelationState):
|
|
|
326
389
|
if not self.relation:
|
|
327
390
|
return
|
|
328
391
|
self.data_interface.delete_relation_data(self.relation.id, [key])
|
|
392
|
+
|
|
393
|
+
@cached_property
|
|
394
|
+
def data(self) -> MutableMapping[str, str]:
|
|
395
|
+
"""Escape hatch method to access the peer data directly."""
|
|
396
|
+
if not self.relation:
|
|
397
|
+
return {}
|
|
398
|
+
return self.relation.data[self.app]
|
|
@@ -6,17 +6,30 @@
|
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
8
|
import socket
|
|
9
|
+
from contextlib import suppress
|
|
9
10
|
from functools import cached_property
|
|
10
11
|
from typing import TYPE_CHECKING, Any, get_args
|
|
11
12
|
|
|
12
13
|
from data_platform_helpers.advanced_statuses import StatusesState, StatusObject
|
|
13
14
|
from data_platform_helpers.advanced_statuses.types import Scope as AdvancedStatusesScope
|
|
14
|
-
from ops import
|
|
15
|
+
from ops import (
|
|
16
|
+
ConfigData,
|
|
17
|
+
JujuVersion,
|
|
18
|
+
ModelError,
|
|
19
|
+
Object,
|
|
20
|
+
Relation,
|
|
21
|
+
RelationNotFoundError,
|
|
22
|
+
SecretNotFoundError,
|
|
23
|
+
Unit,
|
|
24
|
+
)
|
|
15
25
|
|
|
16
26
|
from single_kernel_postgresql.config.enums import Substrates
|
|
17
27
|
from single_kernel_postgresql.config.literals import (
|
|
18
28
|
APP_SCOPE,
|
|
29
|
+
DATABASE,
|
|
19
30
|
PEER_RELATION,
|
|
31
|
+
REPLICATION_CONSUMER_RELATION,
|
|
32
|
+
REPLICATION_OFFER_RELATION,
|
|
20
33
|
SCOPES,
|
|
21
34
|
STATUS_PEERS_RELATION,
|
|
22
35
|
)
|
|
@@ -153,6 +166,27 @@ class CharmState(Object):
|
|
|
153
166
|
if binding := self.model.get_binding(PEER_RELATION):
|
|
154
167
|
return str(binding.network.bind_address)
|
|
155
168
|
|
|
169
|
+
@property
|
|
170
|
+
def database_ip(self) -> str | None:
|
|
171
|
+
"""Database endpoint address."""
|
|
172
|
+
with suppress(RelationNotFoundError):
|
|
173
|
+
if binding := self.model.get_binding(DATABASE):
|
|
174
|
+
return str(binding.network.bind_address)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def replication_offer_ip(self) -> str | None:
|
|
178
|
+
"""Async replication offer endpoint address."""
|
|
179
|
+
with suppress(RelationNotFoundError):
|
|
180
|
+
if binding := self.model.get_binding(REPLICATION_OFFER_RELATION):
|
|
181
|
+
return str(binding.network.bind_address)
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def replication_consumer_ip(self) -> str | None:
|
|
185
|
+
"""Async replication consumer endpoint address."""
|
|
186
|
+
with suppress(RelationNotFoundError):
|
|
187
|
+
if binding := self.model.get_binding(REPLICATION_CONSUMER_RELATION):
|
|
188
|
+
return str(binding.network.bind_address)
|
|
189
|
+
|
|
156
190
|
@property
|
|
157
191
|
def fqdn(self) -> str | None:
|
|
158
192
|
"""Current unit fqdn."""
|
|
@@ -165,7 +199,7 @@ class CharmState(Object):
|
|
|
165
199
|
def endpoint(self) -> str | None:
|
|
166
200
|
"""Current unit endpoint."""
|
|
167
201
|
if self.substrate == Substrates.K8S:
|
|
168
|
-
return self.
|
|
202
|
+
return f"{self.peer.unit.name.replace('/', '-')}.{self.application.app.name}-endpoints"
|
|
169
203
|
else:
|
|
170
204
|
return self.unit_ip
|
|
171
205
|
|
|
@@ -173,9 +207,11 @@ class CharmState(Object):
|
|
|
173
207
|
def endpoints(self) -> set[str]:
|
|
174
208
|
"""Returns the list of endpoints of the current members of the cluster."""
|
|
175
209
|
if self.peer_relation:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
210
|
+
if self.substrate == Substrates.K8S:
|
|
211
|
+
return self.application.endpoints
|
|
212
|
+
else:
|
|
213
|
+
return self.peer_members_ips
|
|
214
|
+
return {self.endpoint} if self.endpoint else set()
|
|
179
215
|
|
|
180
216
|
@property
|
|
181
217
|
def model_name(self) -> str:
|
|
@@ -185,7 +221,7 @@ class CharmState(Object):
|
|
|
185
221
|
@cached_property
|
|
186
222
|
def patroni_url(self) -> str:
|
|
187
223
|
"""Patroni REST API URL."""
|
|
188
|
-
return f"https://{self.
|
|
224
|
+
return f"https://{self.endpoint}:8008"
|
|
189
225
|
|
|
190
226
|
@property
|
|
191
227
|
def peer_members_ips(self) -> set[str]:
|
|
@@ -227,7 +263,12 @@ class CharmState(Object):
|
|
|
227
263
|
ips = []
|
|
228
264
|
if self.unit_ip:
|
|
229
265
|
ips.append(self.unit_ip)
|
|
230
|
-
|
|
266
|
+
if self.database_ip and self.database_ip not in ips:
|
|
267
|
+
ips.append(self.database_ip)
|
|
268
|
+
if self.replication_offer_ip and self.replication_offer_ip not in ips:
|
|
269
|
+
ips.append(self.replication_offer_ip)
|
|
270
|
+
if self.replication_consumer_ip and self.replication_consumer_ip not in ips:
|
|
271
|
+
ips.append(self.replication_consumer_ip)
|
|
231
272
|
return ips
|
|
232
273
|
|
|
233
274
|
# -- Secrets
|
|
@@ -403,3 +444,38 @@ class CharmState(Object):
|
|
|
403
444
|
if re.fullmatch(regex_pattern, present_status.message) is not None:
|
|
404
445
|
return present_status
|
|
405
446
|
return None
|
|
447
|
+
|
|
448
|
+
@cached_property
|
|
449
|
+
def synchronous_node_count(self) -> int:
|
|
450
|
+
"""Number of expected sync standbys."""
|
|
451
|
+
planned_units = self.application.planned_units
|
|
452
|
+
if self.config.synchronous_node_count == "all":
|
|
453
|
+
return planned_units - 1
|
|
454
|
+
elif self.config.synchronous_node_count == "majority":
|
|
455
|
+
return planned_units // 2
|
|
456
|
+
# -1 for leader
|
|
457
|
+
return (
|
|
458
|
+
self.config.synchronous_node_count
|
|
459
|
+
if self.config.synchronous_node_count < planned_units - 1
|
|
460
|
+
else planned_units - 1
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
@cached_property
|
|
464
|
+
def synchronous_configuration(self) -> dict[str, Any]:
|
|
465
|
+
"""Synchronous mode configuration."""
|
|
466
|
+
# Try to update synchronous_node_count.
|
|
467
|
+
return {
|
|
468
|
+
"synchronous_node_count": self.synchronous_node_count,
|
|
469
|
+
"synchronous_mode_strict": len(self.application.members_ips) > 1
|
|
470
|
+
and self.config.synchronous_mode_strict
|
|
471
|
+
and self.synchronous_node_count > 0,
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
def _build_service_name(self, service: str) -> str:
|
|
475
|
+
"""Build a full k8s service name based on the service name."""
|
|
476
|
+
return f"{self.application.app.name}-{service}.{self.model_name}.svc.cluster.local"
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def primary_endpoint(self) -> str:
|
|
480
|
+
"""Returns the endpoint of the primary instance's service."""
|
|
481
|
+
return self._build_service_name("primary")
|
|
@@ -177,8 +177,8 @@ class PostgreSQLEventsHandler(Object):
|
|
|
177
177
|
|
|
178
178
|
self.tls_manager.configure_internal_peer_ca()
|
|
179
179
|
|
|
180
|
-
if self.charm.substrate == Substrates.VM:
|
|
181
|
-
|
|
180
|
+
# if self.charm.substrate == Substrates.VM:
|
|
181
|
+
# self.config_manager.update_config()
|
|
182
182
|
|
|
183
183
|
# TODO: Add next steps of leader elected
|
|
184
184
|
|
|
@@ -11,7 +11,6 @@ from data_platform_helpers.advanced_statuses.types import Scope as AdvancedStatu
|
|
|
11
11
|
|
|
12
12
|
from single_kernel_postgresql.config.statuses import GeneralStatuses
|
|
13
13
|
from single_kernel_postgresql.core.state import CharmState
|
|
14
|
-
from single_kernel_postgresql.utils.postgresql import PostgreSQL as PostgreSQLClient
|
|
15
14
|
from single_kernel_postgresql.workload.base import BaseWorkload
|
|
16
15
|
|
|
17
16
|
logger = logging.getLogger(__name__)
|
|
@@ -25,13 +24,10 @@ class BaseManager(ManagerStatusProtocol):
|
|
|
25
24
|
|
|
26
25
|
state: CharmState
|
|
27
26
|
|
|
28
|
-
def __init__(
|
|
29
|
-
self, state: CharmState, workload: BaseWorkload, name: str, client: PostgreSQLClient
|
|
30
|
-
):
|
|
27
|
+
def __init__(self, state: CharmState, workload: BaseWorkload, name: str):
|
|
31
28
|
self.state = state
|
|
32
29
|
self.workload = workload
|
|
33
30
|
self.name = name
|
|
34
|
-
self.postgresql_client = client
|
|
35
31
|
|
|
36
32
|
def get_statuses(
|
|
37
33
|
self, scope: AdvancedStatusesScope, recompute: bool = False
|
|
@@ -34,7 +34,6 @@ from single_kernel_postgresql.managers.base import BaseManager
|
|
|
34
34
|
from single_kernel_postgresql.utils import new_password
|
|
35
35
|
from single_kernel_postgresql.utils.postgresql import PostgreSQL as PostgreSQLClient
|
|
36
36
|
from single_kernel_postgresql.workload.base import BaseWorkload
|
|
37
|
-
from single_kernel_postgresql.workload.vm import VMWorkload
|
|
38
37
|
|
|
39
38
|
logger = logging.getLogger(__name__)
|
|
40
39
|
|
|
@@ -45,15 +44,15 @@ class ClusterManager(BaseManager):
|
|
|
45
44
|
This manager is responsible for handling cluster-wide operations.
|
|
46
45
|
"""
|
|
47
46
|
|
|
48
|
-
def __init__(self, state: CharmState, workload: BaseWorkload
|
|
49
|
-
super().__init__(state, workload, "cluster_manager"
|
|
47
|
+
def __init__(self, state: CharmState, workload: BaseWorkload):
|
|
48
|
+
super().__init__(state, workload, "cluster_manager")
|
|
50
49
|
|
|
51
50
|
def install_workload(self) -> None:
|
|
52
51
|
"""Install the workload."""
|
|
53
|
-
if self.state.substrate == Substrates.VM
|
|
54
|
-
self.workload.install_snap_package(revision=None)
|
|
55
|
-
self.workload.create_snap_alias("patronictl")
|
|
56
|
-
self.workload.create_snap_alias("psql")
|
|
52
|
+
if self.state.substrate == Substrates.VM:
|
|
53
|
+
self.workload.install_snap_package(revision=None) # type: ignore
|
|
54
|
+
self.workload.create_snap_alias("patronictl") # type:ignore
|
|
55
|
+
self.workload.create_snap_alias("psql") # type: ignore
|
|
57
56
|
else:
|
|
58
57
|
logger.debug(
|
|
59
58
|
"No workload installation steps defined for substrate %s", self.state.substrate
|
|
@@ -108,14 +107,16 @@ class ClusterManager(BaseManager):
|
|
|
108
107
|
except ModelError:
|
|
109
108
|
logger.exception("failed to open port")
|
|
110
109
|
|
|
111
|
-
def can_connect_to_postgresql(
|
|
110
|
+
def can_connect_to_postgresql(
|
|
111
|
+
self, postgresql_client: PostgreSQLClient, retry: bool = True
|
|
112
|
+
) -> bool:
|
|
112
113
|
"""Whether the local PostgreSQL instance is reachable and responding."""
|
|
113
|
-
if not
|
|
114
|
+
if not postgresql_client.password or not postgresql_client.current_host:
|
|
114
115
|
return False
|
|
115
116
|
|
|
116
117
|
def _check_connection():
|
|
117
118
|
try:
|
|
118
|
-
if not
|
|
119
|
+
if not postgresql_client.get_postgresql_timezones():
|
|
119
120
|
logger.debug("Cannot connect to database (CannotConnectError)")
|
|
120
121
|
raise PostgreSQLCannotConnectError
|
|
121
122
|
except Exception as e:
|