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

1#! /usr/bin/env python3 

2 

3"""Database Update Main Script 

4 

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

9 

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. 

14 

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. 

19 

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 

23 

24################################################################################ 

25 

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 

30 

31################################################################################ 

32 

33import errno 

34import fcntl 

35import os 

36import sys 

37import time 

38from glob import glob 

39from re import findall 

40from typing import Literal, NoReturn 

41 

42import apt_pkg 

43import psycopg2 

44 

45from daklib import utils 

46from daklib.config import Config 

47from daklib.dak_exceptions import DBUpdateError 

48from daklib.daklog import Logger 

49 

50################################################################################ 

51 

52 

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 

58 

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

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

61 ) 

62 sys.exit(exit_code) 

63 

64 ################################################################################ 

65 

66 def update_db_to_zero(self) -> None: 

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

68 

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

84 

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) 

92 

93 ################################################################################ 

94 

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

96 # We keep database revision info the config table 

97 # Try and access it 

98 

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] 

105 

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 

113 

114 ################################################################################ 

115 

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 

127 

128 ################################################################################ 

129 

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

136 

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

147 

148 self.db = psycopg2.connect(connect_str) 

149 

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

153 

154 except Exception as e: 

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

156 sys.exit(1) 

157 

158 database_revision = int(self.get_db_rev()) 

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

160 

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

175 

176 self.update_db_to_zero() 

177 database_revision = 0 

178 

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 ) 

183 

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

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

186 

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) 

207 

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

230 

231 ################################################################################ 

232 

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

243 

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

245 

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) 

252 

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 

269 

270 self.update_db() 

271 

272 

273################################################################################ 

274 

275 

276def main() -> None: 

277 app = UpdateDB() 

278 app.init() 

279 

280 

281if __name__ == "__main__": 

282 main()