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
41import apt_pkg
42import psycopg2
44from daklib import utils
45from daklib.config import Config
46from daklib.dak_exceptions import DBUpdateError
47from daklib.daklog import Logger
49################################################################################
51Cnf = None
53################################################################################
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
62 -h, --help show this help and exit.
63 -y, --yes do not ask for confirmation"""
64 )
65 sys.exit(exit_code)
67 ################################################################################
69 def update_db_to_zero(self):
70 """This function will attempt to update a pre-zero database schema to zero"""
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()
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)
96 ################################################################################
98 def get_db_rev(self):
99 # We keep database revision info the config table
100 # Try and access it
102 try:
103 c = self.db.cursor()
104 c.execute("SELECT value FROM config WHERE name = 'db_revision';")
105 return c.fetchone()[0]
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
115 ################################################################################
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
127 ################################################################################
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 = []
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"]))
147 self.db = psycopg2.connect(connect_str)
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))
153 except Exception as e:
154 print("FATAL: Failed connect to database (%s)" % str(e))
155 sys.exit(1)
157 database_revision = int(self.get_db_rev())
158 logger.log(["transaction id before update: %s" % self.get_transaction_id()])
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")
175 self.update_db_to_zero()
176 database_revision = 0
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 )
183 print("dak database schema at %d" % database_revision)
184 print("dak version requires schema %d" % required_database_schema)
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)
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()
230 ################################################################################
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] = ""
243 arguments = apt_pkg.parse_commandline(cnf.Cnf, arguments, sys.argv)
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)
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
269 self.update_db()
272################################################################################
274if __name__ == "__main__":
275 app = UpdateDB()
276 app.init()
279def main():
280 app = UpdateDB()
281 app.init()