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
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

Subscribers

People subscribed via source and target branches

to all changes: