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