aboutsummaryrefslogtreecommitdiff
path: root/rpki/sql.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpki/sql.py')
-rw-r--r--rpki/sql.py424
1 files changed, 424 insertions, 0 deletions
diff --git a/rpki/sql.py b/rpki/sql.py
new file mode 100644
index 00000000..c753278c
--- /dev/null
+++ b/rpki/sql.py
@@ -0,0 +1,424 @@
+# $Id$
+#
+# Copyright (C) 2009-2013 Internet Systems Consortium ("ISC")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+#
+# Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
+# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+# AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
+# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+# PERFORMANCE OF THIS SOFTWARE.
+
+"""
+SQL interface code.
+"""
+
+import weakref
+
+from rpki.mysql_import import (MySQLdb, _mysql_exceptions)
+
+import rpki.x509
+import rpki.resource_set
+import rpki.sundial
+import rpki.log
+
+class session(object):
+ """
+ SQL session layer.
+ """
+
+ ## @var ping_threshold
+ # Timeout after which we should issue a ping command before the real
+ # one. Intent is to keep the MySQL connection alive without pinging
+ # before every single command.
+
+ ping_threshold = rpki.sundial.timedelta(seconds = 60)
+
+ def __init__(self, cfg):
+
+ self.username = cfg.get("sql-username")
+ self.database = cfg.get("sql-database")
+ self.password = cfg.get("sql-password")
+
+ self.conv = MySQLdb.converters.conversions.copy()
+ self.conv.update({
+ rpki.sundial.datetime : MySQLdb.converters.DateTime2literal,
+ MySQLdb.converters.FIELD_TYPE.DATETIME : rpki.sundial.datetime.DateTime_or_None })
+
+ self.cache = weakref.WeakValueDictionary()
+ self.dirty = set()
+
+ self.connect()
+
+ def connect(self):
+ self.db = MySQLdb.connect(user = self.username,
+ db = self.database,
+ passwd = self.password,
+ conv = self.conv)
+ self.cur = self.db.cursor()
+ self.db.autocommit(True)
+ self.timestamp = rpki.sundial.now()
+
+ def close(self):
+ if self.cur:
+ self.cur.close()
+ self.cur = None
+ if self.db:
+ self.db.close()
+ self.db = None
+
+ def _wrap_execute(self, func, query, args):
+ try:
+ now = rpki.sundial.now()
+ if now > self.timestamp + self.ping_threshold:
+ self.db.ping(True)
+ self.timestamp = now
+ return func(query, args)
+ except _mysql_exceptions.MySQLError:
+ if self.dirty:
+ rpki.log.warn("MySQL exception with dirty objects in SQL cache!")
+ raise
+
+ def execute(self, query, args = None):
+ return self._wrap_execute(self.cur.execute, query, args)
+
+ def executemany(self, query, args):
+ return self._wrap_execute(self.cur.executemany, query, args)
+
+ def fetchall(self):
+ return self.cur.fetchall()
+
+ def lastrowid(self):
+ return self.cur.lastrowid
+
+ def cache_clear(self):
+ """
+ Clear the SQL object cache. Shouldn't be necessary now that the
+ cache uses weak references, but should be harmless.
+ """
+ rpki.log.debug("Clearing SQL cache")
+ self.assert_pristine()
+ self.cache.clear()
+
+ def assert_pristine(self):
+ """
+ Assert that there are no dirty objects in the cache.
+ """
+ assert not self.dirty, "Dirty objects in SQL cache: %s" % self.dirty
+
+ def sweep(self):
+ """
+ Write any dirty objects out to SQL.
+ """
+ for s in self.dirty.copy():
+ #if s.sql_cache_debug:
+ rpki.log.debug("Sweeping (%s) %r" % ("deleting" if s.sql_deleted else "storing", s))
+ if s.sql_deleted:
+ s.sql_delete()
+ else:
+ s.sql_store()
+ self.assert_pristine()
+
+class template(object):
+ """
+ SQL template generator.
+ """
+
+ def __init__(self, table_name, index_column, *data_columns):
+ """
+ Build a SQL template.
+ """
+ type_map = dict((x[0], x[1]) for x in data_columns if isinstance(x, tuple))
+ data_columns = tuple(isinstance(x, tuple) and x[0] or x for x in data_columns)
+ columns = (index_column,) + data_columns
+ self.table = table_name
+ self.index = index_column
+ self.columns = columns
+ self.map = type_map
+ self.select = "SELECT %s FROM %s" % (", ".join("%s.%s" % (table_name, c) for c in columns), table_name)
+ self.insert = "INSERT %s (%s) VALUES (%s)" % (table_name,
+ ", ".join(data_columns),
+ ", ".join("%(" + s + ")s" for s in data_columns))
+ self.update = "UPDATE %s SET %s WHERE %s = %%(%s)s" % (table_name,
+ ", ".join(s + " = %(" + s + ")s" for s in data_columns),
+ index_column,
+ index_column)
+ self.delete = "DELETE FROM %s WHERE %s = %%s" % (table_name, index_column)
+
+class sql_persistent(object):
+ """
+ Mixin for persistent class that needs to be stored in SQL.
+ """
+
+ ## @var sql_in_db
+ # Whether this object is already in SQL or not.
+
+ sql_in_db = False
+
+ ## @var sql_deleted
+ # Whether our cached copy of this object has been deleted.
+
+ sql_deleted = False
+
+ ## @var sql_debug
+ # Enable logging of SQL actions
+
+ sql_debug = False
+
+ ## @var sql_cache_debug
+ # Enable debugging of SQL cache actions
+
+ sql_cache_debug = False
+
+ @classmethod
+ def sql_fetch(cls, gctx, id): # pylint: disable=W0622
+ """
+ Fetch one object from SQL, based on its primary key.
+
+ Since in this one case we know that the primary index is also the
+ cache key, we check for a cache hit directly in the hope of
+ bypassing the SQL lookup entirely.
+
+ This method is usually called via a one-line class-specific
+ wrapper. As a convenience, we also accept an id of None, and just
+ return None in this case.
+ """
+
+ if id is None:
+ return None
+ assert isinstance(id, (int, long)), "id should be an integer, was %r" % type(id)
+ key = (cls, id)
+ if key in gctx.sql.cache:
+ return gctx.sql.cache[key]
+ else:
+ return cls.sql_fetch_where1(gctx, "%s = %%s" % cls.sql_template.index, (id,))
+
+ @classmethod
+ def sql_fetch_where1(cls, gctx, where, args = None, also_from = None):
+ """
+ Fetch one object from SQL, based on an arbitrary SQL WHERE expression.
+ """
+ results = cls.sql_fetch_where(gctx, where, args, also_from)
+ if len(results) == 0:
+ return None
+ elif len(results) == 1:
+ return results[0]
+ else:
+ raise rpki.exceptions.DBConsistancyError, \
+ "Database contained multiple matches for %s where %s: %r" % \
+ (cls.__name__, where % tuple(repr(a) for a in args), results)
+
+ @classmethod
+ def sql_fetch_all(cls, gctx):
+ """
+ Fetch all objects of this type from SQL.
+ """
+ return cls.sql_fetch_where(gctx, None)
+
+ @classmethod
+ def sql_fetch_where(cls, gctx, where, args = None, also_from = None):
+ """
+ Fetch objects of this type matching an arbitrary SQL WHERE expression.
+ """
+ if where is None:
+ assert args is None and also_from is None
+ if cls.sql_debug:
+ rpki.log.debug("sql_fetch_where(%r)" % cls.sql_template.select)
+ gctx.sql.execute(cls.sql_template.select)
+ else:
+ query = cls.sql_template.select
+ if also_from is not None:
+ query += "," + also_from
+ query += " WHERE " + where
+ if cls.sql_debug:
+ rpki.log.debug("sql_fetch_where(%r, %r)" % (query, args))
+ gctx.sql.execute(query, args)
+ results = []
+ for row in gctx.sql.fetchall():
+ key = (cls, row[0])
+ if key in gctx.sql.cache:
+ results.append(gctx.sql.cache[key])
+ else:
+ results.append(cls.sql_init(gctx, row, key))
+ return results
+
+ @classmethod
+ def sql_init(cls, gctx, row, key):
+ """
+ Initialize one Python object from the result of a SQL query.
+ """
+ self = cls()
+ self.gctx = gctx
+ self.sql_decode(dict(zip(cls.sql_template.columns, row)))
+ gctx.sql.cache[key] = self
+ self.sql_in_db = True
+ self.sql_fetch_hook()
+ return self
+
+ def sql_mark_dirty(self):
+ """
+ Mark this object as needing to be written back to SQL.
+ """
+ if self.sql_cache_debug and not self.sql_is_dirty:
+ rpki.log.debug("Marking %r SQL dirty" % self)
+ self.gctx.sql.dirty.add(self)
+
+ def sql_mark_clean(self):
+ """
+ Mark this object as not needing to be written back to SQL.
+ """
+ if self.sql_cache_debug and self.sql_is_dirty:
+ rpki.log.debug("Marking %r SQL clean" % self)
+ self.gctx.sql.dirty.discard(self)
+
+ @property
+ def sql_is_dirty(self):
+ """
+ Query whether this object needs to be written back to SQL.
+ """
+ return self in self.gctx.sql.dirty
+
+ def sql_mark_deleted(self):
+ """
+ Mark this object as needing to be deleted in SQL.
+ """
+ self.sql_deleted = True
+ self.sql_mark_dirty()
+
+ def sql_store(self):
+ """
+ Store this object to SQL.
+ """
+ args = self.sql_encode()
+ if not self.sql_in_db:
+ if self.sql_debug:
+ rpki.log.debug("sql_store(%r, %r)" % (self.sql_template.insert, args))
+ self.gctx.sql.execute(self.sql_template.insert, args)
+ setattr(self, self.sql_template.index, self.gctx.sql.lastrowid())
+ self.gctx.sql.cache[(self.__class__, self.gctx.sql.lastrowid())] = self
+ self.sql_insert_hook()
+ else:
+ if self.sql_debug:
+ rpki.log.debug("sql_store(%r, %r)" % (self.sql_template.update, args))
+ self.gctx.sql.execute(self.sql_template.update, args)
+ self.sql_update_hook()
+ key = (self.__class__, getattr(self, self.sql_template.index))
+ assert key in self.gctx.sql.cache and self.gctx.sql.cache[key] == self
+ self.sql_mark_clean()
+ self.sql_in_db = True
+
+ def sql_delete(self):
+ """
+ Delete this object from SQL.
+ """
+ if self.sql_in_db:
+ id = getattr(self, self.sql_template.index) # pylint: disable=W0622
+ if self.sql_debug:
+ rpki.log.debug("sql_delete(%r, %r)" % (self.sql_template.delete, id))
+ self.sql_delete_hook()
+ self.gctx.sql.execute(self.sql_template.delete, id)
+ key = (self.__class__, id)
+ if self.gctx.sql.cache.get(key) == self:
+ del self.gctx.sql.cache[key]
+ self.sql_in_db = False
+ self.sql_mark_clean()
+
+ def sql_encode(self):
+ """
+ Convert object attributes into a dict for use with canned SQL
+ queries. This is a default version that assumes a one-to-one
+ mapping between column names in SQL and attribute names in Python.
+ If you need something fancier, override this.
+ """
+ d = dict((a, getattr(self, a, None)) for a in self.sql_template.columns)
+ for i in self.sql_template.map:
+ if d.get(i) is not None:
+ d[i] = self.sql_template.map[i].to_sql(d[i])
+ return d
+
+ def sql_decode(self, vals):
+ """
+ Initialize an object with values returned by self.sql_fetch().
+ This is a default version that assumes a one-to-one mapping
+ between column names in SQL and attribute names in Python. If you
+ need something fancier, override this.
+ """
+ for a in self.sql_template.columns:
+ if vals.get(a) is not None and a in self.sql_template.map:
+ setattr(self, a, self.sql_template.map[a].from_sql(vals[a]))
+ else:
+ setattr(self, a, vals[a])
+
+ def sql_fetch_hook(self):
+ """
+ Customization hook.
+ """
+ pass
+
+ def sql_insert_hook(self):
+ """
+ Customization hook.
+ """
+ pass
+
+ def sql_update_hook(self):
+ """
+ Customization hook.
+ """
+ self.sql_delete_hook()
+ self.sql_insert_hook()
+
+ def sql_delete_hook(self):
+ """
+ Customization hook.
+ """
+ pass
+
+
+def cache_reference(func):
+ """
+ Decorator for use with property methods which just do an SQL lookup based on an ID.
+ Check for an existing reference to the object, just return that if we find it,
+ otherwise perform the SQL lookup.
+
+ Not 100% certain this is a good idea, but I //think// it should work well with the
+ current weak reference SQL cache, so long as we create no circular references.
+ So don't do that.
+ """
+
+ attr_name = "_" + func.__name__
+
+ def wrapped(self):
+ try:
+ value = getattr(self, attr_name)
+ assert value is not None
+ except AttributeError:
+ value = func(self)
+ if value is not None:
+ setattr(self, attr_name, value)
+ return value
+
+ wrapped.__name__ = func.__name__
+ wrapped.__doc__ = func.__doc__
+ wrapped.__dict__.update(func.__dict__)
+
+ return wrapped