소스 검색

HUE-1330 [oozie] Make kill action target configurable

Abraham Elmahrek 12 년 전
부모
커밋
c517088

+ 5 - 13
apps/oozie/src/oozie/import_workflow.py

@@ -230,8 +230,7 @@ def _node_relationships(workflow, parent, child_el):
           raise RuntimeError(_("Node %s has a link that is missing 'start' attribute.") % parent.name)
         to = el.attrib['start']
         name = 'start'
-      elif name == 'error':
-        to = 'kill'
+
       else:
         if 'to' not in el.attrib:
           raise RuntimeError(_("Node %s has a link that is missing 'to' attribute.") % parent.name)
@@ -240,7 +239,10 @@ def _node_relationships(workflow, parent, child_el):
       try:
         child = Node.objects.get(workflow=workflow, name=to)
       except Node.DoesNotExist, e:
-        raise RuntimeError("Node %s has not been defined" % to)
+        if name == 'error':
+          child, create = Kill.objects.get_or_create(name='kill', workflow=workflow, node_type=Kill.node_type)
+        else:
+          raise RuntimeError("Node %s has not been defined" % to)
 
       obj = Link.objects.create(name=name, parent=parent, child=child)
       obj.save()
@@ -404,11 +406,6 @@ def _resolve_decision_relationships(workflow):
     # Assume receive full node.
     children = [link.child.get_full_node() for link in node.get_children_links().exclude(name__in=['error','default'])]
 
-    # Will not be a kill node because we skip error links.
-    # Error links should not go to a regular node.
-    if node.get_parent_links().filter(name='error').exists():
-      raise RuntimeError(_('Error links cannot point to an ordinary node.'))
-
     # Multiple parents means that we've found an end.
     # Joins will always have more than one parent.
     fan_in_count = len(node.get_parent_links().exclude(name__in=['error','default']))
@@ -549,11 +546,6 @@ def _save_nodes(workflow, nodes):
     except Node.DoesNotExist:
       node.save()
 
