Browse Source

HUE-883 [oozie] Add Generic action

Accept any XML action and insert it verbatim into the Oozie action.
Fix Fs example
Add example
Add test
Manually ran example
Fix kill popups on Dashboards
Improve rerun workflow popup
Fix rerun of workflows started by a coordinator
Romain Rigaux 13 years ago
parent
commit
ee92c4f

+ 0 - 0
apps/oozie/examples/generic/empty


+ 203 - 62
apps/oozie/src/oozie/fixtures/initial_oozie_examples.json

@@ -140,7 +140,21 @@
     }
   },
   {
-    "pk": 17,
+    "pk": 18,
+    "model": "oozie.job",
+    "fields": {
+      "is_shared": true,
+      "name": "Email",
+      "parameters": "[{\"name\": \"oozie.use.system.libpath\", \"value\": \"true\"}]",
+      "deployment_dir": "/user/hue/oozie/workspaces/email",
+      "schema_version": "uri:oozie:workflow:0.4",
+      "last_modified": "2012-12-01 00:00:00",
+      "owner": 1100713,
+      "description": "Example of Email action"
+    }
+  },
+  {
+    "pk": 19,
     "model": "oozie.job",
     "fields": {
       "is_shared": true,
@@ -150,21 +164,21 @@
       "schema_version": "uri:oozie:workflow:0.4",
       "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
-      "description": "Example of Fs actions"
+      "description": "Example of sequential Fs actions"
     }
   },
   {
-    "pk": 18,
+    "pk": 20,
     "model": "oozie.job",
     "fields": {
       "is_shared": true,
-      "name": "Email",
+      "name": "Generic",
       "parameters": "[{\"name\": \"oozie.use.system.libpath\", \"value\": \"true\"}]",
-      "deployment_dir": "/user/hue/oozie/workspaces/email",
+      "deployment_dir": "/user/hue/oozie/workspaces/generic",
       "schema_version": "uri:oozie:workflow:0.4",
       "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
-      "description": "Example of Email action"
+      "description": "Execute a generic action with a custom extension"
     }
   },
   {
@@ -267,24 +281,35 @@
     }
   },
   {
-    "pk": 17,
+    "pk": 18,
     "model": "oozie.workflow",
     "fields": {
       "job_xml": "",
-      "start": 75,
+      "start": 79,
       "is_single": false,
-      "end": 74,
+      "end": 78,
       "job_properties": "[]"
     }
   },
   {
-    "pk": 18,
+    "pk": 19,
     "model": "oozie.workflow",
     "fields": {
       "job_xml": "",
-      "start": 79,
+      "start": 83,
       "is_single": false,
-      "end": 78,
+      "end": 82,
+      "job_properties": "[]"
+    }
+  },
+  {
+    "pk": 20,
+    "model": "oozie.workflow",
+    "fields": {
+      "job_xml": "",
+      "start": 87,
+      "is_single": false,
+      "end": 86,
       "job_properties": "[]"
     }
   },
