-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