Selaa lähdekoodia

[beeswax] Support mutliple Beeswaxd servers

Updated all the db_utils.get_client() to make the Query Server configurable
Adding server_name, server_host, server_port to 'QueryHistory' model
Adding a support_ddl flag for retrieving a Query Server supporting DDL
Adding QueryServer select box to 'Query Editor' and 'Configuration' pages
Displaying 'Query Server' name in 'History' page
Loading query servers clusters from config
Adding DB schema migration file
Adding cookie for remembering the previously selected 'Query Server'
INI files updated and old beeswax configuration made backward compatible
Romain Rigaux 13 vuotta sitten
vanhempi
commit
aa2049edbd
39 muutettua tiedostoa jossa 576 lisäystä ja 239 poistoa
  1. 34 2
      apps/beeswax/src/beeswax/conf.py
  2. 1 1
      apps/beeswax/src/beeswax/data_export.py
  3. 49 14
      apps/beeswax/src/beeswax/db_utils.py
  4. 16 4
      apps/beeswax/src/beeswax/forms.py
  5. 8 8
      apps/beeswax/src/beeswax/management/0003_auto__chg_field_queryhistory_query.py
  6. 0 1
      apps/beeswax/src/beeswax/management/commands/beeswax_install_examples.py
  7. 6 1
      apps/beeswax/src/beeswax/management/commands/beeswax_server.py
  8. 7 7
      apps/beeswax/src/beeswax/migrations/0001_initial.py
  9. 8 8
      apps/beeswax/src/beeswax/migrations/0002_auto__add_field_queryhistory_notify.py
  10. 110 0
      apps/beeswax/src/beeswax/migrations/0003_auto__add_field_queryhistory_server_name__add_field_queryhistory_serve.py
  11. 6 0
      apps/beeswax/src/beeswax/models.py
  12. 1 1
      apps/beeswax/src/beeswax/templates/choose_delimiter.mako
  13. 1 1
      apps/beeswax/src/beeswax/templates/choose_file.mako
  14. 26 8
      apps/beeswax/src/beeswax/templates/configuration.mako
  15. 1 1
      apps/beeswax/src/beeswax/templates/confirm.html
  16. 1 1
      apps/beeswax/src/beeswax/templates/create_table_index.mako
  17. 5 5
      apps/beeswax/src/beeswax/templates/create_table_manually.mako
  18. 1 1
      apps/beeswax/src/beeswax/templates/define_columns.mako
  19. 2 2
      apps/beeswax/src/beeswax/templates/describe_partitions.mako
  20. 5 3
      apps/beeswax/src/beeswax/templates/describe_table.mako
  21. 22 7
      apps/beeswax/src/beeswax/templates/execute.mako
  22. 1 1
      apps/beeswax/src/beeswax/templates/index.mako
  23. 1 1
      apps/beeswax/src/beeswax/templates/layout.mako
  24. 2 2
      apps/beeswax/src/beeswax/templates/list_designs.mako
  25. 80 75
      apps/beeswax/src/beeswax/templates/list_history.mako
  26. 1 1
      apps/beeswax/src/beeswax/templates/load_table.mako
  27. 2 2
      apps/beeswax/src/beeswax/templates/my_queries.mako
  28. 4 1
      apps/beeswax/src/beeswax/templates/parameterization.mako
  29. 1 1
      apps/beeswax/src/beeswax/templates/save_results.mako
  30. 2 2
      apps/beeswax/src/beeswax/templates/show_tables.mako
  31. 2 2
      apps/beeswax/src/beeswax/templates/watch_results.mako
  32. 2 2
      apps/beeswax/src/beeswax/templates/watch_wait.mako
  33. 10 4
      apps/beeswax/src/beeswax/test_base.py
  34. 38 17
      apps/beeswax/src/beeswax/tests.py
  35. 65 44
      apps/beeswax/src/beeswax/views.py
  36. 1 1
      apps/jobsub/src/jobsub/conf.py
  37. 27 3
      desktop/conf.dist/hue.ini
  38. 26 3
      desktop/conf/pseudo-distributed.ini.tmpl
  39. 1 1
      desktop/core/src/desktop/lib/thrift_util.py

+ 34 - 2
apps/beeswax/src/beeswax/conf.py

@@ -14,23 +14,55 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""Configuration options for the Hive UI (Beeswax)."""
-from desktop.lib.conf import Config, coerce_bool
 import os.path
+
 from django.utils.translation import ugettext_lazy as _
 
+from desktop.lib.conf import Config, UnspecifiedConfigSection, ConfigSection, coerce_bool
+
+
+QUERY_SERVERS = UnspecifiedConfigSection(
+  "query_servers",
+  help=_("One entry for each Query Server that can execute some queries."),
+  each=ConfigSection(
+    help=_("Information about a single Query Server"),
+    members=dict(
+      SERVER_HOST = Config(
+        key="server_host",
+        help=_("Host where the Query Server Thrift daemon is running."),
+        private=True,
+        default="localhost"),
+      SERVER_PORT = Config(
+        key="server_port",
+        help=_("Configure the port the Query Server Thrift server."),
+        default=8002,
+        type=int),
+      SUPPORT_DDL = Config(
+        key='support_ddl',
+        default=True,
+        type=coerce_bool,
+        help=_('If DDL queries are supported (e.g. DROP can be sent directly to this server).'))
+    )
+  )
+)
+
+# Deprecated! To remove in Hue 3
+# Multiple sections are now available in QUERY_SERVERS
 BEESWAX_SERVER_HOST = Config(
   key="beeswax_server_host",
   help=_("Host where Beeswax server Thrift daemon is running."),
   private=True,
   default="localhost")
 
+# Deprecated! To remove in Hue 3
+# Multiple sections are now available in QUERY_SERVERS
 BEESWAX_SERVER_PORT = Config(
   key="beeswax_server_port",
   help=_("Configure the port the Beeswax Thrift server runs on."),
   default=8002,
   type=int)
 
+
 BEESWAX_META_SERVER_HOST = Config(
   key="beeswax_meta_server_host",
   help=_("Host where Beeswax internal metastore Thrift daemon is running."),

+ 1 - 1
apps/beeswax/src/beeswax/data_export.py

@@ -78,7 +78,7 @@ def data_generator(query_model, formatter):
   while True:
     # Make sure that we have the next batch of ready results
     while results is None or not results.ready:
-      results = db_utils.db_client().fetch(handle, start_over=is_first_row, fetch_size=-1)
+      results = db_utils.db_client(query_model.get_query_server()).fetch(handle, start_over=is_first_row, fetch_size=-1)
       if not results.ready:
         time.sleep(_DATA_WAIT_SLEEP)
 

+ 49 - 14
apps/beeswax/src/beeswax/db_utils.py

@@ -23,7 +23,6 @@ import logging
 import thrift
 import time
 
-import desktop.conf
 import hadoop.cluster
 
 from beeswax import conf
@@ -41,7 +40,8 @@ from django.utils.translation import ugettext_lazy as _
 
 LOG = logging.getLogger(__name__)
 
-def execute_directly(user, query_msg, design=None, notify=False):
+
+def execute_directly(user, query_msg, query_server=None, design=None, notify=False):
   """
   execute_directly(user, query_msg [,design]) -> QueryHistory object
 