@@ -829,73 +854,123 @@
     }
   },
   {
-    "pk": 96,
+    "pk": 99,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "to",
-      "parent": 75,
-      "child": 76
+      "parent": 79,
+      "child": 80
     }
   },
   {
-    "pk": 97,
+    "pk": 100,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "related",
-      "parent": 75,
-      "child": 74
+      "parent": 79,
+      "child": 78
     }
   },
   {
-    "pk": 98,
+    "pk": 101,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "ok",
-      "parent": 76,
-      "child": 74
+      "parent": 80,
+      "child": 78
     }
   },
   {
-    "pk": 99,
+    "pk": 102,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "error",
+      "parent": 80,
+      "child": 77
+    }
+  },
+  {
+    "pk": 103,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "to",
-      "parent": 79,
-      "child": 80
+      "parent": 83,
+      "child": 84
     }
   },
   {
-    "pk": 100,
+    "pk": 104,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "related",
-      "parent": 79,
-      "child": 78
+      "parent": 83,
+      "child": 82
     }
   },
   {
-    "pk": 101,
+    "pk": 105,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "ok",
-      "parent": 80,
-      "child": 78
+      "parent": 84,
+      "child": 82
     }
   },
   {
-    "pk": 102,
+    "pk": 106,
     "model": "oozie.link",
     "fields": {
       "comment": "",
       "name": "error",
-      "parent": 80,
-      "child": 77
+      "parent": 84,
+      "child": 81
+    }
+  },
+  {
+    "pk": 107,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "to",
+      "parent": 87,
+      "child": 88
+    }
+  },
+  {
+    "pk": 108,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "related",
+      "parent": 87,
+      "child": 86
+    }
+  },
+  {
+    "pk": 109,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "ok",
+      "parent": 88,
+      "child": 86
+    }
+  },
+  {
+    "pk": 110,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "error",
+      "parent": 88,
+      "child": 85
     }
   },
   {
@@ -1349,83 +1424,123 @@
     }
   },
   {
-    "pk": 73,
+    "pk": 77,
     "model": "oozie.node",
     "fields": {
       "node_type": "kill",
-      "workflow": 17,
+      "workflow": 18,
       "name": "kill",
       "description": ""
     }
   },
   {
-    "pk": 74,
+    "pk": 78,
     "model": "oozie.node",
     "fields": {
       "node_type": "end",
-      "workflow": 17,
+      "workflow": 18,
       "name": "end",
       "description": ""
     }
   },
   {
-    "pk": 75,
+    "pk": 79,
     "model": "oozie.node",
     "fields": {
       "node_type": "start",
-      "workflow": 17,
+      "workflow": 18,
       "name": "start",
       "description": ""
     }
   },
   {
-    "pk": 76,
+    "pk": 80,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "email",
+      "workflow": 18,
+      "name": "Email",
+      "description": "Send an email"
+    }
+  },
+  {
+    "pk": 81,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "kill",
+      "workflow": 19,
+      "name": "kill",
+      "description": ""
+    }
+  },
+  {
+    "pk": 82,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "end",
+      "workflow": 19,
+      "name": "end",
+      "description": ""
+    }
+  },
+  {
+    "pk": 83,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "start",
+      "workflow": 19,
+      "name": "start",
+      "description": ""
+    }
+  },
+  {
+    "pk": 84,
     "model": "oozie.node",
     "fields": {
       "node_type": "fs",
-      "workflow": 17,
-      "name": "ManageFiles",
+      "workflow": 19,
+      "name": "Fs",
       "description": ""
     }
   },
   {
-    "pk": 77,
+    "pk": 85,
     "model": "oozie.node",
     "fields": {
       "node_type": "kill",
-      "workflow": 18,
+      "workflow": 20,
       "name": "kill",
       "description": ""
     }
   },
   {
-    "pk": 78,
+    "pk": 86,
     "model": "oozie.node",
     "fields": {
       "node_type": "end",
-      "workflow": 18,
+      "workflow": 20,
       "name": "end",
       "description": ""
     }
   },
   {
-    "pk": 79,
+    "pk": 87,
     "model": "oozie.node",
     "fields": {
       "node_type": "start",
-      "workflow": 18,
+      "workflow": 20,
       "name": "start",
       "description": ""
     }
   },
   {
-    "pk": 80,
+    "pk": 88,
     "model": "oozie.node",
     "fields": {
-      "node_type": "email",
-      "workflow": 18,
-      "name": "Email",
-      "description": "Send an email"
+      "node_type": "generic",
+      "workflow": 20,
+      "name": "GenericFs",
+      "description": ""
     }
   },
   {
@@ -1611,7 +1726,7 @@
     }
   },
   {
-    "pk": 76,
+    "pk": 84,
     "model": "oozie.fs",
     "fields": {
       "touchzs": "[{\"name\": \"${output}/testfs/new_file\"}]",
@@ -1631,6 +1746,13 @@
       "subject": "I love"
     }
   },
