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 psycopg2 

34import sys 

35import fcntl 

36import os 

37import apt_pkg 

38import time 

39import errno 

40from glob import glob 

41from re import findall 

42 

43from daklib import utils 

44from daklib.config import Config 

45from daklib.dak_exceptions import DBUpdateError 

46from daklib.daklog import Logger 

47 

48################################################################################ 

49 

50Cnf = None 

51 

52################################################################################ 

53 

54 

55class UpdateDB: 

56 def usage(self, exit_code=0): 

57 print("""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 sys.exit(exit_code) 

63 

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

65 

66 def update_db_to_zero(self): 

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("""CREATE TABLE config ( 

74 id SERIAL PRIMARY KEY NOT NULL, 

75 name TEXT UNIQUE NOT NULL, 

76 value TEXT 

77 );""") 

78 c.execute("INSERT INTO config VALUES ( nextval('config_id_seq'), 'db_revision', '0')") 

79 self.db.commit() 

80 

81 except psycopg2.ProgrammingError: 

82 self.db.rollback() 

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

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

85 print("") 

86 print("Aborting update.") 

87 sys.exit(-255) 

88 

89################################################################################ 

90 

91 def get_db_rev(self): 

92 # We keep database revision info the config table 

93 # Try and access it 

94 

95 try: 

96 c = self.db.cursor() 

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

98 return c.fetchone()[0] 

99 

100 except psycopg2.ProgrammingError: 

101 # Whoops .. no config table ... 

102 self.db.rollback() 

103 print("No configuration table found, assuming dak database revision to be pre-zero") 

104 return -1 

105 

106################################################################################ 

107 

108 def get_transaction_id(self): 

109 ''' 

110 Returns the current transaction id as a string. 

111 ''' 

112 cursor = self.db.cursor() 

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

114 id = cursor.fetchone()[0] 

115 cursor.close() 

116 return id 

117 

118################################################################################ 

119 

120 def update_db(self): 

121 # Ok, try and find the configuration table 

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

123 cnf = Config() 

124 logger = Logger('update-db') 

125 modules = [] 

126 

127 try: 

128 # Build a connect string 

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

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

131 else: 

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

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

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

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

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

137 

138 self.db = psycopg2.connect(connect_str) 

139 

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

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

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

143 

144 except Exception as e: 

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

146 sys.exit(1) 

147 

148 database_revision = int(self.get_db_rev()) 

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

150 

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

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

153 print("") 

154 print("This script will attempt to upgrade it to the lastest, but may fail.") 

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

156 print("") 

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

158 time.sleep(5) 

159 print("") 

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

161 

162 self.update_db_to_zero() 

163 database_revision = 0 

164 

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

166 required_database_schema = max(int(x) for x in findall(r'update(\d+).py', " ".join(dbfiles))) 

167 

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

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

170 

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

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

173 for i in range(database_revision, required_database_schema): 

174 i += 1 

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

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

177 print("Update %d: %s" % (i, next(s for s in update_module.__doc__.split("\n") if s))) 177 ↛ exitline 177 didn't finish the generator expression on line 177

178 modules.append((update_module, i)) 

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

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

181 answer = utils.input_or_exit(prompt) 

182 if answer.upper() != 'Y': 

183 sys.exit(0) 

184 else: 

185 print("no updates required") 

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

187 sys.exit(0) 

188 

189 for module in modules: 

190 (update_module, i) = module 

191 try: 

192 update_module.do_update(self) 

193 message = "updated database schema from %d to %d" % (database_revision, i) 

194 print(message) 

195 logger.log([message]) 

196 except DBUpdateError as e: 

197 # Seems the update did not work. 

198 print("Was unable to update database schema from %d to %d." % (database_revision, i)) 

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

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

201 logger.close() 

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

203 database_revision += 1 

204 logger.close() 

205 

206################################################################################ 

207 

208 def init(self): 

209 cnf = Config() 

210 arguments = [('h', "help", "Update-DB::Options::Help"), 

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

212 for i in ["help"]: 

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

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

215 cnf[key] = "" 

216 

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

218 

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

220 if options["Help"]: 

221 self.usage() 

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

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

224 self.usage(exit_code=1) 

225 

226 try: 

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

228 lock_fd = os.open(os.path.join(cnf["Dir::Lock"], 'daily.lock'), os.O_RDONLY | os.O_CREAT) 

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

230 else: 

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

232 except OSError as e: 

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

234 utils.fubar("Couldn't obtain lock, looks like archive is doing something, try again later.") 

235 else: 

236 raise 

237 

238 self.update_db() 

239 

240 

241################################################################################ 

242 

243if __name__ == '__main__': 

244 app = UpdateDB() 

245 app.init() 

246 

247 

248def main(): 

249 app = UpdateDB() 

250 app.init()