4c99fb2be40cffd212f98d4e71c323a988320609
[sfa.git] / sfa / storage / dbschema.py
1 import sys
2 import traceback
3
4 from sqlalchemy import MetaData, Table
5 from sqlalchemy.exc import NoSuchTableError
6
7 import migrate.versioning.api as migrate
8
9 from sfa.util.sfalogging import logger
10 import sfa.storage.model as model
11
12 ########## this class takes care of database upgrades
13 ### upgrade from a pre-2.1 db 
14 # * 1.0 and up to 1.1-4:  ('very old')    
15 #       was piggybacking the planetlab5 database
16 #       this is kind of out of our scope here, we don't have the credentials 
17 #       to connect to planetlab5, but this is documented in
18 #       https://svn.planet-lab.org/wiki/SFATutorialConfigureSFA#Upgradingnotes
19 #       and essentially this is seamless to users
20 # * from 1.1-5 up to 2.0-x: ('old')
21 #       uses the 'sfa' db and essentially the 'records' table,
22 #       as well as record_types
23 #       together with an 'sfa_db_version' table (version, subversion)
24 # * from 2.1:
25 #       we have an 'records' table, plus 'users' and the like
26 #       and once migrate has kicked in there is a table named (see migrate.cfg)
27 #       migrate_db_version (repository_id, repository_path, version)
28 ### after 2.1 
29 #       Starting with 2.1, we use sqlalchemy-migrate scripts in a standard way
30 #       Note that the model defined in sfa.storage.model needs to be maintained 
31 #       as the 'current/latest' version, and newly installed deployments will 
32 #       then 'jump' to the latest version number without going through the migrations
33 ###
34 # An initial attempt to run this as a 001_*.py migrate script 
35 # did not quite work out (essentially we need to set the current version
36 # number out of the migrations logic)
37 # also this approach has less stuff in the initscript, which seems just right
38
39 class DBSchema:
40
41     header="Upgrading to 2.1 or higher"
42
43     def __init__ (self):
44         from sfa.storage.alchemy import alchemy
45         self.url=alchemy.url
46         self.engine=alchemy.engine
47         self.repository="/usr/share/sfa/migrations"
48
49     def current_version (self):
50         try:
51             return migrate.db_version (self.url, self.repository)
52         except:
53             return None
54
55     def table_exists (self, tablename):
56         try:
57             metadata = MetaData (bind=self.engine)
58             table=Table (tablename, metadata, autoload=True)
59             return True
60         except NoSuchTableError:
61             return False
62
63     def drop_table (self, tablename):
64         if self.table_exists (tablename):
65             print >>sys.stderr, "%s: Dropping table %s"%(DBSchema.header,tablename)
66             self.engine.execute ("drop table %s cascade"%tablename)
67         else:
68             print >>sys.stderr, "%s: no need to drop table %s"%(DBSchema.header,tablename)
69         
70     def handle_old_releases (self):
71         try:
72             # try to find out which old version this can be
73             if not self.table_exists ('records'):
74                 # this likely means 
75                 # (.) we've just created the db, so it's either a fresh install, or
76                 # (.) we come from a 'very old' depl.
77                 # in either case, an import is required but there's nothing to clean up
78                 print >> sys.stderr,"%s: make sure to run import"%(DBSchema.header,)
79             elif self.table_exists ('sfa_db_version'):
80                 # we come from an 'old' version
81                 self.drop_table ('records')
82                 self.drop_table ('record_types')
83                 self.drop_table ('sfa_db_version')
84             else:
85                 # we should be good here
86                 pass
87         except:
88             print >> sys.stderr, "%s: unknown exception"%(DBSchema.header,)
89             traceback.print_exc ()
90
91     # after this call the db schema and the version as known by migrate should 
92     # reflect the current data model and the latest known version
93     def init_or_upgrade (self):
94         # check if under version control, and initialize it otherwise
95         if self.current_version() is None:
96             before="Unknown"
97             # can be either a very old version, or a fresh install
98             # for very old versions:
99             self.handle_old_releases()
100             # in any case, initialize db from current code and reflect in migrate
101             model.init_tables(self.engine)
102             code_version = migrate.version (self.repository)
103             migrate.version_control (self.url, self.repository, code_version)
104             after="%s"%self.current_version()
105             logger.info("DBSchema : jumped to version %s"%(after))
106         else:
107             # use migrate in the usual way
108             before="%s"%self.current_version()
109             migrate.upgrade (self.url, self.repository)
110             after="%s"%self.current_version()
111             if before != after:
112                 logger.info("DBSchema : upgraded version from %s to %s"%(before,after))
113             else:
114                 logger.debug("DBSchema : no change needed in db schema (%s==%s)"%(before,after))
115     
116     # this trashes the db altogether, from the current model in sfa.storage.model
117     # I hope this won't collide with ongoing migrations and all
118     # actually, now that sfa uses its own db, this is essentially equivalent to 
119     # dropping the db entirely, modulo a 'service sfa start'
120     def nuke (self):
121         model.drop_tables(self.engine)
122         # so in this case it's like we haven't initialized the db at all
123         try:
124             migrate.drop_version_control (self.url, self.repository)
125         except migrate.exceptions.DatabaseNotControlledError:
126             logger.log_exc("Failed to drop version control")
127         
128
129 if __name__ == '__main__':
130     DBSchema().init_or_upgrade()