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