Merge ~cjwatson/launchpad-layers:db-layer into launchpad-layers:main

Proposed by Colin Watson
Status: Merged
Merge reported by: Colin Watson
Merged at revision: 42a4b4c4f62936b1d050c775e84f7364dfb5efc0
Proposed branch: ~cjwatson/launchpad-layers:db-layer
Merge into: launchpad-layers:main
Prerequisite: ~cjwatson/launchpad-layers:payload-layer
Diff against target: 557 lines (+219/-170)
14 files modified
launchpad-base/config.yaml (+0/-11)
launchpad-base/layer.yaml (+0/-1)
launchpad-base/lib/charms/launchpad/base.py (+0/-110)
launchpad-base/metadata.yaml (+0/-2)
launchpad-base/reactive/launchpad-base.py (+3/-34)
launchpad-base/templates/launchpad-base-lazr.conf (+0/-4)
launchpad-db/config.yaml (+12/-0)
launchpad-db/layer.yaml (+4/-0)
launchpad-db/lib/charms/launchpad/db.py (+121/-0)
launchpad-db/metadata.yaml (+3/-0)
launchpad-db/reactive/launchpad-db.py (+57/-0)
launchpad-db/templates/launchpad-db-lazr.conf (+19/-0)
launchpad-payload/lib/charms/launchpad/payload.py (+0/-4)
launchpad-payload/reactive/launchpad-payload.py (+0/-4)
Reviewer Review Type Date Requested Status
Clinton Fung Approve
Guruprasad Approve
Review via email: mp+442155@code.launchpad.net

Commit message

Split out a launchpad-db layer

Description of the change

`launchpad-loggerhead` needs most of the `launchpad-base` layer, but doesn't have any direct database connectivity, so we don't want to require it to have a `db` relation before it will do anything. Split out database interaction into a separate layer.

This will require applications to change: they will need to import some functions from `charms.launchpad.db` rather than `charms.launchpad.base`, they will need to react to `launchpad.db.configured` rather than `launchpad.base.configured`, and their LAZR configuration files will need to extend `launchpad-db-lazr.conf` rather than `launchpad-base-lazr.conf`.

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍🏼

review: Approve
Revision history for this message
Clinton Fung (clinton-fung) wrote :

Makes sense

