소스 검색

HUE-1162 [oozie] Export oozie workflow

Add metadata to separate file and download as zip.
Abraham Elmahrek 12 년 전
부모
커밋
53e8edb

+ 3 - 0
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -71,6 +71,9 @@ ${ layout.menubar(section='workflows') }
           <li>
             <a id="clone-btn" href="#" data-clone-url="${ url('oozie:clone_workflow', workflow=workflow.id) }" title="${ _('Copy this workflow') }" rel="tooltip" data-placement="right"><i class="icon-copy"></i> ${ _('Copy') }</a>
           </li>
+          <li>
+            <a id="export-btn" href="${ url('oozie:export_workflow', workflow=workflow.id) }" title="${ _('Export this workflow') }" rel="tooltip" data-placement="right"><i class="icon-upload-alt"></i> ${ _('Export') }</a>
+          </li>
         % endif
       </ul>
     </div>

+ 10 - 1
apps/oozie/src/oozie/templates/editor/list_workflows.mako

@@ -46,6 +46,7 @@ ${ layout.menubar(section='workflows') }
         <button class="btn toolbarBtn" id="submit-btn" disabled="disabled"><i class="icon-play"></i> ${ _('Submit') }</button>
         <button class="btn toolbarBtn" id="schedule-btn" disabled="disabled"><i class="icon-calendar"></i> ${ _('Schedule') }</button>
         <button class="btn toolbarBtn" id="clone-btn" disabled="disabled"><i class="icon-copy"></i> ${ _('Copy') }</button>
+        <button class="btn toolbarBtn" id="export-btn" disabled="disabled"><i class="icon-upload-alt"></i> ${ _('Export') }</button>
         <div id="delete-dropdown" class="btn-group" style="vertical-align: middle">
           <button id="trash-btn" class="btn toolbarBtn" disabled="disabled"><i class="icon-remove"></i> ${_('Move to trash')}</button>
           <button id="trash-btn-caret" class="btn toolbarBtn dropdown-toggle" data-toggle="dropdown" disabled="disabled">
@@ -88,6 +89,7 @@ ${ layout.menubar(section='workflows') }
                   data-submit-url="${ url('oozie:submit_workflow', workflow=workflow.id) }"
                   data-schedule-url="${ url('oozie:schedule_workflow', workflow=workflow.id) }"
                   data-clone-url="${ url('oozie:clone_workflow', workflow=workflow.id) }"
+                  data-export-url="${ url('oozie:export_workflow', workflow=workflow.id) }"
               % endif
               % if workflow.is_editable(user):
                   data-delete-id="${ workflow.id }"