@@ -51,9 +51,15 @@ def execute_directly(user, query_msg, design=None, notify=False):
   design - The SavedQuery object (i.e. design) associated with this query.
   notify - Whether to notify the user upon completion.
   """
+  if query_server is None:
+    query_server = get_query_server(support_ddl=True) # For convenience with DDL queries only
+
   query_history = QueryHistory(
                             owner=user,
                             query=query_msg.query,
+                            server_host='%(server_host)s' % query_server,
+                            server_port='%(server_port)d' % query_server,
+                            server_name='%(server_name)s' % query_server,
                             last_state=QueryHistory.STATE.submitted.index,
                             design=design,
                             notify=notify)
@@ -63,7 +69,7 @@ def execute_directly(user, query_msg, design=None, notify=False):
 
   # Now submit it
   try:
-    handle = db_client().query(query_msg)
+    handle = db_client(query_server).query(query_msg)
     if not handle or not handle.id:
       # It really shouldn't happen
       msg = _("BeeswaxServer returning invalid handle for query id %(id)d [%(query)s]...") % \
@@ -104,7 +110,7 @@ def wait_for_results(query_history, timeout_sec=30.0):
   curr = time.time()
   end = curr + timeout_sec
   while curr <= end:
-    results = db_client().fetch(handle, START_OVER, fetch_size=-1)
+    results = db_client(query_history.get_query_server()).fetch(handle, START_OVER, fetch_size=-1)
     if results.ready:
       return results
     time.sleep(SLEEP_INTERVAL)
@@ -112,13 +118,13 @@ def wait_for_results(query_history, timeout_sec=30.0):
   return None
 
 
-def execute_and_wait(user, query_msg, timeout_sec=30.0):
+def execute_and_wait(user, query_msg, query_server=None, timeout_sec=30.0):
   """
   execute_and_wait(user, query_msg) -> results or None
   Combine execute_directly() and wait_for_results()
   May raise BeeswaxException
   """
-  history = execute_directly(user, query_msg)
+  history = execute_directly(user, query_msg, query_server)
   result = wait_for_results(history, timeout_sec)
   return result
 
@@ -141,7 +147,7 @@ def get_query_state(query_history):
   # Now check the server state
   handle = QueryHandle(id=server_id, log_context=query_history.log_context)
   try:
-    server_state = db_client().get_state(handle)
+    server_state = db_client(query_history.get_query_server()).get_state(handle)
     return models.QueryHistory.STATE_MAP[server_state]
   except QueryNotFoundException:
     LOG.debug("Query id %s has expired" % (query_history.id,))
@@ -152,10 +158,39 @@ def get_query_state(query_history):
     return None
 
 
+def get_query_server(name='default', support_ddl=False):
+  if 'beeswax_server_host' in conf.QUERY_SERVERS['bind_to'] or 'beeswax_server_port' in conf.QUERY_SERVERS['bind_to']:
+    return {
+          'server_name': 'default',
+          'server_host': conf.BEESWAX_SERVER_HOST.get(),
+          'server_port': conf.BEESWAX_SERVER_PORT.get(),
+          'support_ddl': True,
+      }
+
+  servers = conf.QUERY_SERVERS
+
+  if support_ddl:
+    if not servers[name].SUPPORT_DDL.get():
+      for key in servers.keys():
+        if servers[key].SUPPORT_DDL.get():
+          name = key
+          break
+
+  config = servers[name]
+
+  query_server = {
+      'server_name': name,
+      'server_host': config.SERVER_HOST.get(),
+      'server_port': config.SERVER_PORT.get(),
+      'support_ddl': config.SUPPORT_DDL.get(),
+  }
+
+  return query_server
+
 #
 # Note that thrift_util does client connection caching for us.
 #
-def db_client():
+def db_client(query_server):
   """Get the Thrift client to talk to beeswax server"""
 
   class UnicodeBeeswaxClient(object):
@@ -218,12 +253,12 @@ def db_client():
   use_sasl = cluster_conf is not None and cluster_conf.SECURITY_ENABLED.get()
 
   client = thrift_util.get_client(BeeswaxService.Client,
-                                conf.BEESWAX_SERVER_HOST.get(),
-                                conf.BEESWAX_SERVER_PORT.get(),
-                                service_name="Beeswax (Hive UI) Server",
-                                kerberos_principal="hue",
-                                use_sasl=use_sasl,
-                                timeout_seconds=conf.BEESWAX_SERVER_CONN_TIMEOUT.get())
+                                  query_server['server_host'],
+                                  query_server['server_port'],
+                                  service_name=query_server['server_name'],
+                                  kerberos_principal="hue",
+                                  use_sasl=use_sasl,
+                                  timeout_seconds=conf.BEESWAX_SERVER_CONN_TIMEOUT.get())
   return UnicodeBeeswaxClient(client)
 
 

+ 16 - 4
apps/beeswax/src/beeswax/forms.py

@@ -22,9 +22,7 @@ import hive_metastore
 
 from desktop.lib.django_forms import simple_formset_factory, DependencyAwareForm
 from desktop.lib.django_forms import ChoiceOrOtherField, MultiForm, SubmitButton
-from beeswax import common
-from beeswax import db_utils
-from beeswax import models
+from beeswax import db_utils, common, conf, models
 from filebrowser.forms import PathField
 
 
@@ -35,7 +33,21 @@ def query_form():
       settings=SettingFormSet,
       file_resources=FileResourceFormSet,
       functions=FunctionFormSet,
-      saveform=SaveForm)
+      saveform=SaveForm,
+      query_servers=QueryServerForm)
+
+
+class QueryServerForm(forms.Form):
+  server = forms.CharField(max_length=128, help_text=_t('Host:port of the query server.'))
+
+  def __init__(self, *args, **kwargs):
+    super(QueryServerForm, self).__init__(*args, **kwargs)
+
+    if conf.QUERY_SERVERS:
+      servers = ((server, server) for server in conf.QUERY_SERVERS.keys())
+    else:
+      servers = (('default', 'default'),)
+    self.fields['server'].widget = forms.Select(choices=servers)
 
 
 class SaveForm(forms.Form):

+ 8 - 8
apps/beeswax/src/beeswax/management/0003_auto__chg_field_queryhistory_query.py

@@ -5,19 +5,19 @@ from south.v2 import SchemaMigration
 from django.db import models
 
 class Migration(SchemaMigration):
-    
+
     def forwards(self, orm):
-        
+
         # Changing field 'QueryHistory.query'
         db.alter_column('beeswax_queryhistory', 'query', self.gf('django.db.models.fields.TextField')())
-    
-    
+
+
     def backwards(self, orm):
-        
+
         # Changing field 'QueryHistory.query'
         db.alter_column('beeswax_queryhistory', 'query', self.gf('django.db.models.fields.CharField')(max_length=1024))
-    
-    
+
+
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -85,5 +85,5 @@ class Migration(SchemaMigration):
             'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
         }
     }
-    
+
     complete_apps = ['beeswax']

+ 0 - 1
apps/beeswax/src/beeswax/management/commands/beeswax_install_examples.py

@@ -41,7 +41,6 @@ from django.core.management.base import NoArgsCommand
 from django.contrib.auth.models import User
 
 import beeswax.conf
-import beeswax.design
 from beeswax import db_utils, models
 from beeswaxd import BeeswaxService
 from beeswaxd.ttypes import BeeswaxException

+ 6 - 1
apps/beeswax/src/beeswax/management/commands/beeswax_server.py

@@ -60,10 +60,15 @@ class Command(NoArgsCommand):
     if dt_host == '0.0.0.0':
       dt_host = '127.0.0.1'
 
+    if 'beeswax_server_port' in beeswax.conf.QUERY_SERVERS['bind_to']:
+      server_port = beeswax.conf.BEESWAX_SERVER_PORT.get()
+    else:
+      server_port = beeswax.conf.QUERY_SERVERS['default'].SERVER_PORT.get()
+
     args = [
       os.path.basename(bin),
       '--beeswax',
-      str(beeswax.conf.BEESWAX_SERVER_PORT.get()),
+      str(server_port),
       '--desktop-host',
       str(dt_host),
       '--desktop-port',

+ 7 - 7
apps/beeswax/src/beeswax/migrations/0001_initial.py

@@ -21,9 +21,9 @@ from south.v2 import SchemaMigration
 from django.db import models
 
 class Migration(SchemaMigration):
- 
+
     def forwards(self, orm):
-     
+
         # Adding model 'MetaInstall'
         try:
             db.create_table('beeswax_metainstall', (
@@ -34,7 +34,7 @@ class Migration(SchemaMigration):
         except:
             logging.warning("Initial db creation being skipped, likely because table already exists.", exc_info=True)
             return
- 
+
         # Adding model 'QueryHistory'
         db.create_table('beeswax_queryhistory', (
             ('submission_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
@@ -65,7 +65,7 @@ class Migration(SchemaMigration):
 
 
     def backwards(self, orm):
-     
+
         # Deleting model 'QueryHistory'
         db.delete_table('beeswax_queryhistory')
 
@@ -74,8 +74,8 @@ class Migration(SchemaMigration):
 
         # Deleting model 'MetaInstall'
         db.delete_table('beeswax_metainstall')
- 
- 
+
+
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -142,5 +142,5 @@ class Migration(SchemaMigration):
             'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
         }
     }
- 
+
     complete_apps = ['beeswax']

+ 8 - 8
apps/beeswax/src/beeswax/migrations/0002_auto__add_field_queryhistory_notify.py

@@ -20,19 +20,19 @@ from south.v2 import SchemaMigration
 from django.db import models
 
 class Migration(SchemaMigration):
- 
+
     def forwards(self, orm):
-     
+
         # Adding field 'QueryHistory.notify'
         db.add_column('beeswax_queryhistory', 'notify', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True), keep_default=False)
- 
- 
+
+
     def backwards(self, orm):
-     
+
         # Deleting field 'QueryHistory.notify'
         db.delete_column('beeswax_queryhistory', 'notify')
- 
- 
+
+
     models = {
         'auth.group': {
             'Meta': {'object_name': 'Group'},
@@ -100,5 +100,5 @@ class Migration(SchemaMigration):
             'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
         }
     }
- 
+
     complete_apps = ['beeswax']

+ 110 - 0
apps/beeswax/src/beeswax/migrations/0003_auto__add_field_queryhistory_server_name__add_field_queryhistory_serve.py

@@ -0,0 +1,110 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Adding field 'QueryHistory.server_name'
+        db.add_column('beeswax_queryhistory', 'server_name', self.gf('django.db.models.fields.CharField')(default='', max_length=128), keep_default=False)
+
+        # Adding field 'QueryHistory.server_host'
+        db.add_column('beeswax_queryhistory', 'server_host', self.gf('django.db.models.fields.CharField')(default='', max_length=128), keep_default=False)
+
+        # Adding field 'QueryHistory.server_port'
+        db.add_column('beeswax_queryhistory', 'server_port', self.gf('django.db.models.fields.SmallIntegerField')(default=''), keep_default=False)
+
+        # Changing field 'QueryHistory.query'
+        db.alter_column('beeswax_queryhistory', 'query', self.gf('django.db.models.fields.TextField')())
+
+
+    def backwards(self, orm):
+
+        # Deleting field 'QueryHistory.server_name'
+        db.delete_column('beeswax_queryhistory', 'server_name')
+
+        # Deleting field 'QueryHistory.server_host'
+        db.delete_column('beeswax_queryhistory', 'server_host')
+
+        # Deleting field 'QueryHistory.server_port'
+        db.delete_column('beeswax_queryhistory', 'server_port')
+
+        # Changing field 'QueryHistory.query'
+        db.alter_column('beeswax_queryhistory', 'query', self.gf('django.db.models.fields.CharField')(max_length=1024))
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'beeswax.metainstall': {
+            'Meta': {'object_name': 'MetaInstall'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'installed_example': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+        },
+        'beeswax.queryhistory': {
+            'Meta': {'object_name': 'QueryHistory'},
+            'design': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['beeswax.SavedQuery']", 'null': 'True'}),
+            'has_results': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_state': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}),
+            'log_context': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'notify': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'query': ('django.db.models.fields.TextField', [], {}),
+            'server_host': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_id': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            'server_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128'}),
+            'server_port': ('django.db.models.fields.SmallIntegerField', [], {'default': "''"}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+        },
+        'beeswax.savedquery': {
+            'Meta': {'object_name': 'SavedQuery'},
+            'data': ('django.db.models.fields.TextField', [], {'max_length': '65536'}),
+            'desc': ('django.db.models.fields.TextField', [], {'max_length': '1024'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_auto': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'mtime': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'type': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        }
+    }
+
+    complete_apps = ['beeswax']

+ 6 - 0
apps/beeswax/src/beeswax/models.py

@@ -58,6 +58,9 @@ class QueryHistory(models.Model):
   # Only query in the "submitted" state is allowed to have no server_id
   server_id = models.CharField(max_length=1024, null=True)
   log_context = models.CharField(max_length=1024, null=True)
+  server_host = models.CharField(max_length=128, help_text=_('Host of the query server.'), default='')
+  server_port = models.SmallIntegerField(help_text=_('Port of the query server.'), default=0)
+  server_name = models.CharField(max_length=128, help_text=_('Name of the query server.'), default='')
   # Some queries (like read/drop table) don't have a design
   design = models.ForeignKey('SavedQuery', to_field='id', null=True)
   # Notify on completion
@@ -112,6 +115,9 @@ class QueryHistory(models.Model):
       self.save_state(QueryHistory.STATE.expired)
       return (False, None)
 
+  def get_query_server(self):
+    return dict(zip(['server_name', 'server_host', 'server_port'],
+                    [self.server_name, self.server_host, self.server_port]))
 
 
 class SavedQuery(models.Model):

+ 1 - 1
apps/beeswax/src/beeswax/templates/choose_delimiter.mako

@@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Beeswax: Create table from file'), "beeswax", "100px")}
+${commonheader(_('Create table from file'), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
 
 <div class="container-fluid">

+ 1 - 1
apps/beeswax/src/beeswax/templates/choose_file.mako

@@ -19,7 +19,7 @@ from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
-${commonheader(_('Beeswax: Create table from file'), "beeswax", "100px")}
+${commonheader(_('Create table from file'), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
 
 

+ 26 - 8
apps/beeswax/src/beeswax/templates/configuration.mako

@@ -19,14 +19,22 @@ from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Hive Configuration Variables'), "beeswax", "100px")}
-${layout.menubar(section='hive configuration')}
+
+${commonheader(_('Configuration Variables'), "beeswax", "100px")}
+${layout.menubar(section='configuration')}
+
 <div class="container-fluid">
-	<h1>${_('Hive Configuration Variables')}</h1>
+	<h1>${_('Configuration Variables')}</h1>
 	<div class="well">
-		<form class="form-search">
-            ${_('Filter:')} <input id="filterInput" class="input-xlarge search-query" placeholder="${_('Search for key, value, etc...')}">
-		    <a href="#" id="clearFilterBtn" class="btn">${_('Clear')}</a>
+		<form class="form-search" method="POST">
+		  <span>
+		    ${server_form['server']}
+		    <button type="submit" class="btn primary">${_('Look')}</button>
+		   </span>
+		   <span class="pull-right">
+              ${_('Filter:')} <input id="filterInput" class="input-xlarge search-query" placeholder="${_('Search for key, value, etc...')}">
+		      <a href="#" id="clearFilterBtn" class="btn">${_('Clear')}</a>
+		   <span>
 		</form>
 	</div>
 	<table class="table table-striped table-condensed datatables">
@@ -47,8 +55,10 @@ ${layout.menubar(section='hive configuration')}
 	</table>
 </div>
 
-<script type="text/javascript" charset="utf-8">
 
+<script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+
+<script type="text/javascript" charset="utf-8">
 	$(document).ready(function(){
 		$(".datatables").dataTable({
 			"bPaginate": false,
@@ -75,7 +85,15 @@ ${layout.menubar(section='hive configuration')}
 	            $(value).show();
 	        });
 	    });
-	});
 
+        $("#id_server").change(function(){
+            $.cookie("hueBeeswaxLastQueryServer", $(this).val(), {expires: 90});
+        });
+
+        if ($.cookie("hueBeeswaxLastQueryServer") != null) {
+            $("#id_server").val($.cookie("hueBeeswaxLastQueryServer"));
+        }
+	});
 </script>
+
 ${commonfooter(messages)}

+ 1 - 1
apps/beeswax/src/beeswax/templates/confirm.html

@@ -21,7 +21,7 @@ limitations under the License.
 	<a href="#" class="close">&times;</a>
 	<h3>Confirm action</h3>
 </div>
-<div class="modal-body">  
+<div class="modal-body">
   <div class="alert-message block-message warning">
         {{title}}
   </div>

+ 1 - 1
apps/beeswax/src/beeswax/templates/create_table_index.mako

@@ -18,7 +18,7 @@ from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
-${commonheader(_('Beeswax: Create Table'), "beeswax", "100px")}
+${commonheader(_('Create Table'), "beeswax", "100px")}
 <div class="container-fluid">
   <div class="toolbar">
   </div>

+ 5 - 5
apps/beeswax/src/beeswax/templates/create_table_manually.mako

@@ -19,7 +19,7 @@
 %>
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
-${commonheader(_("Beeswax: Create table manually"), "beeswax", "100px")}
+${commonheader(_("Create table manually"), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
 
 <div class="container-fluid">
@@ -119,7 +119,7 @@ ${layout.menubar(section='tables')}
 	    <fieldset>
 	        <div id="step3Delimited" class="stepDetailsInner">
 	            <div class="alert alert-info"><h3>${_('Configure Record Serialization')}</h3>
-	                ${_('Hive only supports single-character delimiters.')}
+	                ${_('Only supports single-character delimiters.')}
 	            </div>
 	            <div class="control-group">
 	                ${comps.bootstrapLabel(table_form["field_terminator"])}
@@ -283,10 +283,10 @@ ${layout.menubar(section='tables')}
 	    <br/><br/>
 	    <fieldset>
 	        <div class="alert alert-info"><h3>${_('Configure Partitions')}</h3>
-	            ${_('If your data is naturally partitioned (by date, for example), partitions are a way to tell Hive that data for a specific partition value are stored together.')}
-	            ${_('Hive establishes a mapping between directories on disk')}
+	            ${_('If your data is naturally partitioned (by date, for example), partitions are a way to tell the query server that data for a specific partition value are stored together.')}
+	            ${_('The query server establishes a mapping between directories on disk')}
 	            (<em>${_('e.g.')},</em> <code>/user/hive/warehouse/logs/dt=20100101/</code>)
-	            ${_('and the data for that day.  Partitions are virtual columns; they are not represented in the data itself, but are determined by the data location. Hive implements query optimizations such that queries that are specific to a single partition need not read the data in other partitions.')}
+	            ${_('and the data for that day.  Partitions are virtual columns; they are not represented in the data itself, but are determined by the data location. The query server implements query optimizations such that queries that are specific to a single partition need not read the data in other partitions.')}
 	        </div>
 	        % for form in partitions_form.forms:
 	            ${render_column(form, True)}

+ 1 - 1
apps/beeswax/src/beeswax/templates/define_columns.mako

@@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Beeswax: Create table from file'), "beeswax", "100px")}
+${commonheader(_('Create table from file'), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
 
 <div class="container-fluid">

+ 2 - 2
apps/beeswax/src/beeswax/templates/describe_partitions.mako

@@ -13,9 +13,9 @@
 ## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ## See the License for the specific language governing permissions and
 ## limitations under the License.
-<%! 
+<%!
   from urllib import quote
-  from filebrowser.views import location_to_url 
+  from filebrowser.views import location_to_url
   from desktop.views import commonheader, commonfooter
   from django.utils.translation import ugettext as _
 %>

+ 5 - 3
apps/beeswax/src/beeswax/templates/describe_table.mako

@@ -17,18 +17,20 @@
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
+
 <%
   if is_view:
     view_or_table_noun = _("View")
   else:
     view_or_table_noun = _("Table")
 %>
-${commonheader(_("Beeswax %s Metadata: %s") % (view_or_table_noun, table.tableName), "beeswax", "100px")}
+${commonheader(_("%s Metadata: %s") % (view_or_table_noun, table.tableName), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
-<%def name="column_table(cols)">
 
+<%def name="column_table(cols)">
     <table class="table table-striped table-condensed datatables">
       <thead>
         <tr>
@@ -51,7 +53,7 @@ ${layout.menubar(section='tables')}
 </%def>
 
 <div class="container-fluid">
-	<h1>${_('Beeswax Table Metadata:')} ${table.tableName}</h1>
+	<h1>${_('Table Metadata:')} ${table.tableName}</h1>
 	<div class="row-fluid">
 		<div class="span3">
 			<div class="well sidebar-nav">

+ 22 - 7
apps/beeswax/src/beeswax/templates/execute.mako

@@ -24,13 +24,13 @@
 <%namespace name="util" file="util.mako" />
 
 <%def name="query()">
-    <h1>${_('Hive Query')}</h1>
+    <h1>${_('Query Editor')}</h1>
     <fieldset>
         % if design and not design.is_auto and design.name:
-              <legend>${design.name}</legend>
-              % if design.desc:
-                <p>${design.desc}</p>
-              % endif
+            <legend>${design.name}</legend>
+            % if design.desc:
+              <p>${design.desc}</p>
+            % endif
 
           % else:
             <legend>${_('Query')}</legend>
@@ -60,7 +60,7 @@
 </%def>
 
 
-${commonheader(_('Hive Query'), "beeswax", "100px")}
+${commonheader(_('Query'), "beeswax", "100px")}
 ${layout.menubar(section='query')}
 
 <div class="container-fluid">
@@ -69,7 +69,11 @@ ${layout.menubar(section='query')}
             <div class="well sidebar-nav">
                 <form id="advancedSettingsForm" action="${action}" method="POST" class="form form-horizontal noPadding">
                     <ul class="nav nav-list">
-                        <li class="nav-header">${_('Hive settings')}</li>
+                        <li class="nav-header">${_('Query server')}</li>
+                        <li>
+                          ${form.query_servers['server']}
+                        </li>
+                        <li class="nav-header">${_('settings')}</li>
                         <li>
                             % for i, f in enumerate(form.settings.forms):
                             <div class="param">
@@ -317,6 +321,9 @@ ${layout.menubar(section='query')}
 </style>
 
 
+
+<script src="/static/ext/js/jquery/plugins/jquery.cookie.js"></script>
+
 <script type="text/javascript" charset="utf-8">
     $(document).ready(function(){
         $("*[rel=tooltip]").tooltip({
@@ -413,6 +420,14 @@ ${layout.menubar(section='query')}
             $(".query").val($("#queryField").val());
             $("#advancedSettingsForm").submit();
         }
+
+        $("#id_query_servers-server").change(function(){
+            $.cookie("hueBeeswaxLastQueryServer", $(this).val(), {expires: 90});
+        });
+
+        if ($.cookie("hueBeeswaxLastQueryServer") != null) {
+            $("#id_query_servers-server").val($.cookie("hueBeeswaxLastQueryServer"));
+        }
     });
 </script>
 

+ 1 - 1
apps/beeswax/src/beeswax/templates/index.mako

@@ -40,7 +40,7 @@ ${layout.menubar(section='tables')}
                     <li><a href="${ url('beeswax.views.execute_query') }">${_('Execute Query')}</a></li>
                     <li><a href="${ url('beeswax.views.list_query_history') }">${_('Query History')}</a></li>
                     <li class="nav-header">${_('Configuration')}</li>
-                    <li><a href="${ url('beeswax.views.configuration') }">${_('Hive Configuration')}</a></li>
+                    <li><a href="${ url('beeswax.views.configuration') }">${_('Configuration')}</a></li>
                     <li><a href="${ url('beeswax.views.configuration') }?include_hadoop=1">${_('Extended Configuration')}</a></li>
                 </ul>
             </div>

+ 1 - 1
apps/beeswax/src/beeswax/templates/layout.mako

@@ -36,7 +36,7 @@ def is_selected(section, matcher):
 			<li class="${is_selected(section, 'saved queries')}"><a href="/beeswax/list_designs">${_('Saved Queries')}</a></li>
 			<li class="${is_selected(section, 'history')}"><a href="/beeswax/query_history">${_('History')}</a></li>
 			<li class="${is_selected(section, 'tables')}"><a href="/beeswax/tables">${_('Tables')}</a></li>
-			<li class="${is_selected(section, 'hive configuration')}"><a href="/beeswax/configuration">${_('Settings')}</a></li>
+			<li class="${is_selected(section, 'configuration')}"><a href="/beeswax/configuration">${_('Settings')}</a></li>
 		</ul>
 	</div>
 </div>

+ 2 - 2
apps/beeswax/src/beeswax/templates/list_designs.mako

@@ -21,10 +21,10 @@
 %>
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
-${commonheader(_('Beeswax: Queries'), "beeswax", "100px")}
+${commonheader(_('Queries'), "beeswax", "100px")}
 ${layout.menubar(section='saved queries')}
 <div class="container-fluid">
-    <h1>${_('Beeswax: Queries')}</h1>
+    <h1>${_('Queries')}</h1>
     <table class="table table-striped table-condensed datatables">
         <thead>
         <tr>

+ 80 - 75
apps/beeswax/src/beeswax/templates/list_history.mako

@@ -18,11 +18,14 @@ import time
 from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
+
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%!  from beeswax.views import collapse_whitespace %>
-${commonheader(_('Beeswax: Query History'), "beeswax", "100px")}
+
+${commonheader(_('Query History'), "beeswax", "100px")}
 ${layout.menubar(section='history')}
+
 <%def name="show_saved_query(design, history)">
   % if design:
     % if request.user == design.owner:
@@ -45,12 +48,12 @@ ${layout.menubar(section='history')}
 </%def>
 
 <div class="container-fluid">
-	<h1>${_('Beeswax: Query History')}</h1>
-	<div class="row-fluid">
-		<div class="span3">
-			<div class="well sidebar-nav">
-				<ul class="nav nav-list">
-					<li class="nav-header">${_('Actions')}</li>
+    <h1>${_('Query History')}</h1>
+    <div class="row-fluid">
+        <div class="span3">
+            <div class="well sidebar-nav">
+                <ul class="nav nav-list">
+                    <li class="nav-header">${_('Actions')}</li>
                     % if share_queries:
                         % if filter_params.get('user') == ':all':
                           <%
@@ -67,76 +70,78 @@ ${layout.menubar(section='history')}
                         % endif
                     % endif
 
-				 	% if filter_params.get('auto_query', None):
-				      <%
-				        my_querydict = filter_params.copy()
-				        my_querydict['auto_query'] = ''
-				      %>
-				      <li><a href="?${my_querydict.urlencode()}">${_('Show user queries')}</a></li>
-				    % else:
-				      <%
-				        my_querydict = filter_params.copy()
-				        my_querydict['auto_query'] = 'on'
-				      %>
-				      <li><a href="?${my_querydict.urlencode()}">${_('Show auto actions')}</a></li>
-				    % endif
-				</ul>
-			</div>
-		</div>
-		<div class="span9">
+                     % if filter_params.get('auto_query', None):
+                      <%
+                        my_querydict = filter_params.copy()
+                        my_querydict['auto_query'] = ''
+                      %>
+                      <li><a href="?${my_querydict.urlencode()}">${_('Show user queries')}</a></li>
+                    % else:
+                      <%
+                        my_querydict = filter_params.copy()
+                        my_querydict['auto_query'] = 'on'
+                      %>
+                      <li><a href="?${my_querydict.urlencode()}">${_('Show auto actions')}</a></li>
+                    % endif
+                </ul>
+            </div>
+        </div>
+        <div class="span9">
 
-		<table class="table table-striped table-condensed datatables">
-		    <thead>
-		      <tr>
-		        <th>${_('Time')}</th>
-		        <th>${_('Name')}</th>
-		        <th>${_('Query')}</th>
-		        <th>${_('User')}</th>
-		        <th>${_('State')}</th>
-		        <th>${_('Result')}</th>
-		      </tr>
-		    </thead>
-		    <tbody>
-		    <%!
-		      from beeswax import models, views
-		    %>
-		    % for query in page.object_list:
-		      <%
-		        qcontext = ""
-			try:
-			  design = query.design
-		          qcontext = views.make_query_context('design', design.id)
-			except:
-			  pass
-		      %>
-		      <tr class="histRow">
+        <table class="table table-striped table-condensed datatables">
+            <thead>
+              <tr>
+                <th width="10%">${_('Time')}</th>
+                <th width="10%">${_('Query Server')}</th>
+                <th width="15%">${_('Name')}</th>
+                <th width="45%">${_('Query')}</th>
+                <th width="10%">${_('User')}</th>
+                <th width="5%">${_('State')}</th>
+                <th width="5%">${_('Result')}</th>
+              </tr>
+            </thead>
+            <tbody>
+            <%!
+              from beeswax import models, views
+            %>
+            % for query in page.object_list:
+              <%
+                qcontext = ""
+                try:
+                  design = query.design
+                  qcontext = views.make_query_context('design', design.id)
+                except:
+                  pass
+              %>
+              <tr class="histRow">
                 <td data-sort-value="${time.mktime(query.submission_date.timetuple())}">${query.submission_date.strftime("%x %X")}</td>
-		        <td>${show_saved_query(design, query)}</td>
-		        <td>
-		          <p>
-		            % if len(query.query) > 100:
-		              <code>${collapse_whitespace(query.query[:100])}...</code>
-		            % else:
-		              <code>${collapse_whitespace(query.query)}</code>
-		            % endif
-		          </p>
-		        </td>
-		        <td>${query.owner}</td>
-		        <td>${models.QueryHistory.STATE[query.last_state]}</td>
-		        <td>
-		          % if qcontext and query.last_state != models.QueryHistory.STATE.expired.index:
-		            <a href="${ url('beeswax.views.watch_query', id=query.id) }?context=${qcontext|u}" data-row-selector="true">${_('Results')}</a>
-		          % else:
-		            ~
-		          % endif
-		        </td>
-		      </tr>
-		    % endfor
-		    </tbody>
-		  </table>
-		 ${comps.pagination(page)}
-		</div>
-	</div>
+                <td>${query.server_name}</td>
+                <td>${show_saved_query(design, query)}</td>
+                <td>
+                  <p>
+                    % if len(query.query) > 100:
+                      <code>${collapse_whitespace(query.query[:100])}...</code>
+                    % else:
+                      <code>${collapse_whitespace(query.query)}</code>
+                    % endif
+                  </p>
+                </td>
+                <td>${query.owner}</td>
+                <td>${models.QueryHistory.STATE[query.last_state]}</td>
+                <td>
+                  % if qcontext and query.last_state != models.QueryHistory.STATE.expired.index:
+                    <a href="${ url('beeswax.views.watch_query', id=query.id) }?context=${qcontext|u}" data-row-selector="true">${_('Results')}</a>
+                  % else:
+                    ~
+                  % endif
+                </td>
+              </tr>
+            % endfor
+            </tbody>
+          </table>
+         ${comps.pagination(page)}
+        </div>
+    </div>
 </div>
 
 <script type="text/javascript" charset="utf-8">

+ 1 - 1
apps/beeswax/src/beeswax/templates/load_table.mako

@@ -19,7 +19,7 @@ from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Beeswax: Load Data into %(table)s') % dict(table=table), "beeswax", "100px")}
+${commonheader(_('Load Data into %(table)s') % dict(table=table), "beeswax", "100px")}
 ${layout.menubar()}
 <div class="container-fluid">
   <h1>${_('Load Data into %(table)s') % dict(table=table)}</h1>

+ 2 - 2
apps/beeswax/src/beeswax/templates/my_queries.mako

@@ -22,7 +22,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
 <%!  from beeswax.views import collapse_whitespace %>
-${commonheader(_('Beeswax: My Queries'), "beeswax", "100px")}
+${commonheader(_('My Queries'), "beeswax", "100px")}
 ${layout.menubar(section='my queries')}
 <style>
     .tab-content {
@@ -30,7 +30,7 @@ ${layout.menubar(section='my queries')}
     }
 </style>
 <div class="container-fluid">
-    <h1>${_('Beeswax: My Queries')}</h1>
+    <h1>${_('My Queries')}</h1>
 
     <div class="well hueWell">
         <div class="btn-group pull-right">

+ 4 - 1
apps/beeswax/src/beeswax/templates/parameterization.mako

@@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="layout" file="layout.mako" />
 
-${commonheader(_('Parameterize Hive Query'), "beeswax", "100px")}
+${commonheader(_('Parameterize Query'), "beeswax", "100px")}
 
 ${layout.menubar()}
 
@@ -44,6 +44,9 @@ ${layout.menubar()}
                     </div>
                 </div>
             % endfor
+            <div class="hide">
+              ${query_servers}
+            </div>
             <div class="form-actions">
                 <button type="submit" class="btn btn-primary">${btn}</button>
                 <a class="btn" href="javascript:history.go(-1);">${_('Cancel')}</a>

+ 1 - 1
apps/beeswax/src/beeswax/templates/save_results.mako

@@ -20,7 +20,7 @@ from django.utils.translation import ugettext as _
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Beeswax: Create table from file'), "beeswax", "100px")}
+${commonheader(_('Create table from file'), "beeswax", "100px")}
 ${layout.menubar(section='history')}
 <div class="container-fluid">
 % if error_msg:

+ 2 - 2
apps/beeswax/src/beeswax/templates/show_tables.mako

@@ -18,11 +18,11 @@ from desktop.views import commonheader, commonfooter
 from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
-${commonheader(_('Beeswax: Table List'), "beeswax", "100px")}
+${commonheader(_('Table List'), "beeswax", "100px")}
 ${layout.menubar(section='tables')}
 
 <div class="container-fluid">
-	<h1>${_('Beeswax: Table List')}</h1>
+	<h1>${_('Table List')}</h1>
 	<div class="row-fluid">
 		<div class="span3">
 			<div class="well sidebar-nav">

+ 2 - 2
apps/beeswax/src/beeswax/templates/watch_results.mako

@@ -20,11 +20,11 @@ from django.utils.translation import ugettext as _
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="util" file="util.mako" />
 <%namespace name="comps" file="beeswax_components.mako" />
-${commonheader(_('Beeswax: Query Results'), "beeswax", "100px")}
+${commonheader(_('Query Results'), "beeswax", "100px")}
 ${layout.menubar(section='query')}
 
 <div class="container-fluid">
-	<h1>${_('Beeswax: Query Results:')} ${util.render_query_context(query_context)}</h1>
+	<h1>${_('Query Results:')} ${util.render_query_context(query_context)}</h1>
 	<div class="row-fluid">
 		<div class="span3">
 			<div class="well sidebar-nav">

+ 2 - 2
apps/beeswax/src/beeswax/templates/watch_wait.mako

@@ -19,13 +19,13 @@ from django.utils.translation import ugettext as _
 %>
 <%namespace name="layout" file="layout.mako" />
 <%namespace name="util" file="util.mako" />
-${commonheader(_('Beeswax: Waiting for query...'), "beeswax", "100px")}
+${commonheader(_('Waiting for query...'), "beeswax", "100px")}
 ${layout.menubar(section='query')}
 
 <meta http-equiv="refresh" content="3;${url('beeswax.views.watch_query', query.id)}?${fwd_params}" />
 
 <div class="container-fluid">
-	<h1>${_('Beeswax: Waiting for query...')} ${util.render_query_context(query_context)}</h1>
+	<h1>${_('Waiting for query...')} ${util.render_query_context(query_context)}</h1>
 	<div class="row-fluid">
 		<div class="span3">
 			<div class="well sidebar-nav">

+ 10 - 4
apps/beeswax/src/beeswax/test_base.py

@@ -38,6 +38,7 @@ from hadoop import pseudo_hdfs4
 import hadoop.conf
 
 import beeswax.conf
+from beeswax.db_utils import get_query_server
 
 
 _INITIALIZED = False
@@ -90,8 +91,9 @@ def get_shared_beeswax_server():
     default_xml = file(beeswax.conf.BEESWAX_HIVE_CONF_DIR.get()+"/hive-default.xml.template").read()
 
     finish = (
-      beeswax.conf.BEESWAX_SERVER_HOST.set_for_testing("localhost"),
-      beeswax.conf.BEESWAX_SERVER_PORT.set_for_testing(BEESWAXD_TEST_PORT),
+      beeswax.conf.QUERY_SERVERS['default'].SERVER_HOST.set_for_testing("localhost"),
+      beeswax.conf.QUERY_SERVERS['default'].SERVER_PORT.set_for_testing(BEESWAXD_TEST_PORT),
+      beeswax.conf.QUERY_SERVERS['default'].SUPPORT_DDL.set_for_testing(True),
       beeswax.conf.BEESWAX_META_SERVER_HOST.set_for_testing("localhost"),
       beeswax.conf.BEESWAX_META_SERVER_PORT.set_for_testing(BEESWAXD_TEST_PORT + 1),
       # Use a bogus path to avoid loading the normal hive-site.xml
@@ -122,7 +124,8 @@ def get_shared_beeswax_server():
       sleep = 0.001
       while not started and time.time() - start < 20.0:
         try:
-          client = beeswax.db_utils.db_client()
+          query_server = get_query_server(support_ddl=True)
+          client = beeswax.db_utils.db_client(query_server)
           meta_client = beeswax.db_utils.meta_client()
 
           client.echo("echo")
@@ -160,6 +163,7 @@ def get_shared_beeswax_server():
 
 REFRESH_RE = re.compile('<\s*meta\s+http-equiv="refresh"\s+content="\d*;([^"]*)"\s*/>', re.I)
 
+
 def wait_for_query_to_finish(client, response, max=30.0):
   logging.info(str(response.template) + ": " + str(response.content))
   start = time.time()
@@ -188,7 +192,7 @@ def wait_for_query_to_finish(client, response, max=30.0):
 def make_query(client, query, submission_type="Execute",
                udfs=None, settings=None, resources=None,
                wait=False, name=None, desc=None, local=True,
-               is_parameterized=True, max=30.0, **kwargs):
+               is_parameterized=True, max=30.0, server='default', **kwargs):
   """
   Prepares arguments for the execute view.
 
