Browse Source

HUE-5864 [oozie] Add an Impala document action

Romain Rigaux 8 years ago
parent
commit
8ccc4c9a06

+ 121 - 15
apps/oozie/src/oozie/models2.py

@@ -850,13 +850,19 @@ class Node():
       self.data['properties']['files'] = [{'value': prop} for prop in action['properties']['files']]
       self.data['properties']['files'] = [{'value': prop} for prop in action['properties']['files']]
       self.data['properties']['archives'] = [{'value': prop} for prop in action['properties']['archives']]
       self.data['properties']['archives'] = [{'value': prop} for prop in action['properties']['archives']]
 
 
-    elif self.data['type'] == ImpalaAction.TYPE:
-      self.data['properties']['shell_command'] = 'impala.sh'
+    elif self.data['type'] == ImpalaAction.TYPE or self.data['type'] == ImpalaDocumentAction.TYPE:
+      shell_command_name = self.data['name'] + '.sh'
+      self.data['properties']['shell_command'] = shell_command_name
       self.data['properties']['env_var'] = []
       self.data['properties']['env_var'] = []
       self.data['properties']['capture_output'] = False
       self.data['properties']['capture_output'] = False
       self.data['properties']['arguments'] = []
       self.data['properties']['arguments'] = []
 
 
-      files = [{'value': 'impala.sh'}]
+      if self.data['type'] == ImpalaAction.TYPE:
+        script_path = self.data['properties'].get('script_path')
+      else:
+        script_path = self.data['name'] + '.sql'
+
+      files = [{'value': shell_command_name}, {'value': script_path}]
       if self.data['properties']['key_tab_path']:
       if self.data['properties']['key_tab_path']:
         files.append({'value': self.data['properties']['key_tab_path']})
         files.append({'value': self.data['properties']['key_tab_path']})
 
 
@@ -1257,16 +1263,6 @@ def _get_hiveserver2_url():
     return 'jdbc:hive2://localhost:10000/default'
     return 'jdbc:hive2://localhost:10000/default'
 
 
 
 
-def _get_impala_url():
-  try:
-    from impala.dbms import get_query_server_config
-    return get_query_server_config()['server_host']
-  except Exception, e:
-    # Might fail is Impala is disabled
-    LOG.exception('Could not get Impalad URL: %s' % smart_str(e))
-    return 'localhost'
-
-
 class HiveServer2Action(Action):
 class HiveServer2Action(Action):
   TYPE = 'hive2'
   TYPE = 'hive2'
   DEFAULT_CREDENTIALS = 'hive2'
   DEFAULT_CREDENTIALS = 'hive2'
@@ -1364,9 +1360,20 @@ class HiveServer2Action(Action):
     return [cls.FIELDS['script_path']]
     return [cls.FIELDS['script_path']]
 
 
 
 
+def _get_impala_url():
+  try:
+    from impala.dbms import get_query_server_config
+    return get_query_server_config()['server_host']
+  except Exception, e:
+    # Might fail is Impala is disabled
+    LOG.exception('Could not get Impalad URL: %s' % smart_str(e))
+    return 'localhost'
+
+
 class ImpalaAction(HiveServer2Action):
 class ImpalaAction(HiveServer2Action):
+  # Executed as shell action until Oozie supports an Impala Action
   TYPE = 'impala'
   TYPE = 'impala'
-  DEFAULT_CREDENTIALS = 'impala' # None at this time
+  DEFAULT_CREDENTIALS = 'impala' # None at this time, need to upload user keytab
 
 
   FIELDS = HiveServer2Action.FIELDS.copy()
   FIELDS = HiveServer2Action.FIELDS.copy()
   del FIELDS['jdbc_url']
   del FIELDS['jdbc_url']
@@ -2154,7 +2161,7 @@ class HiveDocumentAction(Action):
           'name': 'parameters',
           'name': 'parameters',
           'label': _('Parameters'),
           'label': _('Parameters'),
           'value': [],
           'value': [],
-          'help_text': _('The %(type)s parameters of the script. E.g. N=5, INPUT=${inputDir}')  % {'type': TYPE.title()},
+          'help_text': _('The parameters of the script. E.g. N=5, INPUT=${inputDir}'),
           'type': ''
           'type': ''
      },
      },
      # Common
      # Common
