Merge ~cjwatson/launchpad:remote-db-creation into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: 1150d72f24412b47bd9e141b8a6784484b63f67d
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:remote-db-creation
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:sqlbase-connect-set-role-after-connecting
Diff against target: 242 lines (+93/-18)
5 files modified
database/schema/Makefile (+12/-11)
lib/lp/services/database/sqlbase.py (+5/-1)
utilities/pgmassacre.py (+31/-5)
utilities/pgoptions.py (+40/-0)
utilities/soyuz-sampledata-setup.py (+5/-1)
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+440069@code.launchpad.net

Commit message

Allow initializing a remote database

Description of the change

This is usable from an instance of the `launchpad-admin` charm: it uses the configuration file to find the necessary database credentials. As such, it can be used to set up a Juju-deployed instance of Launchpad from scratch.

So that I sleep better at night, I added an explicit check that refuses to destroy the production database this way.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/database/schema/Makefile b/database/schema/Makefile
2index c3a6b0c..317d19e 100644
3--- a/database/schema/Makefile
4+++ b/database/schema/Makefile
5@@ -31,8 +31,9 @@ TEST_PLAYGROUND_DBNAME=launchpad_ftest_playground
6 # The session database name.
7 SESSION_DBNAME=session_dev
8
9-# The command we use to drop a database.
10-DROPDB=../../utilities/pgmassacre.py
11+# Database options to pass to PostgreSQL tools.
12+DBOPTS:=$(shell ../../utilities/pgoptions.py)
13+
14 # The command we use to drop (if exists) and recreate a database.
15 CREATEDB=../../utilities/pgmassacre.py -t
16
17@@ -70,7 +71,7 @@ test: create
18 @ echo "* Creating database \"$(TEMPLATE_WITH_TEST_SAMPLEDATA)\"."
19 @ ${CREATEDB} ${EMPTY_DBNAME} ${TEMPLATE_WITH_TEST_SAMPLEDATA}
20 @ echo "* Loading sample data"
21- @ psql -v ON_ERROR_STOP=1 -d ${TEMPLATE_WITH_TEST_SAMPLEDATA} -f $(SAMPLEDATA) > /dev/null
22+ @ psql $(DBOPTS) -v ON_ERROR_STOP=1 -d ${TEMPLATE_WITH_TEST_SAMPLEDATA} -f $(SAMPLEDATA) > /dev/null
23 @ echo "* Rebuilding full text indexes"
24 @ ./fti.py --live-rebuild -q -d ${TEMPLATE_WITH_TEST_SAMPLEDATA}
25 @ echo "* Resetting sequences"
26@@ -78,7 +79,7 @@ test: create
27 @ echo "* Disabling autovacuum"
28 @ ./unautovacuumable.py -d ${TEMPLATE_WITH_TEST_SAMPLEDATA}
29 @ echo "* Vacuuming"
30- @ vacuumdb -fz ${TEMPLATE_WITH_TEST_SAMPLEDATA}
31+ @ vacuumdb $(DBOPTS) -fz -d ${TEMPLATE_WITH_TEST_SAMPLEDATA}
32
33 # Create a launchpad_dev_template DB and load the dev sample data into it.
34 # Also create a launchpad_ftest_playground DB as a copy of
35@@ -87,7 +88,7 @@ dev: test
36 @ echo "* Creating ${TEMPLATE_WITH_DEV_SAMPLEDATA}"
37 @ ${CREATEDB} ${EMPTY_DBNAME} ${TEMPLATE_WITH_DEV_SAMPLEDATA}
38 @ echo "* Loading sample data"
39- @ psql -v ON_ERROR_STOP=1 -d ${TEMPLATE_WITH_DEV_SAMPLEDATA} -f $(SAMPLEDATA_DEV) > /dev/null
40+ @ psql $(DBOPTS) -v ON_ERROR_STOP=1 -d ${TEMPLATE_WITH_DEV_SAMPLEDATA} -f $(SAMPLEDATA_DEV) > /dev/null
41 @ echo "* Rebuilding full text indexes"
42 @ ./fti.py --live-rebuild -q -d ${TEMPLATE_WITH_DEV_SAMPLEDATA}
43 @ echo "* Resetting sequences"
44@@ -95,7 +96,7 @@ dev: test
45 @ echo "* Disabling autovacuum"
46 @ ./unautovacuumable.py -d ${TEMPLATE_WITH_DEV_SAMPLEDATA}
47 @ echo "* Vacuuming"
48- @ vacuumdb -fz ${TEMPLATE_WITH_DEV_SAMPLEDATA}
49+ @ vacuumdb $(DBOPTS) -fz -d ${TEMPLATE_WITH_DEV_SAMPLEDATA}
50 @ echo "* Creating ${DBNAME_DEV}"
51 @ ${CREATEDB} ${TEMPLATE_WITH_DEV_SAMPLEDATA} ${DBNAME_DEV}
52 @ echo "* Creating ${TEST_PLAYGROUND_DBNAME}"
53@@ -112,7 +113,7 @@ create:
54 @ echo "* Creating database \"$(EMPTY_DBNAME)\"."
55 @ ${CREATEDB} template0 ${EMPTY_DBNAME}
56 @ echo "* Loading base database schema"
57- @ psql -d ${EMPTY_DBNAME} -f ${BASELINE} | grep : | cat
58+ @ psql $(DBOPTS) -d ${EMPTY_DBNAME} -f ${BASELINE} | grep : | cat
59 @ echo "* Patching the database schema"
60 @ ./upgrade.py --separate-sessions -d ${EMPTY_DBNAME}
61 @ echo "* Security setup"
62@@ -120,16 +121,16 @@ create:
63 @ echo "* Disabling autovacuum"
64 @ ./unautovacuumable.py -d ${EMPTY_DBNAME}
65 @ echo "* Vacuuming"
66- @ vacuumdb -fz ${EMPTY_DBNAME}
67+ @ vacuumdb $(DBOPTS) -fz -d ${EMPTY_DBNAME}
68
69 @ echo "* Creating session database '${SESSION_DBNAME}' (if necessary)"
70- @if [ "$$((`psql -l | grep -w ${SESSION_DBNAME} | wc -l`))" = '0' ]; \
71+ @if [ "$$((`psql $(DBOPTS) -l | grep -w ${SESSION_DBNAME} | wc -l`))" = '0' ]; \
72 then ${CREATEDB} template0 ${SESSION_DBNAME} ; \
73- psql -q -d ${SESSION_DBNAME} -f launchpad_session.sql ; \
74+ psql $(DBOPTS) -q -d ${SESSION_DBNAME} -f launchpad_session.sql ; \
75 fi
76 @ echo "* Creating session database '${TEST_SESSION_DBNAME}'"
77 @ ${CREATEDB} template0 ${TEST_SESSION_DBNAME}
78- @ psql -q -d ${TEST_SESSION_DBNAME} -f launchpad_session.sql
79+ @ psql $(DBOPTS) -q -d ${TEST_SESSION_DBNAME} -f launchpad_session.sql
80
81 # Confirm that launchpad-XX-00-0.sql hasn't been messed with - this file
82 # is our baseline telling us what was installed into production
83diff --git a/lib/lp/services/database/sqlbase.py b/lib/lp/services/database/sqlbase.py
84index 04b3c7f..2a6aafe 100644
85--- a/lib/lp/services/database/sqlbase.py
86+++ b/lib/lp/services/database/sqlbase.py
87@@ -599,7 +599,11 @@ def connect(user=None, dbname=None, isolation=ISOLATION_LEVEL_DEFAULT):
88
89 con = psycopg2.connect(dsn)
90 con.set_isolation_level(isolation)
91- if dbconfig.set_role_after_connecting and user != parsed_dsn["user"]:
92+ if (
93+ dbconfig.set_role_after_connecting
94+ and user is not None
95+ and user != parsed_dsn["user"]
96+ ):
97 con.cursor().execute("SET ROLE %s", (user,))
98 return con
99
100diff --git a/utilities/pgmassacre.py b/utilities/pgmassacre.py
101index 4fbf9e2..deb5793 100755
102--- a/utilities/pgmassacre.py
103+++ b/utilities/pgmassacre.py
104@@ -15,22 +15,29 @@ Cut off access, slaughter connections and burn the database to the ground
105
106 import _pythonpath # noqa: F401
107
108+import os
109 import sys
110 import time
111 from optparse import OptionParser
112
113 import psycopg2
114-import psycopg2.extensions
115+from psycopg2.extensions import make_dsn, parse_dsn
116
117+from lp.services.config import config, dbconfig
118 from lp.services.database import activity_cols
119
120
121 def connect(dbname="template1"):
122 """Connect to the database, returning the DB-API connection."""
123+ parsed_dsn = parse_dsn(dbconfig.rw_main_primary)
124 if options.user is not None:
125- return psycopg2.connect("dbname=%s user=%s" % (dbname, options.user))
126- else:
127- return psycopg2.connect("dbname=%s" % dbname)
128+ parsed_dsn["user"] = options.user
129+ # For database administration, we only pass a username if we're
130+ # connecting over TCP.
131+ elif "host" not in parsed_dsn:
132+ parsed_dsn.pop("user", None)
133+ parsed_dsn["dbname"] = dbname
134+ return psycopg2.connect(make_dsn(**parsed_dsn))
135
136
137 def rollback_prepared_transactions(database):
138@@ -223,7 +230,16 @@ options = None
139
140
141 def main():
142- parser = OptionParser("Usage: %prog [options] DBNAME")
143+ parser = OptionParser(
144+ "Usage: %prog [options] DBNAME",
145+ description=(
146+ "Set LPCONFIG to choose which database cluster to connect to; "
147+ "credentials are taken from the database.rw_main_primary "
148+ "configuration option, ignoring the 'dbname' parameter. The "
149+ "default behaviour is to connect to a cluster on the local "
150+ "machine using PostgreSQL's default port."
151+ ),
152+ )
153 parser.add_option(
154 "-U",
155 "--user",
156@@ -254,6 +270,16 @@ def main():
157 parser.error(
158 "Running this script against template1 or template0 is nuts."
159 )
160+ if (
161+ "host" in parse_dsn(dbconfig.rw_main_primary)
162+ and os.environ.get("LP_DESTROY_REMOTE_DATABASE") != "yes"
163+ ):
164+ parser.error(
165+ "For safety, refusing to destroy a remote database. Set "
166+ "LP_DESTROY_REMOTE_DATABASE=yes to override this."
167+ )
168+ if config.vhost.mainsite.hostname == "launchpad.net":
169+ parser.error("Flatly refusing to destroy production.")
170
171 con = connect()
172 cur = con.cursor()
173diff --git a/utilities/pgoptions.py b/utilities/pgoptions.py
174new file mode 100755
175index 0000000..05996b0
176--- /dev/null
177+++ b/utilities/pgoptions.py
178@@ -0,0 +1,40 @@
179+#!/usr/bin/python3 -S
180+#
181+# Copyright 2022 Canonical Ltd. This software is licensed under the
182+# GNU Affero General Public License version 3 (see the file LICENSE).
183+
184+"""
185+Print PostgreSQL connection options matching the current Launchpad primary
186+database configuration.
187+
188+To avoid leaking information via process command lines, any password in the
189+configured connection string is ignored; passwords should be set in
190+~/.pgpass instead.
191+"""
192+
193+import _pythonpath # noqa: F401
194+
195+from optparse import OptionParser
196+
197+from psycopg2.extensions import parse_dsn
198+
199+from lp.services.config import dbconfig
200+
201+if __name__ == "__main__":
202+ parser = OptionParser()
203+ _, args = parser.parse_args()
204+ if args:
205+ parser.error("Too many options given")
206+ parsed_dsn = parse_dsn(dbconfig.rw_main_primary)
207+ conn_opts = []
208+ if "host" in parsed_dsn:
209+ conn_opts.append("--host=%s" % parsed_dsn["host"])
210+ if "port" in parsed_dsn:
211+ conn_opts.append("--port=%s" % parsed_dsn["port"])
212+ # For database administration, we only pass a username if we're
213+ # connecting over TCP.
214+ if "host" in parsed_dsn and "user" in parsed_dsn:
215+ conn_opts.append("--username=%s" % parsed_dsn["user"])
216+ if "dbname" in parsed_dsn:
217+ conn_opts.append("--dbname=%s" % parsed_dsn["dbname"])
218+ print(" ".join(conn_opts))
219diff --git a/utilities/soyuz-sampledata-setup.py b/utilities/soyuz-sampledata-setup.py
220index 68aa035..0c44cd6 100755
221--- a/utilities/soyuz-sampledata-setup.py
222+++ b/utilities/soyuz-sampledata-setup.py
223@@ -38,6 +38,7 @@ from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
224 from lp.registry.interfaces.person import IPersonSet
225 from lp.registry.interfaces.series import SeriesStatus
226 from lp.registry.model.codeofconduct import SignedCodeOfConduct
227+from lp.services.config import config
228 from lp.services.database.interfaces import IPrimaryStore, IStandbyStore
229 from lp.services.scripts.base import LaunchpadScript
230 from lp.soyuz.enums import SourcePackageFormat
231@@ -88,7 +89,10 @@ def check_preconditions(options):
232 # run. Don't even accept --force there.
233 forbidden_configs = re.compile("(edge|lpnet|production)")
234 current_config = os.getenv("LPCONFIG", "an unknown config")
235- if forbidden_configs.match(current_config):
236+ if (
237+ forbidden_configs.match(current_config)
238+ or config.vhost.mainsite.hostname == "launchpad.net"
239+ ):
240 raise DoNotRunOnProduction(
241 "I won't delete Ubuntu data on %s and you can't --force me."
242 % current_config

Subscribers

People subscribed via source and target branches

to status/vote changes: