Browse Source

HUE-833 Add Subworkflow action

Only owned workflows can be used as sub-worklows
Add test
Romain Rigaux 13 years ago
parent
commit
81f6feb929

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

@@ -1145,7 +1145,7 @@
       "node_type": "hive",
       "node_type": "hive",
       "workflow": 10,
       "workflow": 10,
       "name": "Hive",
       "name": "Hive",
-      "description": "Show databases"
+      "description": "Show tables"
     }
     }
   },
   },
   {
   {
@@ -1556,7 +1556,7 @@
       "files": "[\"hive-site.xml\"]",
       "files": "[\"hive-site.xml\"]",
       "job_xml": "hive-site.xml",
       "job_xml": "hive-site.xml",
       "job_properties": "[{\"name\":\"oozie.hive.defaults\",\"value\":\"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": "[]",
       "archives": "[]",
       "prepares": "[]",
       "prepares": "[]",
       "script_path": "hive.sql"
       "script_path": "hive.sql"

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

@@ -19,12 +19,14 @@ import logging
 
 
 from django import forms
 from django import forms
 from django.db.models import Q
 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 django.utils.translation import ugettext_lazy as _t
 
 
 from desktop.lib.django_forms import MultiForm, SplitDateTimeWidget
 from desktop.lib.django_forms import MultiForm, SplitDateTimeWidget
 from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator,\
 from oozie.models import Workflow, Node, Java, Mapreduce, Streaming, Coordinator,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
   Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs,\
-  Email
+  Email, SubWorkflow
 
 
 LOG = logging.getLogger(__name__)
 LOG = logging.getLogger(__name__)
 
 
@@ -249,6 +251,28 @@ class EmailForm(forms.ModelForm):
       'body': forms.Textarea(attrs={'class': 'span8'}),
       '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):
 class LinkForm(forms.ModelForm):
   comment = forms.CharField(label='if', max_length=1024, required=True, widget=forms.TextInput(attrs={'class': 'span8'}))
   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,
   DistCp.node_type: DistCpForm,
   Fs.node_type: FsForm,
   Fs.node_type: FsForm,
   Email.node_type: EmailForm,
   Email.node_type: EmailForm,
+  SubWorkflow.node_type: SubWorkflowForm,
 }
 }
 
 
 
 
@@ -396,8 +421,13 @@ class RerunForm(forms.Form):
     self.fields['skip_nodes'].initial = initial_skip_nodes
     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):
 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
       node = self.fs
     elif self.node_type == Email.node_type:
     elif self.node_type == Email.node_type:
       node = self.email
       node = self.email
+    elif self.node_type == SubWorkflow.node_type:
+      node = self.subworkflow
     elif self.node_type == Streaming.node_type:
     elif self.node_type == Streaming.node_type:
       node = self.streaming
       node = self.streaming
     elif self.node_type == Java.node_type:
     elif self.node_type == Java.node_type:
@@ -926,18 +928,29 @@ class Email(Action):
   PARAM_FIELDS = ('to', 'cc', 'subject', 'body')
   PARAM_FIELDS = ('to', 'cc', 'subject', 'body')
   node_type = 'email'
   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,
 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):
 class ControlFlow(Node):
@@ -1406,6 +1419,7 @@ ACTION_TYPES = {
   DistCp.node_type: DistCp,
   DistCp.node_type: DistCp,
   Fs.node_type: Fs,
   Fs.node_type: Fs,
   Email.node_type: Email,
   Email.node_type: Email,
+  SubWorkflow.node_type: SubWorkflow,
 }
 }
 
 
 NODE_TYPES = ACTION_TYPES.copy()
 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>
   </div>
   </div>
 
 
-  % if parameters:
+  % if parameters and len(parameters) < 10:
     <div class="row-fluid">
     <div class="row-fluid">
       <div class="span3">
       <div class="span3">
         ${ _('Variables') }
         ${ _('Variables') }

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

@@ -47,7 +47,7 @@
           <div class="control-group">
           <div class="control-group">
             <label class="control-label"></label>
             <label class="control-label"></label>
             <div class="controls">
             <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.') }
               ${ _('All the paths are relative to the deployment directory. They can be absolute but this is not recommended.') }
               <br/>
               <br/>
               ${ _('You can parameterize values using case sensitive') } <code>${"${"}PARAMETER}</code>.
               ${ _('You can parameterize values using case sensitive') } <code>${"${"}PARAMETER}</code>.
