Browse Source

[api] Add new API endpoint to install app examples with suitable setup functions (#4016)

Harsh Gupta 9 months ago
parent
commit
415e0f01cd

+ 97 - 11
desktop/core/src/desktop/api2.py

@@ -22,18 +22,10 @@ import logging
 import zipfile
 import zipfile
 import tempfile
 import tempfile
 from builtins import map
 from builtins import map
-
-from celery.app.control import Control
-
-from desktop.conf import TASK_SERVER_V2
-
-if hasattr(TASK_SERVER_V2, 'get') and TASK_SERVER_V2.ENABLED.get():
-  from desktop.celery import app as celery_app
-  from filebrowser.utils import parse_broker_url
-from collections import defaultdict
 from datetime import datetime
 from datetime import datetime
 from io import StringIO as string_io
 from io import StringIO as string_io
 
 
+from celery.app.control import Control
 from django.core import management
 from django.core import management
 from django.db import transaction
 from django.db import transaction
 from django.http import HttpResponse, JsonResponse
 from django.http import HttpResponse, JsonResponse
@@ -43,6 +35,8 @@ from django.utils.translation import gettext as _
 from django.views.decorators.csrf import ensure_csrf_cookie
 from django.views.decorators.csrf import ensure_csrf_cookie
 from django.views.decorators.http import require_POST
 from django.views.decorators.http import require_POST
 
 
+from beeswax import common
+from beeswax.management.commands import beeswax_install_examples
 from beeswax.models import Namespace
 from beeswax.models import Namespace
 from desktop import appmanager
 from desktop import appmanager
 from desktop.auth.backend import is_admin
 from desktop.auth.backend import is_admin
@@ -53,11 +47,11 @@ from desktop.conf import (
   ENABLE_GIST_PREVIEW,
   ENABLE_GIST_PREVIEW,
   ENABLE_NEW_STORAGE_BROWSER,
   ENABLE_NEW_STORAGE_BROWSER,
   ENABLE_SHARING,
   ENABLE_SHARING,
-  IS_K8S_ONLY,
   TASK_SERVER_V2,
   TASK_SERVER_V2,
   get_clusters,
   get_clusters,
 )
 )
 from desktop.lib.conf import GLOBAL_CONFIG, BoundContainer, is_anonymous
 from desktop.lib.conf import GLOBAL_CONFIG, BoundContainer, is_anonymous
+from desktop.lib.connectors.models import Connector
 from desktop.lib.django_util import JsonResponse, login_notrequired, render
 from desktop.lib.django_util import JsonResponse, login_notrequired, render
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.exceptions_renderable import PopupException
 from desktop.lib.export_csvxls import make_response
 from desktop.lib.export_csvxls import make_response