@@ -194,7 +196,8 @@ ${ layout.menubar(section='workflows') }
         var action_buttons = [
           ['#submit-btn', 'data-submit-url'],
           ['#schedule-btn', 'data-schedule-url'],
-          ['#clone-btn', 'data-clone-url']
+          ['#clone-btn', 'data-clone-url'],
+          ['#export-btn', 'data-export-url']
         ];
         $.each(action_buttons, function (index) {
           if (selector.attr(this[1])) {
@@ -251,6 +254,12 @@ ${ layout.menubar(section='workflows') }
       window.location.replace(_url);
     });
 
+    $("#export-btn").click(function (e) {
+      var _this = $(".hueCheckbox[checked='checked']");
+      var _url = _this.attr("data-export-url");
+      window.location.replace(_url);
+    });
+
     var oTable = $("#workflowTable").dataTable({
       "sPaginationType":"bootstrap",
       'iDisplayLength':50,

+ 94 - 1
apps/oozie/src/oozie/tests.py

@@ -20,6 +20,8 @@ import json
 import logging
 import re
 import os
+import StringIO
+import zipfile
 
 from itertools import chain
 
@@ -29,7 +31,7 @@ from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 
 from desktop.lib.django_test_util import make_logged_in_client
-from desktop.lib.test_utils import grant_access, add_permission, add_to_group
+from desktop.lib.test_utils import grant_access, add_permission, add_to_group, reformat_json, reformat_xml
 from desktop.models import Document
 
 from jobsub.models import OozieDesign, OozieMapreduceAction
@@ -1465,6 +1467,97 @@ class TestEditor(OozieMockBase):
     assert_equal(previous_available - 1, Document.objects.available_docs(Workflow, self.user).count())
 
 
+  def test_workflow_export(self):
+    response = self.c.get(reverse('oozie:export_workflow', args=[self.wf.id]))
+    zfile = zipfile.ZipFile(StringIO.StringIO(response.content))
+    assert_true('workflow.xml' in zfile.namelist(), 'workflow.xml not in response')
+    assert_true('workflow-metadata.json' in zfile.namelist(), 'workflow-metadata.json not in response')
+    assert_equal(2, len(zfile.namelist()))
+
+    workflow_xml = reformat_xml("""<workflow-app name="wf-name-1" xmlns="uri:oozie:workflow:0.2">
+    <global>
+        <job-xml>jobconf.xml</job-xml>
+        <configuration>
+            <property>
+                <name>sleep-all</name>
+                <value>${SLEEP}</value>
+            </property>
+        </configuration>
+    </global>
+    <start to="action-name-1"/>
+    <action name="action-name-1">
+        <map-reduce>
+            <job-tracker>${jobTracker}</job-tracker>
+            <name-node>${nameNode}</name-node>
+            <prepare>
+                <delete path="${nameNode}${output}"/>
+                <mkdir path="${nameNode}/test"/>
+            </prepare>
+            <configuration>
+                <property>
+                    <name>sleep</name>
+                    <value>${SLEEP}</value>
+                </property>
+            </configuration>
+        </map-reduce>
+        <ok to="action-name-2"/>
+        <error to="kill"/>
+    </action>
+    <action name="action-name-2">
+        <map-reduce>
+            <job-tracker>${jobTracker}</job-tracker>
+            <name-node>${nameNode}</name-node>
+            <prepare>
+                <delete path="${nameNode}${output}"/>
+                <mkdir path="${nameNode}/test"/>
+            </prepare>
+            <configuration>
+                <property>
+                    <name>sleep</name>
+                    <value>${SLEEP}</value>
+                </property>
+            </configuration>
+        </map-reduce>
+        <ok to="action-name-3"/>
+        <error to="kill"/>
+    </action>
+    <action name="action-name-3">
+        <map-reduce>
+            <job-tracker>${jobTracker}</job-tracker>
+            <name-node>${nameNode}</name-node>
+            <prepare>
+                <delete path="${nameNode}${output}"/>
+                <mkdir path="${nameNode}/test"/>
+            </prepare>
+            <configuration>
+                <property>
+                    <name>sleep</name>
+                    <value>${SLEEP}</value>
+                </property>
+            </configuration>
+        </map-reduce>
+        <ok to="end"/>
+        <error to="kill"/>
+    </action>
+    <kill name="kill">
+        <message>Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]</message>
+    </kill>
+    <end name="end"/>
+</workflow-app>""")
+    workflow_metadata_json = reformat_json("""{
+  "action-name-1": {
+    "jar_path": "/user/hue/oozie/examples/lib/hadoop-examples.jar"
+  },
+  "action-name-2": {
+    "jar_path": "/user/hue/oozie/examples/lib/hadoop-examples.jar"
+  },
+  "action-name-3": {
+    "jar_path": "/user/hue/oozie/examples/lib/hadoop-examples.jar"
+  }
+}""")
+    assert_equal(workflow_xml, reformat_xml(zfile.read('workflow.xml')))
+    assert_equal(workflow_metadata_json, reformat_json(zfile.read('workflow-metadata.json')))
+
 class TestEditorBundle(OozieMockBase):
 
   def setUp(self):

+ 1 - 0
apps/oozie/src/oozie/urls.py

@@ -34,6 +34,7 @@ urlpatterns = patterns(
   url(r'^submit_workflow/(?P<workflow>\d+)$', 'submit_workflow', name='submit_workflow'),
   url(r'^schedule_workflow/(?P<workflow>\d+)$', 'schedule_workflow', name='schedule_workflow'),
   url(r'^import_workflow/$', 'import_workflow', name='import_workflow'),
+  url(r'^export_workflow/(?P<workflow>\d+)$', 'export_workflow', name='export_workflow'),
 
   url(r'^list_coordinators/(?P<workflow_id>[-\w]+)?$', 'list_coordinators', name='list_coordinators'),
   url(r'^list_trashed_coordinators/$', 'list_trashed_coordinators', name='list_trashed_coordinators'),

+ 32 - 0
apps/oozie/src/oozie/views/editor.py

@@ -22,6 +22,9 @@ except ImportError:
   import simplejson as json
 import logging
 import shutil
+import StringIO
+import time
+import zipfile
 
 from django.core.urlresolvers import reverse
 from django.db.models import Q
@@ -29,7 +32,9 @@ from django.forms.formsets import formset_factory
 from django.forms.models import inlineformset_factory
 from django.http import HttpResponse
 from django.shortcuts import redirect
+from django.utils.encoding import smart_str
 from django.utils.functional import curry
+from django.utils.http import http_date
 from django.utils.translation import ugettext as _, activate as activate_translation
 
 from desktop.lib.django_util import render, extract_field_data
@@ -183,6 +188,33 @@ def import_workflow(request):
     'workflow': workflow,
   })
 
+@check_job_access_permission()
+def export_workflow(request, workflow):
+  parameters = workflow.find_all_parameters()
+  mapping = dict([(param['name'], param['value']) for param in parameters])
+  workflow_xml = workflow.to_xml(mapping)
+
+  zip_file = StringIO.StringIO()
+
+  metadata = {}
+  for node in workflow.node_list:
+    if hasattr(node, 'jar_path'):
+      metadata[node.name] = {}
+      metadata[node.name]['jar_path'] = node.jar_path
+  metadata_json = json.dumps(metadata)
+
+  zfile = zipfile.ZipFile(zip_file, 'w')
+  zfile.writestr("workflow.xml", smart_str(workflow_xml))
+  zfile.writestr("workflow-metadata.json", smart_str(metadata_json))
+  zfile.close()
+
+  response = HttpResponse(mimetype="application/zip")
+  response["Last-Modified"] = http_date(time.time())
+  response["Content-Length"] = len(zip_file.getvalue())
+  response['Content-Disposition'] = 'attachment; filename="workflow-%s-%d.zip"' % (workflow.name, workflow.id)
+  response.write(zip_file.getvalue())
+  return response
+
 @check_job_access_permission()
 def edit_workflow(request, workflow):
   history = History.objects.filter(submitter=request.user, job=workflow).order_by('-submission_date')

+ 17 - 0
desktop/core/src/desktop/lib/test_utils.py

@@ -15,6 +15,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
+from lxml import objectify, etree
+
 from django.contrib.auth.models import Group, User
 from useradmin.models import HuePermission, GroupPermission, get_default_user_group
 
@@ -42,3 +45,17 @@ def add_to_group(username, groupname=get_default_user_group().name):
     if not user.groups.filter(name=group.name).exists():
         user.groups.add(group)
         user.save()
+
+
+def reformat_json(json_obj):
+    if isinstance(json_obj, basestring):
+        return json.dumps(json.loads(json_obj))
+    else:
+        return json.dumps(json_obj)
+
+
+def reformat_xml(xml_obj):
+    if isinstance(xml_obj, basestring):
+        return etree.tostring(objectify.fromstring(xml_obj))
+    else:
+        return etree.tostring(xml_obj)