@@ -204,8 +208,10 @@ def make_query(client, query, submission_type="Execute",
   # Prepares arguments for the execute view.
   parameters = {
     'query-query': query,
+    'query_servers-server': server,
     'query-is_parameterized': is_parameterized and "on"
   }
+
   if submission_type == 'Execute':
     parameters['button-submit'] = 'Whatever'
   elif submission_type == 'Explain':

+ 38 - 17
apps/beeswax/src/beeswax/tests.py

@@ -42,7 +42,8 @@ import beeswax.models
 import beeswax.views
 from beeswax import conf
 from beeswax.views import parse_results, collapse_whitespace
-from beeswax.test_base import make_query, wait_for_query_to_finish, verify_history
+from beeswax.test_base import make_query, wait_for_query_to_finish, verify_history, get_query_server,\
+  BEESWAXD_TEST_PORT
 from beeswax.test_base import BeeswaxSampleProvider
 from beeswaxd import BeeswaxService
 from beeswaxd import ttypes
@@ -100,8 +101,10 @@ class TestBeeswaxWithHadoop(BeeswaxSampleProvider):
     assert_true("Table test already exists" in response.context["error_message"])
 
   def test_configuration(self):
-    response = self.client.get("/beeswax/configuration")
-    response_verbose = self.client.get("/beeswax/configuration?include_hadoop=true")
+    params = {'server': 'default'}
+
+    response = self.client.post("/beeswax/configuration", params)
+    response_verbose = self.client.post("/beeswax/configuration?include_hadoop=true", params)
 
     assert_true("hive.exec.scratchdir" in response.content)
     assert_true("hive.exec.scratchdir" in response_verbose.content)
@@ -166,7 +169,7 @@ for x in sys.stdin:
     Test basic query submission
     """
     # Minimal server operation
-    assert_equal("echo", beeswax.db_utils.db_client().echo("echo"))
+    assert_equal("echo", beeswax.db_utils.db_client(get_query_server()).echo("echo"))
 
     # Table should have been created
     response = self.client.get("/beeswax/tables")
@@ -294,14 +297,14 @@ for x in sys.stdin:
     """
     query_msg.configuration = []
     query_msg.hadoop_user = "test"
-    handle = beeswax.db_utils.db_client().executeAndWait(query_msg, "")
+    handle = beeswax.db_utils.db_client(get_query_server()).executeAndWait(query_msg, "")
 
-    results = beeswax.db_utils.db_client().fetch(handle, True, 5)
+    results = beeswax.db_utils.db_client(get_query_server()).fetch(handle, True, 5)
     row_list = list(parse_results(results.data))
     assert_equal(len(row_list), 5)
 
-    beeswax.db_utils.db_client().close(handle)
-    beeswax.db_utils.db_client().clean(handle.log_context)
+    beeswax.db_utils.db_client(get_query_server()).close(handle)
+    beeswax.db_utils.db_client(get_query_server()).clean(handle.log_context)
 
 
   def test_sync_query_error(self):
@@ -317,7 +320,7 @@ for x in sys.stdin:
     query_msg.configuration = []
     query_msg.hadoop_user = "test"
     try:
-      handle = beeswax.db_utils.db_client().executeAndWait(query_msg, "")
+      handle = beeswax.db_utils.db_client(get_query_server()).executeAndWait(query_msg, "")
     except ttypes.BeeswaxException, bex:
       assert_equal(bex.errorCode, 40000)
       assert_equal(bex.SQLState, "42000")
@@ -339,7 +342,7 @@ for x in sys.stdin:
       def __init__(self, **entries):
         self.__dict__.update(entries)
 
-    client = beeswax.db_utils.db_client()
+    client = beeswax.db_utils.db_client(get_query_server())
 
     prev_get_default_configuration = client.get_default_configuration
     prev_client = client._client
@@ -379,12 +382,12 @@ for x in sys.stdin:
     design_id = response.context["design"].id
 
     # Don't fill out the form
-    response = self.client.post("/beeswax/execute_parameterized/%d" % design_id)
+    response = self.client.post("/beeswax/execute_parameterized/%d" % design_id, { 'query_servers-server': 'default' })
     assert_true("parameterization.mako", response.template)
 
     # Now fill it out
     response = self.client.post("/beeswax/execute_parameterized/%d" % design_id,
-      { "parameterization-x": str(1), "parameterization-y": str(2) }, follow=True)
+      { "parameterization-x": str(1), "parameterization-y": str(2), 'query_servers-server': 'default' }, follow=True)
 
     assert_true("watch_wait.mako" in response.template)
     # Check that substitution happened!