@@ -88,15 +82,25 @@ from filebrowser.conf import (
 from filebrowser.tasks import check_disk_usage_and_clean_task, document_cleanup_task
 from filebrowser.tasks import check_disk_usage_and_clean_task, document_cleanup_task
 from filebrowser.views import MAX_FILEEDITOR_SIZE
 from filebrowser.views import MAX_FILEEDITOR_SIZE
 from hadoop.cluster import is_yarn
 from hadoop.cluster import is_yarn
+from hbase.management.commands import hbase_setup
+from indexer.management.commands import indexer_setup
 from metadata.catalog_api import (
 from metadata.catalog_api import (
   _highlight,
   _highlight,
   search_entities as metadata_search_entities,
   search_entities as metadata_search_entities,
   search_entities_interactive as metadata_search_entities_interactive,
   search_entities_interactive as metadata_search_entities_interactive,
 )
 )
 from metadata.conf import has_catalog
 from metadata.conf import has_catalog
-from notebook.connectors.base import Notebook
+from notebook.connectors.base import Notebook, get_interpreter
+from notebook.management.commands import notebook_setup
+from oozie.management.commands import oozie_setup
+from pig.management.commands import pig_setup
+from search.management.commands import search_setup
 from useradmin.models import Group, User
 from useradmin.models import Group, User
 
 
+if hasattr(TASK_SERVER_V2, 'get') and TASK_SERVER_V2.ENABLED.get():
+  from desktop.celery import app as celery_app
+  from filebrowser.utils import parse_broker_url
+
 LOG = logging.getLogger()
 LOG = logging.getLogger()
 
 
 
 
@@ -1392,3 +1396,85 @@ def _paginate(request, queryset):
   limit = int(request.GET.get('limit', 0))
   limit = int(request.GET.get('limit', 0))
 
 
   return __paginate(page, limit, queryset)
   return __paginate(page, limit, queryset)
+
+
+@api_error_handler
+def install_app_examples(request):
+  app_name = request.POST.get('app_name')
+  if not app_name:
+    return HttpResponse("Missing parameter: app_name is required.", status=400)
+
+  if not is_admin(request.user):
+    return HttpResponse("You must be a Hue admin to access this endpoint.", status=403)
+
+  # Define supported apps and their setup functions
+  setup_functions = {
+    'hive': _setup_hive_impala_examples,
+    'impala': _setup_hive_impala_examples,
+    'hbase': _setup_hbase_examples,
+    'pig': _setup_pig_examples,
+    'oozie': _setup_oozie_examples,
+    'notebook': _setup_notebook_examples,
+    'search': _setup_search_examples,
+  }
+
+  if app_name not in setup_functions:
+    return HttpResponse(f"Unsupported app name: {app_name}", status=400)
+
+  response = setup_functions[app_name](request)
+  return response if response else HttpResponse(f"Successfully installed examples for {app_name}.", status=200)
+
+
+def _setup_hive_impala_examples(request):
+  dialect = request.POST.get('dialect', 'hive')
+  db_name = request.POST.get('database_name', 'default')
+
+  if dialect not in ('hive', 'impala'):
+    return HttpResponse("Invalid dialect: Must be 'hive' or 'impala'", status=400)
+
+  interpreter = common.find_compute(dialect=dialect, user=request.user)
+
+  # Execute Hive/Impala examples installation
+  beeswax_install_examples.Command().handle(dialect=dialect, db_name=db_name, user=request.user, request=request, interpreter=interpreter)
+
+
+def _setup_hbase_examples(request):
+  hbase_setup.Command().handle(user=request.user)
+
+
+def _setup_pig_examples(request):
+  pig_setup.Command().handle()
+
+
+def _setup_oozie_examples(request):
+  oozie_setup.Command().handle()
+
+
+def _setup_notebook_examples(request):
+  connector_id = request.POST.get('connector_id')
+  if not connector_id:
+    return HttpResponse("Missing parameter: connector_id is required.", status=400)
+
+  connector = Connector.objects.get(id=connector_id)
+  if connector:
+    dialect = connector.dialect
+    db_name = request.POST.get('database_name', 'default')
+
+    # Execute Notebook examples installation using the specified connector
+    beeswax_install_examples.Command().handle(
+      dialect=dialect,
+      db_name=db_name,
+      user=request.user,
+      interpreter=get_interpreter(connector_type=connector.to_dict()['type'], user=request.user),
+      request=request,
+    )
+  else:
+    notebook_setup.Command().handle(user=request.user, dialect=request.POST.get('dialect', 'hive'))
+
+
+def _setup_search_examples(request):
+  data = request.POST.get('data')
+  indexer_setup.Command().handle(data=data)
+
+  if data == 'log_analytics_demo':
+    search_setup.Command().handle()

+ 193 - 224
desktop/core/src/desktop/api2_tests.py

@@ -16,31 +16,31 @@
 # See the License for the specific language governing permissions and
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # limitations under the License.
 
 
-from builtins import object
+import re
 import json
 import json
+from builtins import object
+from unittest.mock import MagicMock, Mock, patch
+
 import pytest
 import pytest
-import re
 
 
 from beeswax.conf import HIVE_SERVER_HOST
 from beeswax.conf import HIVE_SERVER_HOST
-from useradmin.models import get_default_user_group, User
-
+from desktop.api2 import _setup_hive_impala_examples, _setup_notebook_examples, _setup_search_examples, install_app_examples
 from desktop.conf import ENABLE_GIST_PREVIEW
 from desktop.conf import ENABLE_GIST_PREVIEW
 from desktop.lib.django_test_util import make_logged_in_client
 from desktop.lib.django_test_util import make_logged_in_client
-from desktop.models import Document2, Directory
+from desktop.models import Directory, Document2
+from useradmin.models import User, get_default_user_group
 
 
 
 
 @pytest.mark.django_db
 @pytest.mark.django_db
 class TestApi2(object):
 class TestApi2(object):
-
   def setup_method(self):
   def setup_method(self):
     self.client = make_logged_in_client(username="api2_user", groupname="default", recreate=True, is_superuser=False)
     self.client = make_logged_in_client(username="api2_user", groupname="default", recreate=True, is_superuser=False)
     self.user = User.objects.get(username="api2_user")
     self.user = User.objects.get(username="api2_user")
 
 
-
   def test_import_document_with_forward_ref(self, client=None):
   def test_import_document_with_forward_ref(self, client=None):
     if client is None:
     if client is None:
       client = self.client
       client = self.client
-    
+
     doc = '''[
     doc = '''[
 {
 {
   "model": "desktop.document2",
   "model": "desktop.document2",
@@ -109,34 +109,28 @@ class TestApi2(object):
     response = client.post("/desktop/api2/doc/import", {'documents': json.dumps(doc)})
     response = client.post("/desktop/api2/doc/import", {'documents': json.dumps(doc)})
     status = json.loads(response.content)['status']
     status = json.loads(response.content)['status']
     assert status == 0
     assert status == 0
-  
-  
+
   def test_search_entities_interactive_xss(self):
   def test_search_entities_interactive_xss(self):
     query = Document2.objects.create(
     query = Document2.objects.create(
-        name='<script>alert(5)</script>',
-        description='<script>alert(5)</script>',
-        type='query-hive',
-        owner=self.user
+      name='<script>alert(5)</script>', description='<script>alert(5)</script>', type='query-hive', owner=self.user
     )
     )
 
 
     try:
     try:
-      response = self.client.post('/desktop/api/search/entities_interactive/', data={
-        'sources': json.dumps(['documents']),
-        'query_s': json.dumps('alert')
-      })
+      response = self.client.post(
+        '/desktop/api/search/entities_interactive/', data={'sources': json.dumps(['documents']), 'query_s': json.dumps('alert')}
+      )
       results = json.loads(response.content)['results']
       results = json.loads(response.content)['results']
       assert results
       assert results
       result_json = json.dumps(results)
       result_json = json.dumps(results)
       assert not re.match('<(?!em)', result_json), result_json
       assert not re.match('<(?!em)', result_json), result_json
       assert not re.match('(?!em)>', result_json), result_json
       assert not re.match('(?!em)>', result_json), result_json
-      assert not '<script>' in result_json, result_json
-      assert not '</script>' in result_json, result_json
+      assert '<script>' not in result_json, result_json
+      assert '</script>' not in result_json, result_json
       assert '&lt;' in result_json, result_json
       assert '&lt;' in result_json, result_json
       assert '&gt;' in result_json, result_json
       assert '&gt;' in result_json, result_json
     finally:
     finally:
       query.delete()
       query.delete()
 
 
-
   def test_get_hue_config(self):
   def test_get_hue_config(self):
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     user = User.objects.get(username="api2_superuser")
     user = User.objects.get(username="api2_superuser")
@@ -162,14 +156,13 @@ class TestApi2(object):
     finally:
     finally:
       clear()
       clear()
 
 
-
   def test_get_hue_config_private(self):
   def test_get_hue_config_private(self):
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     user = User.objects.get(username="api2_superuser")
     user = User.objects.get(username="api2_superuser")
 
 
     # Not showing private if not asked for
     # Not showing private if not asked for
     response = client.get('/desktop/api2/get_hue_config', data={})
     response = client.get('/desktop/api2/get_hue_config', data={})
-    assert not b'bind_password' in response.content
+    assert b'bind_password' not in response.content
 
 
     # Masking passwords if private
     # Masking passwords if private
     private_response = client.get('/desktop/api2/get_hue_config', data={'private': True})
     private_response = client.get('/desktop/api2/get_hue_config', data={'private': True})
@@ -177,15 +170,13 @@ class TestApi2(object):
     config_json = json.loads(private_response.content)
     config_json = json.loads(private_response.content)
     desktop_config = [conf for conf in config_json['config'] if conf['key'] == 'desktop']
     desktop_config = [conf for conf in config_json['config'] if conf['key'] == 'desktop']
     ldap_desktop_config = [val for conf in desktop_config for val in conf['values'] if val['key'] == 'ldap']
     ldap_desktop_config = [val for conf in desktop_config for val in conf['values'] if val['key'] == 'ldap']
-    assert any(
-        val['value'] == '**********'
-        for conf in ldap_desktop_config for val in conf['values'] if val['key'] == 'bind_password'
-      ), ldap_desktop_config
+    assert any(val['value'] == '**********' for conf in ldap_desktop_config for val in conf['values'] if val['key'] == 'bind_password'), (
+      ldap_desktop_config
+    )
 
 
     # There should be more private than non-private
     # There should be more private than non-private
     assert len(response.content) < len(private_response.content)
     assert len(response.content) < len(private_response.content)
 
 
-
   def test_url_password_hiding(self):
   def test_url_password_hiding(self):
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     client = make_logged_in_client(username="api2_superuser", groupname="default", recreate=True, is_superuser=True)
     user = User.objects.get(username="api2_superuser")
     user = User.objects.get(username="api2_superuser")
@@ -198,7 +189,6 @@ class TestApi2(object):
     finally:
     finally:
       clear()
       clear()
 
 
-
   def test_get_config(self):
   def test_get_config(self):
     response = self.client.get('/desktop/api2/get_config')
     response = self.client.get('/desktop/api2/get_config')
 
 
@@ -208,18 +198,10 @@ class TestApi2(object):
     assert 'types' in config['documents']
     assert 'types' in config['documents']
     assert 'is_admin' in config['hue_config']
     assert 'is_admin' in config['hue_config']
     assert 'is_yarn_enabled' in config['hue_config']
     assert 'is_yarn_enabled' in config['hue_config']
-    assert not 'query-TestApi2.test_get_config' in config['documents']['types'], config
+    assert 'query-TestApi2.test_get_config' not in config['documents']['types'], config
 
 
-    doc = Document2.objects.create(
-        name='Query xxx',
-        type='query-TestApi2.test_get_config',
-        owner=self.user
-    )
-    doc2 = Document2.objects.create(
-        name='Query xxx 2',
-        type='query-TestApi2.test_get_config',
-        owner=self.user
-    )
+    doc = Document2.objects.create(name='Query xxx', type='query-TestApi2.test_get_config', owner=self.user)
+    doc2 = Document2.objects.create(name='Query xxx 2', type='query-TestApi2.test_get_config', owner=self.user)
 
 
     try:
     try:
       response = self.client.get('/desktop/api2/get_config')
       response = self.client.get('/desktop/api2/get_config')
@@ -235,7 +217,6 @@ class TestApi2(object):
 
 
 @pytest.mark.django_db
 @pytest.mark.django_db
 class TestDocumentApiSharingPermissions(object):
 class TestDocumentApiSharingPermissions(object):
-
   def setup_method(self):
   def setup_method(self):
     self.client = make_logged_in_client(username="perm_user", groupname="default", recreate=True, is_superuser=False)
     self.client = make_logged_in_client(username="perm_user", groupname="default", recreate=True, is_superuser=False)
     self.client_not_me = make_logged_in_client(username="not_perm_user", groupname="default", recreate=True, is_superuser=False)
     self.client_not_me = make_logged_in_client(username="not_perm_user", groupname="default", recreate=True, is_superuser=False)
@@ -243,44 +224,25 @@ class TestDocumentApiSharingPermissions(object):
     self.user = User.objects.get(username="perm_user")
     self.user = User.objects.get(username="perm_user")
     self.user_not_me = User.objects.get(username="not_perm_user")
     self.user_not_me = User.objects.get(username="not_perm_user")
 
 
-
   def _add_doc(self, name):
   def _add_doc(self, name):
-    return Document2.objects.create(
-        name=name,
-        type='query-hive',
-        owner=self.user
-    )
+    return Document2.objects.create(name=name, type='query-hive', owner=self.user)
 
 
   def share_doc(self, doc, permissions, client=None):
   def share_doc(self, doc, permissions, client=None):
     if client is None:
     if client is None:
       client = self.client
       client = self.client
 
 
-    return client.post("/desktop/api2/doc/share", {
-        'uuid': json.dumps(doc.uuid),
-        'data': json.dumps(permissions)
-    })
+    return client.post("/desktop/api2/doc/share", {'uuid': json.dumps(doc.uuid), 'data': json.dumps(permissions)})
 
 
   def share_link_doc(self, doc, perm, client=None):
   def share_link_doc(self, doc, perm, client=None):
     if client is None:
     if client is None:
       client = self.client
       client = self.client
 
 
-    return client.post("/desktop/api2/doc/share/link", {
-        'uuid': json.dumps(doc.uuid),
-        'perm': json.dumps(perm)
-    })
+    return client.post("/desktop/api2/doc/share/link", {'uuid': json.dumps(doc.uuid), 'perm': json.dumps(perm)})
 
 
   def test_update_permissions(self):
   def test_update_permissions(self):
     doc = self._add_doc('test_update_permissions')
     doc = self._add_doc('test_update_permissions')
 
 
-    response = self.share_doc(
-        doc,
-        {
-          'read': {
-            'user_ids': [self.user_not_me.id],
-            'group_ids': []
-          }
-        }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [self.user_not_me.id], 'group_ids': []}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -308,18 +270,8 @@ class TestDocumentApiSharingPermissions(object):
     assert not doc.can_write(self.user_not_me)
     assert not doc.can_write(self.user_not_me)
 
 
     # Share by user
     # Share by user
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-            self.user_not_me.id
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
+    response = self.share_doc(
+      doc, {'read': {'user_ids': [self.user_not_me.id], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}}
     )
     )
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
@@ -336,18 +288,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     # Un-share
     # Un-share
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -365,18 +306,7 @@ class TestDocumentApiSharingPermissions(object):
     # Share by group
     # Share by group
     default_group = get_default_user_group()
     default_group = get_default_user_group()
 
 
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': [default_group.id]
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': [default_group.id]}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -392,18 +322,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     # Un-share
     # Un-share
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -419,17 +338,8 @@ class TestDocumentApiSharingPermissions(object):
     assert not json.loads(response.content)['documents']
     assert not json.loads(response.content)['documents']
 
 
     # Modify by other user
     # Modify by other user
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [self.user_not_me.id],
-          'group_ids': []
-        }
-      }
+    response = self.share_doc(
+      doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [self.user_not_me.id], 'group_ids': []}}
     )
     )
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
@@ -446,18 +356,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     # Un-share
     # Un-share
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -473,18 +372,7 @@ class TestDocumentApiSharingPermissions(object):
     assert not json.loads(response.content)['documents']
     assert not json.loads(response.content)['documents']
 
 
     # Modify by group
     # Modify by group
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': [default_group.id]
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': [default_group.id]}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -500,18 +388,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     # Un-share
     # Un-share
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
-    )
+    response = self.share_doc(doc, {'read': {'user_ids': [], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}})
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
 
 
@@ -526,23 +403,12 @@ class TestDocumentApiSharingPermissions(object):
     response = self.client_not_me.get('/desktop/api2/docs/')
     response = self.client_not_me.get('/desktop/api2/docs/')
     assert not json.loads(response.content)['documents']
     assert not json.loads(response.content)['documents']
 
 
