فهرست منبع

HUE-833 Add Subworkflow action

Only owned workflows can be used as sub-worklows
Add test
Romain Rigaux 13 سال پیش
والد
کامیت
81f6feb929

+ 2 - 2
apps/oozie/src/oozie/fixtures/initial_oozie_examples.json

@@ -1145,7 +1145,7 @@
       "node_type": "hive",
       "workflow": 10,
       "name": "Hive",
-      "description": "Show databases"
+      "description": "Show tables"
     }
   },
   {
@@ -1556,7 +1556,7 @@
       "files": "[\"hive-site.xml\"]",
       "job_xml": "hive-site.xml",
       "job_properties": "[{\"name\":\"oozie.hive.defaults\",\"value\":\"hive-site.xml\"}]",
-      "params": "[{\"value\":\"INPUT=/user/hue/oozie/workspaces/data\",\"type\":\"param\"}]",
+      "params": "[]",
       "archives": "[]",
       "prepares": "[]",
       "script_path": "hive.sql"

+ 33 - 3
apps/oozie/src/oozie/forms.py

@@ -19,12 +19,14 @@ import logging
 
 from django import forms
 from django.db.models import Q
+from django.core.exceptions import ValidationError
+from django.utils.functional import curry
 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
+  Email, SubWorkflow
 
 LOG = logging.getLogger(__name__)
 
@@ -249,6 +251,28 @@ class EmailForm(forms.ModelForm):
       'body': forms.Textarea(attrs={'class': 'span8'}),
     }
 
+class SubWorkflowForm(forms.ModelForm):
+
+  def __init__(self, *args, **kwargs):
+    user = kwargs.pop('user')
+    workflow = kwargs.pop('workflow')
+    super(SubWorkflowForm, self).__init__(*args, **kwargs)
+    choices=((wf.id, wf) for wf in Workflow.objects.filter(owner=user).exclude(id=workflow.id))
+    self.fields['sub_workflow'] = forms.ChoiceField(choices=choices, widget=forms.RadioSelect(attrs={'class':'radio'}))
+
+  class Meta:
+    model = SubWorkflow
+    exclude = NodeForm.Meta.ALWAYS_HIDE
+    widgets = {
+      'job_properties': forms.widgets.HiddenInput(),
+    }
+
+  def clean_sub_workflow(self):
+    try:
+      return Workflow.objects.get(id=int(self.cleaned_data.get('sub_workflow')))
+    except Exception, e:
+      raise ValidationError(_('The sub-workflow could not be found: %s' % e))
+
 
 class LinkForm(forms.ModelForm):
   comment = forms.CharField(label='if', max_length=1024, required=True, widget=forms.TextInput(attrs={'class': 'span8'}))
@@ -371,6 +395,7 @@ _node_type_TO_FORM_CLS = {
   DistCp.node_type: DistCpForm,
   Fs.node_type: FsForm,
   Email.node_type: EmailForm,
+  SubWorkflow.node_type: SubWorkflowForm,
 }
 
 
@@ -396,8 +421,13 @@ class RerunForm(forms.Form):
     self.fields['skip_nodes'].initial = initial_skip_nodes
 
 
-def design_form_by_type(node_type):
-  return _node_type_TO_FORM_CLS[node_type]
+def design_form_by_type(node_type, user, workflow):
+  klass_form = _node_type_TO_FORM_CLS[node_type]
+
+  if node_type == 'subworkflow':
+    klass_form = curry(klass_form, user=user, workflow=workflow)
+
+  return klass_form
 
 
 def design_form_by_instance(design_obj, data=None):

+ 309 - 0
apps/oozie/src/oozie/migrations/0012_auto__add_subworkflow__chg_field_email_subject__chg_field_email_body.py