review: Approve
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/launchpad-base/config.yaml b/launchpad-base/config.yaml
2index b804392..3263531 100644
3--- a/launchpad-base/config.yaml
4+++ b/launchpad-base/config.yaml
5@@ -43,17 +43,6 @@ options:
6 type: string
7 description: URL of file used to control whether cron scripts run.
8 default: "file:cronscripts.ini"
9- databases:
10- type: string
11- description: >
12- YAML-encoded information about database relations, overriding the
13- defaults. For example, setting this to "db: {name: launchpad_prod}"
14- changes the name of the database used by the "db" relation.
15- default: ""
16- db_statement_timeout:
17- type: int
18- description: SQL statement timeout in milliseconds.
19- default:
20 default_batch_size:
21 type: int
22 description: The default size used in a batched listing of results.
23diff --git a/launchpad-base/layer.yaml b/launchpad-base/layer.yaml
24index 5abc6ac..ed3e8dc 100644
25--- a/launchpad-base/layer.yaml
26+++ b/launchpad-base/layer.yaml
27@@ -1,6 +1,5 @@
28 includes:
29 - layer:basic
30- - layer:ols-pg
31 - interface:rabbitmq
32 - layer:launchpad-payload
33 repo: https://git.launchpad.net/launchpad-layers
34diff --git a/launchpad-base/lib/charms/launchpad/base.py b/launchpad-base/lib/charms/launchpad/base.py
35index 2d441de..043cd4f 100644
36--- a/launchpad-base/lib/charms/launchpad/base.py
37+++ b/launchpad-base/lib/charms/launchpad/base.py
38@@ -1,22 +1,16 @@
39 # Copyright 2022 Canonical Ltd. This software is licensed under the
40 # GNU Affero General Public License version 3 (see the file LICENSE).
41
42-import grp
43 import os.path
44-import pwd
45-import re
46 import subprocess
47-from dataclasses import dataclass
48 from email.utils import parseaddr
49 from urllib.parse import urlparse
50
51 from charmhelpers.core import hookenv, host, templating
52 from charms.launchpad.payload import config_file_path
53 from charms.launchpad.payload import get_service_config as get_payload_config
54-from charms.launchpad.payload import home_dir
55 from charms.reactive import endpoint_from_flag
56 from ols import base
57-from psycopg2.extensions import make_dsn, parse_dsn
58
59
60 def oopses_dir():
61@@ -136,107 +130,3 @@ def configure_rsync(config, template, name):
62 templating.render(template, rsync_path, config, perms=0o644)
63 elif os.path.exists(rsync_path):
64 os.unlink(rsync_path)
65-
66-
67-@dataclass
68-class PgPassLine:
69- hostname: str
70- port: str
71- database: str
72- username: str
73- password: str
74-
75-
76-def update_pgpass(dsn):
77- # See https://www.postgresql.org/docs/current/libpq-pgpass.html.
78-
79- def unescape(entry):
80- return re.sub(r"\\(.)", r"\1", entry)
81-
82- def escape(entry):
83- return re.sub(r"([:\\])", r"\\\1", entry)
84-
85- parsed_dsn = parse_dsn(dsn)
86- pgpass_path = os.path.join(home_dir(), ".pgpass")
87- pgpass = []
88- try:
89- with open(pgpass_path) as f:
90- for line in f:
91- if line.startswith("#"):
92- continue
93- match = re.match(
94- r"""
95- ^
96- (?P<hostname>(?:[^:\\]|\\.)*):
97- (?P<port>(?:[^:\\]|\\.)*):
98- (?P<database>(?:[^:\\]|\\.)*):
99- (?P<username>(?:[^:\\]|\\.)*):
100- (?P<password>(?:[^:\\]|\\.)*)
101- $
102- """,
103- line.rstrip("\n"),
104- flags=re.X,
105- )
106- if match is None:
107- continue
108- pgpass.append(
109- PgPassLine(
110- hostname=unescape(match.group("hostname")),
111- port=unescape(match.group("port")),
112- database=unescape(match.group("database")),
113- username=unescape(match.group("username")),
114- password=unescape(match.group("password")),
115- )
116- )
117- except OSError:
118- pass
119-
120- modified = False
121- for line in pgpass:
122- if (
123- line.hostname in ("*", parsed_dsn["host"])
124- and line.port in ("*", parsed_dsn["port"])
125- and line.database in ("*", parsed_dsn["dbname"])
126- and line.username in ("*", parsed_dsn["user"])
127- ):
128- if line.password != parsed_dsn["password"]:
129- line.password = parsed_dsn["password"]
130- modified = True
131- break
132- else:
133- pgpass.append(
134- PgPassLine(
135- hostname=parsed_dsn["host"],
136- port=parsed_dsn["port"],
137- database=parsed_dsn["dbname"],
138- username=parsed_dsn["user"],
139- password=parsed_dsn["password"],
140- )
141- )
142- modified = True
143-
144- if modified:
145- uid = pwd.getpwnam(base.user()).pw_uid
146- gid = grp.getgrnam(base.user()).gr_gid
147- with open(pgpass_path, "w") as f:
148- for line in pgpass:
149- print(
150- ":".join(
151- [
152- escape(line.hostname),
153- escape(line.port),
154- escape(line.database),
155- escape(line.username),
156- escape(line.password),
157- ]
158- ),
159- file=f,
160- )
161- os.fchown(f.fileno(), uid, gid)
162- os.fchmod(f.fileno(), 0o600)
163-
164-
165-def strip_dsn_authentication(dsn):
166- parsed_dsn = parse_dsn(dsn)
167- parsed_dsn.pop("password", None)
168- return make_dsn(**parsed_dsn)
169diff --git a/launchpad-base/metadata.yaml b/launchpad-base/metadata.yaml
170index eafdcc0..f2480ad 100644
171--- a/launchpad-base/metadata.yaml
172+++ b/launchpad-base/metadata.yaml
173@@ -1,5 +1,3 @@
174 requires:
175- db:
176- interface: pgsql
177 rabbitmq:
178 interface: rabbitmq
179diff --git a/launchpad-base/reactive/launchpad-base.py b/launchpad-base/reactive/launchpad-base.py
180index 186e7df..d8e3376 100644
181--- a/launchpad-base/reactive/launchpad-base.py
182+++ b/launchpad-base/reactive/launchpad-base.py
183@@ -7,8 +7,6 @@ from charms.launchpad.base import (
184 configure_rsync,
185 ensure_lp_directories,
186 get_service_config,
187- strip_dsn_authentication,
188- update_pgpass,
189 )
190 from charms.launchpad.payload import configure_lazr
191 from charms.reactive import (
192@@ -22,7 +20,7 @@ from charms.reactive import (
193 when_not,
194 when_not_all,
195 )
196-from ols import base, postgres
197+from ols import base
198
199
200 @when("rabbitmq.connected")
201@@ -59,30 +57,15 @@ def rabbitmq_unavailable():
202 clear_flag("launchpad.rabbitmq.available")
203
204
205-@when(
206- "launchpad.payload.configured",
207- "db.master.available",
208- "launchpad.rabbitmq.available",
209-)
210+@when("launchpad.payload.configured", "launchpad.rabbitmq.available")
211 @when_not("launchpad.base.configured")
212 def configure():
213- db = endpoint_from_flag("db.master.available")
214 rabbitmq = endpoint_from_flag("rabbitmq.available")
215 # Interactive use shouldn't be frequent, but it's still needed
216 # sometimes, so make it less annoying.
217 change_shell(base.user(), "/bin/bash")
218 ensure_lp_directories()
219 config = get_service_config()
220- db_primary, db_standby = postgres.get_db_uris(db)
221- # XXX cjwatson 2022-09-23: Mangle the connection strings into forms
222- # Launchpad understands. In the long term it would be better to have
223- # Launchpad be able to consume unmodified connection strings.
224- for dsn in [db_primary] + db_standby:
225- update_pgpass(dsn)
226- config["db_primary"] = strip_dsn_authentication(db_primary)
227- config["db_standby"] = ",".join(
228- strip_dsn_authentication(dsn) for dsn in db_standby
229- )
230 config["rabbitmq_broker_urls"] = sorted(get_rabbitmq_uris(rabbitmq))
231 configure_lazr(
232 config,
233@@ -102,30 +85,16 @@ def configure():
234
235
236 @when("launchpad.base.configured")
237-@when_not_all(
238- "launchpad.payload.configured",
239- "db.master.available",
240- "launchpad.rabbitmq.available",
241-)
242+@when_not_all("launchpad.payload.configured", "launchpad.rabbitmq.available")
243 def deconfigure():
244 clear_flag("launchpad.base.configured")
245- clear_flag("service.configured")
246
247
248 @when("config.changed")
249 def config_changed():
250 clear_flag("launchpad.base.configured")
251- clear_flag("service.configured")
252-
253-
254-@when("db.database.changed", "launchpad.base.configured")
255-def db_changed():
256- clear_flag("launchpad.base.configured")
257- clear_flag("service.configured")
258- clear_flag("db.database.changed")
259
260
261 @hook("{requires:rabbitmq}-relation-changed")
262 def rabbitmq_relation_changed(*args):
263 clear_flag("launchpad.base.configured")
264- clear_flag("service.configured")
265diff --git a/launchpad-base/templates/launchpad-base-lazr.conf b/launchpad-base/templates/launchpad-base-lazr.conf
266index b9707cb..a73f081 100644
267--- a/launchpad-base/templates/launchpad-base-lazr.conf
268+++ b/launchpad-base/templates/launchpad-base-lazr.conf
269@@ -44,10 +44,6 @@ git_ssh_root: git+ssh://{{ domain_git }}/
270 {%- endif %}
271
272 [database]
273-{{- opt("db_statement_timeout", db_statement_timeout) }}
274-rw_main_primary: {{ db_primary }}
275-rw_main_standby: {{ db_standby or db_primary }}
276-set_role_after_connecting: True
277 {{- opt("soft_request_timeout", soft_request_timeout) }}
278
279 [error_reports]
280diff --git a/launchpad-db/config.yaml b/launchpad-db/config.yaml
281new file mode 100644
282index 0000000..cd088e2
283--- /dev/null
284+++ b/launchpad-db/config.yaml
285@@ -0,0 +1,12 @@
286+options:
287+ databases:
288+ type: string
289+ description: >
290+ YAML-encoded information about database relations, overriding the
291+ defaults. For example, setting this to "db: {name: launchpad_prod}"
292+ changes the name of the database used by the "db" relation.
293+ default: ""
294+ db_statement_timeout:
295+ type: int
296+ description: SQL statement timeout in milliseconds.
297+ default:
298diff --git a/launchpad-db/layer.yaml b/launchpad-db/layer.yaml
299new file mode 100644
300index 0000000..3ea5d6a
301--- /dev/null
302+++ b/launchpad-db/layer.yaml
303@@ -0,0 +1,4 @@
304+includes:
305+ - layer:launchpad-base
306+ - layer:ols-pg
307+repo: https://git.launchpad.net/launchpad-layers
308diff --git a/launchpad-db/lib/charms/launchpad/db.py b/launchpad-db/lib/charms/launchpad/db.py
309new file mode 100644
310index 0000000..e1d1520
311--- /dev/null
312+++ b/launchpad-db/lib/charms/launchpad/db.py
313@@ -0,0 +1,121 @@
314+# Copyright 2022 Canonical Ltd. This software is licensed under the
315+# GNU Affero General Public License version 3 (see the file LICENSE).
316+
317+import grp
318+import os.path
319+import pwd
320+import re
321+from dataclasses import dataclass
322+
323+from charms.launchpad.base import lazr_config_files as base_config_files
324+from charms.launchpad.payload import config_file_path, home_dir
325+from ols import base
326+from psycopg2.extensions import make_dsn, parse_dsn
327+
328+
329+def lazr_config_files():
330+ return base_config_files() + [config_file_path("launchpad-db-lazr.conf")]
331+
332+
333+@dataclass
334+class PgPassLine:
335+ hostname: str
336+ port: str
337+ database: str
338+ username: str
339+ password: str
340+
341+
342+def update_pgpass(dsn):
343+ # See https://www.postgresql.org/docs/current/libpq-pgpass.html.
344+
345+ def unescape(entry):
346+ return re.sub(r"\\(.)", r"\1", entry)
347+
348+ def escape(entry):
349+ return re.sub(r"([:\\])", r"\\\1", entry)
350+
351+ parsed_dsn = parse_dsn(dsn)
352+ pgpass_path = os.path.join(home_dir(), ".pgpass")
353+ pgpass = []
354+ try:
355+ with open(pgpass_path) as f:
356+ for line in f:
357+ if line.startswith("#"):
358+ continue
359+ match = re.match(
360+ r"""
361+ ^
362+ (?P<hostname>(?:[^:\\]|\\.)*):
363+ (?P<port>(?:[^:\\]|\\.)*):
364+ (?P<database>(?:[^:\\]|\\.)*):
365+ (?P<username>(?:[^:\\]|\\.)*):
366+ (?P<password>(?:[^:\\]|\\.)*)
367+ $
368+ """,
369+ line.rstrip("\n"),
370+ flags=re.X,
371+ )
372+ if match is None:
373+ continue
374+ pgpass.append(
375+ PgPassLine(
376+ hostname=unescape(match.group("hostname")),
377+ port=unescape(match.group("port")),
378+ database=unescape(match.group("database")),
379+ username=unescape(match.group("username")),
380+ password=unescape(match.group("password")),
381+ )
382+ )
383+ except OSError:
384+ pass
385+
386+ modified = False
387+ for line in pgpass:
388+ if (
389+ line.hostname in ("*", parsed_dsn["host"])
390+ and line.port in ("*", parsed_dsn["port"])
391+ and line.database in ("*", parsed_dsn["dbname"])
392+ and line.username in ("*", parsed_dsn["user"])
393+ ):
394+ if line.password != parsed_dsn["password"]:
395+ line.password = parsed_dsn["password"]
396+ modified = True
397+ break
398+ else:
399+ pgpass.append(
400+ PgPassLine(
401+ hostname=parsed_dsn["host"],
402+ port=parsed_dsn["port"],
403+ database=parsed_dsn["dbname"],
404+ username=parsed_dsn["user"],
405+ password=parsed_dsn["password"],
406+ )
407+ )
408+ modified = True
409+
410+ if modified:
411+ uid = pwd.getpwnam(base.user()).pw_uid
412+ gid = grp.getgrnam(base.user()).gr_gid
413+ with open(pgpass_path, "w") as f:
414+ for line in pgpass:
415+ print(
416+ ":".join(
417+ [
418+ escape(line.hostname),
419+ escape(line.port),
420+ escape(line.database),
421+ escape(line.username),
422+ escape(line.password),
423+ ]
424+ ),
425+ file=f,
426+ )
427+ os.fchown(f.fileno(), uid, gid)
428+ os.fchmod(f.fileno(), 0o600)
429+
430+
431+def strip_dsn_authentication(dsn):
432+ parsed_dsn = parse_dsn(dsn)
433+ parsed_dsn.pop("password", None)
434+ return make_dsn(**parsed_dsn)
435diff --git a/launchpad-db/metadata.yaml b/launchpad-db/metadata.yaml
436new file mode 100644
437index 0000000..6de4a93
438--- /dev/null
439+++ b/launchpad-db/metadata.yaml
440@@ -0,0 +1,3 @@
441+requires:
442+ db:
443+ interface: pgsql
444diff --git a/launchpad-db/reactive/launchpad-db.py b/launchpad-db/reactive/launchpad-db.py
445new file mode 100644
446index 0000000..4d8c7a8
447--- /dev/null
448+++ b/launchpad-db/reactive/launchpad-db.py
449@@ -0,0 +1,57 @@
450+# Copyright 2022 Canonical Ltd. This software is licensed under the
451+# GNU Affero General Public License version 3 (see the file LICENSE).
452+
453+from charms.launchpad.base import get_service_config
454+from charms.launchpad.db import strip_dsn_authentication, update_pgpass
455+from charms.launchpad.payload import configure_lazr
456+from charms.reactive import (
457+ clear_flag,
458+ endpoint_from_flag,
459+ hook,
460+ set_flag,
461+ when,
462+ when_not,
463+ when_not_all,
464+)
465+from ols import postgres
466+
467+
468+@when("launchpad.base.configured", "db.master.available")
469+@when_not("launchpad.db.configured")
470+def configure():
471+ db = endpoint_from_flag("db.master.available")
472+ config = get_service_config()
473+ db_primary, db_standby = postgres.get_db_uris(db)
474+ # XXX cjwatson 2022-09-23: Mangle the connection strings into forms
475+ # Launchpad understands. In the long term it would be better to have
476+ # Launchpad be able to consume unmodified connection strings.
477+ for dsn in [db_primary] + db_standby:
478+ update_pgpass(dsn)
479+ config["db_primary"] = strip_dsn_authentication(db_primary)
480+ config["db_standby"] = ",".join(
481+ strip_dsn_authentication(dsn) for dsn in db_standby
482+ )
483+ configure_lazr(config, "launchpad-db-lazr.conf", "launchpad-db-lazr.conf")
484+ set_flag("launchpad.db.configured")
485+
486+
487+@when("launchpad.db.configured")
488+@when_not_all("launchpad.base.configured", "db.master.available")
489+def deconfigure():
490+ clear_flag("launchpad.db.configured")
491+
492+
493+@when("config.changed")
494+def config_changed():
495+ clear_flag("launchpad.db.configured")
496+
497+
498+@when("db.database.changed", "launchpad.db.configured")
499+def db_changed():
500+ clear_flag("launchpad.db.configured")
501+ clear_flag("db.database.changed")
502+
503+
504+@hook("{requires:rabbitmq}-relation-changed")
505+def rabbitmq_relation_changed(*args):
506+ clear_flag("launchpad.db.configured")
507diff --git a/launchpad-db/templates/launchpad-db-lazr.conf b/launchpad-db/templates/launchpad-db-lazr.conf
508new file mode 100644
509index 0000000..e6f4f80
510--- /dev/null
511+++ b/launchpad-db/templates/launchpad-db-lazr.conf
512@@ -0,0 +1,19 @@
513+# Public configuration data. The contents of this file may be freely shared
514+# with developers if needed for debugging.
515+
516+# A schema's sections, keys, and values are automatically inherited, except
517+# for '.optional' sections. Update this config to override key values.
518+# Values are strings, except for numbers that look like ints. The tokens
519+# true, false, and none are treated as True, False, and None.
520+
521+{% from "macros.j2" import opt -%}
522+
523+[meta]
524+extends: launchpad-base-lazr.conf
525+
526+[database]
527+{{- opt("db_statement_timeout", db_statement_timeout) }}
528+rw_main_primary: {{ db_primary }}
529+rw_main_standby: {{ db_standby or db_primary }}
530+set_role_after_connecting: True
531+
532diff --git a/launchpad-payload/lib/charms/launchpad/payload.py b/launchpad-payload/lib/charms/launchpad/payload.py
533index a633ca4..d6669f8 100644
534--- a/launchpad-payload/lib/charms/launchpad/payload.py
535+++ b/launchpad-payload/lib/charms/launchpad/payload.py
536@@ -1,8 +1,4 @@
537-<<<<<<< launchpad-payload/lib/charms/launchpad/payload.py
538 # Copyright 2022 Canonical Ltd. This software is licensed under the
539-=======
540-# Copyright 2022-2023 Canonical Ltd. This software is licensed under the
541->>>>>>> launchpad-payload/lib/charms/launchpad/payload.py
542 # GNU Affero General Public License version 3 (see the file LICENSE).
543
544 import os.path
545diff --git a/launchpad-payload/reactive/launchpad-payload.py b/launchpad-payload/reactive/launchpad-payload.py
546index 479aeab..13cbd2d 100644
547--- a/launchpad-payload/reactive/launchpad-payload.py
548+++ b/launchpad-payload/reactive/launchpad-payload.py
549@@ -1,8 +1,4 @@
550-<<<<<<< launchpad-payload/reactive/launchpad-payload.py
551 # Copyright 2022 Canonical Ltd. This software is licensed under the
552-=======
553-# Copyright 2022-2023 Canonical Ltd. This software is licensed under the
554->>>>>>> launchpad-payload/reactive/launchpad-payload.py
555 # GNU Affero General Public License version 3 (see the file LICENSE).
556
557 import subprocess

Subscribers

People subscribed via source and target branches