-
   def test_update_permissions_cannot_escalate_privileges(self):
   def test_update_permissions_cannot_escalate_privileges(self):
     doc = self._add_doc('test_update_permissions_cannot_escape_privileges')
     doc = self._add_doc('test_update_permissions_cannot_escape_privileges')
 
 
     # Share read permissions
     # Share read permissions
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-            self.user_not_me.id
-          ],
-          'group_ids': []
-        },
-        'write': {
-          'user_ids': [],
-          'group_ids': []
-        }
-      }
+    response = self.share_doc(
+      doc, {'read': {'user_ids': [self.user_not_me.id], 'group_ids': []}, 'write': {'user_ids': [], 'group_ids': []}}
     )
     )
 
 
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
@@ -553,33 +419,29 @@ class TestDocumentApiSharingPermissions(object):
     assert not doc.can_write(self.user_not_me)
     assert not doc.can_write(self.user_not_me)
 
 
     # Try, and fail to escalate privileges.
     # Try, and fail to escalate privileges.
-    response = self.share_doc(doc, {
-        'read': {
-          'user_ids': [
-            self.user_not_me.id
-          ],
-          'group_ids': []
-        },
+    response = self.share_doc(
+      doc,
+      {
+        'read': {'user_ids': [self.user_not_me.id], 'group_ids': []},
         'write': {
         'write': {
           'user_ids': [
           'user_ids': [
             self.user_not_me.id,
             self.user_not_me.id,
           ],
           ],
-          'group_ids': []
-        }
+          'group_ids': [],
+        },
       },
       },
-      self.client_not_me
+      self.client_not_me,
     )
     )
 
 
     content = json.loads(response.content)
     content = json.loads(response.content)
     assert content['status'] == -1
     assert content['status'] == -1
