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

1"""Database Update Main Script 

2 

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

7 

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. 

12 

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. 

17 

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 

21 

22################################################################################ 

23 

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 

28 

29################################################################################ 

30 

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 

41 

42import apt_pkg 

43import psycopg2 

44 

45import dak.dakdb 

46from daklib import utils 

47from daklib.config import Config 

48from daklib.dak_exceptions import DBUpdateError 

49from daklib.daklog import Logger 

50 

51################################################################################ 

52 

53 

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 

59 

60 -h, --help show this help and exit. 

61 -y, --yes do not ask for confirmation""" 

62 ) 

63 sys.exit(exit_code) 

64 

65 ################################################################################ 

66 

67 def update_db_to_zero(self) -> None: 

68 """This function will attempt to update a pre-zero database schema to zero""" 

69 

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

85 

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) 

93 

94 ################################################################################ 

95 

96 def get_db_rev(self) -> str | Literal[-1]: 

97 # We keep database revision info the config table 

98 # Try and access it 

99 

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] 

106 

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 

114 

115 ################################################################################ 

116 

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 

128 

129 ################################################################################ 

130 

131 def apply_update(self, update_module: ModuleType, revision: int) -> None: 

132 print(update_module.__doc__) 

133 try: 

134 c = self.db.cursor() 

135 

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) 

142 

143 c.execute( 

144 "UPDATE config SET value = %s WHERE name = 'db_revision'", 

145 (str(revision),), 

146 ) 

147 self.db.commit() 

148 

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 

157 

158 ################################################################################ 

159 

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 = [] 

166 

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"])) 

177 

178 self.db = psycopg2.connect(connect_str) 

179 

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

183 

184 except Exception as e: 

185 print("FATAL: Failed connect to database (%s)" % str(e)) 

186 sys.exit(1) 

187 

188 database_revision = int(self.get_db_rev()) 

189 logger.log(["transaction id before update: %s" % self.get_transaction_id()]) 

190 

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

205 

206 self.update_db_to_zero() 

207 database_revision = 0 

208 

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 ) 

215 

216 print("dak database schema at %d" % database_revision) 

217 print("dak version requires schema %d" % required_database_schema) 

218 

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) 

236 

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

259 

260 ################################################################################ 

261 

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] = "" 

272 

273 arguments = apt_pkg.parse_commandline(cnf.Cnf, arguments, sys.argv) # type: ignore[attr-defined] 

274 

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) 

281 

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 

298 

299 self.update_db() 

300 

301 

302################################################################################ 

303 

304 

305def main() -> None: 

306 app = UpdateDB() 

307 app.init()