-added readonly_connection.py, which opens a second DB connection using readonly credentials found in the global config
-added special QuerySet subclasses to model_logic that wrap DB access in a readonly connection
-made query_objects wrap the DB access in a readonly connection when given extra_args or extra_where (which can contain arbitrary SQL from the user)
-got rid of unnecessary call to query_objects
git-svn-id: http://test.kernel.org/svn/autotest/trunk@1775 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index d8bae9f..308b1b3 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -4,7 +4,7 @@
from django.db import models as dbmodels, backend, connection
from django.utils import datastructures
-
+from frontend.afe import readonly_connection
class ValidationError(Exception):
"""\
@@ -13,6 +13,72 @@
"""
+def _wrap_with_readonly(method):
+ def wrapper_method(*args, **kwargs):
+ readonly_connection.connection.set_django_connection()
+ try:
+ return method(*args, **kwargs)
+ finally:
+ readonly_connection.connection.unset_django_connection()
+ wrapper_method.__name__ = method.__name__
+ return wrapper_method
+
+
+def _wrap_generator_with_readonly(generator):
+ """
+ We have to wrap generators specially. Assume it performs
+ the query on the first call to next().
+ """
+ def wrapper_generator(*args, **kwargs):
+ generator_obj = generator(*args, **kwargs)
+ readonly_connection.connection.set_django_connection()
+ try:
+ first_value = generator_obj.next()
+ finally:
+ readonly_connection.connection.unset_django_connection()
+ yield first_value
+
+ while True:
+ yield generator_obj.next()
+
+ wrapper_generator.__name__ = generator.__name__
+ return wrapper_generator
+
+
+def _make_queryset_readonly(queryset):
+ """
+ Wrap all methods that do database queries with a readonly connection.
+ """
+ db_query_methods = ['count', 'get', 'get_or_create', 'latest', 'in_bulk',
+ 'delete']
+ for method_name in db_query_methods:
+ method = getattr(queryset, method_name)
+ wrapped_method = _wrap_with_readonly(method)
+ setattr(queryset, method_name, wrapped_method)
+
+ queryset.iterator = _wrap_generator_with_readonly(queryset.iterator)
+
+
+class ReadonlyQuerySet(dbmodels.query.QuerySet):
+ """
+ QuerySet object that performs all database queries with the read-only
+ connection.
+ """
+ def __init__(self, model=None):
+ super(ReadonlyQuerySet, self).__init__(model)
+ _make_queryset_readonly(self)
+
+
+ def values(self, *fields):
+ return self._clone(klass=ReadonlyValuesQuerySet, _fields=fields)
+
+
+class ReadonlyValuesQuerySet(dbmodels.query.ValuesQuerySet):
+ def __init__(self, model=None):
+ super(ReadonlyValuesQuerySet, self).__init__(model)
+ _make_queryset_readonly(self)
+
+
class ExtendedManager(dbmodels.Manager):
"""\
Extended manager supporting subquery filtering.
@@ -348,6 +414,7 @@
# other arguments
if extra_args:
query = query.extra(**extra_args)
+ query = query._clone(klass=ReadonlyQuerySet)
# sorting + paging
assert isinstance(sort_by, list) or isinstance(sort_by, tuple)
diff --git a/frontend/afe/readonly_connection.py b/frontend/afe/readonly_connection.py
new file mode 100644
index 0000000..19ac51e
--- /dev/null
+++ b/frontend/afe/readonly_connection.py
@@ -0,0 +1,75 @@
+from django.db import connection as django_connection
+from django.conf import settings
+from django.dispatch import dispatcher
+from django.core import signals
+
+class ReadOnlyConnection(object):
+ """
+ This class constructs a new connection to the DB using the read-only
+ credentials from settings. It reaches into some internals of
+ django.db.connection which are undocumented as far as I know, but I believe
+ it works across many, if not all, of the backends.
+ """
+ def __init__(self):
+ self._connection = None
+
+
+ def _open_connection(self):
+ if self._connection is not None:
+ return
+ self._save_django_state()
+ self._connection = self._get_readonly_connection()
+ self._restore_django_state()
+
+
+ def _save_django_state(self):
+ self._old_connection = django_connection.connection
+ self._old_username = settings.DATABASE_USER
+ self._old_password = settings.DATABASE_PASSWORD
+
+
+ def _restore_django_state(self):
+ django_connection.connection = self._old_connection
+ settings.DATABASE_USER = self._old_username
+ settings.DATABASE_PASSWORD = self._old_password
+
+
+ def _get_readonly_connection(self):
+ settings.DATABASE_USER = settings.DATABASE_READONLY_USER
+ settings.DATABASE_PASSWORD = settings.DATABASE_READONLY_PASSWORD
+ django_connection.connection = None
+ # cursor() causes a new connection to be created
+ cursor = django_connection.cursor()
+ assert django_connection.connection is not None
+ return django_connection.connection
+
+
+ def set_django_connection(self):
+ assert (django_connection.connection != self._connection or
+ self._connection is None)
+ self._open_connection()
+ self._old_connection = django_connection.connection
+ django_connection.connection = self._connection
+
+
+ def unset_django_connection(self):
+ assert self._connection is not None
+ assert django_connection.connection == self._connection
+ django_connection.connection = self._old_connection
+
+
+ def cursor(self):
+ self._open_connection()
+ return self._connection.cursor()
+
+
+ def close(self):
+ if self._connection is not None:
+ assert django_connection != self._connection
+ self._connection.close()
+ self._connection = None
+
+
+connection = ReadOnlyConnection()
+# close any open connection when request finishes
+dispatcher.connect(connection.close, signal=signals.request_finished)
diff --git a/frontend/afe/rpc_interface.py b/frontend/afe/rpc_interface.py
index 450fd56..ea435c9 100644
--- a/frontend/afe/rpc_interface.py
+++ b/frontend/afe/rpc_interface.py
@@ -29,7 +29,8 @@
__author__ = 'showard@google.com (Steve Howard)'
-import models, model_logic, control_file, rpc_utils
+from frontend.afe import models, model_logic, control_file, rpc_utils
+from frontend.afe import readonly_connection
from autotest_lib.client.common_lib import global_config
@@ -296,8 +297,8 @@
# check that each metahost request has enough hosts under the label
if meta_hosts:
- labels = models.Label.query_objects(
- {'name__in': requested_host_counts.keys()})
+ labels = models.Label.objects.filter(
+ name__in=requested_host_counts.keys())
for label in labels:
count = label.host_set.count()
if requested_host_counts[label.name] > count:
diff --git a/frontend/settings.py b/frontend/settings.py
index a47eeca..5eb6650 100644
--- a/frontend/settings.py
+++ b/frontend/settings.py
@@ -20,12 +20,18 @@
# Not used with sqlite3.
c = global_config.global_config
-DATABASE_HOST = c.get_config_value("AUTOTEST_WEB", "host")
+_section = 'AUTOTEST_WEB'
+DATABASE_HOST = c.get_config_value(_section, "host")
# Or path to database file if using sqlite3.
-DATABASE_NAME = c.get_config_value("AUTOTEST_WEB", "database")
+DATABASE_NAME = c.get_config_value(_section, "database")
# The following not used with sqlite3.
-DATABASE_USER = c.get_config_value("AUTOTEST_WEB", "user")
-DATABASE_PASSWORD = c.get_config_value("AUTOTEST_WEB", "password")
+DATABASE_USER = c.get_config_value(_section, "user")
+DATABASE_PASSWORD = c.get_config_value(_section, "password")
+
+DATABASE_READONLY_USER = c.get_config_value(_section, "readonly_user",
+ default=DATABASE_USER)
+DATABASE_READONLY_PASSWORD = c.get_config_value(_section, "readonly_password",
+ default=DATABASE_PASSWORD)
# prefix applied to all URLs - useful if requests are coming through apache,
diff --git a/global_config.ini b/global_config.ini
index 2a5295f..aec9f29 100644
--- a/global_config.ini
+++ b/global_config.ini
@@ -2,8 +2,10 @@
host: localhost
database: tko
db_type: mysql
-user: nobody
+user:
password:
+readonly_user: nobody
+readonly_password:
query_timeout: 3600
min_retry_delay: 20
max_retry_delay: 60