-    assert "Document does not exist or you don\'t have the permission to access it." in content['message'], content['message']
+    assert "Document does not exist or you don't have the permission to access it." in content['message'], content['message']
 
 
     assert doc.can_read(self.user)
     assert doc.can_read(self.user)
     assert doc.can_write(self.user)
     assert doc.can_write(self.user)
     assert doc.can_read(self.user_not_me)
     assert doc.can_read(self.user_not_me)
     assert not doc.can_write(self.user_not_me)
     assert not doc.can_write(self.user_not_me)
 
 
-
   def test_link_sharing_permissions(self):
   def test_link_sharing_permissions(self):
     # Add doc
     # Add doc
     doc = self._add_doc('test_link_sharing_permissions')
     doc = self._add_doc('test_link_sharing_permissions')
@@ -597,7 +459,6 @@ class TestDocumentApiSharingPermissions(object):
     response = self.client_not_me.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     response = self.client_not_me.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     assert -1 == json.loads(response.content)['status'], response.content
     assert -1 == json.loads(response.content)['status'], response.content
 
 
-
     assert doc.can_read(self.user)
     assert doc.can_read(self.user)
     assert doc.can_write(self.user)
     assert doc.can_write(self.user)
     assert not doc.can_read(self.user_not_me)
     assert not doc.can_read(self.user_not_me)