@@ -0,0 +1,309 @@
+# 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 'SubWorkflow'
+        db.create_table('oozie_subworkflow', (
+            ('propagate_configuration', self.gf('django.db.models.fields.BooleanField')(default=True, blank=True)),
+            ('job_properties', self.gf('django.db.models.fields.TextField')(default='[]')),
+            ('sub_workflow', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['oozie.Workflow'])),
+            ('node_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['oozie.Node'], unique=True, primary_key=True)),
+        ))
+        db.send_create_signal('oozie', ['SubWorkflow'])
+
+        # Changing field 'Email.subject'
+        db.alter_column('oozie_email', 'subject', self.gf('django.db.models.fields.TextField')())
+
+        # Changing field 'Email.body'
+        db.alter_column('oozie_email', 'body', self.gf('django.db.models.fields.TextField')())
+
+
+    def backwards(self, orm):
+
+        # Deleting model 'SubWorkflow'
+        db.delete_table('oozie_subworkflow')
+
+        # Changing field 'Email.subject'
+        db.alter_column('oozie_email', 'subject', self.gf('django.db.models.fields.TextField')(blank=True))
+
+        # Changing field 'Email.body'
+        db.alter_column('oozie_email', 'body', self.gf('django.db.models.fields.TextField')(blank=True))
+
+
+    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, 9, 41, 5, 498975)'}),
+            '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, 9, 41, 5, 498945)'}),
+            '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, 9, 41, 5, 499540)'}),
+            '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.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']

+ 23 - 9
apps/oozie/src/oozie/models.py

@@ -467,6 +467,8 @@ class Node(models.Model):
       node = self.fs
     elif self.node_type == Email.node_type:
       node = self.email
+    elif self.node_type == SubWorkflow.node_type:
+      node = self.subworkflow
     elif self.node_type == Streaming.node_type:
       node = self.streaming
     elif self.node_type == Java.node_type:
@@ -926,18 +928,29 @@ class Email(Action):
   PARAM_FIELDS = ('to', 'cc', 'subject', 'body')
   node_type = 'email'
 
-  to = models.TextField(default='', verbose_name=_t('to addresses'),
-                            help_text=_t('Comma-separated values.'))
-  cc = models.TextField(default='', verbose_name=_t('cc addresses (optional)'), blank=True,
-                            help_text=_t('Comma-separated values.'))
-  subject = models.TextField(default="[]", verbose_name=_t('Subject'), blank=True,
-                            help_text=_t('Plain-text.'))
-  body = models.TextField(default="[]", verbose_name=_t('Body'), blank=True,
-                            help_text=_t('Plain-text.'))
+  to = models.TextField(default='', verbose_name=_t('to addresses'), help_text=_t('Comma-separated values.'))
+  cc = models.TextField(default='', verbose_name=_t('cc addresses (optional)'), blank=True, help_text=_t('Comma-separated values.'))
+  subject = models.TextField(default='', verbose_name=_t('Subject'), help_text=_t('Plain-text.'))
+  body = models.TextField(default='', verbose_name=_t('Body'), help_text=_t('Plain-text.'))
+
+
+class SubWorkflow(Action):
+  PARAM_FIELDS = ('subworkflow', 'propagate_configuration', 'job_properties')
+  node_type = 'subworkflow'
+
+  sub_workflow = models.ForeignKey(Workflow, db_index=True, verbose_name=_t('Sub workflow'),
+                            help_text=_t('The sub workflow application to include. You must own all the sub-workflows.'))
+  propagate_configuration = models.BooleanField(default=True, verbose_name=_t('Propagate configuration'), blank=True,
+                            help_text=_t('If the workflow job configuration should be propagated to the child workflow.'))
+  job_properties = models.TextField(default='[]', verbose_name=_t('Hadoop job properties'),
+                                    help_text=_t('Can be used to specify the job properties that are required to run the child workflow job.'))
+
+  def get_properties(self):
+    return json.loads(self.job_properties)
 
 
 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)
+                DistCp.node_type, Fs.node_type, Email.node_type, SubWorkflow.node_type)
 
 
 class ControlFlow(Node):
@@ -1406,6 +1419,7 @@ ACTION_TYPES = {
   DistCp.node_type: DistCp,
   Fs.node_type: Fs,
   Email.node_type: Email,
+  SubWorkflow.node_type: SubWorkflow,
 }
 
 NODE_TYPES = ACTION_TYPES.copy()

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

@@ -89,7 +89,7 @@ ${ layout.menubar(section='dashboard') }
     </div>
   </div>
 
-  % if parameters:
+  % if parameters and len(parameters) < 10:
     <div class="row-fluid">
       <div class="span3">
         ${ _('Variables') }

+ 2 - 2
apps/oozie/src/oozie/templates/editor/action_utils.mako

@@ -47,7 +47,7 @@
           <div class="control-group">
             <label class="control-label"></label>
             <div class="controls">
-            <p class="alert alert-info span5">
+            <p class="alert alert-info span7">
               ${ _('All the paths are relative to the deployment directory. They can be absolute but this is not recommended.') }
               <br/>
               ${ _('You can parameterize values using case sensitive') } <code>${"${"}PARAMETER}</code>.
@@ -67,7 +67,7 @@
 
           % for field in action_form:
             % if field.html_name not in ('name', 'description', 'node_type', 'job_xml'):
-              % if field.html_name in ('capture_output', 'is_single'):
+              % if field.html_name in ('capture_output', 'is_single', 'sub_workflow'):
                 ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'checked: %s' % field.name}) }
               % else:
                 ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'value: %s' % field.name}) }

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

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

+ 31 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-subworkflow.xml.mako

@@ -0,0 +1,31 @@
+## 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.
+
+<%namespace name="common" file="workflow-common.xml.mako" />
+
+    <action name="${ node }">
+        <sub-workflow>
+            <app-path>${'${'}nameNode}${ node.sub_workflow.deployment_dir }</app-path>
+
+            % if node.propagate_configuration:
+              <propagate-configuration/>
+            % endif
+
+            ${ common.configuration(node.get_properties()) }
+        </sub-workflow>
+        <ok to="${ node.get_child('ok') }"/>
+        <error to="${ node.get_child('error') }"/>
+    </action>

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

@@ -732,6 +732,38 @@ class TestEditor(OozieMockBase):
     </action>""" in xml, xml)
 
 
+  def test_workflow_subworkflow_gen_xml(self):
+    self.wf.node_set.filter(name='action-name-1').delete()
+
+    wf_dict = WORKFLOW_DICT.copy()
+    wf_dict['name'] = [u'wf-name-2']
+    wf2 = create_workflow(self.c, wf_dict)
+
+    action1 = add_node(self.wf, 'action-name-1', 'subworkflow', [self.wf.start], {
+        u'name': 'MySubworkflow',
+        u'description': 'Execute a subworkflow action',
+        u'sub_workflow': wf2,
+        u'propagate_configuration': True,
+        u'job_properties': '[{"value":"World!","name":"argument"}]'
+    })
+    Link(parent=action1, child=self.wf.end, name="ok").save()
+
+    xml = self.wf.to_xml()
+
+    assert_true(re.search(
+        '<sub-workflow>\W+'
+            '<app-path>\${nameNode}/user/hue/oozie/workspaces/_test_-oozie-(.+?)</app-path>\W+'
+            '<propagate-configuration/>\W+'
+                '<configuration>\W+'
+                '<property>\W+'
+                    '<name>argument</name>\W+'
+                    '<value>World!</value>\W+'
+                '</property>\W+'
+            '</configuration>\W+'
+        '</sub-workflow>', xml, re.MULTILINE), xml)
+
+    wf2.delete()
+
   def test_workflow_flatten_list(self):
     assert_equal('[<Start: start>, <Mapreduce: action-name-1>, <Mapreduce: action-name-2>, <Mapreduce: action-name-3>, '
                  '<Kill: kill>, <End: end>]',

+ 21 - 9
apps/oozie/src/oozie/views/api.py

@@ -37,10 +37,15 @@ LOG = logging.getLogger(__name__)
 
 JSON_FIELDS = ('parameters', 'job_properties', 'files', 'archives', 'prepares', 'params',
                'deletes', 'mkdirs', 'moves', 'chmods', 'touchzs')
+NUMBER_FIELDS = ('sub_workflow',)
+
 def format_field_value(field, value):
   if field in JSON_FIELDS:
     if not isinstance(value, basestring):
       return json.dumps(value)
+  if field in NUMBER_FIELDS:
+    if not isinstance(value, int):
+      return int(value)
   return value
 
 
@@ -50,7 +55,7 @@ def format_dict_field_values(dictionary):
   return dictionary
 
 
-def workflow_validate_action_json(node_type, node_dict, errors={}):
+def workflow_validate_action_json(node_type, node_dict, errors, user, workflow):
   """
   Validates a single action.
   node_type is the node type of the action information passed.
@@ -59,7 +64,7 @@ def workflow_validate_action_json(node_type, node_dict, errors={}):
   Returns Boolean.
   """
   assert isinstance(errors, dict), "errors must be a dict."
-  form_class = design_form_by_type(node_type)
+  form_class = design_form_by_type(node_type, user, workflow)
   form = form_class(data=node_dict)
 
   if form.is_valid():
@@ -85,8 +90,13 @@ def get_or_create_node(workflow, node_data):
 
   node_type = id[0:separator_index]
   node_model = NODE_TYPES.get(node_type, None)
+  kwargs = {'workflow': workflow, 'node_type': node_data['node_type']}
+
+  if node_data['node_type'] == 'subworkflow':
+    kwargs['sub_workflow'] = Workflow.objects.get(id=int(node_data['sub_workflow']))
+
   if node_model:
-    node = node_model(workflow=workflow, node_type=node_data['node_type'])
+    node = node_model(**kwargs)
   else:
     raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Could not find node of type'), data=node_data, error_code=500)
   node.save()
@@ -105,23 +115,26 @@ def update_workflow(json_workflow):
   return workflow
 
 
-def update_workflow_nodes(workflow, json_nodes, id_map):
+def update_workflow_nodes(workflow, json_nodes, id_map, user):
+  """Ideally would get objects from form validation instead."""
   for json_node in json_nodes:
     errors = {}
-    if json_node['node_type'] in ACTION_TYPES and not workflow_validate_action_json(json_node['node_type'], format_dict_field_values(json_node), errors):
+    if json_node['node_type'] in ACTION_TYPES and \
+        not workflow_validate_action_json(json_node['node_type'], format_dict_field_values(json_node), errors, user, workflow):
       raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Invalid action'), data={'errors': errors}, error_code=400)
 
   nodes = []
 
   for json_node in json_nodes:
     node = get_or_create_node(workflow, json_node)
+
     if node.node_type == 'fork' and json_node['node_type'] == 'decision':
       node = node.convert_to_decision()
 
     id_map[str(json_node['id'])] = node.id
 
     for key in json_node:
-      if key not in ('node_ptr', 'child_nodes', 'workflow', 'id'):
+      if key not in ('node_ptr', 'child_nodes', 'workflow', 'id', 'sub_workflow'):
         setattr(node, key, format_field_value(key, json_node[key]))
 
     node.workflow = workflow
@@ -147,7 +160,7 @@ def workflow_validate_action(request, workflow, node_type):
 
   action_dict = format_dict_field_values(json.loads(str(request.POST.get('node'))))
 
-  if workflow_validate_action_json(node_type, action_dict, response['data']):
+  if workflow_validate_action_json(node_type, action_dict, response['data'], request.user, workflow):
     response['status'] = 0
   else:
     response['status'] = -1
@@ -159,7 +172,6 @@ def workflow_validate_action(request, workflow, node_type):
 @check_job_access_permission(exception_class=(lambda x: StructuredException(code="UNAUTHORIZED_REQUEST_ERROR", message=x, data=None, error_code=401)))
 @check_job_edition_permission(exception_class=(lambda x: StructuredException(code="UNAUTHORIZED_REQUEST_ERROR", message=x, data=None, error_code=401)))
 def workflow_save(request, workflow):
-  print request.POST
   json_workflow = format_dict_field_values(json.loads(str(request.POST.get('workflow'))))
   json_workflow.setdefault('schema_version', workflow.schema_version)
 
@@ -172,7 +184,7 @@ def workflow_save(request, workflow):
   id_map = {}
 
   workflow = update_workflow(json_workflow)
-  nodes = update_workflow_nodes(workflow, json_nodes, id_map)
+  nodes = update_workflow_nodes(workflow, json_nodes, id_map, request.user)
 
   # Update links
   index = 0

+ 3 - 3
apps/oozie/src/oozie/views/editor.py

@@ -130,7 +130,8 @@ def edit_workflow(request, workflow):
     'job_properties': extract_field_data(workflow_form['job_properties']),
     'link_form': LinkForm(),
     'default_link_form': DefaultLinkForm(action=workflow.start),
-    'action_forms': [(node_type, design_form_by_type(node_type)()) for node_type in ACTION_TYPES.iterkeys()]
+    'action_forms': [(node_type, design_form_by_type(node_type, request.user, workflow)())
+                     for node_type in ACTION_TYPES.iterkeys()]
   })
 
 
@@ -462,8 +463,7 @@ def submit_coordinator(request, coordinator):
 
 def _submit_coordinator(request, coordinator, mapping):
   try:
-    submission = Submission(request.user, coordinator.workflow, request.fs, mapping)
-    wf_dir = submission.deploy()
+    wf_dir = Submission(request.user, coordinator.workflow, request.fs, {}).deploy()
 
     properties = {'wf_application_path': request.fs.get_hdfs_path(wf_dir)}
     properties.update(mapping)

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

@@ -433,6 +433,19 @@ $.extend(EmailModel.prototype, {
   child_links: []
 });
 
+var SubWorkflowModel = ModelModule($);
+$.extend(SubWorkflowModel.prototype, {
+  id: 0,
+  name: '',
+  description: '',
+  node_type: 'subworkflow',
+  workflow: 0,
+  sub_workflow: 0,
+  propagate_configuration: true,
+  job_properties: '[]',
+  child_links: []
+});
+
 function nodeModelChooser(node_type) {
   switch(node_type) {
     case 'mapreduce':
@@ -457,6 +470,8 @@ function nodeModelChooser(node_type) {
         return FsModel;
     case 'email':
         return EmailModel;
+    case 'subworkflow':
+        return SubWorkflowModel;
     case 'fork':
       return ForkModel;
     case 'decision':
@@ -478,6 +493,7 @@ var IdGeneratorTable = {
   distcp: new IdGenerator({prefix: 'distcp'}),
   fs: new IdGenerator({prefix: 'fs'}),
   email: new IdGenerator({prefix: 'email'}),
+  subworkflow: new IdGenerator({prefix: 'subworkflow'}),
   fork: new IdGenerator({prefix: 'fork'}),
   decision: new IdGenerator({prefix: 'decision'}),
   join: new IdGenerator({prefix: 'join'}),

+ 9 - 0
desktop/libs/liboozie/src/liboozie/submittion.py

@@ -121,6 +121,15 @@ class Submission(object):
     oozie_xml = self.job.to_xml()
     self._do_as(self.user.username , self._copy_files, deployment_dir, oozie_xml)
 
+    if hasattr(self.job, 'actions'):
+      for action in self.job.actions:
+        # Make sure XML is there
+        # Don't support shared sub-worfklow
+        if action.node_type == 'subworkflow':
+          node = action.get_full_node()
+          sub_deploy = Submission(self.user, node.sub_workflow, self.fs, self.properties)
+          sub_deploy.deploy()
+
     return deployment_dir
 
   def _update_properties(self, jobtracker_addr, deployment_dir):