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 

40 

41import apt_pkg 

42import psycopg2 

43 

44from daklib import utils 

45from daklib.config import Config 

46from daklib.dak_exceptions import DBUpdateError 

47from daklib.daklog import Logger 

48 

49################################################################################ 

50 

51Cnf = None 

52 

53################################################################################ 

54 

55 

56class UpdateDB: 

57 def usage(self, exit_code=0): 

58 print( 

59 """Usage: dak update-db 

60Updates dak's database schema to the lastest version. You should disable crontabs while this is running 

61 

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

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

64 ) 

65 sys.exit(exit_code) 

66 

67 ################################################################################ 

68 

69 def update_db_to_zero(self): 

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

71 

72 # First, do the sure thing, and create the configuration table 

73 try: 

74 print("Creating configuration table ...") 

75 c = self.db.cursor() 

76 c.execute( 

77 """CREATE TABLE config ( 

78 id SERIAL PRIMARY KEY NOT NULL, 

79 name TEXT UNIQUE NOT NULL, 

80 value TEXT 

81 );""" 

82 ) 

83 c.execute( 

84 "INSERT INTO config VALUES ( nextval('config_id_seq'), 'db_revision', '0')" 

85 ) 

86 self.db.commit() 

87 

88 except psycopg2.ProgrammingError: 

89 self.db.rollback() 

90 print("Failed to create configuration table.") 

91 print("Can the projectB user CREATE TABLE?") 

92 print("") 

93 print("Aborting update.") 

94 sys.exit(-255) 

95 

96 ################################################################################ 

97 

98 def get_db_rev(self): 

99 # We keep database revision info the config table 

100 # Try and access it 

101 

102 try: 

103 c = self.db.cursor() 

104 c.execute("SELECT value FROM config WHERE name = 'db_revision';") 

105 return c.fetchone()[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): 

118 """ 

119 Returns the current transaction id as a string. 

120 """ 

121 cursor = self.db.cursor() 

122 cursor.execute("SELECT txid_current();") 

123 id = cursor.fetchone()[0] 

124 cursor.close() 

125 return id 

126 

127 ################################################################################ 

128 

129 def update_db(self): 

130 # Ok, try and find the configuration table 

131 print("Determining dak database revision ...") 

132 cnf = Config() 

133 logger = Logger("update-db") 

134 modules = [] 

135 

136 try: 

137 # Build a connect string 

138 if "DB::Service" in cnf: 138 ↛ 139line 138 didn't jump to line 139, because the condition on line 138 was never true

139 connect_str = "service=%s" % cnf["DB::Service"] 

140 else: 

141 connect_str = "dbname=%s" % (cnf["DB::Name"]) 

142 if "DB::Host" in cnf and cnf["DB::Host"] != "": 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true

143 connect_str += " host=%s" % (cnf["DB::Host"]) 

144 if "DB::Port" in cnf and cnf["DB::Port"] != "-1": 144 ↛ 145line 144 didn't jump to line 145, because the condition on line 144 was never true

145 connect_str += " port=%d" % (int(cnf["DB::Port"])) 

146 

147 self.db = psycopg2.connect(connect_str) 

148 

149 db_role = cnf.get("DB::Role") 

150 if db_role: 150 ↛ 157line 150 didn't jump to line 157, because the condition on line 150 was never false

151 self.db.cursor().execute('SET ROLE "{}"'.format(db_role)) 

152 

153 except Exception as e: 

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

155 sys.exit(1) 

156 

157 database_revision = int(self.get_db_rev()) 

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

159 

160 if database_revision == -1: 160 ↛ 161line 160 didn't jump to line 161, because the condition on line 160 was never true

161 print("dak database schema predates update-db.") 

162 print("") 

163 print( 

164 "This script will attempt to upgrade it to the lastest, but may fail." 

165 ) 

166 print( 

167 "Please make sure you have a database backup handy. If you don't, press Ctrl-C now!" 

168 ) 

169 print("") 

170 print("Continuing in five seconds ...") 

171 time.sleep(5) 

172 print("") 

173 print("Attempting to upgrade pre-zero database to zero") 

174 

175 self.update_db_to_zero() 

176 database_revision = 0 

177 

178 dbfiles = glob(os.path.join(os.path.dirname(__file__), "dakdb/update*.py")) 

179 required_database_schema = max( 

180 int(x) for x in findall(r"update(\d+).py", " ".join(dbfiles)) 

181 ) 

182 

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

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

185 

186 if database_revision < required_database_schema: 186 ↛ 203line 186 didn't jump to line 203, because the condition on line 186 was never false

187 print("\nUpdates to be applied:") 

188 for i in range(database_revision, required_database_schema): 

189 i += 1 

190 dakdb = __import__("dakdb", globals(), locals(), ["update" + str(i)]) 

191 update_module = getattr(dakdb, "update" + str(i)) 

192 print( 192 ↛ exitline 192 didn't jump to the function exit

193 "Update %d: %s" 

194 % (i, next(s for s in update_module.__doc__.split("\n") if s)) 

195 ) 

196 modules.append((update_module, i)) 

197 if not Config().find_b("Update-DB::Options::Yes", False): 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true

198 prompt = "\nUpdate database? (y/N) " 

199 answer = utils.input_or_exit(prompt) 

200 if answer.upper() != "Y": 

201 sys.exit(0) 

202 else: 

203 print("no updates required") 

204 logger.log(["no updates required"]) 

205 sys.exit(0) 

206 

207 for module in modules: 

208 (update_module, i) = module 

209 try: 

210 update_module.do_update(self) 

211 message = "updated database schema from %d to %d" % ( 

212 database_revision, 

213 i, 

214 ) 

215 print(message) 

216 logger.log([message]) 

217 except DBUpdateError as e: 

218 # Seems the update did not work. 

219 print( 

220 "Was unable to update database schema from %d to %d." 

221 % (database_revision, i) 

222 ) 

223 print("The error message received was %s" % (e)) 

224 logger.log(["DB Schema upgrade failed"]) 

225 logger.close() 

226 utils.fubar("DB Schema upgrade failed") 

227 database_revision += 1 

228 logger.close() 

229 

230 ################################################################################ 

231 

232 def init(self): 

233 cnf = Config() 

234 arguments = [ 

235 ("h", "help", "Update-DB::Options::Help"), 

236 ("y", "yes", "Update-DB::Options::Yes"), 

237 ] 

238 for i in ["help"]: 

239 key = "Update-DB::Options::%s" % i 

240 if key not in cnf: 240 ↛ 238line 240 didn't jump to line 238, because the condition on line 240 was never false

241 cnf[key] = "" 

242 

243 arguments = apt_pkg.parse_commandline(cnf.Cnf, arguments, sys.argv) 

244 

245 options = cnf.subtree("Update-DB::Options") 

246 if options["Help"]: 

247 self.usage() 

248 elif arguments: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 utils.warn("dak update-db takes no arguments.") 

250 self.usage(exit_code=1) 

251 

252 try: 

253 if os.path.isdir(cnf["Dir::Lock"]): 253 ↛ 254line 253 didn't jump to line 254, because the condition on line 253 was never true

254 lock_fd = os.open( 

255 os.path.join(cnf["Dir::Lock"], "daily.lock"), 

256 os.O_RDONLY | os.O_CREAT, 

257 ) 

258 fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 

259 else: 

260 utils.warn("Lock directory doesn't exist yet - not locking") 

261 except OSError as e: 

262 if e.errno in (errno.EACCES, errno.EAGAIN): 

263 utils.fubar( 

264 "Couldn't obtain lock, looks like archive is doing something, try again later." 

265 ) 

266 else: 

267 raise 

268 

269 self.update_db() 

270 

271 

272################################################################################ 

273 

274if __name__ == "__main__": 

275 app = UpdateDB() 

276 app.init() 

277 

278 

279def main(): 

280 app = UpdateDB() 

281 app.init()