+  {
+    "pk": 88,
+    "model": "oozie.generic",
+    "fields": {
+      "xml": "<fs>\n    <mkdir path='${nameNode}${full_path}'/>\n</fs> "
+    }
+  },
   {
     "pk": 18,
     "model": "oozie.start",
@@ -1695,17 +1817,24 @@
     }
   },
   {
-    "pk": 75,
+    "pk": 79,
     "model": "oozie.start",
     "fields": {
-      "node_ptr": 75
+      "node_ptr": 79
     }
   },
   {
-    "pk": 79,
+    "pk": 83,
     "model": "oozie.start",
     "fields": {
-      "node_ptr": 79
+      "node_ptr": 83
+    }
+  },
+  {
+    "pk": 87,
+    "model": "oozie.start",
+    "fields": {
+      "node_ptr": 87
     }
   },
   {
@@ -1754,12 +1883,17 @@
     "fields": {}
   },
   {
-    "pk": 74,
+    "pk": 78,
     "model": "oozie.end",
     "fields": {}
   },
   {
-    "pk": 78,
+    "pk": 82,
+    "model": "oozie.end",
+    "fields": {}
+  },
+  {
+    "pk": 86,
     "model": "oozie.end",
     "fields": {}
   },
@@ -1827,14 +1961,21 @@
     }
   },
   {
-    "pk": 73,
+    "pk": 77,
     "model": "oozie.kill",
     "fields": {
       "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]"
     }
   },
   {
-    "pk": 77,
+    "pk": 81,
+    "model": "oozie.kill",
+    "fields": {
+      "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]"
+    }
+  },
+  {
+    "pk": 85,
     "model": "oozie.kill",
     "fields": {
       "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]"

+ 24 - 9
apps/oozie/src/oozie/forms.py

@@ -26,7 +26,7 @@ from django.utils.translation import ugettext_lazy as _t
 from desktop.lib.django_forms import MultiForm, SplitDateTimeWidget
 from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
-  Email, SubWorkflow
+  Email, SubWorkflow, Generic
 
 LOG = logging.getLogger(__name__)
 
@@ -35,18 +35,23 @@ class ParameterForm(forms.Form):
   name = forms.CharField(max_length=40, widget=forms.widgets.HiddenInput())
   value = forms.CharField(max_length=100, required=False)
 
-  NON_PARAMETERS = ('user.name',
-                    'oozie.wf.rerun.failnodes',
-                    'oozie.wf.rerun.skip.nodes',
-                    'oozie.wf.application.path',
-                    'jobTracker',
-                    'nameNode',
-                    'hue-id-w')
+  NON_PARAMETERS = (
+      'user.name',
+      'oozie.wf.rerun.failnodes',
+      'oozie.wf.rerun.skip.nodes',
+      'oozie.wf.application.path',
+      'oozie.coord.application.path',
+      'mapreduce.job.user.name',
+      'wf_application_path',
+      'jobTracker',
+      'nameNode',
+      'hue-id-w',
+      'hue-id-c',
+  )
 
   @staticmethod
   def get_initial_params(conf_dict):
     params = filter(lambda key: key not in ParameterForm.NON_PARAMETERS, conf_dict.keys())
-
     return [{'name': name, 'value': conf_dict[name]} for name in params]
 
 
@@ -274,6 +279,15 @@ class SubWorkflowForm(forms.ModelForm):
       raise ValidationError(_('The sub-workflow could not be found: %s' % e))
 
 
+class GenericForm(forms.ModelForm):
+  class Meta:
+    model = Generic
+    exclude = NodeForm.Meta.ALWAYS_HIDE
+    widgets = {
+      'xml': forms.Textarea(attrs={'class': 'span8'})
+    }
+
+
 class LinkForm(forms.ModelForm):
   comment = forms.CharField(label='if', max_length=1024, required=True, widget=forms.TextInput(attrs={'class': 'span8'}))
 
@@ -396,6 +410,7 @@ _node_type_TO_FORM_CLS = {
   Fs.node_type: FsForm,
   Email.node_type: EmailForm,
   SubWorkflow.node_type: SubWorkflowForm,
+  Generic.node_type: GenericForm,
 }
 
 

+ 300 - 0
apps/oozie/src/oozie/migrations/0013_auto__add_generic.py

@@ -0,0 +1,300 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+
+        # Adding model 'Generic'
+        db.create_table('oozie_generic', (
+            ('xml', self.gf('django.db.models.fields.TextField')(default='')),
+            ('node_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['oozie.Node'], unique=True, primary_key=True)),
+        ))
+        db.send_create_signal('oozie', ['Generic'])
+
+
+    def backwards(self, orm):
+
+        # Deleting model 'Generic'
+        db.delete_table('oozie_generic')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'oozie.coordinator': {
+            'Meta': {'object_name': 'Coordinator', '_ormbases': ['oozie.Job']},
+            'concurrency': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'end': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 12, 16, 17, 37, 55, 517589)'}),
+            'execution': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'frequency_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+            'frequency_unit': ('django.db.models.fields.CharField', [], {'default': "'days'", 'max_length': '20'}),
+            'job_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Job']", 'unique': 'True', 'primary_key': 'True'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 12, 13, 17, 37, 55, 517558)'}),
+            'throttle': ('django.db.models.fields.PositiveSmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'timeout': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'America/Los_Angeles'", 'max_length': '24'}),
+            'workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']", 'null': 'True'})
+        },
+        'oozie.datainput': {
+            'Meta': {'object_name': 'DataInput'},
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'dataset': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Dataset']", 'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'})
+        },
+        'oozie.dataoutput': {
+            'Meta': {'object_name': 'DataOutput'},
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'dataset': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Dataset']", 'unique': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'})
+        },
+        'oozie.dataset': {
+            'Meta': {'object_name': 'Dataset'},
+            'coordinator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Coordinator']"}),
+            'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'done_flag': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '64', 'blank': 'True'}),
+            'frequency_number': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+            'frequency_unit': ('django.db.models.fields.CharField', [], {'default': "'days'", 'max_length': '20'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 12, 13, 17, 37, 55, 518148)'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'America/Los_Angeles'", 'max_length': '24'}),
+            'uri': ('django.db.models.fields.CharField', [], {'default': "'/data/${YEAR}${MONTH}${DAY}'", 'max_length': '1024'})
+        },
+        'oozie.decision': {
+            'Meta': {'object_name': 'Decision'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.distcp': {
+            'Meta': {'object_name': 'DistCp'},
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.email': {
+            'Meta': {'object_name': 'Email'},
+            'body': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'cc': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'subject': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'to': ('django.db.models.fields.TextField', [], {'default': "''"})
+        },
+        'oozie.end': {
+            'Meta': {'object_name': 'End'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.fork': {
+            'Meta': {'object_name': 'Fork'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.fs': {
+            'Meta': {'object_name': 'Fs'},
+            'chmods': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'deletes': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'mkdirs': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'moves': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'touchzs': ('django.db.models.fields.TextField', [], {'default': "'[]'", 'blank': 'True'})
+        },
+        'oozie.generic': {
+            'Meta': {'object_name': 'Generic'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'xml': ('django.db.models.fields.TextField', [], {'default': "''"})
+        },
+        'oozie.history': {
+            'Meta': {'object_name': 'History'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Job']"}),
+            'oozie_job_id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'properties': ('django.db.models.fields.TextField', [], {}),
+            'submission_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'submitter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'oozie.hive': {
+            'Meta': {'object_name': 'Hive'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': '\'[{"name":"oozie.hive.defaults","value":"hive-site.xml"}]\''}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        'oozie.java': {
+            'Meta': {'object_name': 'Java'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'args': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'blank': 'True'}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'jar_path': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'java_opts': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'main_class': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.job': {
+            'Meta': {'object_name': 'Job'},
+            'deployment_dir': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_shared': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True', 'blank': 'True'}),
+            'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'parameters': ('django.db.models.fields.TextField', [], {'default': '\'[{"name":"oozie.use.system.libpath","value":"true"}]\''}),
+            'schema_version': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+        },
+        'oozie.join': {
+            'Meta': {'object_name': 'Join'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.kill': {
+            'Meta': {'object_name': 'Kill'},
+            'message': ('django.db.models.fields.CharField', [], {'default': "'Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]'", 'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'})
+        },
+        'oozie.link': {
+            'Meta': {'object_name': 'Link'},
+            'child': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'parent_node'", 'to': "orm['oozie.Node']"}),
+            'comment': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'child_node'", 'to': "orm['oozie.Node']"})
+        },
+        'oozie.mapreduce': {
+            'Meta': {'object_name': 'Mapreduce'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'jar_path': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True'}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.node': {
+            'Meta': {'object_name': 'Node'},
+            'children': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'parents'", 'symmetrical': 'False', 'through': "orm['oozie.Link']", 'to': "orm['oozie.Node']"}),
+            'description': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '1024', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+            'node_type': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
+            'workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']"})
+        },
+        'oozie.pig': {
+            'Meta': {'object_name': 'Pig'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.CharField', [], {'max_length': '256'})
+        },
+        'oozie.shell': {
+            'Meta': {'object_name': 'Shell'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'capture_output': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'command': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"})
+        },
+        'oozie.sqoop': {
+            'Meta': {'object_name': 'Sqoop'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'prepares': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'script_path': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'})
+        },
+        'oozie.ssh': {
+            'Meta': {'object_name': 'Ssh'},
+            'capture_output': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'command': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'host': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'params': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'user': ('django.db.models.fields.CharField', [], {'max_length': '64'})
+        },
+        'oozie.start': {
+            'Meta': {'object_name': 'Start'},
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True'})
+        },
+        'oozie.streaming': {
+            'Meta': {'object_name': 'Streaming'},
+            'archives': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'files': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'mapper': ('django.db.models.fields.CharField', [], {'max_length': '512'}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'reducer': ('django.db.models.fields.CharField', [], {'max_length': '512'})
+        },
+        'oozie.subworkflow': {
+            'Meta': {'object_name': 'SubWorkflow'},
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'node_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Node']", 'unique': 'True', 'primary_key': 'True'}),
+            'propagate_configuration': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+            'sub_workflow': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['oozie.Workflow']"})
+        },
+        'oozie.workflow': {
+            'Meta': {'object_name': 'Workflow', '_ormbases': ['oozie.Job']},
+            'end': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'end_workflow'", 'null': 'True', 'to': "orm['oozie.End']"}),
+            'is_single': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+            'job_properties': ('django.db.models.fields.TextField', [], {'default': "'[]'"}),
+            'job_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['oozie.Job']", 'unique': 'True', 'primary_key': 'True'}),
+            'job_xml': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
+            'start': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'start_workflow'", 'null': 'True', 'to': "orm['oozie.Start']"})
+        }
+    }
+
+    complete_apps = ['oozie']

+ 20 - 2
apps/oozie/src/oozie/models.py

@@ -473,6 +473,8 @@ class Node(models.Model):
       node = self.streaming
     elif self.node_type == Java.node_type:
       node = self.java
+    elif self.node_type == Generic.node_type:
+      node = self.generic
     elif self.node_type == Start.node_type:
       node = self.start
     elif self.node_type == End.node_type:
@@ -578,6 +580,8 @@ class Action(Node):
 #  - Action.types below
 #  - Node.get_full_node()
 #  - forms.py _node_type_TO_FORM_CLS
+#  - workflow.js
+#  - maybe actions_utils.mako
 
 class Mapreduce(Action):
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'jar_path', 'prepares')
@@ -949,8 +953,21 @@ class SubWorkflow(Action):
     return json.loads(self.job_properties)
 
 
+class Generic(Action):
+  PARAM_FIELDS = ('xml',)
+  node_type = 'generic'
+
+  xml = models.TextField(default='', verbose_name=_t('XML of the custom action'),
+                         help_text=_t('This will be inserted verbatim in the action %(action)s. '
+                                      'e.g. all the XML content like %(xml_action)s '
+                                      'will be inserted into the action and produce %(full_action)s') % {
+                                      'action': '&lt;action name="email"&gt;...&lt;/action&gt;',
+                                      'xml_action': '&lt;email&gt;&lt;cc&gt;hue@hue.org&lt;/cc&gt;&lt;/email&gt;',
+                                      'full_action': '&lt;action name="email"&gt;&lt;email&gt;&lt;cc&gt;hue@hue.org&lt;/cc&gt;&lt;/email&gt;&lt;ok/&gt;&lt;error/&gt;&lt;/action&gt;'})
+
+
 Action.types = (Mapreduce.node_type, Streaming.node_type, Java.node_type, Pig.node_type, Hive.node_type, Sqoop.node_type, Ssh.node_type, Shell.node_type,
-                DistCp.node_type, Fs.node_type, Email.node_type, SubWorkflow.node_type)
+                DistCp.node_type, Fs.node_type, Email.node_type, SubWorkflow.node_type, Generic.node_type)
 
 
 class ControlFlow(Node):
@@ -1009,7 +1026,7 @@ class Fork(ControlFlow):
 
     # Defaults to end
     if not has_default:
-      link = Links.objects.create(name="default", parent=decision, child=self.workflow.end)
+      link = Link.objects.create(name="default", parent=decision, child=self.workflow.end)
       link.save()
 
     self.delete()
@@ -1420,6 +1437,7 @@ ACTION_TYPES = {
   Fs.node_type: Fs,
   Email.node_type: Email,
   SubWorkflow.node_type: SubWorkflow,
+  Generic.node_type: Generic,
 }
 
 NODE_TYPES = ACTION_TYPES.copy()

+ 2 - 2
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinator.mako

@@ -242,12 +242,12 @@ ${ layout.menubar(section='dashboard') }
     var _this = $(this);
     $("#confirmation .message").text(_this.attr("data-confirmation-message"));
     $("#confirmation").modal("show");
-    $("#confirmation a.btn-primary").click(function() {
+    $("#confirmation a.btn-danger").click(function() {
       _this.trigger('confirmation');
     });
   });
 
-  $("#kill-coordinator").bind('confirmation', function() {
+  $(".confirmationModal").bind('confirmation', function() {
     var _this = this;
     $.post($(this).attr("data-url"),
       { 'notification': $(this).attr("data-message") },

+ 1 - 1
apps/oozie/src/oozie/templates/dashboard/list_oozie_coordinators.mako

@@ -289,7 +289,7 @@ ${layout.menubar(section='dashboard')}
       var _this = $(this);
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation").modal("show");
-      $("#confirmation a.btn-primary").click(function() {
+      $("#confirmation a.btn-danger").click(function() {
         _this.trigger('confirmation');
       });
     });

+ 1 - 1
apps/oozie/src/oozie/templates/dashboard/list_oozie_workflow.mako

@@ -306,7 +306,7 @@ ${ layout.menubar(section='dashboard') }
       var _this = $(this);
       $("#confirmation .message").text(_this.data("confirmation-message"));
       $("#confirmation").modal("show");
-      $("#confirmation a.btn-primary").click(function() {
+      $("#confirmation a.btn-danger").click(function() {
         _this.trigger('confirmation');
       });
     });

+ 1 - 1
apps/oozie/src/oozie/templates/dashboard/list_oozie_workflows.mako

@@ -284,7 +284,7 @@ ${ layout.menubar(section='dashboard') }
       var _this = $(this);
       $("#confirmation .message").text(_this.attr("data-confirmation-message"));
       $("#confirmation").modal("show");
-      $("#confirmation a.btn-primary").click(function() {
+      $("#confirmation a.btn-danger").click(function() {
         _this.trigger('confirmation');
       });
     });

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

@@ -147,6 +147,12 @@ ${ layout.menubar(section='workflows') }
                   <i class="icon-plus"></i> ${ _('Sub-workflow') }
                 </a>
                 <p/>
+                <p>
+                <a data-node-type="generic"
+                  title="${ _('Click to add to the end of the workflow') }" class="btn new-node-link">
+                  <i class="icon-plus"></i> ${ _('Generic') }
+                </a>
+                <p/>
               </div>
             </div>
              % endif

+ 21 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-generic.xml.mako

@@ -0,0 +1,21 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+
+    <action name="${ node }">
+        ${ node.xml }
+        <ok to="${ node.get_child('ok') }"/>
+        <error to="${ node.get_child('error') }"/>
+    </action>

+ 1 - 1
apps/oozie/src/oozie/templates/utils.inc.mako

@@ -186,7 +186,7 @@
 
 <%def name="render_field_with_error_js(field, error_name, show_label=True, extra_attrs={})">
   % if not field.is_hidden:
-    <div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }" data-bind="attr: {'class': ( errors.${ error_name }().length > 0 ) ? 'control-group error' : 'control-group'}">
+    <div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text | h }" data-bind="attr: {'class': ( errors.${ error_name }().length > 0 ) ? 'control-group error' : 'control-group'}">
       % if show_label:
         <label class="control-label">${ field.label | h }</label>
       % endif

+ 29 - 0
apps/oozie/src/oozie/tests.py

@@ -778,6 +778,35 @@ class TestEditor(OozieMockBase):
                  str(self.wf.node_list))
 
 
+  def test_workflow_generic_gen_xml(self):
+    self.wf.node_set.filter(name='action-name-1').delete()
+
+    action1 = add_node(self.wf, 'action-name-1', 'generic', [self.wf.start], {
+        u'name': 'Generic',
+        u'description': 'Execute a Generic email action',
+        u'xml': """
+        <email xmlns="uri:oozie:email-action:0.1">
+            <to>hue@hue.org,django@python.org</to>
+            <subject>My subject</subject>
+            <body>My body</body>
+        </email>""",
+    })
+    Link(parent=action1, child=self.wf.end, name="ok").save()
+
+    xml = self.wf.to_xml()
+
+    assert_true("""
+    <action name="Generic">
+        <email xmlns="uri:oozie:email-action:0.1">
+            <to>hue@hue.org,django@python.org</to>
+            <subject>My subject</subject>
+            <body>My body</body>
+        </email>
+        <ok to="end"/>
+        <error to="kill"/>
+    </action>""" in xml, xml)
+
+
   def test_create_coordinator(self):
     create_coordinator(self.wf, self.c)
 

+ 4 - 2
apps/oozie/src/oozie/views/editor.py

@@ -178,7 +178,8 @@ def submit_workflow(request, workflow):
       request.error(_('Invalid submission form: %s' % params_form.errors))
   else:
     parameters = workflow.find_all_parameters()
-    params_form = ParametersFormSet(initial=parameters)
+    initial_params = ParameterForm.get_initial_params(dict([(param['name'], param['value']) for param in parameters]))
+    params_form = ParametersFormSet(initial=initial_params)
 
   popup = render('editor/submit_job_popup.mako', request, {
                    'params_form': params_form,
@@ -452,7 +453,8 @@ def submit_coordinator(request, coordinator):
       request.error(_('Invalid submission form: %s' % params_form.errors))
   else:
     parameters = coordinator.find_all_parameters()
-    params_form = ParametersFormSet(initial=parameters)
+    initial_params = ParameterForm.get_initial_params(dict([(param['name'], param['value']) for param in parameters]))
+    params_form = ParametersFormSet(initial=initial_params)
 
   popup = render('editor/submit_job_popup.mako', request, {
                  'params_form': params_form,

+ 14 - 0
apps/oozie/static/js/workflow.js

@@ -446,6 +446,17 @@ $.extend(SubWorkflowModel.prototype, {
   child_links: []
 });
 
+var GenericModel = ModelModule($);
+$.extend(GenericModel.prototype, {
+  id: 0,
+  name: '',
+  description: '',
+  node_type: 'generic',
+  workflow: 0,
+  xml: '',
+  child_links: []
+});
+
 function nodeModelChooser(node_type) {
   switch(node_type) {
     case 'mapreduce':
@@ -472,6 +483,8 @@ function nodeModelChooser(node_type) {
         return EmailModel;
     case 'subworkflow':
         return SubWorkflowModel;
+    case 'generic':
+        return GenericModel;
     case 'fork':
       return ForkModel;
     case 'decision':
@@ -494,6 +507,7 @@ var IdGeneratorTable = {
   fs: new IdGenerator({prefix: 'fs'}),
   email: new IdGenerator({prefix: 'email'}),
   subworkflow: new IdGenerator({prefix: 'subworkflow'}),
+  generic: new IdGenerator({prefix: 'generic'}),
   fork: new IdGenerator({prefix: 'fork'}),
   decision: new IdGenerator({prefix: 'decision'}),
   join: new IdGenerator({prefix: 'join'}),