Merge lp:~silverdrake11/landscape-charm/manual_db_config_LNDENG-308 into lp:~mitchburton/landscape-charm/op-framework
- manual_db_config_LNDENG-308
- Merge into op-framework
Proposed by
Kevin Nasto
Status: | Merged |
---|---|
Merge reported by: | Mitch Burton |
Merged at revision: | not available |
Proposed branch: | lp:~silverdrake11/landscape-charm/manual_db_config_LNDENG-308 |
Merge into: | lp:~mitchburton/landscape-charm/op-framework |
Diff against target: |
465 lines (+295/-65) 4 files modified
config.yaml (+20/-0) src/charm.py (+77/-30) src/settings_files.py (+17/-0) tests/test_charm.py (+181/-35) |
To merge this branch: | bzr merge lp:~silverdrake11/landscape-charm/manual_db_config_LNDENG-308 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mitch Burton | Approve | ||
Review via email: mp+436859@code.launchpad.net |
Commit message
Added postgres manual configs to charm
Description of the change
To post a comment you must log in.
- 434. By Kevin Nasto
-
Added waiting status if is successful
Revision history for this message
Mitch Burton (mitchburton) wrote : | # |
LGTM after changes
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2022-10-18 18:27:37 +0000 |
3 | +++ config.yaml 2023-02-06 16:13:35 +0000 |
4 | @@ -115,3 +115,23 @@ |
5 | description: | |
6 | Comma-separated list of nagios servicegroups. If empty, the |
7 | nagios-context will be used as the servicegroup. |
8 | + db_host: |
9 | + type: string |
10 | + default: |
11 | + description: | |
12 | + Optionally specify the host instead of getting it from the postgres charm |
13 | + db_password: |
14 | + type: string |
15 | + default: |
16 | + description: Optionally specify db password |
17 | + db_port: |
18 | + type: string |
19 | + default: |
20 | + description: | |
21 | + Optionally specify the db port instead of getting it from the postgres |
22 | + charm. Falls back to the default postgres port |
23 | + db_user: |
24 | + type: string |
25 | + default: |
26 | + description: | |
27 | + Optionally specify the postgres user instead of getting it from the charm |
28 | |
29 | === modified file 'src/charm.py' |
30 | --- src/charm.py 2023-01-25 19:57:43 +0000 |
31 | +++ src/charm.py 2023-02-06 16:13:35 +0000 |
32 | @@ -37,7 +37,7 @@ |
33 | |
34 | from settings_files import ( |
35 | prepend_default_settings, update_default_settings, update_service_conf, |
36 | - write_license_file, write_ssl_cert) |
37 | + write_license_file, write_ssl_cert, update_db_conf, DEFAULT_POSTGRES_PORT) |
38 | |
39 | logger = logging.getLogger(__name__) |
40 | |
41 | @@ -185,6 +185,27 @@ |
42 | "package-upload": {"root-url": root_url}, |
43 | }) |
44 | |
45 | + config_host = self.model.config.get("db_host") |
46 | + config_password = self.model.config.get("db_password") |
47 | + config_port = self.model.config.get("db_port") |
48 | + config_user = self.model.config.get("db_user") |
49 | + db_kargs = {} |
50 | + if config_host: |
51 | + db_kargs["host"] = config_host |
52 | + if config_password: |
53 | + db_kargs["password"] = config_password |
54 | + if config_port: |
55 | + db_kargs["port"] = config_port |
56 | + if config_user: |
57 | + db_kargs["user"] = config_user |
58 | + if db_kargs: |
59 | + update_db_conf(**db_kargs) |
60 | + if self._migrate_schema_bootstrap(): |
61 | + self.unit.status = WaitingStatus("Waiting on relations") |
62 | + self._stored.ready["db"] = True |
63 | + else: |
64 | + return |
65 | + |
66 | if isinstance(prev_status, BlockedStatus): |
67 | self.unit.status = prev_status |
68 | |
69 | @@ -297,10 +318,15 @@ |
70 | unit_data = event.relation.data[event.unit] |
71 | |
72 | required_relation_data = ["master", "allowed-units", "port", "user"] |
73 | - missing_relation_data = [i for i in required_relation_data if i not in unit_data] |
74 | + missing_relation_data = [ |
75 | + i for i in required_relation_data if i not in unit_data |
76 | + ] |
77 | if missing_relation_data: |
78 | - logger.info("db relation not yet ready. Missing keys: {}".format( |
79 | - missing_relation_data)) |
80 | + logger.info( |
81 | + "db relation not yet ready. Missing keys: {}".format( |
82 | + missing_relation_data |
83 | + ) |
84 | + ) |
85 | self.unit.status = ActiveStatus("Unit is ready") |
86 | self._update_ready_status() |
87 | return |
88 | @@ -317,36 +343,57 @@ |
89 | |
90 | # We can't use unit_data["host"] because it can return the IP of the secondary |
91 | master = dict(s.split("=", 1) for s in unit_data["master"].split(" ")) |
92 | - host = master["host"] |
93 | - password = master["password"] |
94 | - port = unit_data["port"] |
95 | - user = unit_data["user"] |
96 | - |
97 | - update_service_conf({ |
98 | - "stores": { |
99 | - "host": "{}:{}".format(host, port), |
100 | - "password": password, |
101 | - }, |
102 | - "schema": { |
103 | - "store_user": user, |
104 | - "store_password": password, |
105 | - }, |
106 | - }) |
107 | - |
108 | - # Ensure the database users and schemas are set up. |
109 | + |
110 | + # Override db config if manually set in juju |
111 | + config_host = self.model.config.get("db_host") |
112 | + if config_host: |
113 | + host = config_host |
114 | + else: |
115 | + host = master["host"] |
116 | + |
117 | + config_password = self.model.config.get("db_password") |
118 | + if config_password: |
119 | + password = config_password |
120 | + else: |
121 | + password = master["password"] |
122 | + |
123 | + config_port = self.model.config.get("db_port") |
124 | + if config_port: |
125 | + port = config_port |
126 | + else: |
127 | + port = unit_data["port"] |
128 | + if not port: |
129 | + port = DEFAULT_POSTGRES_PORT # Fall back to postgres default port if still not set |
130 | + |
131 | + config_user = self.model.config.get("db_user") |
132 | + if config_user: |
133 | + user = config_user |
134 | + else: |
135 | + user = unit_data["user"] |
136 | + |
137 | + update_db_conf(host=host, port=port, user=user, password=password) |
138 | + |
139 | + if not self._migrate_schema_bootstrap(): |
140 | + return |
141 | + |
142 | + self._stored.ready["db"] = True |
143 | + self.unit.status = ActiveStatus("Unit is ready") |
144 | + self._update_ready_status() |
145 | + |
146 | + def _migrate_schema_bootstrap(self): |
147 | + """ |
148 | + Migrates schema along with the bootstrap command which ensures that the |
149 | + databases along with the landscape user exists. Returns True on success |
150 | + """ |
151 | try: |
152 | - check_call(["/usr/bin/landscape-schema", "--bootstrap"]) |
153 | + check_call([SCHEMA_SCRIPT, "--bootstrap"]) |
154 | + return True |
155 | except CalledProcessError as e: |
156 | logger.error( |
157 | "Landscape Server schema update failed with return code %d", |
158 | - e.returncode) |
159 | - self.unit.status = BlockedStatus( |
160 | - "Failed to update database schema") |
161 | - return |
162 | - |
163 | - self._stored.ready["db"] = True |
164 | - self.unit.status = ActiveStatus("Unit is ready") |
165 | - self._update_ready_status() |
166 | + e.returncode, |
167 | + ) |
168 | + self.unit.status = BlockedStatus("Failed to update database schema") |
169 | |
170 | def _amqp_relation_joined(self, event: RelationJoinedEvent) -> None: |
171 | self._stored.ready["amqp"] = False |
172 | |
173 | === modified file 'src/settings_files.py' |
174 | --- src/settings_files.py 2023-01-03 22:10:55 +0000 |
175 | +++ src/settings_files.py 2023-02-06 16:13:35 +0000 |
176 | @@ -7,6 +7,7 @@ |
177 | |
178 | import os |
179 | from base64 import b64decode, binascii |
180 | +from collections import defaultdict |
181 | from configparser import ConfigParser |
182 | from urllib.request import urlopen |
183 | from urllib.error import URLError |
184 | @@ -23,6 +24,8 @@ |
185 | SERVICE_CONF = "/etc/landscape/service.conf" |
186 | SSL_CERT_PATH = "/etc/ssl/certs/landscape_server_ca.crt" |
187 | |
188 | +DEFAULT_POSTGRES_PORT = "5432" |
189 | + |
190 | |
191 | class LicenseFileReadException(Exception): |
192 | pass |
193 | @@ -130,3 +133,17 @@ |
194 | except binascii.Error: |
195 | raise SSLCertReadException( |
196 | "Unable to decode b64-encoded SSL certificate") |
197 | + |
198 | + |
199 | +def update_db_conf(host=None, password=None, port=DEFAULT_POSTGRES_PORT, user=None): |
200 | + """Postgres specific settings override""" |
201 | + to_update = defaultdict(dict) |
202 | + if host: # Note that host is required if port is changed |
203 | + to_update["stores"]["host"] = "{}:{}".format(host, port) |
204 | + if password: |
205 | + to_update["stores"]["password"] = password |
206 | + to_update["schema"]["store_password"] = password |
207 | + if user: |
208 | + to_update["schema"]["store_user"] = user |
209 | + if to_update: |
210 | + update_service_conf(to_update) |
211 | \ No newline at end of file |
212 | |
213 | === modified file 'tests/test_charm.py' |
214 | --- tests/test_charm.py 2023-01-25 19:57:43 +0000 |
215 | +++ tests/test_charm.py 2023-02-06 16:13:35 +0000 |
216 | @@ -10,7 +10,7 @@ |
217 | from pwd import struct_passwd |
218 | from subprocess import CalledProcessError |
219 | from tempfile import TemporaryDirectory |
220 | -from unittest.mock import DEFAULT, Mock, patch |
221 | +from unittest.mock import DEFAULT, Mock, patch, call |
222 | |
223 | import yaml |
224 | |
225 | @@ -302,29 +302,108 @@ |
226 | "password": "testpass", |
227 | }, |
228 | } |
229 | - patches = patch.multiple( |
230 | - "charm", |
231 | - check_call=DEFAULT, |
232 | - update_service_conf=DEFAULT, |
233 | - ) |
234 | |
235 | - with patches as mocks: |
236 | - self.harness.charm._db_relation_changed(mock_event) |
237 | + with patch("charm.check_call") as check_call_mock: |
238 | + with patch( |
239 | + "settings_files.update_service_conf" |
240 | + ) as update_service_conf_mock: |
241 | + self.harness.charm._db_relation_changed(mock_event) |
242 | |
243 | status = self.harness.charm.unit.status |
244 | self.assertIsInstance(status, WaitingStatus) |
245 | self.assertTrue(self.harness.charm._stored.ready["db"]) |
246 | |
247 | - mocks["update_service_conf"].assert_called_once_with({ |
248 | - "stores": { |
249 | - "host": "1.2.3.4:5678", |
250 | - "password": "testpass", |
251 | - }, |
252 | - "schema": { |
253 | - "store_user": "testuser", |
254 | - "store_password": "testpass", |
255 | - }, |
256 | - }) |
257 | + update_service_conf_mock.assert_called_once_with( |
258 | + { |
259 | + "stores": { |
260 | + "host": "1.2.3.4:5678", |
261 | + "password": "testpass", |
262 | + }, |
263 | + "schema": { |
264 | + "store_user": "testuser", |
265 | + "store_password": "testpass", |
266 | + }, |
267 | + } |
268 | + ) |
269 | + |
270 | + def test_db_manual_configs_used(self): |
271 | + self.harness.disable_hooks() |
272 | + self.harness.update_config( |
273 | + { |
274 | + "db_host": "hello", |
275 | + "db_port": "world", |
276 | + "db_user": "test", |
277 | + "db_password": "test_pass", |
278 | + } |
279 | + ) |
280 | + mock_event = Mock() |
281 | + mock_event.relation.data = { |
282 | + mock_event.unit: { |
283 | + "allowed-units": self.harness.charm.unit.name, |
284 | + "master": "host=1.2.3.4 password=testpass", |
285 | + "host": "1.2.3.4", |
286 | + "port": "5678", |
287 | + "user": "testuser", |
288 | + "password": "testpass", |
289 | + }, |
290 | + } |
291 | + |
292 | + with patch("charm.check_call") as check_call_mock: |
293 | + with patch( |
294 | + "settings_files.update_service_conf" |
295 | + ) as update_service_conf_mock: |
296 | + self.harness.charm._db_relation_changed(mock_event) |
297 | + |
298 | + update_service_conf_mock.assert_called_once_with( |
299 | + { |
300 | + "stores": { |
301 | + "host": "hello:world", |
302 | + "password": "test_pass", |
303 | + }, |
304 | + "schema": { |
305 | + "store_user": "test", |
306 | + "store_password": "test_pass", |
307 | + }, |
308 | + } |
309 | + ) |
310 | + |
311 | + def test_db_manual_configs_used_partial(self): |
312 | + """ |
313 | + Test that if some of the manual configs are provided, the rest are |
314 | + gotten from the postgres unit |
315 | + """ |
316 | + self.harness.disable_hooks() |
317 | + self.harness.update_config({"db_host": "hello", "db_port": "world"}) |
318 | + mock_event = Mock() |
319 | + mock_event.relation.data = { |
320 | + mock_event.unit: { |
321 | + "allowed-units": self.harness.charm.unit.name, |
322 | + "master": "host=1.2.3.4 password=testpass", |
323 | + "host": "1.2.3.4", |
324 | + "port": "5678", |
325 | + "user": "testuser", |
326 | + "password": "testpass", |
327 | + }, |
328 | + } |
329 | + |
330 | + with patch("charm.check_call") as check_call_mock: |
331 | + with patch( |
332 | + "settings_files.update_service_conf" |
333 | + ) as update_service_conf_mock: |
334 | + self.harness.charm._db_relation_changed(mock_event) |
335 | + |
336 | + update_service_conf_mock.assert_called_once_with( |
337 | + { |
338 | + "stores": { |
339 | + "host": "hello:world", |
340 | + "password": "testpass", |
341 | + }, |
342 | + "schema": { |
343 | + "store_user": "testuser", |
344 | + "store_password": "testpass", |
345 | + }, |
346 | + } |
347 | + ) |
348 | |
349 | def test_db_relation_changed_called_process_error(self): |
350 | mock_event = Mock() |
351 | @@ -338,30 +417,97 @@ |
352 | "password": "testpass", |
353 | }, |
354 | } |
355 | - patches = patch.multiple( |
356 | - "charm", |
357 | - check_call=DEFAULT, |
358 | - update_service_conf=DEFAULT, |
359 | - ) |
360 | |
361 | - with patches as mocks: |
362 | - mocks["check_call"].side_effect = CalledProcessError(127, "ouch") |
363 | - self.harness.charm._db_relation_changed(mock_event) |
364 | + with patch("charm.check_call") as check_call_mock: |
365 | + with patch( |
366 | + "settings_files.update_service_conf" |
367 | + ) as update_service_conf_mock: |
368 | + check_call_mock.side_effect = CalledProcessError(127, "ouch") |
369 | + self.harness.charm._db_relation_changed(mock_event) |
370 | |
371 | status = self.harness.charm.unit.status |
372 | self.assertIsInstance(status, BlockedStatus) |
373 | self.assertFalse(self.harness.charm._stored.ready["db"]) |
374 | |
375 | - mocks["update_service_conf"].assert_called_once_with({ |
376 | - "stores": { |
377 | - "host": "1.2.3.4:5678", |
378 | - "password": "testpass", |
379 | - }, |
380 | - "schema": { |
381 | - "store_user": "testuser", |
382 | - "store_password": "testpass", |
383 | + update_service_conf_mock.assert_called_once_with( |
384 | + { |
385 | + "stores": { |
386 | + "host": "1.2.3.4:5678", |
387 | + "password": "testpass", |
388 | + }, |
389 | + "schema": { |
390 | + "store_user": "testuser", |
391 | + "store_password": "testpass", |
392 | + }, |
393 | } |
394 | - }) |
395 | + ) |
396 | + |
397 | + def test_on_manual_db_config_change(self): |
398 | + """ |
399 | + Test that the manual db settings are reflected if a config change happens later |
400 | + """ |
401 | + |
402 | + mock_event = Mock() |
403 | + mock_event.relation.data = { |
404 | + mock_event.unit: { |
405 | + "allowed-units": self.harness.charm.unit.name, |
406 | + "master": "host=1.2.3.4 password=testpass", |
407 | + "host": "1.2.3.4", |
408 | + "port": "5678", |
409 | + "user": "testuser", |
410 | + "password": "testpass", |
411 | + }, |
412 | + } |
413 | + |
414 | + with patch("charm.check_call") as check_call_mock: |
415 | + with patch( |
416 | + "settings_files.update_service_conf" |
417 | + ) as update_service_conf_mock: |
418 | + self.harness.charm._db_relation_changed(mock_event) |
419 | + self.harness.update_config({"db_host": "hello", "db_port": "world"}) |
420 | + |
421 | + self.assertEqual(update_service_conf_mock.call_count, 2) |
422 | + self.assertEqual( |
423 | + update_service_conf_mock.call_args_list[1], |
424 | + call( |
425 | + { |
426 | + "stores": { |
427 | + "host": "hello:world", |
428 | + }, |
429 | + } |
430 | + ), |
431 | + ) |
432 | + |
433 | + def test_on_manual_db_config_change_block_if_error(self): |
434 | + """ |
435 | + If the schema migration doesn't go through on a manual config change, |
436 | + then block unit status |
437 | + """ |
438 | + mock_event = Mock() |
439 | + mock_event.relation.data = { |
440 | + mock_event.unit: { |
441 | + "allowed-units": self.harness.charm.unit.name, |
442 | + "master": "host=1.2.3.4 password=testpass", |
443 | + "host": "1.2.3.4", |
444 | + "port": "5678", |
445 | + "user": "testuser", |
446 | + "password": "testpass", |
447 | + }, |
448 | + } |
449 | + |
450 | + with patch("charm.check_call") as check_call_mock: |
451 | + with patch( |
452 | + "settings_files.update_service_conf" |
453 | + ) as update_service_conf_mock: |
454 | + self.harness.charm._db_relation_changed(mock_event) |
455 | + |
456 | + with patch("charm.check_call") as check_call_mock: |
457 | + check_call_mock.side_effect = CalledProcessError(127, "ouch") |
458 | + self.harness.update_config({"db_host": "hello", "db_port": "world"}) |
459 | + |
460 | + status = self.harness.charm.unit.status |
461 | + self.assertIsInstance(status, BlockedStatus) |
462 | + |
463 | |
464 | def test_amqp_relation_joined(self): |
465 | unit = self.harness.charm.unit |
I'm getting a few test regressions when I run ./run_tests (after fixing the newline issue mentioned inline).
If these preexist this change, then I apologize. They still should be fixed though.
Here's tests I did and the results: .*-debversion postgresql- plpython3- *) in a non-juju managed LXC
1. deploy a juju model with landscape-server, haproxy, and rabbitmq-server
2. install postgresql and other deps (python3-apt postgresql-contrib postgresql-
3. edit pg_hba.conf and postgresql.conf to allow the landscape unit to manage postgres
4. juju config with all four db_* config options
Result:
Next test
Same as above except skip step 3
Result: "failed to update database schema"
then I performed step 3, ran juju config --reset db_host, db_password, db_port, db_user, then perofrmed step 4
Result: "failed to update database schema", even though databases and tables were created.
Looks like once that BlockedStatus (or WaitingStatus) is tripped, it's difficult to recover from it short of replacing the landscape-server unit entirely.
I think you need to update the _stored["db"] in _on_config_changed as you removed that update from _migrate_ schema_ bootstrap.