Skip to content

Commit ed85571

Browse files
committed
Add system users secret management.
1 parent d6fb2fe commit ed85571

File tree

6 files changed

+85
-28
lines changed

6 files changed

+85
-28
lines changed

charmcraft.yaml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ charm-libs:
2525

2626
config:
2727
options:
28+
cluster_name:
29+
# TODO: description
30+
type: string
31+
default: Test Cluster
2832
profile:
2933
# TODO: description
3034
type: string
3135
default: production
32-
cluster_name:
36+
system_users:
3337
# TODO: description
34-
type: string
35-
default: Test Cluster
38+
type: secret
39+
default: ""
3640

3741
peers:
3842
cassandra-peers:

src/core/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class CharmConfig(BaseConfigModel):
1616
"""Structured charm config."""
1717

1818
cluster_name: str
19+
system_users: str
1920
profile: ConfigProfile
2021

2122
@field_validator("cluster_name")

src/events/cassandra.py

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
CollectStatusEvent,
1313
ConfigChangedEvent,
1414
InstallEvent,
15+
ModelError,
1516
Object,
17+
SecretChangedEvent,
18+
SecretNotFoundError,
1619
StartEvent,
1720
UpdateStatusEvent,
1821
)
@@ -54,6 +57,7 @@ def __init__(
5457
self.framework.observe(self.charm.on.start, self._on_start)
5558
self.framework.observe(self.charm.on.install, self._on_install)
5659
self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
60+
self.framework.observe(self.charm.on.secret_changed, self._on_secret_changed)
5761
self.framework.observe(self.charm.on.update_status, self._on_update_status)
5862
self.framework.observe(self.charm.on.collect_unit_status, self._on_collect_unit_status)
5963
self.framework.observe(self.charm.on.collect_app_status, self._on_collect_app_status)
@@ -75,21 +79,20 @@ def _on_start(self, event: StartEvent) -> None:
7579
return
7680

7781
try:
78-
if self.charm.unit.is_leader():
79-
self.state.cluster.cluster_name = self.charm.config.cluster_name
80-
self.state.cluster.seeds = [self.state.unit.peer_url]
8182
self.config_manager.render_env(
8283
cassandra_limit_memory_mb=1024 if self.charm.config.profile == "testing" else None
8384
)
85+
if self.charm.unit.is_leader():
86+
self.state.cluster.cluster_name = self.charm.config.cluster_name
87+
self.state.cluster.seeds = [self.state.unit.peer_url]
88+
self.state.cluster.cassandra_password_secret = self._acquire_cassandra_password()
89+
self._start_password_change(event)
90+
return
8491
except ValidationError as e:
8592
logger.debug(f"Config haven't passed validation: {e}")
8693
event.defer()
8794
return
8895

89-
if not self.state.cluster.cassandra_password_secret:
90-
self._start_password_change(event)
91-
return
92-
9396
self.config_manager.render_cassandra_config(
9497
cluster_name=self.state.cluster.cluster_name,
9598
listen_address=self.state.unit.ip,
@@ -98,9 +101,27 @@ def _on_start(self, event: StartEvent) -> None:
98101
)
99102
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
100103

104+
def _acquire_cassandra_password(self) -> str:
105+
if self.charm.config.system_users:
106+
try:
107+
if (
108+
password := self.model.get_secret(id=self.charm.config.system_users)
109+
.get_content(refresh=True)
110+
.get("cassandra-password")
111+
):
112+
return password
113+
except SecretNotFoundError:
114+
# TODO: logging.
115+
pass
116+
except ModelError:
117+
pass
118+
if self.state.cluster.cassandra_password_secret:
119+
return self.state.cluster.cassandra_password_secret
120+
return self.workload.generate_password()
121+
101122
def _start_password_change(self, event: StartEvent) -> None:
102123
self.config_manager.render_cassandra_config(
103-
cluster_name=self.charm.config.cluster_name,
124+
cluster_name=self.state.cluster.cluster_name,
104125
listen_address="127.0.0.1",
105126
seeds=["127.0.0.1:7000"],
106127
authentication=False,
@@ -113,25 +134,31 @@ def _finalize_password_change(self, event: StartEvent) -> None:
113134
if not self.cluster_manager.is_healthy:
114135
event.defer()
115136
return
116-
password = self.workload.generate_password()
117-
self.database_manager.update_system_user_password("cassandra", password)
118-
self.state.cluster.cassandra_password_secret = password
119-
self.cluster_manager.flush_tables("system_auth", ["roles"])
137+
self.database_manager.update_system_user_password(
138+
"cassandra", self.state.cluster.cassandra_password_secret
139+
)
140+
self.cluster_manager.prepare_shutdown()
120141
self.config_manager.render_cassandra_config(
121-
cluster_name=self.charm.config.cluster_name,
142+
cluster_name=self.state.cluster.cluster_name,
122143
listen_address=self.state.unit.ip,
123144
seeds=self.state.cluster.seeds,
124145
authentication=True,
125146
)
126147
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
127148

128-
def _on_config_changed(self, _: ConfigChangedEvent) -> None:
129-
if self.state.unit.workload_state not in [
130-
UnitWorkloadState.STARTING,
131-
UnitWorkloadState.ACTIVE,
132-
]:
149+
def _on_config_changed(self, event: ConfigChangedEvent) -> None:
150+
if self.state.unit.workload_state == UnitWorkloadState.INSTALLING:
151+
return
152+
if self.state.unit.workload_state != UnitWorkloadState.ACTIVE:
153+
event.defer()
133154
return
134155
try:
156+
if self.charm.unit.is_leader() and self.state.cluster.cassandra_password_secret != (
157+
password := self._acquire_cassandra_password()
158+
):
159+
self.database_manager.update_system_user_password("cassandra", password)
160+
self.state.cluster.cassandra_password_secret = password
161+
self.cluster_manager.prepare_shutdown()
135162
# TODO: cluster_name change
136163
self.config_manager.render_env(
137164
cassandra_limit_memory_mb=1024 if self.charm.config.profile == "testing" else None
@@ -141,8 +168,30 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
141168
logger.debug(f"Config haven't passed validation: {e}")
142169
return
143170

144-
if self.state.unit.workload_state == UnitWorkloadState.ACTIVE:
145-
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
171+
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
172+
173+
def _on_secret_changed(self, event: SecretChangedEvent) -> None:
174+
if not self.charm.unit.is_leader():
175+
return
176+
177+
if self.state.unit.workload_state == UnitWorkloadState.INSTALLING:
178+
return
179+
180+
try:
181+
if event.secret.id != self.charm.config.system_users:
182+
return
183+
184+
if self.state.unit.workload_state != UnitWorkloadState.ACTIVE:
185+
event.defer()
186+
return
187+
188+
if self.state.cluster.cassandra_password_secret != (
189+
password := self._acquire_cassandra_password()
190+
):
191+
self.database_manager.update_system_user_password("cassandra", password)
192+
self.state.cluster.cassandra_password_secret = password
193+
except ValidationError:
194+
return
146195

147196
def _on_update_status(self, _: UpdateStatusEvent) -> None:
148197
# TODO: add peer relation change hook for subordinates to update leader address too

src/managers/cluster.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ def network_address(self) -> tuple[str, str]:
3535
hostname = socket.gethostname()
3636
return socket.gethostbyname(hostname), hostname
3737

38-
def flush_tables(self, keyspace: str, tables: list[str]) -> None:
39-
"""Flush tables in keyspace to disk."""
40-
self._workload.exec(["charmed-cassandra.nodetool", "flush", keyspace, *tables])
38+
def prepare_shutdown(self) -> None:
39+
"""TODO."""
40+
self._workload.exec([_NODETOOL, "drain"])

src/managers/database.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ def __init__(self, hosts: list[str], user: str, password: str):
3333
def update_system_user_password(self, user: str, password: str) -> None:
3434
"""Change password for the role in system_auth."""
3535
with self._session() as session:
36+
# TODO: increase replication factor of system_auth.
3637
session.execute(
37-
"UPDATE system_auth.roles SET salted_hash = %s WHERE role = %s",
38+
"UPDATE system_auth.roles SET"
39+
" can_login = true, is_superuser = true, salted_hash = %s"
40+
" WHERE role = %s",
3841
[
3942
hashpw(password.encode(), gensalt(prefix=b"2a")).decode(),
4043
user,

src/workload.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def exec(self, command: list[str], suppress_error_log: bool = False) -> tuple[st
102102
check=True,
103103
text=True,
104104
capture_output=True,
105-
timeout=10,
105+
timeout=300,
106106
)
107107
stdout = result.stdout.strip()
108108
stderr = result.stderr.strip()

0 commit comments

Comments
 (0)