Skip to content

Commit fa94af5

Browse files
committed
Add system users secret management.
1 parent 9f92dde commit fa94af5

File tree

6 files changed

+85
-29
lines changed

6 files changed

+85
-29
lines changed

charmcraft.yaml

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

2727
config:
2828
options:
29+
cluster_name:
30+
# TODO: description
31+
type: string
32+
default: Test Cluster
2933
profile:
3034
# TODO: description
3135
type: string
3236
default: production
33-
cluster_name:
37+
system_users:
3438
# TODO: description
35-
type: string
36-
default: Test Cluster
39+
type: secret
40+
default: ""
3741

3842
peers:
3943
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 & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
CollectStatusEvent,
1414
ConfigChangedEvent,
1515
InstallEvent,
16+
ModelError,
1617
Object,
18+
SecretChangedEvent,
19+
SecretNotFoundError,
1720
StartEvent,
1821
UpdateStatusEvent,
1922
)
@@ -60,6 +63,7 @@ def __init__(
6063
self.framework.observe(self.charm.on.start, self._on_start)
6164
self.framework.observe(self.charm.on.install, self._on_install)
6265
self.framework.observe(self.charm.on.config_changed, self._on_config_changed)
66+
self.framework.observe(self.charm.on.secret_changed, self._on_secret_changed)
6367
self.framework.observe(self.charm.on.update_status, self._on_update_status)
6468
self.framework.observe(self.charm.on.collect_unit_status, self._on_collect_unit_status)
6569
self.framework.observe(self.charm.on.collect_app_status, self._on_collect_app_status)
@@ -91,21 +95,20 @@ def _on_start(self, event: StartEvent) -> None:
9195
return
9296

9397
try:
94-
if self.charm.unit.is_leader():
95-
self.state.cluster.cluster_name = self.charm.config.cluster_name
96-
self.state.cluster.seeds = [self.state.unit.peer_url]
9798
self.config_manager.render_env(
9899
cassandra_limit_memory_mb=1024 if self.charm.config.profile == "testing" else None
99100
)
101+
if self.charm.unit.is_leader():
102+
self.state.cluster.cluster_name = self.charm.config.cluster_name
103+
self.state.cluster.seeds = [self.state.unit.peer_url]
104+
self.state.cluster.cassandra_password_secret = self._acquire_cassandra_password()
105+
self._start_password_change(event)
106+
return
100107
except ValidationError as e:
101108
logger.debug(f"Config haven't passed validation: {e}")
102109
event.defer()
103110
return
104111

105-
if not self.state.cluster.cassandra_password_secret:
106-
self._start_password_change(event)
107-
return
108-
109112
self.config_manager.render_cassandra_config(
110113
cluster_name=self.state.cluster.cluster_name,
111114
listen_address=self.state.unit.ip,
@@ -118,9 +121,27 @@ def _on_start(self, event: StartEvent) -> None:
118121
)
119122
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
120123

124+
def _acquire_cassandra_password(self) -> str:
125+
if self.charm.config.system_users:
126+
try:
127+
if (
128+
password := self.model.get_secret(id=self.charm.config.system_users)
129+
.get_content(refresh=True)
130+
.get("cassandra-password")
131+
):
132+
return password
133+
except SecretNotFoundError:
134+
# TODO: logging.
135+
pass
136+
except ModelError:
137+
pass
138+
if self.state.cluster.cassandra_password_secret:
139+
return self.state.cluster.cassandra_password_secret
140+
return self.workload.generate_password()
141+
121142
def _start_password_change(self, event: StartEvent) -> None:
122143
self.config_manager.render_cassandra_config(
123-
cluster_name=self.charm.config.cluster_name,
144+
cluster_name=self.state.cluster.cluster_name,
124145
listen_address="127.0.0.1",
125146
seeds=["127.0.0.1:7000"],
126147
authentication=False,
@@ -133,26 +154,31 @@ def _finalize_password_change(self, event: StartEvent) -> None:
133154
if not self.cluster_manager.is_healthy:
134155
event.defer()
135156
return
136-
password = self.workload.generate_password()
137-
self.database_manager.update_system_user_password("cassandra", password)
138-
self.state.cluster.cassandra_password_secret = password
139-
self.cluster_manager.flush_tables("system_auth", ["roles"])
157+
self.database_manager.update_system_user_password(
158+
"cassandra", self.state.cluster.cassandra_password_secret
159+
)
160+
self.cluster_manager.prepare_shutdown()
140161
self.config_manager.render_cassandra_config(
141-
cluster_name=self.charm.config.cluster_name,
162+
cluster_name=self.state.cluster.cluster_name,
142163
listen_address=self.state.unit.ip,
143164
seeds=self.state.cluster.seeds,
144165
authentication=True,
145166
)
146167
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
147168

148-
def _on_config_changed(self, _: ConfigChangedEvent) -> None:
149-
# TODO: add peer relation change hook for subordinates to update leader address too
150-
if self.state.unit.workload_state not in [
151-
UnitWorkloadState.STARTING,
152-
UnitWorkloadState.ACTIVE,
153-
]:
169+
def _on_config_changed(self, event: ConfigChangedEvent) -> None:
170+
if self.state.unit.workload_state == UnitWorkloadState.INSTALLING:
171+
return
172+
if self.state.unit.workload_state != UnitWorkloadState.ACTIVE:
173+
event.defer()
154174
return
155175
try:
176+
if self.charm.unit.is_leader() and self.state.cluster.cassandra_password_secret != (
177+
password := self._acquire_cassandra_password()
178+
):
179+
self.database_manager.update_system_user_password("cassandra", password)
180+
self.state.cluster.cassandra_password_secret = password
181+
self.cluster_manager.prepare_shutdown()
156182
# TODO: cluster_name change
157183
self.config_manager.render_env(
158184
cassandra_limit_memory_mb=1024 if self.charm.config.profile == "testing" else None
@@ -171,8 +197,30 @@ def _on_config_changed(self, _: ConfigChangedEvent) -> None:
171197
self.state.unit.peer_tls.rotation = False
172198
self.state.unit.client_tls.rotation = False
173199

174-
if self.state.unit.workload_state == UnitWorkloadState.ACTIVE:
175-
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
200+
self.charm.on[str(self.bootstrap_manager.name)].acquire_lock.emit()
201+
202+
def _on_secret_changed(self, event: SecretChangedEvent) -> None:
203+
if not self.charm.unit.is_leader():
204+
return
205+
206+
if self.state.unit.workload_state == UnitWorkloadState.INSTALLING:
207+
return
208+
209+
try:
210+
if event.secret.id != self.charm.config.system_users:
211+
return
212+
213+
if self.state.unit.workload_state != UnitWorkloadState.ACTIVE:
214+
event.defer()
215+
return
216+
217+
if self.state.cluster.cassandra_password_secret != (
218+
password := self._acquire_cassandra_password()
219+
):
220+
self.database_manager.update_system_user_password("cassandra", password)
221+
self.state.cluster.cassandra_password_secret = password
222+
except ValidationError:
223+
return
176224

177225
def _on_update_status(self, _: UpdateStatusEvent) -> None:
178226
# 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
@@ -109,7 +109,7 @@ def exec(
109109
check=True,
110110
text=True,
111111
capture_output=True,
112-
timeout=10,
112+
timeout=300,
113113
cwd=cwd,
114114
)
115115
stdout = result.stdout.strip()

0 commit comments

Comments
 (0)