@@ -618,7 +479,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     response = self.client_not_me.get('/desktop/api2/docs/?text=test_link_sharing_permissions')
     response = self.client_not_me.get('/desktop/api2/docs/?text=test_link_sharing_permissions')
-    assert not json.loads(response.content)['documents']  #  Link sharing does not list docs in Home, only provides direct access
+    assert not json.loads(response.content)['documents']  # Link sharing does not list docs in Home, only provides direct access
 
 
     response = self.client.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     response = self.client.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
@@ -685,7 +546,7 @@ class TestDocumentApiSharingPermissions(object):
     assert json.loads(response.content)['documents']
     assert json.loads(response.content)['documents']
 
 
     response = self.client_not_me.get('/desktop/api2/docs/?text=test_link_sharing_permissions')
     response = self.client_not_me.get('/desktop/api2/docs/?text=test_link_sharing_permissions')
-    assert not json.loads(response.content)['documents']  #  Link sharing does not list docs in Home, only provides direct access
+    assert not json.loads(response.content)['documents']  # Link sharing does not list docs in Home, only provides direct access
 
 
     response = self.client.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     response = self.client.get('/desktop/api2/doc/?uuid=%s' % doc_id)
     assert 0 == json.loads(response.content)['status'], response.content
     assert 0 == json.loads(response.content)['status'], response.content
@@ -718,7 +579,6 @@ class TestDocumentApiSharingPermissions(object):
 
 
 @pytest.mark.django_db
 @pytest.mark.django_db
 class TestDocumentGist(object):
 class TestDocumentGist(object):
-
   def setup_method(self):
   def setup_method(self):
     self.client = make_logged_in_client(username="gist_user", groupname="default", recreate=True, is_superuser=False)
     self.client = make_logged_in_client(username="gist_user", groupname="default", recreate=True, is_superuser=False)
     self.client_not_me = make_logged_in_client(username="other_gist_user", groupname="default", recreate=True, is_superuser=False)
     self.client_not_me = make_logged_in_client(username="other_gist_user", groupname="default", recreate=True, is_superuser=False)
@@ -726,12 +586,13 @@ class TestDocumentGist(object):
     self.user = User.objects.get(username="gist_user")
     self.user = User.objects.get(username="gist_user")
     self.user_not_me = User.objects.get(username="other_gist_user")
     self.user_not_me = User.objects.get(username="other_gist_user")
 
 
-
   def _create_gist(self, statement, doc_type, name='', description='', client=None):
   def _create_gist(self, statement, doc_type, name='', description='', client=None):
     if client is None:
     if client is None:
       client = self.client
       client = self.client
 
 
-    return client.post("/desktop/api2/gist/create", {
+    return client.post(
+      "/desktop/api2/gist/create",
+      {
         'statement': statement,
         'statement': statement,
         'doc_type': doc_type,
         'doc_type': doc_type,
         'name': name,
         'name': name,
@@ -739,7 +600,6 @@ class TestDocumentGist(object):
       },
       },
     )
     )
 
 
-
   def _get_gist(self, uuid, client=None, is_crawler_bot=False):
   def _get_gist(self, uuid, client=None, is_crawler_bot=False):
     if client is None:
     if client is None:
       client = self.client
       client = self.client
@@ -749,42 +609,38 @@ class TestDocumentGist(object):
     else:
     else:
       headers = {}
       headers = {}
 
 
-    return client.get("/desktop/api2/gist/open", {
+    return client.get(
+      "/desktop/api2/gist/open",
+      {
         'uuid': uuid,
         'uuid': uuid,
       },
       },
-      **headers
+      **headers,
     )
     )
 
 
-
   def test_create(self):
   def test_create(self):
     assert not Document2.objects.filter(type='gist', name='test_gist_create')
     assert not Document2.objects.filter(type='gist', name='test_gist_create')
 
 
     response = self._create_gist(
     response = self._create_gist(
-        statement='SELECT 1',
-        doc_type='hive-query',
-        name='test_gist_create',
+      statement='SELECT 1',
+      doc_type='hive-query',
+      name='test_gist_create',
     )
     )
     gist = json.loads(response.content)
     gist = json.loads(response.content)
 
 
     assert Document2.objects.filter(type='gist', name='test_gist_create')
     assert Document2.objects.filter(type='gist', name='test_gist_create')
     assert Document2.objects.filter(type='gist', uuid=gist['uuid'])
     assert Document2.objects.filter(type='gist', uuid=gist['uuid'])
-    assert (
-        'SELECT 1' ==
-        json.loads(Document2.objects.get(type='gist', uuid=gist['uuid']).data)['statement_raw'])
+    assert 'SELECT 1' == json.loads(Document2.objects.get(type='gist', uuid=gist['uuid']).data)['statement_raw']
 
 
     response2 = self._create_gist(
     response2 = self._create_gist(
-        statement='SELECT 2',
-        doc_type='hive-query',
-        name='test_gist_create2',
+      statement='SELECT 2',
+      doc_type='hive-query',
+      name='test_gist_create2',
     )
     )
     gist2 = json.loads(response2.content)
     gist2 = json.loads(response2.content)
 
 
     assert Document2.objects.filter(type='gist', name='test_gist_create2')
     assert Document2.objects.filter(type='gist', name='test_gist_create2')
     assert Document2.objects.filter(type='gist', uuid=gist2['uuid'])
     assert Document2.objects.filter(type='gist', uuid=gist2['uuid'])
-    assert (
-        'SELECT 2' ==
-        json.loads(Document2.objects.get(type='gist', uuid=gist2['uuid']).data)['statement_raw'])
-
+    assert 'SELECT 2' == json.loads(Document2.objects.get(type='gist', uuid=gist2['uuid']).data)['statement_raw']
 
 
   def test_multiple_gist_dirs_on_gist_create(self):
   def test_multiple_gist_dirs_on_gist_create(self):
     home_dir = Directory.objects.get_home_directory(self.user)
     home_dir = Directory.objects.get_home_directory(self.user)
@@ -815,12 +671,11 @@ class TestDocumentGist(object):
     assert gist_dir1.uuid == gist_home.uuid
     assert gist_dir1.uuid == gist_home.uuid
     assert Document2.objects.get(name='test_gist_child', type='gist', owner=self.user).parent_directory == gist_home
     assert Document2.objects.get(name='test_gist_child', type='gist', owner=self.user).parent_directory == gist_home
 
 
-
   def test_get(self):
   def test_get(self):
     response = self._create_gist(
     response = self._create_gist(
-        statement='SELECT 1',
-        doc_type='hive-query',
-        name='test_gist_get',
+      statement='SELECT 1',
+      doc_type='hive-query',
+      name='test_gist_get',
     )
     )
     gist = json.loads(response.content)
     gist = json.loads(response.content)
 
 
@@ -832,7 +687,6 @@ class TestDocumentGist(object):
     assert 302 == response.status_code
     assert 302 == response.status_code
     assert '/hue/editor?gist=%(uuid)s&type=hive-query' % gist == response.url
     assert '/hue/editor?gist=%(uuid)s&type=hive-query' % gist == response.url
 
 
-
   def test_gist_directory_creation(self):
   def test_gist_directory_creation(self):
     home_dir = Directory.objects.get_home_directory(self.user)
     home_dir = Directory.objects.get_home_directory(self.user)
 
 
@@ -842,23 +696,19 @@ class TestDocumentGist(object):
 
 
     assert home_dir.children.filter(name=Document2.GIST_DIR, owner=self.user).exists()
     assert home_dir.children.filter(name=Document2.GIST_DIR, owner=self.user).exists()
 
 
-
   def test_get_unfurl(self):
   def test_get_unfurl(self):
     # Unfurling on
     # Unfurling on
     f = ENABLE_GIST_PREVIEW.set_for_testing(True)
     f = ENABLE_GIST_PREVIEW.set_for_testing(True)
 
 
     try:
     try:
       response = self._create_gist(
       response = self._create_gist(
-          statement='SELECT 1',
-          doc_type='hive-query',
-          name='test_gist_get',
+        statement='SELECT 1',
+        doc_type='hive-query',
+        name='test_gist_get',
       )
       )
       gist = json.loads(response.content)
       gist = json.loads(response.content)
 
 
-      response = self._get_gist(
-        uuid=gist['uuid'],
-        is_crawler_bot=True
-      )
+      response = self._get_gist(uuid=gist['uuid'], is_crawler_bot=True)
 
 
       assert 200 == response.status_code
       assert 200 == response.status_code
       assert b'<meta name="twitter:card" content="summary">' in response.content, response.content
       assert b'<meta name="twitter:card" content="summary">' in response.content, response.content
@@ -870,12 +720,131 @@ class TestDocumentGist(object):
     f = ENABLE_GIST_PREVIEW.set_for_testing(False)
     f = ENABLE_GIST_PREVIEW.set_for_testing(False)
 
 
     try:
     try:
-      response = self._get_gist(
-        uuid=gist['uuid'],
-        is_crawler_bot=True
-      )
+      response = self._get_gist(uuid=gist['uuid'], is_crawler_bot=True)
 
 
       assert 302 == response.status_code
       assert 302 == response.status_code
       assert '/hue/editor?gist=%(uuid)s&type=hive-query' % gist == response.url
       assert '/hue/editor?gist=%(uuid)s&type=hive-query' % gist == response.url
     finally:
     finally:
       f()
       f()