@@ -2229,6 +2236,43 @@ class HiveDocumentAction(Action):
     return [cls.FIELDS['uuid']]
     return [cls.FIELDS['uuid']]
 
 
 
 
+class ImpalaDocumentAction(HiveDocumentAction):
+  TYPE = 'impala-document'
+  DEFAULT_CREDENTIALS = 'impala' # None at this time, need to upload user keytab
+
+  FIELDS = HiveServer2Action.FIELDS.copy()
+  del FIELDS['jdbc_url']
+  del FIELDS['password']
+  FIELDS['impalad_host'] = {
+      'name': 'impalad_host',
+      'label': _('Impalad hostname'),
+      'value': "",
+      'help_text': _('e.g. impalad-001.cluster.com. The hostname of the Impalad to send the query to.'),
+      'type': ''
+  }
+  FIELDS['key_tab_path'] = {
+      'name': 'key_tab_path',
+      'label': _('Keytab path'),
+      'value': '',
+      'help_text': _('Path to the keytab to use when on a secure cluster, e.g. /user/joe/joe.keytab.'),
+      'type': ''
+  }
+  FIELDS['user_principal'] = {
+      'name': 'user_principal',
+      'label': _('User principal'),
+      'value': 'joe@PROD.EDH',
+      'help_text': _('Name of the principal to use in the kinit, e.g.: kinit -k -t /home/joe/joe.keytab joe@PROD.EDH.'),
+      'type': ''
+  }
+  FIELDS['uuid'] = {
+      'name': 'uuid',
+      'label': _('Hive query'),
+      'value': '',
+      'help_text': _('Select a saved Hive query you want to schedule.'),
+      'type': 'impala'
+  }
+
+
 class JavaDocumentAction(Action):
 class JavaDocumentAction(Action):
   TYPE = 'java-document'
   TYPE = 'java-document'
   FIELDS = {
   FIELDS = {
@@ -2785,6 +2829,7 @@ NODES = {
   'spark-widget': SparkAction,
   'spark-widget': SparkAction,
   'generic-widget': GenericAction,
   'generic-widget': GenericAction,
   'hive-document-widget': HiveDocumentAction,
   'hive-document-widget': HiveDocumentAction,
+  'impala-document-widget': ImpalaDocumentAction,
   'java-document-widget': JavaDocumentAction,
   'java-document-widget': JavaDocumentAction,
   'spark-document-widget': SparkDocumentAction,
   'spark-document-widget': SparkDocumentAction,
   'pig-document-widget': PigDocumentAction,
   'pig-document-widget': PigDocumentAction,
@@ -3613,6 +3658,8 @@ class WorkflowBuilder():
         node = self.get_java_document_node(document)
         node = self.get_java_document_node(document)
       elif document.type == 'query-hive':
       elif document.type == 'query-hive':
         node = self.get_hive_document_node(document, user)
         node = self.get_hive_document_node(document, user)
+      elif document.type == 'query-impala':
+        node = self.get_impala_document_node(document, user)
       elif document.type == 'query-spark2':
       elif document.type == 'query-spark2':
         node = self.get_spark_document_node(document, user)
         node = self.get_spark_document_node(document, user)
       elif document.type == 'query-pig':
       elif document.type == 'query-pig':
@@ -3647,6 +3694,8 @@ class WorkflowBuilder():
         node = self.get_java_snippet_node(snippet)
         node = self.get_java_snippet_node(snippet)
       elif snippet['type'] == 'query-hive':
       elif snippet['type'] == 'query-hive':
         node = self.get_hive_snippet_node(snippet, user)
         node = self.get_hive_snippet_node(snippet, user)
+      elif snippet['type'] == 'query-impala':
+        node = self.get_impala_snippet_node(snippet, user)
       elif snippet['type'] == 'shell':
       elif snippet['type'] == 'shell':
         node = self.get_shell_snippet_node(snippet)
         node = self.get_shell_snippet_node(snippet)
       else:
       else:
@@ -3722,6 +3771,63 @@ class WorkflowBuilder():
 
 
     return node
     return node
 
 
+  def _get_impala_node(self, node_id, user, is_document_node=False):
+    credentials = []
+
+    return {
+        u'id': node_id,
+        u'name': u'impala-%s' % node_id[:4],
+        u"type": u"impala-document-widget",
+        u'properties': {
+            u'files': [],
+            u'job_xml': u'',
+            u'retry_interval': [],
+            u'retry_max': [],
+            u'job_properties': [],
+            u'arguments': [],
+            u'parameters': [],
+            u'sla': [
+                {u'key': u'enabled', u'value': False},
+                {u'key': u'nominal-time', u'value': u'${nominal_time}'},
+                {u'key': u'should-start', u'value': u''},
+                {u'key': u'should-end', u'value': u'${30 * MINUTES}'},
+                {u'key': u'max-duration', u'value': u''},
+                {u'key': u'alert-events', u'value': u''},
+                {u'key': u'alert-contact', u'value': u''},
+                {u'key': u'notification-msg', u'value': u''},
+                {u'key': u'upstream-apps', u'value': u''},
+            ],
+            u'archives': [],
+            u'prepares': [],
+            u'credentials': credentials,
+            u'impalad_host': u'',
+            u'key_tab_path': u'',
+            u'user_principal': u''
+        },
+        u'children': [
+            {u'to': u'33430f0f-ebfa-c3ec-f237-3e77efa03d0a'},
+            {u'error': u'17c9c895-5a16-7443-bb81-f34b30b21548'}
+        ],
+        u'actionParameters': [],
+    }
+
+  def get_impala_snippet_node(self, snippet, user):
+    node = self._get_impala_node(snippet['id'], user)
+
+    node['properties']['parameters'] = [{'value': '%(name)s=%(value)s' % v} for v in snippet['variables']]
+    node['properties']['statements'] = 'USE %s;\n\n%s' % (snippet['database'], snippet['statement_raw'])
+
+    return node
+
+  def get_impala_document_node(self, document, user):
+    node = self._get_impala_node(document.uuid, user, is_document_node=True)
+
+    notebook = Notebook(document=document)
+    node['properties']['parameters'] = [{'value': '%(name)s=%(value)s' % v} for v in notebook.get_data()['snippets'][0]['variables']]
+    node['properties']['uuid'] = document.uuid
+
+    return node
+
   def _get_spark_node(self, node_id, user, is_document_node=False):
   def _get_spark_node(self, node_id, user, is_document_node=False):
     credentials = []
     credentials = []
 
 

+ 2 - 1
apps/oozie/src/oozie/static/oozie/js/workflow-editor.ko.js

@@ -174,7 +174,7 @@ var Node = function (node, vm) {
     });
     });
   }
   }
 
 