@@ -393,7 +396,8 @@ for x in sys.stdin:
 
     # Check that error handling is reasonable
     response = self.client.post("/beeswax/execute_parameterized/%d" % design_id,
-      { "parameterization-x": "'_this_is_not SQL ", "parameterization-y": str(2) }, follow=True)
+      { "parameterization-x": "'_this_is_not SQL ", "parameterization-y": str(2), 'query_servers-server': 'default' },
+      follow=True)
     assert_true("execute.mako" in response.template)
     log = response.context["log"]
     assert_true(search_log_line('ql.Driver', 'FAILED: ParseException', log), log)
@@ -488,8 +492,10 @@ for x in sys.stdin:
     query_msg.query = 'SELECT foo FROM test limit %d' % (limit,)
     query_msg.configuration = []
     query_msg.hadoop_user = "test"
-    handle = beeswax.db_utils.db_client().query(query_msg)
+    handle = beeswax.db_utils.db_client(get_query_server()).query(query_msg)
     query_data = beeswax.models.QueryHistory(server_id=handle.id, log_context=handle.log_context)
+    query_data.server_host = beeswax.conf.QUERY_SERVERS['default'].SERVER_HOST.get() # Needed as we query directly
+    query_data.server_port = beeswax.conf.QUERY_SERVERS['default'].SERVER_PORT.get()
     # Get the result in csv. Should have 3 + 1 header row.
     csv_resp = beeswax.data_export.download(query_data, 'csv')
     assert_equal(len(csv_resp.content.strip().split('\n')), limit + 1)
