Merge lp:~silverdrake11/landscape-charm/manual_db_config_LNDENG-308 into lp:~mitchburton/landscape-charm/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
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

To post a comment you must log in.
Revision history for this message
Mitch Burton (mitchburton) wrote :

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:
1. deploy a juju model with landscape-server, haproxy, and rabbitmq-server
2. install postgresql and other deps (python3-apt postgresql-contrib postgresql-.*-debversion postgresql-plpython3-*) in a non-juju managed LXC
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.

review: Needs Fixing
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
=== modified file 'config.yaml'
--- config.yaml 2022-10-18 18:27:37 +0000
+++ config.yaml 2023-02-06 16:13:35 +0000
@@ -115,3 +115,23 @@
115 description: |115 description: |
116 Comma-separated list of nagios servicegroups. If empty, the116 Comma-separated list of nagios servicegroups. If empty, the
117 nagios-context will be used as the servicegroup.117 nagios-context will be used as the servicegroup.
118 db_host:
119 type: string
120 default:
121 description: |
122 Optionally specify the host instead of getting it from the postgres charm
123 db_password:
124 type: string
125 default:
126 description: Optionally specify db password
127 db_port:
128 type: string
129 default:
130 description: |
131 Optionally specify the db port instead of getting it from the postgres
132 charm. Falls back to the default postgres port
133 db_user:
134 type: string
135 default:
136 description: |
137 Optionally specify the postgres user instead of getting it from the charm
118138
=== modified file 'src/charm.py'
--- src/charm.py 2023-01-25 19:57:43 +0000
+++ src/charm.py 2023-02-06 16:13:35 +0000
@@ -37,7 +37,7 @@
3737
38from settings_files import (38from settings_files import (
39 prepend_default_settings, update_default_settings, update_service_conf,39 prepend_default_settings, update_default_settings, update_service_conf,
40 write_license_file, write_ssl_cert)40 write_license_file, write_ssl_cert, update_db_conf, DEFAULT_POSTGRES_PORT)
4141
42logger = logging.getLogger(__name__)42logger = logging.getLogger(__name__)
4343
@@ -185,6 +185,27 @@
185 "package-upload": {"root-url": root_url},185 "package-upload": {"root-url": root_url},
186 })186 })
187187
188 config_host = self.model.config.get("db_host")
189 config_password = self.model.config.get("db_password")
190 config_port = self.model.config.get("db_port")
191 config_user = self.model.config.get("db_user")
192 db_kargs = {}
193 if config_host:
194 db_kargs["host"] = config_host
195 if config_password:
196 db_kargs["password"] = config_password
197 if config_port:
198 db_kargs["port"] = config_port
199 if config_user:
200 db_kargs["user"] = config_user
201 if db_kargs:
202 update_db_conf(**db_kargs)
203 if self._migrate_schema_bootstrap():
204 self.unit.status = WaitingStatus("Waiting on relations")
205 self._stored.ready["db"] = True
206 else:
207 return
208
188 if isinstance(prev_status, BlockedStatus):209 if isinstance(prev_status, BlockedStatus):
189 self.unit.status = prev_status210 self.unit.status = prev_status
190211
@@ -297,10 +318,15 @@
297 unit_data = event.relation.data[event.unit]318 unit_data = event.relation.data[event.unit]
298319
299 required_relation_data = ["master", "allowed-units", "port", "user"]320 required_relation_data = ["master", "allowed-units", "port", "user"]
300 missing_relation_data = [i for i in required_relation_data if i not in unit_data]321 missing_relation_data = [
322 i for i in required_relation_data if i not in unit_data
323 ]
301 if missing_relation_data:324 if missing_relation_data:
302 logger.info("db relation not yet ready. Missing keys: {}".format(325 logger.info(
303 missing_relation_data))326 "db relation not yet ready. Missing keys: {}".format(
327 missing_relation_data
328 )
329 )
304 self.unit.status = ActiveStatus("Unit is ready")330 self.unit.status = ActiveStatus("Unit is ready")
305 self._update_ready_status()331 self._update_ready_status()
306 return332 return
@@ -317,36 +343,57 @@
317343
318 # We can't use unit_data["host"] because it can return the IP of the secondary344 # We can't use unit_data["host"] because it can return the IP of the secondary
319 master = dict(s.split("=", 1) for s in unit_data["master"].split(" "))345 master = dict(s.split("=", 1) for s in unit_data["master"].split(" "))
320 host = master["host"]346
321 password = master["password"]347 # Override db config if manually set in juju
322 port = unit_data["port"]348 config_host = self.model.config.get("db_host")
323 user = unit_data["user"]349 if config_host:
324350 host = config_host
325 update_service_conf({351 else:
326 "stores": {352 host = master["host"]
327 "host": "{}:{}".format(host, port),353
328 "password": password,354 config_password = self.model.config.get("db_password")
329 },355 if config_password:
330 "schema": {356 password = config_password
331 "store_user": user,357 else:
332 "store_password": password,358 password = master["password"]
333 },359
334 })360 config_port = self.model.config.get("db_port")
335361 if config_port:
336 # Ensure the database users and schemas are set up.362 port = config_port
363 else:
364 port = unit_data["port"]
365 if not port:
366 port = DEFAULT_POSTGRES_PORT # Fall back to postgres default port if still not set
367
368 config_user = self.model.config.get("db_user")
369 if config_user:
370 user = config_user
371 else:
372 user = unit_data["user"]
373
374 update_db_conf(host=host, port=port, user=user, password=password)
375
376 if not self._migrate_schema_bootstrap():
377 return
378
379 self._stored.ready["db"] = True
380 self.unit.status = ActiveStatus("Unit is ready")
381 self._update_ready_status()
382
383 def _migrate_schema_bootstrap(self):
384 """
385 Migrates schema along with the bootstrap command which ensures that the
386 databases along with the landscape user exists. Returns True on success
387 """
337 try:388 try:
338 check_call(["/usr/bin/landscape-schema", "--bootstrap"])389 check_call([SCHEMA_SCRIPT, "--bootstrap"])
390 return True
339 except CalledProcessError as e:391 except CalledProcessError as e:
340 logger.error(392 logger.error(
341 "Landscape Server schema update failed with return code %d",393 "Landscape Server schema update failed with return code %d",
342 e.returncode)394 e.returncode,
343 self.unit.status = BlockedStatus(395 )
344 "Failed to update database schema")396 self.unit.status = BlockedStatus("Failed to update database schema")
345 return
346
347 self._stored.ready["db"] = True
348 self.unit.status = ActiveStatus("Unit is ready")
349 self._update_ready_status()
350397
351 def _amqp_relation_joined(self, event: RelationJoinedEvent) -> None:398 def _amqp_relation_joined(self, event: RelationJoinedEvent) -> None:
352 self._stored.ready["amqp"] = False399 self._stored.ready["amqp"] = False
353400
=== modified file 'src/settings_files.py'
--- src/settings_files.py 2023-01-03 22:10:55 +0000
+++ src/settings_files.py 2023-02-06 16:13:35 +0000
@@ -7,6 +7,7 @@
77
8import os8import os
9from base64 import b64decode, binascii9from base64 import b64decode, binascii
10from collections import defaultdict
10from configparser import ConfigParser11from configparser import ConfigParser
11from urllib.request import urlopen12from urllib.request import urlopen
12from urllib.error import URLError13from urllib.error import URLError
@@ -23,6 +24,8 @@
23SERVICE_CONF = "/etc/landscape/service.conf"24SERVICE_CONF = "/etc/landscape/service.conf"
24SSL_CERT_PATH = "/etc/ssl/certs/landscape_server_ca.crt"25SSL_CERT_PATH = "/etc/ssl/certs/landscape_server_ca.crt"
2526
27DEFAULT_POSTGRES_PORT = "5432"
28
2629
27class LicenseFileReadException(Exception):30class LicenseFileReadException(Exception):
28 pass31 pass
@@ -130,3 +133,17 @@
130 except binascii.Error:133 except binascii.Error:
131 raise SSLCertReadException(134 raise SSLCertReadException(
132 "Unable to decode b64-encoded SSL certificate")135 "Unable to decode b64-encoded SSL certificate")
136
137
138def update_db_conf(host=None, password=None, port=DEFAULT_POSTGRES_PORT, user=None):
139 """Postgres specific settings override"""
140 to_update = defaultdict(dict)
141 if host: # Note that host is required if port is changed
142 to_update["stores"]["host"] = "{}:{}".format(host, port)
143 if password:
144 to_update["stores"]["password"] = password
145 to_update["schema"]["store_password"] = password
146 if user:
147 to_update["schema"]["store_user"] = user
148 if to_update:
149 update_service_conf(to_update)
133\ No newline at end of file150\ No newline at end of file
134151
=== modified file 'tests/test_charm.py'
--- tests/test_charm.py 2023-01-25 19:57:43 +0000
+++ tests/test_charm.py 2023-02-06 16:13:35 +0000
@@ -10,7 +10,7 @@
10from pwd import struct_passwd10from pwd import struct_passwd
11from subprocess import CalledProcessError11from subprocess import CalledProcessError
12from tempfile import TemporaryDirectory12from tempfile import TemporaryDirectory
13from unittest.mock import DEFAULT, Mock, patch13from unittest.mock import DEFAULT, Mock, patch, call
1414
15import yaml15import yaml
1616
@@ -302,29 +302,108 @@
302 "password": "testpass",302 "password": "testpass",
303 },303 },
304 }304 }
305 patches = patch.multiple(
306 "charm",
307 check_call=DEFAULT,
308 update_service_conf=DEFAULT,
309 )
310305
311 with patches as mocks:306 with patch("charm.check_call") as check_call_mock:
312 self.harness.charm._db_relation_changed(mock_event)307 with patch(
308 "settings_files.update_service_conf"
309 ) as update_service_conf_mock:
310 self.harness.charm._db_relation_changed(mock_event)
313311
314 status = self.harness.charm.unit.status312 status = self.harness.charm.unit.status
315 self.assertIsInstance(status, WaitingStatus)313 self.assertIsInstance(status, WaitingStatus)
316 self.assertTrue(self.harness.charm._stored.ready["db"])314 self.assertTrue(self.harness.charm._stored.ready["db"])
317315
318 mocks["update_service_conf"].assert_called_once_with({316 update_service_conf_mock.assert_called_once_with(
319 "stores": {317 {
320 "host": "1.2.3.4:5678",318 "stores": {
321 "password": "testpass",319 "host": "1.2.3.4:5678",
322 },320 "password": "testpass",
323 "schema": {321 },
324 "store_user": "testuser",322 "schema": {
325 "store_password": "testpass",323 "store_user": "testuser",
326 },324 "store_password": "testpass",
327 })325 },
326 }
327 )
328
329 def test_db_manual_configs_used(self):
330 self.harness.disable_hooks()
331 self.harness.update_config(
332 {
333 "db_host": "hello",
334 "db_port": "world",
335 "db_user": "test",
336 "db_password": "test_pass",
337 }
338 )
339 mock_event = Mock()
340 mock_event.relation.data = {
341 mock_event.unit: {
342 "allowed-units": self.harness.charm.unit.name,
343 "master": "host=1.2.3.4 password=testpass",
344 "host": "1.2.3.4",
345 "port": "5678",
346 "user": "testuser",
347 "password": "testpass",
348 },
349 }
350
351 with patch("charm.check_call") as check_call_mock:
352 with patch(
353 "settings_files.update_service_conf"
354 ) as update_service_conf_mock:
355 self.harness.charm._db_relation_changed(mock_event)
356
357 update_service_conf_mock.assert_called_once_with(
358 {
359 "stores": {
360 "host": "hello:world",
361 "password": "test_pass",
362 },
363 "schema": {
364 "store_user": "test",
365 "store_password": "test_pass",
366 },
367 }
368 )
369
370 def test_db_manual_configs_used_partial(self):
371 """
372 Test that if some of the manual configs are provided, the rest are
373 gotten from the postgres unit
374 """
375 self.harness.disable_hooks()
376 self.harness.update_config({"db_host": "hello", "db_port": "world"})
377 mock_event = Mock()
378 mock_event.relation.data = {
379 mock_event.unit: {
380 "allowed-units": self.harness.charm.unit.name,
381 "master": "host=1.2.3.4 password=testpass",
382 "host": "1.2.3.4",
383 "port": "5678",
384 "user": "testuser",
385 "password": "testpass",
386 },
387 }
388
389 with patch("charm.check_call") as check_call_mock:
390 with patch(
391 "settings_files.update_service_conf"
392 ) as update_service_conf_mock:
393 self.harness.charm._db_relation_changed(mock_event)
394
395 update_service_conf_mock.assert_called_once_with(
396 {
397 "stores": {
398 "host": "hello:world",
399 "password": "testpass",
400 },
401 "schema": {
402 "store_user": "testuser",
403 "store_password": "testpass",
404 },
405 }
406 )
328407
329 def test_db_relation_changed_called_process_error(self):408 def test_db_relation_changed_called_process_error(self):
330 mock_event = Mock()409 mock_event = Mock()
@@ -338,30 +417,97 @@
338 "password": "testpass",417 "password": "testpass",
339 },418 },
340 }419 }
341 patches = patch.multiple(
342 "charm",
343 check_call=DEFAULT,
344 update_service_conf=DEFAULT,
345 )
346420
347 with patches as mocks:421 with patch("charm.check_call") as check_call_mock:
348 mocks["check_call"].side_effect = CalledProcessError(127, "ouch")422 with patch(
349 self.harness.charm._db_relation_changed(mock_event)423 "settings_files.update_service_conf"
424 ) as update_service_conf_mock:
425 check_call_mock.side_effect = CalledProcessError(127, "ouch")
426 self.harness.charm._db_relation_changed(mock_event)
350427
351 status = self.harness.charm.unit.status428 status = self.harness.charm.unit.status
352 self.assertIsInstance(status, BlockedStatus)429 self.assertIsInstance(status, BlockedStatus)
353 self.assertFalse(self.harness.charm._stored.ready["db"])430 self.assertFalse(self.harness.charm._stored.ready["db"])
354431
355 mocks["update_service_conf"].assert_called_once_with({432 update_service_conf_mock.assert_called_once_with(
356 "stores": {433 {
357 "host": "1.2.3.4:5678",434 "stores": {
358 "password": "testpass",435 "host": "1.2.3.4:5678",
359 },436 "password": "testpass",
360 "schema": {437 },
361 "store_user": "testuser",438 "schema": {
362 "store_password": "testpass",439 "store_user": "testuser",
440 "store_password": "testpass",
441 },
363 }442 }
364 })443 )
444
445 def test_on_manual_db_config_change(self):
446 """
447 Test that the manual db settings are reflected if a config change happens later
448 """
449
450 mock_event = Mock()
451 mock_event.relation.data = {
452 mock_event.unit: {
453 "allowed-units": self.harness.charm.unit.name,
454 "master": "host=1.2.3.4 password=testpass",
455 "host": "1.2.3.4",
456 "port": "5678",
457 "user": "testuser",
458 "password": "testpass",
459 },
460 }
461
462 with patch("charm.check_call") as check_call_mock:
463 with patch(
464 "settings_files.update_service_conf"
465 ) as update_service_conf_mock:
466 self.harness.charm._db_relation_changed(mock_event)
467 self.harness.update_config({"db_host": "hello", "db_port": "world"})
468
469 self.assertEqual(update_service_conf_mock.call_count, 2)
470 self.assertEqual(
471 update_service_conf_mock.call_args_list[1],
472 call(
473 {
474 "stores": {
475 "host": "hello:world",
476 },
477 }
478 ),
479 )
480
481 def test_on_manual_db_config_change_block_if_error(self):
482 """
483 If the schema migration doesn't go through on a manual config change,
484 then block unit status
485 """
486 mock_event = Mock()
487 mock_event.relation.data = {
488 mock_event.unit: {
489 "allowed-units": self.harness.charm.unit.name,
490 "master": "host=1.2.3.4 password=testpass",
491 "host": "1.2.3.4",
492 "port": "5678",
493 "user": "testuser",
494 "password": "testpass",
495 },
496 }
497
498 with patch("charm.check_call") as check_call_mock:
499 with patch(
500 "settings_files.update_service_conf"
501 ) as update_service_conf_mock:
502 self.harness.charm._db_relation_changed(mock_event)
503
504 with patch("charm.check_call") as check_call_mock:
505 check_call_mock.side_effect = CalledProcessError(127, "ouch")
506 self.harness.update_config({"db_host": "hello", "db_port": "world"})
507
508 status = self.harness.charm.unit.status
509 self.assertIsInstance(status, BlockedStatus)
510
365511
366 def test_amqp_relation_joined(self):512 def test_amqp_relation_joined(self):
367 unit = self.harness.charm.unit513 unit = self.harness.charm.unit

Subscribers

People subscribed via source and target branches

to all changes: