Coverage for dak/update_db.py: 67%
167 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1"""Database Update Main Script
3@contact: Debian FTP Master <ftpmaster@debian.org>
4# Copyright (C) 2008 Michael Casadevall <mcasadevall@debian.org>
5@license: GNU General Public License version 2 or later
6"""
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22################################################################################
24# <Ganneff> when do you have it written?
25# <NCommander> Ganneff, after you make my debian account
26# <Ganneff> blackmail wont work
27# <NCommander> damn it
29################################################################################
31import errno
32import fcntl
33import importlib
34import os
35import pkgutil
36import re
37import sys
38import time
39from types import ModuleType
40from typing import Literal, NoReturn
42import apt_pkg
43import psycopg2
45import dak.dakdb
46from daklib import utils
47from daklib.config import Config
48from daklib.dak_exceptions import DBUpdateError
49from daklib.daklog import Logger
51################################################################################
54class UpdateDB:
55 def usage(self, exit_code=0) -> NoReturn:
56 print(
57 """Usage: dak update-db
58Updates dak's database schema to the lastest version. You should disable crontabs while this is running
60 -h, --help show this help and exit.
61 -y, --yes do not ask for confirmation"""
62 )
63 sys.exit(exit_code)
65 ################################################################################
67 def update_db_to_zero(self) -> None:
68 """This function will attempt to update a pre-zero database schema to zero"""
70 # First, do the sure thing, and create the configuration table
71 try:
72 print("Creating configuration table ...")
73 c = self.db.cursor()
74 c.execute(
75 """CREATE TABLE config (
76 id SERIAL PRIMARY KEY NOT NULL,
77 name TEXT UNIQUE NOT NULL,
78 value TEXT
79 );"""
80 )
81 c.execute(
82 "INSERT INTO config VALUES ( nextval('config_id_seq'), 'db_revision', '0')"
83 )
84 self.db.commit()
86 except psycopg2.ProgrammingError:
87 self.db.rollback()
88 print("Failed to create configuration table.")
89 print("Can the projectB user CREATE TABLE?")
90 print()
91 print("Aborting update.")
92 sys.exit(-255)
94 ################################################################################
96 def get_db_rev(self) -> str | Literal[-1]:
97 # We keep database revision info the config table
98 # Try and access it
100 try:
101 c = self.db.cursor()
102 c.execute("SELECT value FROM config WHERE name = 'db_revision';")
103 row = c.fetchone()
104 assert row is not None
105 return row[0]
107 except psycopg2.ProgrammingError:
108 # Whoops .. no config table ...
109 self.db.rollback()
110 print(
111 "No configuration table found, assuming dak database revision to be pre-zero"
112 )
113 return -1
115 ################################################################################
117 def get_transaction_id(self) -> str:
118 """
119 Returns the current transaction id as a string.
120 """
121 cursor = self.db.cursor()
122 cursor.execute("SELECT txid_current();")
123 row = cursor.fetchone()
124 assert row is not None
125 id = row[0]
126 cursor.close()
127 return id
129 ################################################################################
131 def apply_update(self, update_module: ModuleType, revision: int) -> None:
132 print(update_module.__doc__)
133 try:
134 c = self.db.cursor()
136 do_update = getattr(update_module, "do_update", None)
137 if do_update is not None:
138 do_update(c)
139 else:
140 for stmt in update_module.statements:
141 c.execute(stmt)
143 c.execute(
144 "UPDATE config SET value = %s WHERE name = 'db_revision'",
145 (str(revision),),
146 )
147 self.db.commit()
149 except psycopg2.ProgrammingError as msg:
150 self.db.rollback()
151 raise DBUpdateError(
152 f"Unable to apply sick update {revision}, rollback issued. Error message: {msg}"
153 )
154 except Exception:
155 self.db.rollback()
156 raise
158 ################################################################################
160 def update_db(self) -> None:
161 # Ok, try and find the configuration table
162 print("Determining dak database revision ...")
163 cnf = Config()
164 logger = Logger("update-db")
165 modules = []
167 try:
168 # Build a connect string
169 if "DB::Service" in cnf: 169 ↛ 170line 169 didn't jump to line 170 because the condition on line 169 was never true
170 connect_str = "service=%s" % cnf["DB::Service"]
171 else:
172 connect_str = "dbname=%s" % (cnf["DB::Name"])
173 if "DB::Host" in cnf and cnf["DB::Host"] != "": 173 ↛ 174line 173 didn't jump to line 174 because the condition on line 173 was never true
174 connect_str += " host=%s" % (cnf["DB::Host"])
175 if "DB::Port" in cnf and cnf["DB::Port"] != "-1": 175 ↛ 176line 175 didn't jump to line 176 because the condition on line 175 was never true
176 connect_str += " port=%d" % (int(cnf["DB::Port"]))
178 self.db = psycopg2.connect(connect_str)
180 db_role = cnf.get("DB::Role")
181 if db_role: 181 ↛ 188line 181 didn't jump to line 188 because the condition on line 181 was always true
182 self.db.cursor().execute('SET ROLE "{}"'.format(db_role))
184 except Exception as e:
185 print("FATAL: Failed connect to database (%s)" % str(e))
186 sys.exit(1)
188 database_revision = int(self.get_db_rev())
189 logger.log(["transaction id before update: %s" % self.get_transaction_id()])
191 if database_revision == -1: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true
192 print("dak database schema predates update-db.")
193 print()
194 print(
195 "This script will attempt to upgrade it to the lastest, but may fail."
196 )
197 print(
198 "Please make sure you have a database backup handy. If you don't, press Ctrl-C now!"
199 )
200 print()
201 print("Continuing in five seconds ...")
202 time.sleep(5)
203 print()
204 print("Attempting to upgrade pre-zero database to zero")
206 self.update_db_to_zero()
207 database_revision = 0
209 re_update_module = re.compile(r"update(\d+)")
210 required_database_schema = max(
211 int(match.group(1))
212 for module_info in pkgutil.iter_modules(dak.dakdb.__path__)
213 if (match := re_update_module.fullmatch(module_info.name))
214 )
216 print("dak database schema at %d" % database_revision)
217 print("dak version requires schema %d" % required_database_schema)
219 if database_revision < required_database_schema: 219 ↛ 233line 219 didn't jump to line 233 because the condition on line 219 was always true
220 print("\nUpdates to be applied:")
221 for i in range(database_revision + 1, required_database_schema + 1):
222 update_module = importlib.import_module(f"dak.dakdb.update{i}")
223 doc = update_module.__doc__
224 assert doc is not None
225 print("Update %d: %s" % (i, next(s for s in doc.split("\n") if s))) 225 ↛ exitline 225 didn't finish the generator expression on line 225
226 modules.append((update_module, i))
227 if not Config().find_b("Update-DB::Options::Yes", False): 227 ↛ 228line 227 didn't jump to line 228 because the condition on line 227 was never true
228 prompt = "\nUpdate database? (y/N) "
229 answer = utils.input_or_exit(prompt)
230 if answer.upper() != "Y":
231 sys.exit(0)
232 else:
233 print("no updates required")
234 logger.log(["no updates required"])
235 sys.exit(0)
237 for module in modules:
238 (update_module, i) = module
239 try:
240 self.apply_update(update_module, i)
241 message = "updated database schema from %d to %d" % (
242 database_revision,
243 i,
244 )
245 print(message)
246 logger.log([message])
247 except DBUpdateError as e:
248 # Seems the update did not work.
249 print(
250 "Was unable to update database schema from %d to %d."
251 % (database_revision, i)
252 )
253 print("The error message received was %s" % (e))
254 logger.log(["DB Schema upgrade failed"])
255 logger.close()
256 utils.fubar("DB Schema upgrade failed")
257 database_revision += 1
258 logger.close()
260 ################################################################################
262 def init(self) -> None:
263 cnf = Config()
264 arguments = [
265 ("h", "help", "Update-DB::Options::Help"),
266 ("y", "yes", "Update-DB::Options::Yes"),
267 ]
268 for i in ["help"]:
269 key = "Update-DB::Options::%s" % i
270 if key not in cnf: 270 ↛ 268line 270 didn't jump to line 268 because the condition on line 270 was always true
271 cnf[key] = ""
273 arguments = apt_pkg.parse_commandline(cnf.Cnf, arguments, sys.argv) # type: ignore[attr-defined]
275 options = cnf.subtree("Update-DB::Options")
276 if options["Help"]:
277 self.usage()
278 elif arguments: 278 ↛ 279line 278 didn't jump to line 279 because the condition on line 278 was never true
279 utils.warn("dak update-db takes no arguments.")
280 self.usage(exit_code=1)
282 try:
283 if os.path.isdir(cnf["Dir::Lock"]): 283 ↛ 284line 283 didn't jump to line 284 because the condition on line 283 was never true
284 lock_fd = os.open(
285 os.path.join(cnf["Dir::Lock"], "daily.lock"),
286 os.O_RDONLY | os.O_CREAT,
287 )
288 fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
289 else:
290 utils.warn("Lock directory doesn't exist yet - not locking")
291 except OSError as e:
292 if e.errno in (errno.EACCES, errno.EAGAIN):
293 utils.fubar(
294 "Couldn't obtain lock, looks like archive is doing something, try again later."
295 )
296 else:
297 raise
299 self.update_db()
302################################################################################
305def main() -> None:
306 app = UpdateDB()
307 app.init()