@@ -499,14 +505,18 @@ for x in sys.stdin:
     query_msg.query = 'SELECT * FROM test'
     query_msg.configuration = []
     query_msg.hadoop_user = "test"
-    handle = beeswax.db_utils.db_client().query(query_msg)
+    handle = beeswax.db_utils.db_client(get_query_server()).query(query_msg)
     query_data = beeswax.models.QueryHistory(server_id=handle.id, log_context=handle.log_context)
+    query_data.server_host = beeswax.conf.QUERY_SERVERS['default'].SERVER_HOST.get() # Needed as we query directly
+    query_data.server_port = beeswax.conf.QUERY_SERVERS['default'].SERVER_PORT.get()
     # Get the result in xls. Then translate it into csv.
     xls_resp = beeswax.data_export.download(query_data, 'xls')
     translated_csv = xls2csv(xls_resp.content)
     # It should have 257 lines (256 + header)
     assert_equal(len(translated_csv.strip('\r\n').split('\r\n')), 257)
-    handle = beeswax.db_utils.db_client().query(query_msg)
+    handle = beeswax.db_utils.db_client(get_query_server()).query(query_msg)
+    query_data.server_host = beeswax.conf.QUERY_SERVERS['default'].SERVER_HOST.get() # Needed as we query directly
+    query_data.server_port = beeswax.conf.QUERY_SERVERS['default'].SERVER_PORT.get()
     # Get the result in csv.
     csv_resp = beeswax.data_export.download(query_data, 'csv')
     assert_equal(csv_resp.content, translated_csv)
