python3 - 2to3 + miscell obvious tweaks
[sfa.git] / sfa / storage / dbschema.py
1
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
42 class DBSchema:
43
44     header = "Upgrading to 2.1 or higher"
45
46     def __init__(self):
47         from sfa.storage.alchemy import alchemy
48         self.url = alchemy.url
49         self.engine = alchemy.engine
50         self.repository = "/usr/share/sfa/migrations"
51
52     def current_version(self):
53         try:
54             return migrate.db_version(self.url, self.repository)
55         except:
56             return None
57
58     def table_exists(self, tablename):
59         try:
60             metadata = MetaData(bind=self.engine)
61             table = Table(tablename, metadata, autoload=True)
62             return True
63         except NoSuchTableError:
64             return False
65
66     def drop_table(self, tablename):
67         if self.table_exists(tablename):
68             print("%s: Dropping table %s" %
69                   (DBSchema.header, tablename), file=sys.stderr)
70             self.engine.execute("drop table %s cascade" % tablename)
71         else:
72             print("%s: no need to drop table %s" %
73                   (DBSchema.header, tablename), file=sys.stderr)
74
75     def handle_old_releases(self):
76         try:
77             # try to find out which old version this can be
78             if not self.table_exists('records'):
79                 # this likely means
80                 # (.) we've just created the db, so it's either a fresh install, or
81                 # (.) we come from a 'very old' depl.
82                 # in either case, an import is required but there's nothing to
83                 # clean up
84                 print("%s: make sure to run import" %
85                       (DBSchema.header,), file=sys.stderr)
86             elif self.table_exists('sfa_db_version'):
87                 # we come from an 'old' version
88                 self.drop_table('records')
89                 self.drop_table('record_types')
90                 self.drop_table('sfa_db_version')
91             else:
92                 # we should be good here
93                 pass
94         except:
95             print("%s: unknown exception" %
96                   (DBSchema.header,), file=sys.stderr)
97             traceback.print_exc()
98
99     # after this call the db schema and the version as known by migrate should
100     # reflect the current data model and the latest known version
101     def init_or_upgrade(self):
102         # check if under version control, and initialize it otherwise
103         if self.current_version() is None:
104             before = "Unknown"
105             # can be either a very old version, or a fresh install
106             # for very old versions:
107             self.handle_old_releases()
108             # in any case, initialize db from current code and reflect in
109             # migrate
110             model.init_tables(self.engine)
111             code_version = migrate.version(self.repository)
112             migrate.version_control(self.url, self.repository, code_version)
113             after = "%s" % self.current_version()
114             logger.info("DBSchema : jumped to version %s" % (after))
115         else:
116             # use migrate in the usual way
117             before = "%s" % self.current_version()
118             migrate.upgrade(self.url, self.repository)
119             after = "%s" % self.current_version()
120             if before != after:
121                 logger.info("DBSchema : upgraded version from %s to %s" %
122                             (before, after))
123             else:
124                 logger.debug(
125                     "DBSchema : no change needed in db schema (%s==%s)" % (before, after))
126
127     # this trashes the db altogether, from the current model in sfa.storage.model
128     # I hope this won't collide with ongoing migrations and all
129     # actually, now that sfa uses its own db, this is essentially equivalent to
130     # dropping the db entirely, modulo a 'service sfa start'
131     def nuke(self):
132         model.drop_tables(self.engine)
133         # so in this case it's like we haven't initialized the db at all
134         try:
135             migrate.drop_version_control(self.url, self.repository)
136         except migrate.exceptions.DatabaseNotControlledError:
137             logger.log_exc("Failed to drop version control")
138
139
140 if __name__ == '__main__':
141     DBSchema().init_or_upgrade()