-  if ((type == 'hive-document-widget' || type == 'spark-document-widget') && typeof self.properties.uuid != "undefined") {
+  if ((type == 'hive-document-widget' || type == 'impala-document-widget' || type == 'spark-document-widget') && typeof self.properties.uuid != "undefined") {
     self.properties.uuid.subscribe(function () {
     self.properties.uuid.subscribe(function () {
       self.actionParametersFetched(false);
       self.actionParametersFetched(false);
       self.fetch_parameters();
       self.fetch_parameters();
@@ -1271,6 +1271,7 @@ var WorkflowEditorViewModel = function (layout_json, workflow_json, credentials_
   self.draggableSparkAction = ko.observable(bareWidgetBuilder("Spark", "spark-widget"));
   self.draggableSparkAction = ko.observable(bareWidgetBuilder("Spark", "spark-widget"));
   self.draggableGenericAction = ko.observable(bareWidgetBuilder("Generic", "generic-widget"));
   self.draggableGenericAction = ko.observable(bareWidgetBuilder("Generic", "generic-widget"));
   self.draggableHiveDocumentAction = ko.observable(bareWidgetBuilder("Hive", "hive-document-widget"));
   self.draggableHiveDocumentAction = ko.observable(bareWidgetBuilder("Hive", "hive-document-widget"));
+  self.draggableImpalaDocumentAction = ko.observable(bareWidgetBuilder("Impala", "impala-document-widget"));
   self.draggableJavaDocumentAction = ko.observable(bareWidgetBuilder("Java", "java-document-widget"));
   self.draggableJavaDocumentAction = ko.observable(bareWidgetBuilder("Java", "java-document-widget"));
   self.draggableSparkDocumentAction = ko.observable(bareWidgetBuilder("Spark", "spark-document-widget"));
   self.draggableSparkDocumentAction = ko.observable(bareWidgetBuilder("Spark", "spark-document-widget"));
   self.draggablePigDocumentAction = ko.observable(bareWidgetBuilder("Pig", "pig-document-widget"));
   self.draggablePigDocumentAction = ko.observable(bareWidgetBuilder("Pig", "pig-document-widget"));

+ 16 - 2
apps/oozie/src/oozie/templates/editor2/common_workflow.mako

@@ -591,7 +591,7 @@
 
 
 
 
 <script type="text/html" id="common-action-credentials">
 <script type="text/html" id="common-action-credentials">
-  <!-- ko if: $parent.widgetType() != 'impala-widget' -->
+  <!-- ko if: $parent.widgetType() != 'impala-widget' && $parent.widgetType() != 'impala-document-widget' -->
     <em data-bind="visible: $root.credentials() == null || $root.credentials().length == 0">${ _('No available credentials.') }</em>
     <em data-bind="visible: $root.credentials() == null || $root.credentials().length == 0">${ _('No available credentials.') }</em>
     <ul data-bind="visible: $root.credentials() != null && $root.credentials().length > 0, foreach: $root.credentials" class="unstyled">
     <ul data-bind="visible: $root.credentials() != null && $root.credentials().length > 0, foreach: $root.credentials" class="unstyled">
       <li>
       <li>
@@ -604,7 +604,7 @@
     </em>
     </em>
   <!-- /ko -->
   <!-- /ko -->
 
 
-  <!-- ko if: $parent.widgetType() == 'impala-widget' -->
+  <!-- ko if: $parent.widgetType() == 'impala-widget' || $parent.widgetType() == 'impala-document-widget' -->
     <input type="text" class="filechooser-input seventy" data-bind="filechooser: properties.key_tab_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.key_tab_path, attr: { placeholder:  $root.workflow_properties.key_tab_path.help_text }"/>
     <input type="text" class="filechooser-input seventy" data-bind="filechooser: properties.key_tab_path, filechooserOptions: globalFilechooserOptions, hdfsAutocomplete: properties.key_tab_path, attr: { placeholder:  $root.workflow_properties.key_tab_path.help_text }"/>
     <input type="text" data-bind="value: properties.user_principal, attr: { placeholder: $root.workflow_properties.user_principal.help_text }" />
     <input type="text" data-bind="value: properties.user_principal, attr: { placeholder: $root.workflow_properties.user_principal.help_text }" />
   <!-- /ko -->
   <!-- /ko -->
@@ -1145,12 +1145,21 @@
       </ul>
       </ul>
       <div class="tab-content">
       <div class="tab-content">
         <div class="tab-pane active" data-bind="attr: { id: 'properties-' + id() }">
         <div class="tab-pane active" data-bind="attr: { id: 'properties-' + id() }">
+          <!-- ko if: $root.workflow_properties.jdbc_url -->
           <span data-bind="text: $root.workflow_properties.jdbc_url.label"></span>
           <span data-bind="text: $root.workflow_properties.jdbc_url.label"></span>
           <input type="text" data-bind="value: properties.jdbc_url, attr: { placeholder: $root.workflow_properties.jdbc_url.help_text }" />
           <input type="text" data-bind="value: properties.jdbc_url, attr: { placeholder: $root.workflow_properties.jdbc_url.help_text }" />
           <br/>
           <br/>
+          <!-- /ko -->
+          <!-- ko if: $root.workflow_properties.password -->
           <span data-bind="text: $root.workflow_properties.password.label"></span>
           <span data-bind="text: $root.workflow_properties.password.label"></span>
           <input type="text" data-bind="value: properties.password, attr: { placeholder: $root.workflow_properties.password.help_text }" />
           <input type="text" data-bind="value: properties.password, attr: { placeholder: $root.workflow_properties.password.help_text }" />
           <br/>
           <br/>
+          <!-- /ko -->
+          <!-- ko if: $root.workflow_properties.impalad_host -->
+          <span data-bind="text: $root.workflow_properties.impalad_host.label"></span>
+          <input type="text" data-bind="value: properties.impalad_host, attr: { placeholder: $root.workflow_properties.impalad_host.help_text }" />
+          <br/>
+          <!-- /ko -->
           <span data-bind="template: { name: 'common-action-properties' }"></span>
           <span data-bind="template: { name: 'common-action-properties' }"></span>
           <br/>
           <br/>
           <br/>
           <br/>
@@ -1175,6 +1184,11 @@
 </script>
 </script>
 
 
 
 
+<script type="text/html" id="impala-document-widget">
+  <span data-bind="template: { name: 'hive-document-widget' }"></span>
+</script>
+
+
 <script type="text/html" id="java-document-widget">
 <script type="text/html" id="java-document-widget">
   <!-- ko if: $root.workflow.getNodeById(id()) -->
   <!-- ko if: $root.workflow.getNodeById(id()) -->
   <div class="row-fluid" data-bind="with: $root.workflow.getNodeById(id())" style="padding: 10px">
   <div class="row-fluid" data-bind="with: $root.workflow.getNodeById(id())" style="padding: 10px">

+ 11 - 2
apps/oozie/src/oozie/templates/editor2/workflow_editor.mako

@@ -151,10 +151,19 @@ ${ layout.menubar(section='workflows', is_editor=True, pullright=buttons) }
     <div data-bind="css: { 'draggable-widget': true },
     <div data-bind="css: { 'draggable-widget': true },
                     draggable: {data: draggableHiveDocumentAction(), isEnabled: true,
                     draggable: {data: draggableHiveDocumentAction(), isEnabled: true,
                     options: {'refreshPositions': true, 'stop': function(){ $root.isDragging(false); }, 'start': function(event, ui){ $root.isDragging(true); $root.currentlyDraggedWidget(draggableHiveDocumentAction());}}}"
                     options: {'refreshPositions': true, 'stop': function(){ $root.isDragging(false); }, 'start': function(event, ui){ $root.isDragging(true); $root.currentlyDraggedWidget(draggableHiveDocumentAction());}}}"
-         title="${_('Saved Hive query')}" rel="tooltip" data-placement="top">
+         title="${_('Hive query')}" rel="tooltip" data-placement="top">
          <a class="draggable-icon"><img src="${ static('oozie/art/icon_beeswax_48.png') }" class="app-icon"><sup style="color: #338bb8; margin-left: -4px; top: -14px; font-size: 12px">2</sup></a>
          <a class="draggable-icon"><img src="${ static('oozie/art/icon_beeswax_48.png') }" class="app-icon"><sup style="color: #338bb8; margin-left: -4px; top: -14px; font-size: 12px">2</sup></a>
     </div>
     </div>
 
 
+    % if ENABLE_IMPALA_ACTION.get():
+      <div data-bind="css: { 'draggable-widget': true },
+                    draggable: {data: draggableImpalaDocumentAction(), isEnabled: true,
+                    options: {'refreshPositions': true, 'stop': function(){ $root.isDragging(false); }, 'start': function(event, ui){ $root.isDragging(true); $root.currentlyDraggedWidget(draggableImpalaDocumentAction());}}}"
+         title="${_('Impala query')}" rel="tooltip" data-placement="top">
+         <a class="draggable-icon"><img src="${ static('oozie/art/icon_impala_48.png') }" class="app-icon"></a>
+      </div>
+    % endif
+
     <div data-bind="css: { 'draggable-widget': true },
     <div data-bind="css: { 'draggable-widget': true },
                     draggable: {data: draggableJavaDocumentAction(), isEnabled: true,
                     draggable: {data: draggableJavaDocumentAction(), isEnabled: true,
                     options: {'refreshPositions': true, 'stop': function(){ $root.isDragging(false); }, 'start': function(event, ui){ $root.isDragging(true); $root.currentlyDraggedWidget(draggableJavaDocumentAction());}}}"
                     options: {'refreshPositions': true, 'stop': function(){ $root.isDragging(false); }, 'start': function(event, ui){ $root.isDragging(true); $root.currentlyDraggedWidget(draggableJavaDocumentAction());}}}"
@@ -386,7 +395,7 @@ ${ workflow.render() }
           <!-- ko if: type() == 'workflow' -->
           <!-- ko if: type() == 'workflow' -->
           <select data-bind="options: $root.subworkflows, optionsText: 'name', optionsValue: 'value', value: value"></select>
           <select data-bind="options: $root.subworkflows, optionsText: 'name', optionsValue: 'value', value: value"></select>
           <!-- /ko -->
           <!-- /ko -->
-          <!-- ko if: ['hive', 'java', 'spark', 'pig', 'sqoop', 'distcp-doc', 'shell-doc', 'mapreduce-doc'].indexOf(type()) != -1 -->
+          <!-- ko if: ['hive', 'impala', 'java', 'spark', 'pig', 'sqoop', 'distcp-doc', 'shell-doc', 'mapreduce-doc'].indexOf(type()) != -1 -->
             <div class="selectize-wrapper" style="width: 300px;">
             <div class="selectize-wrapper" style="width: 300px;">
               <select placeholder="${ _('Search your documents...') }" data-bind="documentChooser: { value: value, document: $root.tempDocument, type: type }"></select>
               <select placeholder="${ _('Search your documents...') }" data-bind="documentChooser: { value: value, document: $root.tempDocument, type: type }"></select>
             </div>
             </div>

+ 1 - 1
desktop/core/src/desktop/static/desktop/js/ko.hue-bindings.js

@@ -4995,7 +4995,7 @@
         'pig': 'query-pig',
         'pig': 'query-pig',
         'sqoop': 'query-sqoop1',
         'sqoop': 'query-sqoop1',
         'distcp-doc': 'query-distcp',
         'distcp-doc': 'query-distcp',
-        'mapreduce-doc': 'query-mapreduce'
+        'mapreduce-doc': 'query-mapreduce',
       }
       }
       var type = 'query-hive';
       var type = 'query-hive';
       if (options.type) {
       if (options.type) {

+ 13 - 3
desktop/libs/liboozie/src/liboozie/submission2.py

@@ -202,10 +202,20 @@ class Submission(object):
           self.job.override_subworkflow_id(action, workflow.id) # For displaying the correct graph
           self.job.override_subworkflow_id(action, workflow.id) # For displaying the correct graph
           self.properties['workspace_%s' % workflow.uuid] = workspace # For pointing to the correct workspace
           self.properties['workspace_%s' % workflow.uuid] = workspace # For pointing to the correct workspace
 
 
-        elif action.data['type'] == 'impala':
+        elif action.data['type'] == 'impala' or action.data['type'] == 'impala-document':
           from oozie.models2 import _get_impala_url
           from oozie.models2 import _get_impala_url
           from impala.impala_flags import get_ssl_server_certificate
           from impala.impala_flags import get_ssl_server_certificate
 
 
+          if action.data['type'] == 'impala-document':
+            from notebook.models import Notebook
+            if action.data['properties'].get('uuid'):
+              notebook = Notebook(document=Document2.objects.get_by_uuid(user=self.user, uuid=action.data['properties']['uuid']))
+              statements = notebook.get_str()
+              script_name = action.data['name'] + '.sql'
+              self._create_file(deployment_dir, script_name, statements)
+          else:
+            script_name = os.path.basename(action.data['properties'].get('script_path'))
+
           if self.api.security_enabled:
           if self.api.security_enabled:
             kinit = 'kinit -k -t *.keytab %(user_principal)s' % {
             kinit = 'kinit -k -t *.keytab %(user_principal)s' % {
               'user_principal': action.data['properties'].get('user_principal')
               'user_principal': action.data['properties'].get('user_principal')
@@ -224,11 +234,11 @@ impala-shell %(kerberos_option)s %(ssl_option)s -i %(impalad_host)s -f %(query_f
   'impalad_host': action.data['properties'].get('impalad_host') or _get_impala_url(),
   'impalad_host': action.data['properties'].get('impalad_host') or _get_impala_url(),
   'kerberos_option': '' if self.api.security_enabled else '-k',
   'kerberos_option': '' if self.api.security_enabled else '-k',
   'ssl_option': '--ssl' if get_ssl_server_certificate() else '',
   'ssl_option': '--ssl' if get_ssl_server_certificate() else '',
-  'query_file': action.data['properties'].get('script_path'),
+  'query_file': script_name,
   'kinit': kinit
   'kinit': kinit
   }
   }
 
 
-          self._create_file(deployment_dir, 'impala.sh', shell_script)
+          self._create_file(deployment_dir, action.data['name'] + '.sh', shell_script)
 
 
         elif action.data['type'] == 'hive-document':
         elif action.data['type'] == 'hive-document':
           from notebook.models import Notebook
           from notebook.models import Notebook