@@ -1031,10 +1041,21 @@ for x in sys.stdin:
     resp = self.client.get('/beeswax/table/myview')
     assert_equal(None, resp.context['top_rows'])
     assert_true(resp.context['is_view'])
-    assert_true("Beeswax View Metadata" in resp.content)
+    assert_true("View Metadata" in resp.content)
     assert_true("Drop View" in resp.content)
 
 
+  def test_select_query_server(self):
+    c = make_logged_in_client()
+
+    _make_query(c, 'SELECT bogus FROM test', server='default') # Improvement: mock another server
+
+    history = beeswax.models.QueryHistory.objects.latest('id')
+    assert_equal('default', history.server_name)
+    assert_equal('localhost', history.server_host)
+    assert_equal(BEESWAXD_TEST_PORT, history.server_port)
+
+
 def test_import_gzip_reader():
   """Test the gzip reader in create table"""
   # Make gzipped data

+ 65 - 44
apps/beeswax/src/beeswax/views.py

@@ -14,9 +14,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""
-Beeswax is a UI for Hive.
-"""
 
 import logging
 import re
@@ -29,6 +26,7 @@ from django.http import HttpResponse, QueryDict
 from django.shortcuts import redirect
 from django.utils.encoding import force_unicode
 from django.utils import simplejson
+from django.utils.translation import ugettext as _
 
 from desktop.lib import django_mako
 from desktop.lib.paginator import Paginator
@@ -37,10 +35,13 @@ from desktop.lib.django_util import login_notrequired, get_desktop_uri_prefix
 from desktop.lib.django_util import PopupException
 
 from hadoop.fs.exceptions import WebHdfsException
+from jobsub.parameterization import find_variables, substitute_variables
+from filebrowser.views import location_to_url
 
 import beeswax.forms
 import beeswax.design
 import beeswax.management.commands.beeswax_install_examples
+
 from beeswaxd import BeeswaxService
 from beeswaxd.ttypes import QueryHandle, BeeswaxException, QueryNotFoundException
 from beeswax import common
@@ -48,12 +49,8 @@ from beeswax import data_export
 from beeswax import db_utils
 from beeswax import models
 from beeswax import conf
+from beeswax.forms import QueryServerForm
 
-from jobsub.parameterization import find_variables, substitute_variables
-
-from filebrowser.views import location_to_url
-
-from django.utils.translation import ugettext as _
 
 LOG = logging.getLogger(__name__)
 
@@ -156,8 +153,8 @@ def drop_table(request, table):
     query_msg = make_beeswax_query(request, hql)
     try:
       return execute_directly(request,
-                               query_msg,
-                               on_success_url=urlresolvers.reverse(show_tables))
+                              query_msg,
+                              on_success_url=urlresolvers.reverse(show_tables))
     except BeeswaxException, ex:
       # Note that this state is difficult to get to.
       error_message, log = expand_exception(ex)
@@ -262,7 +259,7 @@ def make_beeswax_query(request, hql, query_form=None):
   return query_msg
 
 
-def execute_directly(request, query_msg, design=None, tablename=None,
+def execute_directly(request, query_msg, query_server=None, design=None, tablename=None,
                      on_success_url=None, on_success_params=None, **kwargs):
   """
   execute_directly(request, query_msg, tablename, design) -> HTTP response for execution