@@ -67,7 +67,7 @@
 
 
           % for field in action_form:
           % for field in action_form:
             % if field.html_name not in ('name', 'description', 'node_type', 'job_xml'):
             % 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}) }
                 ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'checked: %s' % field.name}) }
               % else:
               % else:
                 ${ utils.render_field_with_error_js(field, field.name, extra_attrs={'data-bind': 'value: %s' % field.name}) }
                 ${ 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') }
                   <i class="icon-plus"></i> ${ _('Email') }
                 </a>
                 </a>
                 <p/>
                 <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>
             </div>
             </div>
              % endif
              % 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)
     </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):
   def test_workflow_flatten_list(self):
     assert_equal('[<Start: start>, <Mapreduce: action-name-1>, <Mapreduce: action-name-2>, <Mapreduce: action-name-3>, '
     assert_equal('[<Start: start>, <Mapreduce: action-name-1>, <Mapreduce: action-name-2>, <Mapreduce: action-name-3>, '
                  '<Kill: kill>, <End: end>]',
                  '<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',
 JSON_FIELDS = ('parameters', 'job_properties', 'files', 'archives', 'prepares', 'params',
                'deletes', 'mkdirs', 'moves', 'chmods', 'touchzs')
                'deletes', 'mkdirs', 'moves', 'chmods', 'touchzs')
+NUMBER_FIELDS = ('sub_workflow',)
+
 def format_field_value(field, value):
 def format_field_value(field, value):
   if field in JSON_FIELDS:
   if field in JSON_FIELDS:
     if not isinstance(value, basestring):
     if not isinstance(value, basestring):
       return json.dumps(value)
       return json.dumps(value)
+  if field in NUMBER_FIELDS:
+    if not isinstance(value, int):
+      return int(value)
   return value
   return value
 
 
 
 
@@ -50,7 +55,7 @@ def format_dict_field_values(dictionary):
   return 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.
   Validates a single action.
   node_type is the node type of the action information passed.
   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.
   Returns Boolean.
   """
   """
   assert isinstance(errors, dict), "errors must be a dict."
   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)
   form = form_class(data=node_dict)
 
 
   if form.is_valid():
   if form.is_valid():
@@ -85,8 +90,13 @@ def get_or_create_node(workflow, node_data):
 
 
   node_type = id[0:separator_index]
   node_type = id[0:separator_index]
   node_model = NODE_TYPES.get(node_type, None)
   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:
   if node_model:
-    node = node_model(workflow=workflow, node_type=node_data['node_type'])
+    node = node_model(**kwargs)
   else:
   else:
     raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Could not find node of type'), data=node_data, error_code=500)
     raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Could not find node of type'), data=node_data, error_code=500)
   node.save()
   node.save()
@@ -105,23 +115,26 @@ def update_workflow(json_workflow):
   return 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:
   for json_node in json_nodes:
     errors = {}
     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)
       raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Invalid action'), data={'errors': errors}, error_code=400)
 
 
   nodes = []
   nodes = []
 
 
   for json_node in json_nodes:
   for json_node in json_nodes:
     node = get_or_create_node(workflow, json_node)
     node = get_or_create_node(workflow, json_node)
+
     if node.node_type == 'fork' and json_node['node_type'] == 'decision':
     if node.node_type == 'fork' and json_node['node_type'] == 'decision':
       node = node.convert_to_decision()
       node = node.convert_to_decision()
 
 
     id_map[str(json_node['id'])] = node.id
     id_map[str(json_node['id'])] = node.id
 
 
     for key in json_node:
     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]))
         setattr(node, key, format_field_value(key, json_node[key]))
 
 
     node.workflow = workflow
     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'))))
   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
     response['status'] = 0
   else:
   else:
     response['status'] = -1
     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_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)))
 @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):
 def workflow_save(request, workflow):
-  print request.POST
   json_workflow = format_dict_field_values(json.loads(str(request.POST.get('workflow'))))
   json_workflow = format_dict_field_values(json.loads(str(request.POST.get('workflow'))))
   json_workflow.setdefault('schema_version', workflow.schema_version)
   json_workflow.setdefault('schema_version', workflow.schema_version)
 
 
@@ -172,7 +184,7 @@ def workflow_save(request, workflow):
   id_map = {}
   id_map = {}
 
 
   workflow = update_workflow(json_workflow)
   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
   # Update links
   index = 0
   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']),
     'job_properties': extract_field_data(workflow_form['job_properties']),
     'link_form': LinkForm(),
     'link_form': LinkForm(),
     'default_link_form': DefaultLinkForm(action=workflow.start),
     '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):
 def _submit_coordinator(request, coordinator, mapping):
   try:
   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 = {'wf_application_path': request.fs.get_hdfs_path(wf_dir)}
     properties.update(mapping)
     properties.update(mapping)

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

@@ -433,6 +433,19 @@ $.extend(EmailModel.prototype, {
   child_links: []
   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) {
 function nodeModelChooser(node_type) {
   switch(node_type) {
   switch(node_type) {
     case 'mapreduce':
     case 'mapreduce':
@@ -457,6 +470,8 @@ function nodeModelChooser(node_type) {
         return FsModel;
         return FsModel;
     case 'email':
     case 'email':
         return EmailModel;
         return EmailModel;
+    case 'subworkflow':
+        return SubWorkflowModel;
     case 'fork':
     case 'fork':
       return ForkModel;
       return ForkModel;
     case 'decision':
     case 'decision':
@@ -478,6 +493,7 @@ var IdGeneratorTable = {
   distcp: new IdGenerator({prefix: 'distcp'}),
   distcp: new IdGenerator({prefix: 'distcp'}),
   fs: new IdGenerator({prefix: 'fs'}),
   fs: new IdGenerator({prefix: 'fs'}),
   email: new IdGenerator({prefix: 'email'}),
   email: new IdGenerator({prefix: 'email'}),
+  subworkflow: new IdGenerator({prefix: 'subworkflow'}),
   fork: new IdGenerator({prefix: 'fork'}),
   fork: new IdGenerator({prefix: 'fork'}),
   decision: new IdGenerator({prefix: 'decision'}),
   decision: new IdGenerator({prefix: 'decision'}),
   join: new IdGenerator({prefix: 'join'}),
   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()
     oozie_xml = self.job.to_xml()
     self._do_as(self.user.username , self._copy_files, deployment_dir, oozie_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
     return deployment_dir
 
 
   def _update_properties(self, jobtracker_addr, deployment_dir):
   def _update_properties(self, jobtracker_addr, deployment_dir):