+
+
+class TestInstallAppExampleAPI:
+  def test_install_app_examples_missing_app_name(self):
+    request = Mock(method='POST', POST={}, user=Mock())
+    response = install_app_examples(request)
+
+    assert response.status_code == 400
+    assert response.content.decode('utf-8') == 'Missing parameter: app_name is required.'
+
+  def test_install_app_examples_unsupported_app_name(self):
+    request = Mock(method='POST', POST={'app_name': 'test_app'}, user=Mock())
+    response = install_app_examples(request)
+
+    assert response.status_code == 400
+    assert response.content.decode('utf-8') == 'Unsupported app name: test_app'
+
+  def test_install_app_examples_non_admin_user(self):
+    with patch('desktop.api2.is_admin') as mock_is_admin:
+      mock_is_admin.return_value = False
+
+      request = Mock(method='POST', POST={'app_name': 'hive'}, user=Mock())
+      response = install_app_examples(request)
+
+      assert response.status_code == 403
+      assert response.content.decode('utf-8') == 'You must be a Hue admin to access this endpoint.'
+
+  def test_install_app_examples_success_hive(self):
+    with patch('desktop.api2.is_admin') as mock_is_admin:
+      with patch('desktop.api2._setup_hive_impala_examples') as mock_setup_hive_impala_examples:
+        mock_is_admin.return_value = True
+        mock_setup_hive_impala_examples.return_value = None
+
+        request = Mock(method='POST', POST={'app_name': 'hive'}, user=Mock())
+        response = install_app_examples(request)
+
+        assert response.status_code == 200
+        assert response.content.decode('utf-8') == 'Successfully installed examples for hive.'
+
+  def test_install_app_examples_success_impala(self):
+    with patch('desktop.api2.is_admin') as mock_is_admin:
+      with patch('desktop.api2._setup_hive_impala_examples') as mock_setup_hive_impala_examples:
+        mock_is_admin.return_value = True
+        mock_setup_hive_impala_examples.return_value = None
+
+        request = Mock(method='POST', POST={'app_name': 'impala'}, user=Mock())
+        response = install_app_examples(request)
+
+        assert response.status_code == 200
+        assert response.content.decode('utf-8') == 'Successfully installed examples for impala.'
+
+  def test_setup_hive_impala_examples_invalid_dialect(self):
+    request = Mock(method='POST', POST={'app_name': 'impala', 'dialect': 'test_dialect'})
+    response = _setup_hive_impala_examples(request)
+
+    assert response.status_code == 400
+    assert response.content.decode('utf-8') == "Invalid dialect: Must be 'hive' or 'impala'"
+
+  def test_setup_hive_impala_examples_calls_command(self):
+    with patch('desktop.api2.common.find_compute') as mock_find_compute:
+      with patch('desktop.api2.beeswax_install_examples.Command.handle') as mock_command:
+        mock_find_compute.return_value = 'mock_interpreter'
+        request = Mock(method='POST', POST={'app_name': 'impala', 'dialect': 'impala', 'database_name': 'test_db'}, user=Mock())
+        _setup_hive_impala_examples(request)
+
+        mock_command.assert_called_once_with(
+          dialect='impala', db_name='test_db', user=request.user, request=request, interpreter='mock_interpreter'
+        )
+
+  def test_setup_notebook_examples_missing_connector_id(self):
+    request = Mock(method='POST', POST={}, user=Mock())
+
+    response = _setup_notebook_examples(request)
+
+    assert response.status_code == 400
+    assert response.content.decode('utf-8') == 'Missing parameter: connector_id is required.'
+
+  def test_setup_notebook_examples_existing_connector(self):
+    with patch('desktop.api2.Connector.objects.get') as mock_get_connector:
+      with patch('desktop.api2.beeswax_install_examples.Command.handle') as mock_command:
+        with patch('desktop.api2.get_interpreter') as mock_get_interpreter:
+          request = Mock(method='POST', POST={'app_name': 'notebook', 'database_name': 'test_db', 'connector_id': '1'}, user=Mock())
+          mock_get_connector.return_value = Mock(to_dict=Mock(return_value={'type': 'test_type'}), dialect='spark')
+          mock_get_interpreter.return_value = 'mock_interpreter'
+
+          _setup_notebook_examples(request)
+
+          mock_command.assert_called_once_with(
+            dialect='spark', db_name='test_db', user=request.user, interpreter='mock_interpreter', request=request
+          )
+
+  def test_setup_notebook_examples_connector_none(self):
+    with patch('desktop.api2.Connector.objects.get') as mock_get_connector:
+      with patch('desktop.api2.beeswax_install_examples.Command.handle') as mock_beeswax_install_command:
+        with patch('desktop.api2.notebook_setup.Command.handle') as mock_notebook_setup_command:
+          request = Mock(method='POST', POST={'app_name': 'notebook', 'dialect': 'spark', 'connector_id': '1'}, user=Mock())
+          mock_get_connector.return_value = None
+
+          _setup_notebook_examples(request)
+
+          assert not mock_beeswax_install_command.called
+          mock_notebook_setup_command.assert_called_once_with(dialect='spark', user=request.user)
+
+  def test_setup_search_examples_with_log_analytics_demo(self):
+    with patch('desktop.api2.search_setup.Command.handle') as mock_search_setup_command:
+      with patch('desktop.api2.indexer_setup.Command.handle') as mock_indexer_setup_command:
+        request = Mock(method='POST', POST={'data': 'log_analytics_demo'}, user=Mock())
+
+        _setup_search_examples(request)
+
+        mock_indexer_setup_command.assert_called_once_with(data='log_analytics_demo')
+        mock_search_setup_command.assert_called_once()
+
+  def test_setup_search_examples_without_log_analytics_demo(self):
+    with patch('desktop.api2.search_setup.Command.handle') as mock_search_setup_command:
+      with patch('desktop.api2.indexer_setup.Command.handle') as mock_indexer_setup_command:
+        request = Mock(method='POST', POST={'data': 'twitter_demo'}, user=Mock())
+
+        _setup_search_examples(request)
+
+        mock_indexer_setup_command.assert_called_once_with(data='twitter_demo')
+        assert not mock_search_setup_command.called

+ 5 - 0
desktop/core/src/desktop/api_public.py

@@ -71,6 +71,11 @@ def download_hue_logs(request):
   return logs_api.download_hue_logs(django_request)
   return logs_api.download_hue_logs(django_request)
 
 
 
 
+@api_view(["POST"])
+def install_app_examples(request):
+  django_request = get_django_request(request)
+  return desktop_api.install_app_examples(django_request)
+
 # Editor
 # Editor
 
 
 
 

+ 1 - 0
desktop/core/src/desktop/api_public_urls_v1.py

@@ -33,6 +33,7 @@ urlpatterns += [
   re_path(r'^banners/?$', api_public.get_banners, name='core_get_banners'),
   re_path(r'^banners/?$', api_public.get_banners, name='core_get_banners'),
   re_path(r'^logs/?$', api_public.get_hue_logs, name='core_get_hue_logs'),
   re_path(r'^logs/?$', api_public.get_hue_logs, name='core_get_hue_logs'),
   re_path(r'^logs/download/?$', api_public.download_hue_logs, name='core_download_hue_logs'),
   re_path(r'^logs/download/?$', api_public.download_hue_logs, name='core_download_hue_logs'),
+  re_path(r'^install_app_examples/?$', api_public.install_app_examples, name='core_install_app_examples'),
   re_path(r'^get_config/?$', api_public.get_config),
   re_path(r'^get_config/?$', api_public.get_config),
   re_path(r'^get_namespaces/(?P<interface>[\w\-]+)/?$', api_public.get_context_namespaces),  # To remove
   re_path(r'^get_namespaces/(?P<interface>[\w\-]+)/?$', api_public.get_context_namespaces),  # To remove
 ]
 ]