@@ -273,6 +270,10 @@ def execute_directly(request, query_msg, design=None, tablename=None,
     query_msg
       The thrift Query object.
 
+    query_server
+      To which Query Server to submit the query.
+      Dictionary with keys: ['server_name', 'server_host', 'server_port'].
+
     design
       The design associated with the query.
 
@@ -293,7 +294,8 @@ def execute_directly(request, query_msg, design=None, tablename=None,
   """
   if design is not None:
     authorized_get_design(request, design.id)
-  history_obj = db_utils.execute_directly(request.user, query_msg, design, **kwargs)
+
+  history_obj = db_utils.execute_directly(request.user, query_msg, query_server, design, **kwargs)
   watch_url = urlresolvers.reverse("beeswax.views.watch_query", kwargs=dict(id=history_obj.id))
 
   # Prepare the GET params for the watch_url
@@ -360,23 +362,24 @@ def execute_query(request, design_id=None):
         break
 
       query_str = _strip_trailing_semicolon(form.query.cleaned_data["query"])
+      query_server = db_utils.get_query_server(form.query_servers.cleaned_data["server"])
 
       # (Optional) Parameterization.
-      parameterization = get_parameterization(request, query_str, form.query, design, to_explain)
+      parameterization = get_parameterization(request, query_str, form, design, to_explain)
       if parameterization:
         return parameterization
 
       query_msg = make_beeswax_query(request, query_str, form)
       try:
         if to_explain:
-          return explain_directly(request, query_str, query_msg, design)
+          return explain_directly(request, query_str, query_msg, design, query_server)
         else:
           notify = form.query.cleaned_data.get('email_notify', False)
-          return execute_directly(request, query_msg, design,
+          return execute_directly(request, query_msg, query_server, design,
                                   on_success_url=on_success_url,
                                   notify=notify)
       except BeeswaxException, ex:
-        error_message, log = expand_exception(ex)
+        error_message, log = expand_exception(ex, query_server)
         # Fall through to re-render the execute form.
     else:
       # GET request
@@ -397,20 +400,20 @@ def execute_query(request, design_id=None):
     'on_success_url': on_success_url,
   })
 
-def get_parameterization(request, query_str, query_form, design, is_explain):
+def get_parameterization(request, query_str, form, design, is_explain):
   """
   Figures out whether a design is parameterizable, and, if so,
   returns a form to fill out.  Returns None if there's no parameterization
   to do.
   """
-  if query_form.cleaned_data["is_parameterized"]:
-    variables = find_variables(query_str)
-    form = make_parameterization_form(query_str)
-    if form:
+  if form.query.cleaned_data["is_parameterized"]:
+    parameters_form = make_parameterization_form(query_str)
+    if parameters_form:
       return render("parameterization.mako", request, dict(
-        form=form(prefix="parameterization"),
+        form=parameters_form(prefix="parameterization"),
         design=design,
-        explain=is_explain))
+        explain=is_explain,
+        query_servers=form.query_servers))
   return None
 
 def make_parameterization_form(query_str):
@@ -446,23 +449,29 @@ def _run_parameterized_query(request, design_id, explain):
   # Reconstitute the form
   design_obj = beeswax.design.HQLdesign.loads(design.data)
   query_form = beeswax.forms.query_form()
-  query_form.bind(design_obj.get_query_dict())
+  params = design_obj.get_query_dict()
+  params.update(request.POST)
+  query_form.bind(params)
   assert query_form.is_valid()
+
   query_str = _strip_trailing_semicolon(query_form.query.cleaned_data["query"])
+  query_server = db_utils.get_query_server(query_form.query_servers.cleaned_data["server"])
+
   parameterization_form_cls = make_parameterization_form(query_str)
   if not parameterization_form_cls:
     raise PopupException(_("Query is not parameterizable."))
+
   parameterization_form = parameterization_form_cls(request.REQUEST, prefix="parameterization")
   if parameterization_form.is_valid():
     real_query = substitute_variables(query_str, parameterization_form.cleaned_data)
     query_msg = make_beeswax_query(request, real_query, query_form)
     try:
       if explain:
-        return explain_directly(request, query_str, query_msg, design)
+        return explain_directly(request, query_str, query_msg, design, query_server)
       else:
-        return execute_directly(request, query_msg, design)
+        return execute_directly(request, query_msg, query_server, design)
     except BeeswaxException, ex:
-      error_message, log = expand_exception(ex)
+      error_message, log = expand_exception(ex, query_server)
       return render('execute.mako', request, {
         'action': urlresolvers.reverse(execute_query),
         'design': design,
@@ -471,12 +480,13 @@ def _run_parameterized_query(request, design_id, explain):
         'log': log,
       })
   else:
-    return render("parameterization.mako", request, dict(form=parameterization_form, design=design, explain=explain))
+    return render("parameterization.mako", request, dict(form=parameterization_form, design=design, explain=explain,
+                                                         query_servers=query_form.query_servers))
 
-def expand_exception(exc):
+def expand_exception(exc, query_server):
   """expand_exception(exc) -> (error msg, log message)"""
   try:
-    log = db_utils.db_client().get_log(exc.log_context)
+    log = db_utils.db_client(query_server).get_log(exc.log_context)
   except:
     # Always show something, even if server has died on the job.
     log = _("Could not retrieve log.")
@@ -782,7 +792,7 @@ def watch_query(request, id):
     return format_preserving_redirect(request, results_url, request.GET)
 
   # Still running
-  log = db_utils.db_client().get_log(server_id)
+  log = db_utils.db_client(query_history.get_query_server()).get_log(server_id)
 
   # Keep waiting
   # - Translate context into something more meaningful (type, data)
@@ -889,7 +899,7 @@ def view_results(request, id, first_row=0):
 
   # Retrieve query results
   try:
-    results = db_utils.db_client().fetch(handle, start_over, -1)
+    results = db_utils.db_client(query_history.get_query_server()).fetch(handle, start_over, -1)
     assert results.ready, _('Trying to display result that is not yet ready. Query id %(id)s') % {'id': id}
     # We display the "Download" button only when we know
     # that there are results:
@@ -897,7 +907,7 @@ def view_results(request, id, first_row=0):
     fetch_error = False
   except BeeswaxException, ex:
     fetch_error = True
-    error_message, log = expand_exception(ex)
+    error_message, log = expand_exception(ex, query_history.get_query_server())
 
   # Handle errors
   if fetch_error:
@@ -911,7 +921,7 @@ def view_results(request, id, first_row=0):
       'can_save': False,
     })
 
-  log = db_utils.db_client().get_log(query_history.server_id)
+  log = db_utils.db_client(query_history.get_query_server()).get_log(query_history.server_id)
   download_urls = {}
   if downloadable:
     for format in common.DL_FORMATS:
@@ -973,7 +983,7 @@ def save_results(request, id):
       assert request.POST.get('save')
       handle = QueryHandle(id=query_history.server_id, log_context=query_history.log_context)
       try:
-        result_meta = db_utils.db_client().get_results_metadata(handle)
+        result_meta = db_utils.db_client(query_history.get_query_server()).get_results_metadata(handle)
       except QueryNotFoundException, ex:
         LOG.exception(ex)
         raise PopupException(_('Cannot find query.'))
@@ -1004,7 +1014,7 @@ def save_results(request, id):
                                       result_meta)
           except BeeswaxException, bex:
             LOG.exception(bex)
-            error_msg, log = expand_exception(bex)
+            error_msg, log = expand_exception(bex, query_history.get_query_server())
       except WebHdfsException, ex:
         raise PopupException(_('The table could not be saved.'), detail=ex)
       except IOError, ex:
@@ -1030,12 +1040,14 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
   Handle saving results as a new table. Returns HTTP response.
   May raise BeeswaxException, IOError.
   """
+  query_server = query_history.get_query_server() # Query server requires DDL support
+
   # Case 1: The results are straight from an existing table
   if result_meta.in_tablename:
     hql = 'CREATE TABLE `%s` AS SELECT * FROM %s' % (target_table, result_meta.in_tablename)
     query_msg = make_beeswax_query(request, hql)
     # Display the CTAS running. Could take a long time.
-    return execute_directly(request, query_msg, on_success_url=urlresolvers.reverse(show_tables))
+    return execute_directly(request, query_msg, query_server, on_success_url=urlresolvers.reverse(show_tables))
 
   # Case 2: The results are in some temporary location
   # 1. Create table
@@ -1063,7 +1075,7 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
         ''' % (target_table, cols, delim.zfill(3))
 
   query_msg = make_beeswax_query(request, hql)
-  db_utils.execute_and_wait(request.user, query_msg)
+  db_utils.execute_and_wait(request.user, query_msg, query_server)
 
   try:
     # 2. Move the results into the table's storage
@@ -1077,7 +1089,7 @@ def _save_results_ctas(request, query_history, target_table, result_meta):
     LOG.error('Error moving data into storage of table %s. Will drop table.' % (target_table,))
     query_msg = make_beeswax_query(request, 'DROP TABLE `%s`' % (target_table,))
     try:
-      db_utils.execute_directly(request.user, query_msg)        # Don't wait for results
+      db_utils.execute_directly(request.user, query_msg, query_server)        # Don't wait for results
     except Exception, double_trouble:
       LOG.exception('Failed to drop table "%s" as well: %s' % (target_table, double_trouble))
     raise ex
@@ -1149,9 +1161,18 @@ def describe_partitions(request, table):
 
 
 def configuration(request):
-  config_values = db_utils.db_client().get_default_configuration(
-                      bool(request.REQUEST.get("include_hadoop", False)))
-  return render("configuration.mako", request, dict(config_values=config_values))
+  if request.method == 'POST':
+    server_form = QueryServerForm(request.POST)
+    if server_form.is_valid():
+      query_server = db_utils.get_query_server(server_form.cleaned_data["server"])
+      config_values = db_utils.db_client(query_server).get_default_configuration(
+                          bool(request.REQUEST.get("include_hadoop", False)))
+  else:
+    server_form = QueryServerForm()
+    config_values = {}
+
+  return render("configuration.mako", request, {'config_values': config_values,
+                                                'server_form': server_form})
 
 def _copy_prefix(prefix, base_dict):
   """Copy keys starting with ``prefix``"""
@@ -1334,11 +1355,11 @@ def collapse_whitespace(s):
   return WHITESPACE.sub(" ", s).strip()
 
 
-def explain_directly(request, query_str, query_msg, design):
+def explain_directly(request, query_str, query_msg, design, query_server):
   """
   Runs explain query.
   """
-  explanation = db_utils.db_client().explain(query_msg)
+  explanation = db_utils.db_client(query_server).explain(query_msg)
   context = ("design", design)
   return render('explain.mako', request,
     dict(query=query_str, explanation=explanation.textual, query_context=context))

+ 1 - 1
apps/jobsub/src/jobsub/conf.py

@@ -40,7 +40,7 @@ SAMPLE_DATA_DIR = Config(
   private=True)
 
 
-# Deprecated!
+# Deprecated! To remove in Hue 3
 # These configs were moved to desktop liboozie.
 # If liboozie is not configured these settings will be used.
 # They also have priority if liboozie defines them.

+ 27 - 3
desktop/conf.dist/hue.ini

@@ -306,12 +306,24 @@
 ###########################################################################
 
 [beeswax]
+
+  # Deprecated! Will be removed in Hue 3
+  # Multiple sections are now available in query_servers
+  # Host where Beeswax internal metastore Thrift daemon is running
+  ## beeswax_meta_server_host=localhost
+
+  # Deprecated! Will be removed in Hue 3
+  # Multiple sections are now available in query_servers  
   # Configure the port the internal metastore daemon runs on. Used only if
-  # hive.metastore.local is true.
+  # hive.metastore.local is true. 
   ## beeswax_meta_server_port=8003
 
-  # Configure the port the beeswax thrift server runs on
-  ## beeswax_server_port=8002
+  # Host where Beeswax internal metastore Thrift daemon is running
+  ## beeswax_meta_server_host=localhost
+  
+  # Configure the port the internal metastore daemon runs on. Used only if
+  # hive.metastore.local is true. 
+  ## beeswax_meta_server_port=8003
 
   # Hive home directory
   ## hive_home_dir=/usr/lib/hive
@@ -334,6 +346,18 @@
   # visible only to the owner and administrators.
   ## share_saved_queries=true
 
+ # One entry for each Query Server that can execute some SQL queries.
+ # This must be in the bottom of the [beewax] section.
+ [[query_servers]]
+
+   [[[default]]]
+   # Host where the Query Server Thrift daemon is running
+   ## server_host=localhost
+   # Configure the port the Query Server Thrift server 
+   ## server_port=8002
+   # If DDL queries are supported (e.g. DROP can be sent directly to this server)
+   ## support_ddl=True
+
 
 ###########################################################################
 # Settings to configure Job Designer

+ 26 - 3
desktop/conf/pseudo-distributed.ini.tmpl

@@ -309,12 +309,24 @@
 ###########################################################################
 
 [beeswax]
+
+  # Deprecated! Will be removed in Hue 3
+  # Multiple sections are now available in query_servers
+  # Host where Beeswax internal metastore Thrift daemon is running
+  ## beeswax_meta_server_host=localhost
+
+  # Deprecated! Will be removed in Hue 3
+  # Multiple sections are now available in query_servers  
   # Configure the port the internal metastore daemon runs on. Used only if
-  # hive.metastore.local is true.
+  # hive.metastore.local is true. 
   ## beeswax_meta_server_port=8003
 
-  # Configure the port the beeswax thrift server runs on
-  ## beeswax_server_port=8002
+  # Host where Beeswax internal metastore Thrift daemon is running
+  ## beeswax_meta_server_host=localhost
+  
+  # Configure the port the internal metastore daemon runs on. Used only if
+  # hive.metastore.local is true. 
+  ## beeswax_meta_server_port=8003
 
   # Hive home directory
   ## hive_home_dir=/usr/lib/hive
@@ -337,6 +349,17 @@
   # visible only to the owner and administrators.
   ## share_saved_queries=true
 
+ # One entry for each Query Server that can execute some SQL queries.
+ # This must be in the bottom of the [beewax] section.
+ [[query_servers]]
+
+   [[[default]]]
+   # Host where the Query Server Thrift daemon is running
+   ## server_host=localhost
+   # Configure the port the Query Server Thrift server 
+   ## server_port=8002
+   # If DDL queries are supported (e.g. DROP can be sent directly to this server)
+   ## support_ddl=True
 
 ###########################################################################
 # Settings to configure Job Designer

+ 1 - 1
desktop/core/src/desktop/lib/thrift_util.py

@@ -458,7 +458,7 @@ def thrift2json(tft):
 
 def _jsonable2thrift_helper(jsonable, type_enum, spec_args, default, recursion_depth=0):
   """
-  Recursive impelementation method of jsonable2thrift.
+  Recursive implementation method of jsonable2thrift.
 
   type_enum corresponds to TType.  spec_args is part of the
   thrift_spec explained in Thrift's code generator.  See