-  # Create kill node
-  # Only need it if we have a node to reference it with.
-  if len(nodes) > 2:
-    Kill.objects.create(name='kill', workflow=workflow, node_type=Kill.node_type)
-
 
 def import_workflow(workflow, workflow_definition, fs=None):
   xslt_definition_fh = open("%(xslt_dir)s/workflow.xslt" % {

+ 26 - 0
apps/oozie/src/oozie/templates/editor/action_utils.mako

@@ -43,6 +43,32 @@
 
           ${ utils.render_constant(_('Action type'), node_type) }
 
+          <div class="control-group ">
+            <label class="control-label">
+              <a href="javascript:void(0);" id="advanced-btn" onclick="$('#node-advanced-container').toggle('hide')">
+                <i class="icon-share-alt"></i> ${ _('advanced') }</a>
+            </label>
+            <div class="controls"></div>
+          </div>
+
+          <div id="node-advanced-container" class="hide">
+            <div class="control-group">
+              <label class="control-label">${_('Error link to')}</label>
+              <div class="controls">
+                <div style="padding-top:4px">
+                  <select data-bind="options: $root.context().nodes,
+                                     optionsText: function(item) {
+                                       return item.name();
+                                     },
+                                     optionsValue: function(item) {
+                                       return item.id();
+                                     },
+                                     value: $root.context().error_node"></select>
+                </div>
+              </div>
+            </div>
+          </div>
+
           <hr/>
 
           <div class="control-group">

+ 33 - 2
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -618,7 +618,35 @@ function edit_node_modal(node, save, cancel, template) {
 
   modal.hide();
   modal.setTemplate(template || node.edit_template);
-  modal.show({node: node, read_only: workflow.read_only()});
+  // Provide node, readonly mode, and error link updater.
+  // Kill node is manually added to list of nodes that users can select from.
+  // Kill node is placed at the front of the list so that it is automatically selected.
+  var context = {
+    node: node,
+    read_only: workflow.read_only(),
+    nodes: ko.computed({
+      read: function() {
+        var arr = ko.utils.arrayFilter(workflow.nodes(), function(value) {
+          return value.id() && value.id() != node.id();
+        });
+        arr.unshift(workflow.kill);
+        return arr;
+      }
+    }),
+    error_node: ko.computed({
+      read: function() {
+        var error_child  = node.getErrorChild();
+        return (error_child) ? error_child.id() : null;
+      },
+      write: function(node_id) {
+        var error_child = workflow.registry.get(node_id);
+        if (error_child) {
+          node.putErrorChild(error_child);
+        }
+      }
+    })
+  };
+  modal.show(context);
   modal.recenter(280, 250);
   modal.addDecorations();
 
@@ -693,7 +721,10 @@ workflow.el.on('mousedown', '.new-node-link', function(e) {
     if (node.validate()) {
       workflow.is_dirty( true );
       modal.hide();
-      node.addChild(workflow.kill);
+      if (!node.getErrorChild()) {
+        node.addChild(workflow.kill);
+      }
+      ko.cleanNode(modal.el[0]);
       workflow.el.trigger('workflow:rebuild');
     }
   };

+ 9 - 1
apps/oozie/src/oozie/templates/editor/jasmine.mako

@@ -4,7 +4,15 @@
   <script src="/static/ext/js/knockout.mapping-2.3.2.js" type="text/javascript" charset="utf-8"></script>
   <script src="/static/ext/js/moment.min.js" type="text/javascript" charset="utf-8"></script>
   <script src="/static/ext/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
-  <script src="static/js/workflow.js" type="text/javascript" charset="utf-8"></script>
+  <script type="text/javascript" src="static/js/workflow.utils.js"></script>
+  <script type="text/javascript" src="static/js/workflow.registry.js"></script>
+  <script type="text/javascript" src="static/js/workflow.modal.js"></script>
+  <script type="text/javascript" src="static/js/workflow.models.js"></script>
+  <script type="text/javascript" src="static/js/workflow.idgen.js"></script>
+  <script type="text/javascript" src="static/js/workflow.node-fields.js"></script>
+  <script type="text/javascript" src="static/js/workflow.node.js"></script>
+  <script type="text/javascript" src="static/js/workflow.js"></script>
+  <script type="text/javascript" src="static/js/workflow.import-node.js"></script>
   <script src="static/jasmine/workflow.js" type="text/javascript" charset="utf-8"></script>
 </%block>
 

+ 27 - 0
apps/oozie/src/oozie/test_data/0.4/test-java-different-error-links.xml

@@ -0,0 +1,27 @@
+<workflow-app name="Sequential Java" xmlns="uri:oozie:workflow:0.4">
+    <start to="TeraGenWorkflow"/>
+    <action name="TeraGenWorkflow">
+        <java>
+            <job-tracker>${jobTracker}</job-tracker>
+            <name-node>${nameNode}</name-node>
+            <main-class>org.apache.hadoop.examples.terasort.TeraGen</main-class>
+            <arg>${records}</arg>
+            <arg>${output_dir}/teragen</arg>
+            <capture-output/>
+        </java>
+        <ok to="TeraSort"/>
+        <error to="TeraSort"/>
+    </action>
+    <action name="TeraSort">
+        <java>
+            <job-tracker>${jobTracker}</job-tracker>
+            <name-node>${nameNode}</name-node>
+            <main-class>org.apache.hadoop.examples.terasort.TeraSort</main-class>
+            <arg>${output_dir}/teragen</arg>
+            <arg>${output_dir}/terasort</arg>
+        </java>
+        <ok to="end"/>
+        <error to="nonsense"/>
+    </action>
+    <end name="end"/>
+</workflow-app>

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

@@ -1813,7 +1813,7 @@ class TestImportWorkflow04(OozieMockBase):
     workflow.delete(skip_trash=True)
 
 
-  def test_import_multi_kill_node(self):
+  def test_import_workflow_multi_kill_node(self):
     """
     Validates import for multiple kill nodes: xml.
 
@@ -1838,6 +1838,32 @@ class TestImportWorkflow04(OozieMockBase):
     assert_false(nodes[1].capture_output)
     workflow.delete(skip_trash=True)
 
+  def test_import_workflow_different_error_link(self):
+    """
+    Validates import with error link to end: main_class, args.
+
+    If an error link cannot be resolved, default to 'kill' node.
+    """
+    workflow = Workflow.objects.new_workflow(self.user)
+    workflow.save()
+    f = open('apps/oozie/src/oozie/test_data/0.4/test-java-different-error-links.xml')
+    import_workflow(workflow, f.read())
+    f.close()
+    workflow.save()
+    assert_equal(5, len(Node.objects.filter(workflow=workflow)))
+    assert_equal(6, len(Link.objects.filter(parent__workflow=workflow)))
+    nodes = [Node.objects.filter(workflow=workflow, node_type='java')[0].get_full_node(),
+             Node.objects.filter(workflow=workflow, node_type='java')[1].get_full_node()]
+    assert_equal('org.apache.hadoop.examples.terasort.TeraGen', nodes[0].main_class)
+    assert_equal('${records} ${output_dir}/teragen', nodes[0].args)
+    assert_equal('org.apache.hadoop.examples.terasort.TeraSort', nodes[1].main_class)
+    assert_equal('${output_dir}/teragen ${output_dir}/terasort', nodes[1].args)
+    assert_true(nodes[0].capture_output)
+    assert_false(nodes[1].capture_output)
+    assert_equal(1, len(Link.objects.filter(parent__workflow=workflow).filter(parent__name='TeraGenWorkflow').filter(name='error').filter(child__node_type='java')))
+    assert_equal(1, len(Link.objects.filter(parent__workflow=workflow).filter(parent__name='TeraSort').filter(name='error').filter(child__node_type='kill')))
+    workflow.delete(skip_trash=True)
+
 
 class TestPermissions(OozieBase):
 

+ 145 - 132
apps/oozie/static/jasmine/workflow.js

@@ -20,98 +20,96 @@ describe("WorkflowModel", function(){
 
   function create_three_step_workflow(workflow_id) {
     var workflow_model = new WorkflowModel({
-      id: 1,
-      name: "Test-Three-Step-Workflow",
-      start: 1,
-      end: 5
-    });
-    var registry = new Registry();
-    var workflow = new Workflow({
-      model: workflow_model,
-      data: {
-        "nodes":[{
-            "description":"",
-            "workflow":workflow_id,
-            "child_links":[{
-                "comment":"",
-                "name":"to",
-                "parent":1,
-                "child":2
-              },{
-                "comment":"",
-                "name":"related",
-                "parent":1,
-                "child":5
-            }],
-            "node_type":"start",
-            "id":1,
-            "name":"start"
-          },{
-            "id":2,
-            "name":"Sleep-1",
-            "workflow":workflow_id,
-            "node_type":"mapreduce",
-            "jar_path":"/user/hue/oozie/workspaces/lib/hadoop-examples.jar",
-            "child_links":[{
-                "comment":"",
-                "name":"ok",
-                "parent":2,
-                "child":3
-              },{
-                "comment":"",
-                "name":"error",
-                "parent":2,
-                "child":6
-              }],
-        },{
-          "id":3,
-          "name":"Sleep-2",
+      "id": workflow_id,
+      "name": "Test-Three-Step-Workflow",
+      "start": 1,
+      "end": 5,
+      "nodes":[{
+          "description":"",
           "workflow":workflow_id,
-          "node_type":"mapreduce",
-          "jar_path":"/user/hue/oozie/workspaces/lib/hadoop-examples.jar",
           "child_links":[{
               "comment":"",
-              "name":"ok",
-              "parent":3,
-              "child":4
+              "name":"to",
+              "parent":1,
+              "child":2
             },{
               "comment":"",
-              "name":"error",
-              "parent":3,
-              "child":6
-            }],
+              "name":"related",
+              "parent":1,
+              "child":5
+          }],
+          "node_type":"start",
+          "id":1,
+          "name":"start"
         },{
-          "id":4,
-          "name":"Sleep-3",
+          "id":2,
+          "name":"Sleep-1",
           "workflow":workflow_id,
           "node_type":"mapreduce",
           "jar_path":"/user/hue/oozie/workspaces/lib/hadoop-examples.jar",
           "child_links":[{
               "comment":"",
               "name":"ok",
-              "parent":4,
-              "child":5
+              "parent":2,
+              "child":3
             },{
               "comment":"",
               "name":"error",
-              "parent":4,
+              "parent":2,
               "child":6
             }],
-        },{
-          "id":6,
-          "name":"kill",
-          "workflow":workflow_id,
-          "node_type":"kill",
-          "child_links":[],
-          "message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
-        },{
-          "id":5,
-          "name":"end",
-          "workflow":workflow_id,
-          "node_type":"end",
-          "child_links":[],
-        }],
-      },
+      },{
+        "id":3,
+        "name":"Sleep-2",
+        "workflow":workflow_id,
+        "node_type":"mapreduce",
+        "jar_path":"/user/hue/oozie/workspaces/lib/hadoop-examples.jar",
+        "child_links":[{
+            "comment":"",
+            "name":"ok",
+            "parent":3,
+            "child":4
+          },{
+            "comment":"",
+            "name":"error",
+            "parent":3,
+            "child":6
+          }],
+      },{
+        "id":4,
+        "name":"Sleep-3",
+        "workflow":workflow_id,
+        "node_type":"mapreduce",
+        "jar_path":"/user/hue/oozie/workspaces/lib/hadoop-examples.jar",
+        "child_links":[{
+            "comment":"",
+            "name":"ok",
+            "parent":4,
+            "child":5
+          },{
+            "comment":"",
+            "name":"error",
+            "parent":4,
+            "child":6
+          }],
+      },{
+        "id":6,
+        "name":"kill",
+        "workflow":workflow_id,
+        "node_type":"kill",
+        "child_links":[],
+        "message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
+      },{
+        "id":5,
+        "name":"end",
+        "workflow":workflow_id,
+        "node_type":"end",
+        "child_links":[],
+      }],
+    });
+    var registry = new Registry();
+    var workflow = new Workflow({
+      model: workflow_model,
       registry: registry
     });
     return workflow;
@@ -119,77 +117,75 @@ describe("WorkflowModel", function(){
 
   function create_pig_workflow(workflow_id) {
     var workflow_model = new WorkflowModel({
-      id: 1,
-      name: "Test-pig-Workflow",
-      start: 1,
-      end: 5
-    });
-    var registry = new Registry();
-    var workflow = new Workflow({
-      model: workflow_model,
-      data: {
-        "nodes":[{
-            "description":"",
-            "workflow":workflow_id,
-            "child_links":[{
-                "comment":"",
-                "name":"to",
-                "parent":1,
-                "child":2
-              },{
-                "comment":"",
-                "name":"related",
-                "parent":1,
-                "child":5
-            }],
-            "node_type":"start",
-            "id":1,
-            "name":"start"
-          },{
-            "id":2,
-            "name":"Pig-1",
-            "workflow":workflow_id,
-            "node_type":"pig",
-            "script_path":"test.pig",
-            "child_links":[{
-                "comment":"",
-                "name":"ok",
-                "parent":2,
-                "child":3
-              },{
-                "comment":"",
-                "name":"error",
-                "parent":2,
-                "child":4
-              }],
-        },{
-          "id":4,
-          "name":"kill",
+      "id": workflow_id,
+      "name": "Test-pig-Workflow",
+      "start": 1,
+      "end": 5,
+      "nodes":[{
+          "description":"",
           "workflow":workflow_id,
-          "node_type":"kill",
-          "child_links":[],
-          "message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
+          "child_links":[{
+              "comment":"",
+              "name":"to",
+              "parent":1,
+              "child":2
+            },{
+              "comment":"",
+              "name":"related",
+              "parent":1,
+              "child":5
+          }],
+          "node_type":"start",
+          "id":1,
+          "name":"start"
         },{
-          "id":3,
-          "name":"end",
+          "id":2,
+          "name":"Pig-1",
           "workflow":workflow_id,
-          "node_type":"end",
-          "child_links":[],
-        }],
-      },
+          "node_type":"pig",
+          "script_path":"test.pig",
+          "child_links":[{
+              "comment":"",
+              "name":"ok",
+              "parent":2,
+              "child":3
+            },{
+              "comment":"",
+              "name":"error",
+              "parent":2,
+              "child":4
+            }],
+      },{
+        "id":4,
+        "name":"kill",
+        "workflow":workflow_id,
+        "node_type":"kill",
+        "child_links":[],
+        "message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]",
+      },{
+        "id":3,
+        "name":"end",
+        "workflow":workflow_id,
+        "node_type":"end",
+        "child_links":[],
+      }],
+    })
+    var registry = new Registry();
+    var workflow = new Workflow({
+      model: workflow_model,
       registry: registry
     });
     return workflow;
   }
 
   describe("Workflow operations", function(){
-    var json = '{"id":1,"name":"Test-pig-Workflow","start":1,"end":5,"description":"","schema_version":0.4,"deployment_dir":"","is_shared":true,"parameters":"[]","job_xml":"","nodes":[{"description":"","workflow":1,"child_links":[{"comment":"","name":"to","parent":1,"child":2},{"comment":"","name":"related","parent":1,"child":5}],"node_type":"start","id":1,"name":"start"},{"id":2,"name":"Pig-1","workflow":1,"node_type":"pig","script_path":"test.pig","child_links":[{"comment":"","name":"ok","parent":2,"child":3},{"comment":"","name":"error","parent":2,"child":4}],"description":"","files":"[]","archives":"[]","job_properties":"[]","prepares":"[]","job_xml":"","params":"[]"},{"id":4,"name":"kill","workflow":1,"node_type":"kill","child_links":[],"message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]","description":""},{"id":3,"name":"end","workflow":1,"node_type":"end","child_links":[],"description":""}]}';
+    var json = '{"id":1,"name":"Test-pig-Workflow","start":1,"end":5,"nodes":[{"description":"","workflow":1,"child_links":[{"comment":"","name":"to","parent":1,"child":2},{"comment":"","name":"related","parent":1,"child":5}],"node_type":"start","id":1,"name":"start"},{"id":2,"name":"Pig-1","workflow":1,"node_type":"pig","script_path":"test.pig","child_links":[{"comment":"","name":"ok","parent":2,"child":3},{"comment":"","name":"error","parent":2,"child":4}],"description":"","files":"[]","archives":"[]","job_properties":"[]","prepares":"[]","job_xml":"","params":"[]"},{"id":3,"name":"end","workflow":1,"node_type":"end","child_links":[],"description":""},{"id":4,"name":"kill","workflow":1,"node_type":"kill","child_links":[],"message":"Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]","description":""}],"parameters":[],"description":"","schema_version":0.4,"deployment_dir":"","is_shared":true,"job_xml":""}';
     var node = null;
     var viewModel = create_pig_workflow(1);
     viewModel.rebuild();
 
     it("Ensure serialized data sent to server is proper", function() {
-      expect(viewModel.toJSON()).toEqual(json);
+      expect(viewModel.toJSON()).toEqual(ko.toJSON($.parseJSON(json)));
     });
 
     it("Ensure data received from server can be loaded", function() {
@@ -231,6 +227,23 @@ describe("WorkflowModel", function(){
       viewModel.rebuild();
       expect(viewModel.nodes().length).toEqual(5);
     });
+
+    it("Should be able to add error node", function() {
+      viewModel.nodes()[1].putErrorChild(viewModel.nodes()[2]);
+      viewModel.rebuild();
+      expect(viewModel.nodes().length).toEqual(5);
+      expect(viewModel.nodes()[1].meta_links().length).toEqual(1);
+      expect(viewModel.nodes()[1].meta_links()[0].name()).toEqual("error");
+      expect(viewModel.nodes()[1].meta_links()[0].parent()).toEqual(viewModel.nodes()[1].id());
+      expect(viewModel.nodes()[1].meta_links()[0].child()).toEqual(viewModel.nodes()[2].id());
+    });
+
+    it("Should be able to get error node", function() {
+      viewModel.nodes()[1].putErrorChild(viewModel.nodes()[2]);
+      viewModel.rebuild();
+      expect(viewModel.nodes().length).toEqual(5);
+      expect(viewModel.nodes()[1].getErrorChild().id()).toEqual(viewModel.nodes()[2].id());
+    });
   });
 
   // describe("Node movement", function(){

+ 1 - 1
apps/oozie/static/js/workflow.js

@@ -289,7 +289,7 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
     // @see http://knockoutjs.com/documentation/plugins-mapping.html
     var mapping = ko.mapping.fromJS(options.model, {
-      ignore: ['initialize', 'toString', 'copy'],
+      ignore: ['initialize', 'toString', 'copy', 'nodes'],
       job_properties: {
         create: function(options) {
           var parent = options.parent;

+ 84 - 0
apps/oozie/static/js/workflow.node.js

@@ -357,6 +357,31 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
       return spliceIndex != -1;
     },
 
+    /**
+     * Remove error child
+     * 1. Find child node link
+     * 2. Remove child node link
+     */
+    removeErrorChildren: function() {
+      var self = this;
+      var spliceIndexes = [];
+
+      $.each(self.child_links(), function(index, link) {
+        if (link.name() == 'error') {
+          spliceIndexes.push(index);
+        }
+      });
+
+      var spliceCount = 0;
+      if (spliceIndexes.length > 0) {
+        $.each(spliceIndexes, function(index, spliceIndex) {
+          self.child_links.splice(spliceIndex - spliceCount++, 1);
+        });
+      }
+
+      return spliceIndexes.length > 0;
+    },
+
     /**
      * Remove all children
      * Removes all children except for related, default, and error links
@@ -416,6 +441,65 @@ var NodeModule = function($, IdGeneratorTable, NodeFields) {
       return index != -1;
     },
 
+    /**
+     * Replace or add error node with another node in the following way:
+     * 1. Find child index
+     * 2. Remove child index
+     * 3. Remove and remember every element after child
+     * 4. Add replacement node
+     * 5. Add every child that was remembered
+     */
+    putErrorChild: function(node) {
+      var self = this;
+      var index = -1;
+
+      $.each(self.child_links(), function(i, link) {
+        if (link.name() == 'error') {
+          index = i;
+        }
+      });
+
+      var link = {
+        parent: ko.observable(self.id()),
+        child: ko.observable(node.id()),
+        name: ko.observable('error'),
+        comment: ko.observable('')
+      };
+
+      if (index > -1) {
+        var child_links = self.child_links();
+        child_links.splice(index, 1);
+        var links = child_links.splice(index);
+        child_links.push(link);
+
+        $.each(links, function(index, link) {
+          child_links.push(link);
+        });
+
+        self.child_links(child_links);
+      } else {
+        self.child_links.push(link);
+      }
+
+      return index != -1;
+    },
+
+    /**
+     * Get the error child
+     */
+    getErrorChild: function() {
+      var self = this;
+
+      var children = [];
+      $.each(self.meta_links(), function(index, link) {
+        if (link.name() == 'error') {
+          children.push(self.registry.get(link.child()));
+        }
+      });
+
+      return (children.length > 0) ? children[0] : null;
+    },
+
     isChild: function(node) {
       var self = this;
       var res = false;