Преглед на файлове

HUE-833 [oozie] Add Fs action

Add example.
Add tests.
Add red color to kill buttons.
Update hive example to 'show tables'.
Romain Rigaux преди 13 години
родител
ревизия
fd6ec82

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


+ 1 - 1
apps/oozie/examples/hive/hive.sql

@@ -1 +1 @@
-show databases;
+show tables;

+ 138 - 13
apps/oozie/src/oozie/fixtures/initial_oozie_examples.json

@@ -8,7 +8,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/sleep",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of MapReduce action that sleeps"
     }
@@ -22,7 +22,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/sleep",
       "schema_version": "uri:oozie:coordinator:0.2",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of MapReduce action"
     }
@@ -36,7 +36,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/terasort",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of sequential Java actions"
     }
@@ -50,7 +50,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/sleepfork",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of multiple forks"
     }
@@ -64,7 +64,7 @@
       "parameters": "[{\"name\":\"oozie.use.system.libpath\",\"value\":\"true\"}]",
       "deployment_dir": "/user/hue/oozie/workspaces/hive",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of Hive action"
     }
@@ -78,7 +78,7 @@
       "parameters": "[{\"name\":\"oozie.use.system.libpath\",\"value\":\"true\"}]",
       "deployment_dir": "/user/hue/oozie/workspaces/pig",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of Pig action"
     }
@@ -92,7 +92,7 @@
       "parameters": "[{\"name\":\"oozie.use.system.libpath\",\"value\":\"true\"}]",
       "deployment_dir": "/user/hue/oozie/workspaces/sqoop",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of Sqoop action"
     }
@@ -106,7 +106,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/ssh",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of Ssh action"
     }
@@ -120,7 +120,7 @@
       "parameters": "[]",
       "deployment_dir": "/user/hue/oozie/workspaces/shell",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of Shell action"
     }
@@ -134,11 +134,25 @@
       "parameters": "[{\"name\":\"oozie.use.system.libpath\",\"value\":\"true\"}]",
       "deployment_dir": "/user/hue/oozie/workspaces/distcp",
       "schema_version": "uri:oozie:workflow:0.4",
-      "last_modified": "2012-11-05 00:00:00",
+      "last_modified": "2012-12-01 00:00:00",
       "owner": 1100713,
       "description": "Example of DistCp action"
     }
   },
+  {
+    "pk": 17,
+    "model": "oozie.job",
+    "fields": {
+      "is_shared": true,
+      "name": "Fs",
+      "parameters": "[{\"name\": \"oozie.use.system.libpath\", \"value\": \"true\"}]",
+      "deployment_dir": "/user/hue/oozie/workspaces/fs",
+      "schema_version": "uri:oozie:workflow:0.4",
+      "last_modified": "2012-12-01 00:00:00",
+      "owner": 1100713,
+      "description": "Example of Fs actions"
+    }
+  },
   {
     "pk": 5,
     "model": "oozie.workflow",
@@ -238,6 +252,17 @@
       "job_properties": "[]"
     }
   },
+  {
+    "pk": 17,
+    "model": "oozie.workflow",
+    "fields": {
+      "job_xml": "",
+      "start": 75,
+      "is_single": false,
+      "end": 74,
+      "job_properties": "[]"
+    }
+  },
   {
     "pk": 23,
     "model": "oozie.link",
@@ -778,6 +803,36 @@
       "child": 69
     }
   },
+  {
+    "pk": 96,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "to",
+      "parent": 75,
+      "child": 76
+    }
+  },
+  {
+    "pk": 97,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "related",
+      "parent": 75,
+      "child": 74
+    }
+  },
+  {
+    "pk": 98,
+    "model": "oozie.link",
+    "fields": {
+      "comment": "",
+      "name": "ok",
+      "parent": 76,
+      "child": 74
+    }
+  },
   {
     "pk": 16,
     "model": "oozie.node",
@@ -1228,6 +1283,46 @@
       "description": "Copy some data and overwrite it if needed"
     }
   },
+  {
+    "pk": 73,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "kill",
+      "workflow": 17,
+      "name": "kill",
+      "description": ""
+    }
+  },
+  {
+    "pk": 74,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "end",
+      "workflow": 17,
+      "name": "end",
+      "description": ""
+    }
+  },
+  {
+    "pk": 75,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "start",
+      "workflow": 17,
+      "name": "start",
+      "description": ""
+    }
+  },
+  {
+    "pk": 76,
+    "model": "oozie.node",
+    "fields": {
+      "node_type": "fs",
+      "workflow": 17,
+      "name": "ManageFiles",
+      "description": ""
+    }
+  },
   {
     "pk": 19,
     "model": "oozie.mapreduce",
@@ -1410,6 +1505,17 @@
       "job_properties": "[]"
     }
   },
+  {
+    "pk": 76,
+    "model": "oozie.fs",
+    "fields": {
+      "touchzs": "[{\"name\": \"${output}/testfs/new_file\"}]",
+      "moves": "[{\"source\": \"${output}/testfs/source\", \"destination\": \"${output}/testfs/renamed\"}]",
+      "chmods": "[{\"path\": \"${output}/testfs/renamed\", \"recursive\": \"\", \"permissions\": \"700\"}]",
+      "mkdirs": "[{\"name\": \"${output}/testfs\"}, {\"name\": \"${output}/testfs/source\"}]",
+      "deletes": "[{\"name\": \"${output}/testfs\"}]"
+    }
+  },
   {
     "pk": 18,
     "model": "oozie.start",
@@ -1473,6 +1579,13 @@
       "node_ptr": 71
     }
   },
+  {
+    "pk": 75,
+    "model": "oozie.start",
+    "fields": {
+      "node_ptr": 75
+    }
+  },
   {
     "pk": 17,
     "model": "oozie.end",
@@ -1518,6 +1631,11 @@
     "model": "oozie.end",
     "fields": {}
   },
+  {
+    "pk": 74,
+    "model": "oozie.end",
+    "fields": {}
+  },
   {
     "pk": 16,
     "model": "oozie.kill",
@@ -1581,6 +1699,13 @@
       "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]"
     }
   },
+  {
+    "pk": 73,
+    "model": "oozie.kill",
+    "fields": {
+      "message": "Action failed, error message[${wf:errorMessage(wf:lastErrorNode())}]"
+    }
+  },
   {
     "pk": 34,
     "model": "oozie.fork",
@@ -1605,12 +1730,12 @@
     "pk": 6,
     "model": "oozie.coordinator",
     "fields": {
-      "end": "2012-11-05 00:00:00",
+      "end": "2012-12-05 00:00:00",
       "concurrency": null,
       "frequency_number": 1,
       "workflow": 5,
       "frequency_unit": "days",
-      "start": "2012-11-01 00:00:00",
+      "start": "2012-12-01 00:00:00",
       "timeout": null,
       "timezone": "America/Los_Angeles",
       "throttle": null,
@@ -1626,7 +1751,7 @@
       "coordinator": 6,
       "frequency_unit": "days",
       "uri": "${DAY}",
-      "start": "2012-11-01 00:00:00",
+      "start": "2012-12-01 00:00:00",
       "timezone": "America/Los_Angeles",
       "done_flag": "",
       "description": ""

+ 14 - 1
apps/oozie/src/oozie/forms.py

@@ -23,7 +23,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
+  Dataset, DataInput, DataOutput, Pig, Link, Hive, Sqoop, Ssh, Shell, DistCp, Fs
 
 LOG = logging.getLogger(__name__)
 
@@ -224,6 +224,18 @@ class DistCpForm(forms.ModelForm):
     }
 
 
+class FsForm(forms.ModelForm):
+  class Meta:
+    model = Fs
+    exclude = NodeForm.Meta.ALWAYS_HIDE
+    widgets = {
+      'deletes': forms.widgets.HiddenInput(),
+      'mkdirs': forms.widgets.HiddenInput(),
+      'moves': forms.widgets.HiddenInput(),
+      'chmods': forms.widgets.HiddenInput(),
+      'touchzs': forms.widgets.HiddenInput(),
+    }
+
 
 class LinkForm(forms.ModelForm):
   comment = forms.CharField(label='if', max_length=1024, required=True, widget=forms.TextInput(attrs={'class': 'span8'}))
@@ -344,6 +356,7 @@ _node_type_TO_FORM_CLS = {
   Ssh.node_type: SshForm,
   Shell.node_type: ShellForm,
   DistCp.node_type: DistCpForm,
+  Fs.node_type: FsForm,
 }
 
 

+ 4 - 4
apps/oozie/src/oozie/import_workflow.py

@@ -98,7 +98,7 @@ def _save_links(workflow, root):
         for case in switch:
           to = case.attrib['to']
           child = Node.objects.get(workflow=workflow, name=to)
-          
+
           if case.tag == 'default':
             name = 'default'
             obj = Link.objects.create(name=name, parent=parent, child=child)
@@ -107,7 +107,7 @@ def _save_links(workflow, root):
             name = 'start'
             comment = case.text.strip()
             obj = Link.objects.create(name=name, parent=parent, child=child, comment=comment)
-          
+
           obj.save()
 
     else:
@@ -258,13 +258,13 @@ def import_workflow(workflow, workflow_definition, schema_version=0.4):
     'xslt_dir': DEFINITION_XSLT_DIR.get(),
     'schema_version': schema_version
   })
-  
+
   # Remove namespace from definition
   workflow_definition = re.sub(r'\s*xmlns=".*?"', '', workflow_definition, count=1)
 
   # Parse Workflow Definition
   xml = etree.fromstring(workflow_definition)
-  
+
   # Get XSLT
   xslt = etree.parse(xslt_definition_fh)
   xslt_definition_fh.close()

+ 284 - 0
apps/oozie/src/oozie/migrations/0010_auto__add_fs.py

@@ -0,0 +1,284 @@
+# 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 'Fs'
+        db.create_table('oozie_fs', (
+            ('mkdirs', self.gf('django.db.models.fields.TextField')(default='[]', blank=True)),
+            ('touchzs', self.gf('django.db.models.fields.TextField')(default='[]', blank=True)),
+            ('chmods', self.gf('django.db.models.fields.TextField')(default='[]', blank=True)),
+            ('node_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['oozie.Node'], unique=True, primary_key=True)),
+            ('moves', self.gf('django.db.models.fields.TextField')(default='[]', blank=True)),
+            ('deletes', self.gf('django.db.models.fields.TextField')(default='[]', blank=True)),
+        ))
+        db.send_create_signal('oozie', ['Fs'])
+
+
+    def backwards(self, orm):
+
+        # Deleting model 'Fs'
+        db.delete_table('oozie_fs')
+
+
+    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, 14, 10, 41, 25, 447926)'}),
+            '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, 11, 10, 41, 25, 447896)'}),
+            '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, 11, 10, 41, 25, 448547)'}),
+            '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.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.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']

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

@@ -463,6 +463,8 @@ class Node(models.Model):
       node = self.shell
     elif self.node_type == DistCp.node_type:
       node = self.distcp
+    elif self.node_type == Fs.node_type:
+      node = self.fs
     elif self.node_type == Streaming.node_type:
       node = self.streaming
     elif self.node_type == Java.node_type:
@@ -505,6 +507,7 @@ class Node(models.Model):
   # Can't use through relation directly with this Django version?
   # https://docs.djangoproject.com/en/1.2/topics/db/models/#intermediary-manytomany
   def get_link(self, name=None):
+    a = Link.objects.filter(parent=self)
     if name is None:
       return Link.objects.exclude(name__in=Link.META_LINKS).get(parent=self)
     else:
@@ -882,8 +885,43 @@ class DistCp(Action):
     return json.loads(self.prepares)
 
 
+class Fs(Action):
+  PARAM_FIELDS = ('deletes', 'mkdirs', 'moves', 'chmods', 'touchzs')
+  node_type = 'fs'
+
+  deletes = models.TextField(default="[]", verbose_name=_t('Delete path'), blank=True,
+                            help_text=_t('Delete the specified path, if it is a directory it deletes recursively all its content and '
+                                         'then deletes the directory.'))
+  mkdirs = models.TextField(default="[]", verbose_name=_t('Create directory'), blank=True,
+                            help_text=_t('Create the specified directory, it creates all missing directories in the path. '
+                                         'If the directory already exist it does a no-op.'))
+  moves = models.TextField(default="[]", verbose_name=_t('Move file'), blank=True,
+                            help_text=_t('Move a file or directory to another path.'))
+  chmods = models.TextField(default="[]", verbose_name=_t('Change permissions'), blank=True,
+                            help_text=_t('Change the permissions for the specified path. Permissions can be specified using the Unix Symbolic '
+                                         'representation (e.g. -rwxrw-rw-) or an octal representation (755).'))
+  touchzs = models.TextField(default="[]", verbose_name=_t('Create or touch a file'), blank=True,
+                            help_text=_t('Creates a zero length file in the specified path if none exists or touch it.'))
+
+
+  def get_deletes(self):
+    return json.loads(self.deletes)
+
+  def get_mkdirs(self):
+    return json.loads(self.mkdirs)
+
+  def get_moves(self):
+    return json.loads(self.moves)
+
+  def get_chmods(self):
+    return json.loads(self.chmods)
+
+  def get_touchzs(self):
+    return json.loads(self.touchzs)
+
+
 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)
+                DistCp.node_type, Fs.node_type)
 
 
 class ControlFlow(Node):
@@ -965,7 +1003,7 @@ class Fork(ControlFlow):
 class Decision(ControlFlow):
   """
   Essentially a fork where the end is not a join, but another node.
-  If two decisions share an end, the decision with the higher level takes the end 
+  If two decisions share an end, the decision with the higher level takes the end
   and the lower level decision will not have an end.
   IE:     D
         D   N
@@ -1350,7 +1388,9 @@ ACTION_TYPES = {
   Ssh.node_type: Ssh,
   Shell.node_type: Shell,
   DistCp.node_type: DistCp,
+  Fs.node_type: Fs,
 }
+
 NODE_TYPES = ACTION_TYPES.copy()
 NODE_TYPES.update({
   Fork.node_type: Fork,

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

@@ -98,7 +98,7 @@ ${ layout.menubar(section='dashboard') }
               % if oozie_coordinator.is_running():
                 <a title="${_('Kill %(coordinator)s') % dict(coordinator=oozie_coordinator.id)}"
                   id="kill-coordinator"
-                  class="btn small confirmationModal"
+                  class="btn btn-danger small confirmationModal"
                   alt="${ _('Are you sure you want to kill coordinator %s?') % oozie_coordinator.id }"
                   href="javascript:void(0)"
                   data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_coordinator.id, action='kill') }"
@@ -120,10 +120,10 @@ ${ layout.menubar(section='dashboard') }
 
     <ul class="nav nav-tabs">
       <li class="active"><a href="#calendar" data-toggle="tab">${ _('Calendar') }</a></li>
-    <li><a href="#actions" data-toggle="tab">${ _('Actions') }</a></li>
-    <li><a href="#configuration" data-toggle="tab">${ _('Configuration') }</a></li>
-    <li><a href="#log" data-toggle="tab">${ _('Log') }</a></li>
-    <li><a href="#definition" data-toggle="tab">${ _('Definition') }</a></li>
+      <li><a href="#actions" data-toggle="tab">${ _('Actions') }</a></li>
+      <li><a href="#configuration" data-toggle="tab">${ _('Configuration') }</a></li>
+      <li><a href="#log" data-toggle="tab">${ _('Log') }</a></li>
+      <li><a href="#definition" data-toggle="tab">${ _('Definition') }</a></li>
     </ul>
 
     <div class="tab-content" style="padding-bottom:200px">

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

@@ -82,7 +82,7 @@ ${layout.menubar(section='dashboard')}
             <td>
               % if has_job_edition_permission(job, user):
                 <a title="${_('Kill %(coordinator)s') % dict(coordinator=job.id)}"
-                  class="btn small confirmationModal"
+                  class="btn btn-danger small confirmationModal"
                   alt="${ _('Are you sure you want to kill coordinator %s?') % job.id }"
                   href="javascript:void(0)"
                   data-message="${ _('The coordinator was killed!') }"

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

@@ -119,7 +119,7 @@ ${ layout.menubar(section='dashboard') }
       % if oozie_workflow.is_running():
         <a title="${_('Kill %(workflow)s') % dict(workflow=oozie_workflow.id)}"
           id="kill-workflow"
-          class="btn small confirmationModal"
+          class="btn btn-danger small confirmationModal"
           alt="${ _('Are you sure you want to kill workflow %s?') %  oozie_workflow.id }"
           href="javascript:void(0)"
           data-url="${ url('oozie:manage_oozie_jobs', job_id=oozie_workflow.id, action='kill') }"

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

@@ -83,15 +83,15 @@ ${ layout.menubar(section='dashboard') }
             <td><a href="${ job.get_absolute_url() }" data-row-selector="true"></a>${ job.id }</td>
             <td>
               % if has_job_edition_permission(job, user):
-	            <a title="${_('Kill %(workflow)s') % dict(workflow=job.id)}"
-	              class="btn small confirmationModal"
-	              alt="${ _('Are you sure you want to kill workflow %s?') %  job.id }"
-	              href="javascript:void(0)"
-	              data-url="${ url('oozie:manage_oozie_jobs', job_id=job.id, action='kill') }"
-	              data-message="${ _('The workflow was killed!') }"
-	              data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }">
-	                ${ _('Kill') }
-	            </a>
+              <a title="${_('Kill %(workflow)s') % dict(workflow=job.id)}"
+                class="btn btn-danger small confirmationModal"
+                alt="${ _('Are you sure you want to kill workflow %s?') %  job.id }"
+                href="javascript:void(0)"
+                data-url="${ url('oozie:manage_oozie_jobs', job_id=job.id, action='kill') }"
+                data-message="${ _('The workflow was killed!') }"
+                data-confirmation-message="${ _('Are you sure you\'d like to kill this job?') }">
+                  ${ _('Kill') }
+              </a>
               % endif
             </td>
           </tr>

+ 123 - 3
apps/oozie/src/oozie/templates/editor/action_utils.mako

@@ -181,6 +181,56 @@
             ${ utils.render_field_with_error_js(action_form['job_xml'], action_form['job_xml'].name, extra_attrs={'data-bind': 'value: %s' % action_form['job_xml'].name}) }
           % endif
 
+          % if 'deletes' in action_form.fields:
+            <%
+            file_field(action_form['deletes'], {
+              'name': 'deletes',
+              'add': 'addDelete',
+              'remove': '$parent.removeDelete'
+            })
+            %>
+          % endif
+
+          % if 'mkdirs' in action_form.fields:
+            <%
+            file_field(action_form['mkdirs'], {
+              'name': 'mkdirs',
+              'add': 'addMkdir',
+              'remove': '$parent.removeMkdir'
+            })
+            %>
+          % endif
+
+          % if 'moves' in action_form.fields:
+            <%
+            move_field(action_form['moves'], {
+              'name': 'moves',
+              'add': 'addMove',
+              'remove': '$parent.removeMove'
+            })
+            %>
+          % endif
+
+          % if 'chmods' in action_form.fields:
+            <%
+            chmod_field(action_form['chmods'], {
+              'name': 'chmods',
+              'add': 'addChmod',
+              'remove': '$parent.removeChmod'
+            })
+            %>
+          % endif
+
+          % if 'touchzs' in action_form.fields:
+            <%
+            file_field(action_form['touchzs'], {
+              'name': 'touchzs',
+              'add': 'addTouchz',
+              'remove': '$parent.removeTouchz'
+            })
+            %>
+          % endif
+
         </fieldset>
       </div>
 
@@ -198,7 +248,7 @@
 
 <%def name="file_field(field, javascript_attrs={})">
 <div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
-  <label class="control-label">${ _('Files') }</label>
+  <label class="control-label">${ field.label }</label>
   <div class="controls">
     % if 'name' in javascript_attrs:
       <table class="table-condensed designTable" data-bind="visible: ${ javascript_attrs['name'] }().length > 0">
@@ -217,7 +267,7 @@
       </table>
 
       % if 'add' in javascript_attrs:
-        <button class="btn" data-bind="click: ${ javascript_attrs['add'] }">${ _('Add File') }</button>
+        <button class="btn" data-bind="click: ${ javascript_attrs['add'] }">${ _('Add Path') }</button>
       % endif
     % endif
   </div>
@@ -253,7 +303,7 @@
 </%def>
 
 <%def name="job_properties_field(field, javascript_attrs={})">
-<div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.label }">
+<div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
   <label class="control-label">${ _('Job Properties') }</label>
   <div class="controls">
     % if 'name' in javascript_attrs:
@@ -286,6 +336,76 @@
 </div>
 </%def>
 
+<%def name="move_field(field, javascript_attrs={})">
+<div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
+  <label class="control-label">${ field.label }</label>
+  <div class="controls">
+    % if 'name' in javascript_attrs:
+      <table class="table-condensed designTable" data-bind="visible: ${ javascript_attrs['name'] }().length > 0">
+        <thead>
+          <tr>
+            <th>${ _('Source') }</th>
+            <th>${ _('Destination') }</th>
+            <th/>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: ${ javascript_attrs['name'] }">
+          <tr>
+            <td><input type="text" class="span4 required propKey" data-bind="fileChooser: $data, value: source, uniqueName: false" /></td>
+            <td><input type="text" class="span4 required pathChooserKo" data-bind="fileChooser: $data, value: destination, uniqueName: false" /></td>
+            <td>
+            % if 'remove' in javascript_attrs:
+              <a class="btn" href="#" data-bind="click: ${ javascript_attrs['remove'] }">${ _('Delete') }</a>
+            % endif
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      % if 'add' in javascript_attrs:
+        <button class="btn" data-bind="click: ${ javascript_attrs['add'] }">${ _('Add move') }</button>
+      % endif
+    % endif
+  </div>
+</div>
+</%def>
+
+<%def name="chmod_field(field, javascript_attrs={})">
+<div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
+  <label class="control-label">${ field.label }</label>
+  <div class="controls">
+    % if 'name' in javascript_attrs:
+      <table class="table-condensed designTable" data-bind="visible: ${ javascript_attrs['name'] }().length > 0">
+        <thead>
+          <tr>
+            <th>${ _('Path') }</th>
+            <th>${ _('Permissions') }</th>
+            <th>${ _('Recursive') }</th>
+            <th/>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: ${ javascript_attrs['name'] }">
+          <tr>
+            <td><input type="text" class="span4 required pathChooserKo" data-bind="fileChooser: $data, value: path, uniqueName: false" /></td>
+            <td><input type="text" class="span2 required propKey" data-bind="value: permissions, uniqueName: false" /></td>
+            <td><input type="checkbox" class="span1 required" data-bind="checked: recursive, uniqueName: false" /></td>
+            <td>
+            % if 'remove' in javascript_attrs:
+              <a class="btn" href="#" data-bind="click: ${ javascript_attrs['remove'] }">${ _('Delete') }</a>
+            % endif
+            </td>
+          </tr>
+        </tbody>
+      </table>
+
+      % if 'add' in javascript_attrs:
+        <button class="btn" data-bind="click: ${ javascript_attrs['add'] }">${ _('Add chmod') }</button>
+      % endif
+    % endif
+  </div>
+</div>
+</%def>
+
 <%def name="params_field(field, javascript_attrs={})">
 <div class="control-group" rel="popover" data-original-title="${ field.label }" data-content="${ field.help_text }">
   <label class="control-label">${ _('Params') }</label>

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

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

+ 3 - 10
apps/oozie/src/oozie/templates/editor/gen/workflow-common.xml.mako

@@ -15,7 +15,8 @@
 ## limitations under the License.
 
 <%!
-import posixpath
+  import posixpath
+  from oozie.utils import smart_path
 %>
 
 
@@ -27,18 +28,10 @@ import posixpath
             <prepare>
                 % for p in prepares:
                   <%
-                    # Same as Fs action convention. No change if path is just a parameter.
                     operation = p['type']
                     path = p['value']
-
-                    if not path.startswith('/') and not path.startswith('$') and not path.startswith('hdfs://'):
-                      path = '/user/%(username)s/%(path)s' % {'username': '${wf:user()}', 'path': path}
-
-                    if not path.startswith('hdfs://'):
-                      path = '%(nameNode)s%(path)s' % {'nameNode': '${nameNode}', 'path': path}
                   %>
-
-                  <${ operation } path="${ path }"/>
+                  <${ operation } path="${ smart_path(path) }"/>
                 % endfor
             </prepare>
         % endif

+ 53 - 0
apps/oozie/src/oozie/templates/editor/gen/workflow-fs.xml.mako

@@ -0,0 +1,53 @@
+## 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.
+
+<%!
+  from oozie.utils import smart_path
+%>
+
+<%namespace name="common" file="workflow-common.xml.mako" />
+
+    <action name="${ node }">
+        <fs>
+            % for param in node.get_deletes():
+              <delete path='${ smart_path(param['name']) }'/>
+            % endfor
+
+            % for param in node.get_mkdirs():
+              <mkdir path='${ smart_path(param['name']) }'/>
+            % endfor
+
+            % for param in node.get_moves():
+              <move source='${ smart_path(param['source']) }' target='${ smart_path(param['destination']) }'/>
+            % endfor
+
+            % for param in node.get_chmods():
+              <%
+                if param['recursive']:
+                  recursive = 'true'
+                else:
+                  recursive = 'false'
+              %>
+              <chmod path='${ smart_path(param['path']) }' permissions='${ param['permissions'] }' dir-files='${ recursive }'/>
+            % endfor
+
+            % for param in node.get_touchzs():
+              <touchz path='${ smart_path(param['name']) }'/>
+            % endfor
+        </fs>
+        <ok to="${ node.get_child('ok') }"/>
+        <error to="${ node.get_child('error') }"/>
+    </action>

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

@@ -606,6 +606,7 @@ class TestEditor(OozieMockBase):
         '    <end name="end"/>\n'
         '</workflow-app>'.split(), self.wf.to_xml().split())
 
+
   def test_workflow_shell_gen_xml(self):
     self.wf.node_set.filter(name='action-name-1').delete()
 
@@ -652,6 +653,41 @@ class TestEditor(OozieMockBase):
         </shell>""" in xml, xml)
 
 
+  def test_workflow_fs_gen_xml(self):
+    self.wf.node_set.filter(name='action-name-1').delete()
+
+    action1 = add_node(self.wf, 'action-name-1', 'fs', [self.wf.start], {
+        u'name': 'MyFs',
+        u'description': 'Execute a Fs action that manage files',
+        u'deletes': '[{"name":"/to/delete"},{"name":"to/delete2"}]',
+        u'mkdirs': '[{"name":"/to/mkdir"},{"name":"${mkdir2}"}]',
+        u'moves': '[{"source":"/to/move/source","destination":"/to/move/destination"},{"source":"/to/move/source2","destination":"/to/move/destination2"}]',
+        u'chmods': '[{"path":"/to/chmod","recursive":true,"permissions":"-rwxrw-rw-"},{"path":"/to/chmod2","recursive":false,"permissions":"755"}]',
+        u'touchzs': '[{"name":"/to/touchz"},{"name":"/to/touchz2"}]'
+    })
+    Link(parent=action1, child=self.wf.end, name="ok").save()
+
+    xml = self.wf.to_xml()
+
+    assert_true("""
+    <action name="MyFs">
+        <fs>
+              <delete path='${nameNode}/to/delete'/>
+              <delete path='${nameNode}/user/${wf:user()}/to/delete2'/>
+              <mkdir path='${nameNode}/to/mkdir'/>
+              <mkdir path='${nameNode}${mkdir2}'/>
+              <move source='${nameNode}/to/move/source' target='${nameNode}/to/move/destination'/>
+              <move source='${nameNode}/to/move/source2' target='${nameNode}/to/move/destination2'/>
+              <chmod path='${nameNode}/to/chmod' permissions='-rwxrw-rw-' dir-files='true'/>
+              <chmod path='${nameNode}/to/chmod2' permissions='755' dir-files='false'/>
+              <touchz path='${nameNode}/to/touchz'/>
+              <touchz path='${nameNode}/to/touchz2'/>
+        </fs>
+        <ok to="end"/>
+        <error to="kill"/>
+    </action>""" in xml, xml)
+
+
   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>]',

+ 13 - 1
apps/oozie/src/oozie/utils.py

@@ -41,4 +41,16 @@ def workflow_to_dict(workflow):
 
   workflow_dict['nodes'] = nodes
 
-  return workflow_dict
+  return workflow_dict
+
+
+def smart_path(path):
+  # Try to prepend home_dir and FS scheme. No change if path starts by a parameter.
+
+  if not path.startswith('/') and not path.startswith('$') and not path.startswith('hdfs://'):
+    path = '/user/%(username)s/%(path)s' % {'username': '${wf:user()}', 'path': path}
+
+  if not path.startswith('hdfs://'):
+    path = '%(nameNode)s%(path)s' % {'nameNode': '${nameNode}', 'path': path}
+
+  return path

+ 4 - 1
apps/oozie/src/oozie/views/api.py

@@ -35,7 +35,8 @@ from oozie.utils import model_to_dict
 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')
 def format_field_value(field, value):
   if field in JSON_FIELDS:
     if not isinstance(value, basestring):
@@ -158,10 +159,12 @@ 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)
 
   form = WorkflowForm(data=json_workflow)
+
   if not form.is_valid():
     raise StructuredException(code="INVALID_REQUEST_ERROR", message=_('Error saving workflow'), data={'errors': form.errors}, error_code=400)
 

+ 82 - 82
apps/oozie/static/help/index.html

@@ -58,10 +58,10 @@ div.rbtoc1348526886180 li {margin-left: 0px;padding-left: 0px;}
 <p>To start Oozie Editor/Dashboard, click the <b>Oozie Editor/Dashboard</b> icon ( <span class="image-wrap" style=""><img src="/oozie/static/art/icon.png" width="24" style="border: 0px solid black"/></span>) in the navigation bar at the top of the Hue browser page. <b>Oozie Editor/Dashboard</b> opens with the following screens:</p>
 
 <ul>
-	<li>Dashboad &#45;&nbsp;shows the running and completed workflow and coordinator jobs. The screen is selected and opened to the Workflows page.</li>
-	<li>Workflows - shows available workflows.</li>
-	<li>Coordinators &#45;&nbsp;shows available coordinators.</li>
-	<li>History - shows a list of submitted jobs.</li>
+  <li>Dashboad &#45;&nbsp;shows the running and completed workflow and coordinator jobs. The screen is selected and opened to the Workflows page.</li>
+  <li>Workflows - shows available workflows.</li>
+  <li>Coordinators &#45;&nbsp;shows available coordinators.</li>
+  <li>History - shows a list of submitted jobs.</li>
 </ul>
 
 
@@ -133,17 +133,17 @@ vertical-align: center;
 <p>For the selected workflow, the following tabs and information is available.</p>
 
 <ul>
-	<li>The <b>Graph</b>&nbsp;tab shows the workflow DAG.</li>
-	<li>The <b>Actions</b> tab shows you details about the actions that make up this workflow.
-	<ul>
-		<li>Click the <b>Id</b>&nbsp;link to see additional details about this action.</li>
-		<li>Click the <b>External Id</b>&nbsp;link to view this action in the Hue Job Browser.</li>
-	</ul>
-	</li>
-	<li>The <b>Details</b> tab shows job statistics including start and end times, and provides a link to the actual workflow definition in the File Browser.</li>
-	<li>The <b>Configuration</b> tab shows selected job configuration settings.</li>
-	<li>The <b>Logs</b> tab shows log output generated by the workflow.</li>
-	<li>The <b>Definition</b> tab shows the Oozie workflow definition, as it appears in the <tt>workflow.xml</tt> file (also linked under the application path properties in the <b>Details</b> tab and the <b>Configuration</b> tab).</li>
+  <li>The <b>Graph</b>&nbsp;tab shows the workflow DAG.</li>
+  <li>The <b>Actions</b> tab shows you details about the actions that make up this workflow.
+  <ul>
+    <li>Click the <b>Id</b>&nbsp;link to see additional details about this action.</li>
+    <li>Click the <b>External Id</b>&nbsp;link to view this action in the Hue Job Browser.</li>
+  </ul>
+  </li>
+  <li>The <b>Details</b> tab shows job statistics including start and end times, and provides a link to the actual workflow definition in the File Browser.</li>
+  <li>The <b>Configuration</b> tab shows selected job configuration settings.</li>
+  <li>The <b>Logs</b> tab shows log output generated by the workflow.</li>
+  <li>The <b>Definition</b> tab shows the Oozie workflow definition, as it appears in the <tt>workflow.xml</tt> file (also linked under the application path properties in the <b>Details</b> tab and the <b>Configuration</b> tab).</li>
 </ul>
 
 
@@ -154,16 +154,16 @@ vertical-align: center;
 
 <p>For the selected coordinator, the following tabs and information is available.</p>
 <ul>
-	<li>The <b>Calendar</b> tab shows the timestamp of the job. Click the timestamp to open the workflow DAG.&nbsp;</li>
-	<li>The&nbsp;<b>Actions</b>&nbsp;tab shows you details about the actions that make up this coordinator.
-	<ul>
-		<li>Click the&nbsp;<b>Id</b>&nbsp;link to see additional details about this action.</li>
-		<li>Click the&nbsp;<b>External Id</b>&nbsp;link to view this action in the Hue Job Browser.</li>
-	</ul>
-	</li>
-	<li>The&nbsp;<b>Configuration</b>&nbsp;tab shows selected job configuration settings.</li>
-	<li>The&nbsp;<b>Logs</b>&nbsp;tab shows log output generated by the coordinator.</li>
-	<li>The&nbsp;<b>Definition</b>&nbsp;tab shows the Oozie coordinator&nbsp;definition, as it appears in the&nbsp;<tt>coordinator.xml</tt>&nbsp;file (also linked under&nbsp;the oozie.coord.application.path property in the &nbsp;<b>Configuration</b>&nbsp;tab).</li>
+  <li>The <b>Calendar</b> tab shows the timestamp of the job. Click the timestamp to open the workflow DAG.&nbsp;</li>
+  <li>The&nbsp;<b>Actions</b>&nbsp;tab shows you details about the actions that make up this coordinator.
+  <ul>
+    <li>Click the&nbsp;<b>Id</b>&nbsp;link to see additional details about this action.</li>
+    <li>Click the&nbsp;<b>External Id</b>&nbsp;link to view this action in the Hue Job Browser.</li>
+  </ul>
+  </li>
+  <li>The&nbsp;<b>Configuration</b>&nbsp;tab shows selected job configuration settings.</li>
+  <li>The&nbsp;<b>Logs</b>&nbsp;tab shows log output generated by the coordinator.</li>
+  <li>The&nbsp;<b>Definition</b>&nbsp;tab shows the Oozie coordinator&nbsp;definition, as it appears in the&nbsp;<tt>coordinator.xml</tt>&nbsp;file (also linked under&nbsp;the oozie.coord.application.path property in the &nbsp;<b>Configuration</b>&nbsp;tab).</li>
 </ul>
 
 
@@ -180,9 +180,9 @@ vertical-align: center;
 
 <p>Each row shows a workflow design: its name, description, timestamp of its last modification. It also shows:</p>
 <ul>
-	<li><b>Steps</b>: the number of steps in the workflow execution path. This is the number of execution steps between the start and end of the workflow. This will not necessarily be the same as the number of actions in the workflow, if there are control flow nodes in the control path.</li>
-	<li><b>Status</b>: who can run the workflow.&nbsp;<b>shared</b>&nbsp;means users other than the owner can access the workflow.&nbsp;<b>personal</b>&nbsp;means only the owner can modify or submit the workflow. The default is personal.</li>
-	<li><b>Owner</b>: the user that created the workflow.</li>
+  <li><b>Steps</b>: the number of steps in the workflow execution path. This is the number of execution steps between the start and end of the workflow. This will not necessarily be the same as the number of actions in the workflow, if there are control flow nodes in the control path.</li>
+  <li><b>Status</b>: who can run the workflow.&nbsp;<b>shared</b>&nbsp;means users other than the owner can access the workflow.&nbsp;<b>personal</b>&nbsp;means only the owner can modify or submit the workflow. The default is personal.</li>
+  <li><b>Owner</b>: the user that created the workflow.</li>
 </ul>
 
 
@@ -194,10 +194,10 @@ vertical-align: center;
 
 <p>To create a workflow:</p>
 <ol>
-	<li>Click the <b>Create</b>&nbsp;button at the top right of the Action Chooser.&nbsp;</li>
-	<li>In the Name field, type a name.</li>
-	<li>To specify the HDFS deployment directory and Oozie schema version click <b>advanced</b>.</li>
-	<li>Click <b>Save</b>. The workflow editor opens. Proceed with&nbsp;<a href="#OozieEditorandDashboard-EditingaWorkflow">Editing a Workflow</a>.</li>
+  <li>Click the <b>Create</b>&nbsp;button at the top right of the Action Chooser.&nbsp;</li>
+  <li>In the Name field, type a name.</li>
+  <li>To specify the HDFS deployment directory and Oozie schema version click <b>advanced</b>.</li>
+  <li>Click <b>Save</b>. The workflow editor opens. Proceed with&nbsp;<a href="#OozieEditorandDashboard-EditingaWorkflow">Editing a Workflow</a>.</li>
 </ol>
 
 
@@ -208,26 +208,26 @@ vertical-align: center;
 <p>In the workflow editor you can add and delete actions, clone actions, create and remove fork and join control nodes, and move actions as follows:</p>
 
 <ul>
-	<li>Add actions to the workflow by doing one of the following:
-	<ul>
-		<li>Click the <b>Add</b> tab.&nbsp;
-		<ol>
-			<li>Click a&nbsp;<b>&#43;&nbsp;</b><b><em>action</em></b> button,<b><em>&nbsp;</em></b>where <em>action</em> is MapReduce, Streaming, Java, Pig, Hive, Sqoop, Shell, or Ssh.</li>
-			<li>Set the action properties and click <b>Save</b>.</li>
-		</ol>
-		</li>
-		<li>Click the <b>Import</b> tab.
-		<ol>
-			<li>Click the <b>+ Job Design</b> button.</li>
-			<li>Click a radio button next to a job design and click&nbsp;<b>Import</b>.</li>
-		</ol>
-		</li>
-	</ul>
-	</li>
-	<li>Clone actions by clicking the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/retweet.png" style="border: 0px solid black"/></span>&nbsp;button.</li>
-	<li>Create or remove a fork and join by moving an action up or down with the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/updown.png" style="border: 0px solid black"/></span>&nbsp;buttons.</li>
-	<li>Change the position of an action by clicking the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/updown.png" style="border: 0px solid black"/></span>&nbsp;buttons in the same direction twice.</li>
-	<li><font color="#222222">To upload a file (for example,</font>&nbsp;<font color="#222222">a Pig script)&nbsp;</font><font color="#222222">to the folder containing the files referenced by the workflow, click the</font>&nbsp;<font color="#222222"><b>Upload</b></font><font color="#222222">&nbsp;button.</font></li>
+  <li>Add actions to the workflow by doing one of the following:
+  <ul>
+    <li>Click the <b>Add</b> tab.&nbsp;
+    <ol>
+      <li>Click a&nbsp;<b>&#43;&nbsp;</b><b><em>action</em></b> button,<b><em>&nbsp;</em></b>where <em>action</em> is MapReduce, Streaming, Java, Pig, Hive, Sqoop, Shell, or Ssh.</li>
+      <li>Set the action properties and click <b>Save</b>.</li>
+    </ol>
+    </li>
+    <li>Click the <b>Import</b> tab.
+    <ol>
+      <li>Click the <b>+ Job Design</b> button.</li>
+      <li>Click a radio button next to a job design and click&nbsp;<b>Import</b>.</li>
+    </ol>
+    </li>
+  </ul>
+  </li>
+  <li>Clone actions by clicking the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/retweet.png" style="border: 0px solid black"/></span>&nbsp;button.</li>
+  <li>Create or remove a fork and join by moving an action up or down with the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/updown.png" style="border: 0px solid black"/></span>&nbsp;buttons.</li>
+  <li>Change the position of an action by clicking the&nbsp; <span class="image-wrap" style=""><img src="/oozie/static/help/updown.png" style="border: 0px solid black"/></span>&nbsp;buttons in the same direction twice.</li>
+  <li><font color="#222222">To upload a file (for example,</font>&nbsp;<font color="#222222">a Pig script)&nbsp;</font><font color="#222222">to the folder containing the files referenced by the workflow, click the</font>&nbsp;<font color="#222222"><b>Upload</b></font><font color="#222222">&nbsp;button.</font></li>
 </ul>
 
 
@@ -239,10 +239,10 @@ vertical-align: center;
 <h2><a name="OozieEditorandDashboard-EditingWorkflowProperties"></a>Editing Workflow Properties</h2>
 
 <ol>
-	<li>In the workflow editor, click the <b>Properties</b>&nbsp;tab.</li>
-	<li>To share the workflow with all users, check the <b>Is shared</b> checkbox.</li>
-	<li>To set advanced execution options, click <b>advanced</b> and edit the deployment directory, add parameters and job properties, or Oozie schema version, .</li>
-	<li>Click <b>Save</b>.</li>
+  <li>In the workflow editor, click the <b>Properties</b>&nbsp;tab.</li>
+  <li>To share the workflow with all users, check the <b>Is shared</b> checkbox.</li>
+  <li>To set advanced execution options, click <b>advanced</b> and edit the deployment directory, add parameters and job properties, or Oozie schema version, .</li>
+  <li>Click <b>Save</b>.</li>
 </ol>
 
 
@@ -269,11 +269,11 @@ vertical-align: center;
 
 <p>To create a coordinator:</p>
 <ol>
-	<li>Click the&nbsp;<b>Create</b>&nbsp;button at the top right of the Action Chooser.&nbsp;</li>
-	<li>In the Name field, type a name.</li>
-	<li>In the Workflow drop-down list, choose a workflow that the coordinator will schedule.</li>
-	<li>In the Frequency area, specify how often the workflow will be scheduled and how many times it will run.</li>
-	<li>Click&nbsp;<b>Save</b>. The coordinator&nbsp;editor opens. Proceed with&nbsp;<a href="#OozieEditorandDashboard-EditingaCoordinator">Editing a Coordinator</a>.</li>
+  <li>Click the&nbsp;<b>Create</b>&nbsp;button at the top right of the Action Chooser.&nbsp;</li>
+  <li>In the Name field, type a name.</li>
+  <li>In the Workflow drop-down list, choose a workflow that the coordinator will schedule.</li>
+  <li>In the Frequency area, specify how often the workflow will be scheduled and how many times it will run.</li>
+  <li>Click&nbsp;<b>Save</b>. The coordinator&nbsp;editor opens. Proceed with&nbsp;<a href="#OozieEditorandDashboard-EditingaCoordinator">Editing a Coordinator</a>.</li>
 </ol>
 
 
@@ -286,13 +286,13 @@ vertical-align: center;
 <p>In the coordinator&nbsp;editor you specify coordinator properties and the datasets on which the workflow scheduled by the coordinator will operate as follows:</p>
 
 <ol>
-	<li>To share the coordinator&nbsp;with all users, check the&nbsp;<b>Is shared</b>&nbsp;checkbox.</li>
-	<li>To set advanced execution options, click <b>advanced</b>&nbsp;and fill in properties that determine how long a coordinator will wait before timing out, how many coordinators can wait and run concurrently, the coordinator scheduling policy, and the coordinator schema version.</li>
-	<li>In the Frequency area, set how many time thes communicator will run for each specified unit, the start and end time of the coordinator, and the timezone of the start and end times.</li>
-	<li><font color="#222222">The inputs and outputs of the workflow must be mapped to some data. Click</font> <font color="#222222"><b>Add</b></font> <font color="#222222">and select a dataset from the Dataset drop-down menu and map it to one variable of your workflow.</font><br/>
+  <li>To share the coordinator&nbsp;with all users, check the&nbsp;<b>Is shared</b>&nbsp;checkbox.</li>
+  <li>To set advanced execution options, click <b>advanced</b>&nbsp;and fill in properties that determine how long a coordinator will wait before timing out, how many coordinators can wait and run concurrently, the coordinator scheduling policy, and the coordinator schema version.</li>
+  <li>In the Frequency area, set how many time thes communicator will run for each specified unit, the start and end time of the coordinator, and the timezone of the start and end times.</li>
+  <li><font color="#222222">The inputs and outputs of the workflow must be mapped to some data. Click</font> <font color="#222222"><b>Add</b></font> <font color="#222222">and select a dataset from the Dataset drop-down menu and map it to one variable of your workflow.</font><br/>
 <font color="#222222">If no datasets exist, follow the procedure in</font>&nbsp;<a href="#OozieEditorandDashboard-CreatingaDataset">Creating a Dataset</a>.</li>
-	<li>Select a dataset from the <b>Dataset</b> drop-down menu.</li>
-	<li>Click <b>Save</b>.</li>
+  <li>Select a dataset from the <b>Dataset</b> drop-down menu.</li>
+  <li>Click <b>Save</b>.</li>
 </ol>
 
 
@@ -301,15 +301,15 @@ vertical-align: center;
 <h2><a name="OozieEditorandDashboard-CreatingaDataset"></a>Creating a Dataset</h2>
 
 <ol>
-	<li>In the coordinator&nbsp;editor, do one of the following:
-	<ul>
-		<li>Click the&nbsp;<b>Datasets</b>&nbsp;tab at the top of the editor.</li>
-		<li>In the Data area, click the&nbsp;<b>Datasets</b>&nbsp;button.</li>
-	</ul>
-	</li>
-	<li>Click <b>Create</b>.</li>
-	<li>In the Start and Frequency fields, specify when and how often input datasets will be available.</li>
-	<li>In the Uri field, specify a URI template for the location of input and output datasets. You can specify the variables&nbsp;
+  <li>In the coordinator&nbsp;editor, do one of the following:
+  <ul>
+    <li>Click the&nbsp;<b>Datasets</b>&nbsp;tab at the top of the editor.</li>
+    <li>In the Data area, click the&nbsp;<b>Datasets</b>&nbsp;button.</li>
+  </ul>
+  </li>
+  <li>Click <b>Create</b>.</li>
+  <li>In the Start and Frequency fields, specify when and how often input datasets will be available.</li>
+  <li>In the Uri field, specify a URI template for the location of input and output datasets. You can specify the variables&nbsp;
 <div class="preformatted panel" style="border-width: 1px;"><div class="preformattedContent panelContent">
 <pre>${YEAR},${MONTH},${DAY},${HOUR},${MINUTE}</pre>
 </div></div>
@@ -317,8 +317,8 @@ vertical-align: center;
 <div class="preformatted panel" style="border-width: 1px;"><div class="preformattedContent panelContent">
 <pre>hdfs://foo:9000/usr/app/stats/${YEAR}/${MONTH}/data</pre>
 </div></div></li>
-	<li>Specify the timezone of the start date.</li>
-	<li>In the Done flag field, specify the flag that identifies when input datasets are no longer ready.</li>
+  <li>Specify the timezone of the start date.</li>
+  <li>In the Done flag field, specify the flag that identifies when input datasets are no longer ready.</li>
 </ol>
 
 
@@ -331,10 +331,10 @@ vertical-align: center;
 
 <p>The Submissions History is where you view the history of workflow and coordinator jobs. Clicking a link in the Name column opens the workflow or coordinator in an editor. Clicking a link in the Submission Id column opens the job in the Dashboard.</p>
 
-				    					    <br/>
+                          <br/>
                         </td>
-		    </tr>
-	    </table>
-	
+        </tr>
+      </table>
+  
     </body>
 </html>

+ 193 - 4
apps/oozie/static/js/workflow.js

@@ -28,7 +28,8 @@ if (!('filter' in Array.prototype)) {
 // Since knockout maps arrays without calling "update" nor "create"
 // Provide a JSON string that will be parsed in custom 'create' and 'update' functions.
 var normalize_model_fields = (function() {
-  var FIELDS = ['parameters', 'job_properties', 'files', 'archives', 'prepares', 'params'];
+  var FIELDS = ['parameters', 'job_properties', 'files', 'archives', 'prepares', 'params',
+                'deletes', 'mkdirs', 'moves', 'chmods', 'touchzs'];
   function fn(node_model) {
     $.each(FIELDS, function(index, field) {
       if (field in node_model && $.isArray(node_model[field])) {
@@ -403,6 +404,21 @@ $.extend(SshModel.prototype, {
   child_links: []
 });
 
+var FsModel = ModelModule($);
+$.extend(FsModel.prototype, {
+  id: 0,
+  name: '',
+  description: '',
+  node_type: 'fs',
+  workflow: 0,
+  deletes: '[]',
+  mkdirs: '[]',
+  moves: '[]',
+  chmods: '[]',
+  touchzs: '[]',
+  child_links: []
+});
+
 function nodeModelChooser(node_type) {
   switch(node_type) {
     case 'mapreduce':
@@ -423,6 +439,8 @@ function nodeModelChooser(node_type) {
       return SshModel;
     case 'distcp':
       return DistCPModel;
+    case 'fs':
+        return FsModel;
     case 'fork':
       return ForkModel;
     case 'decision':
@@ -442,6 +460,7 @@ var IdGeneratorTable = {
   shell: new IdGenerator({prefix: 'shell'}),
   ssh: new IdGenerator({prefix: 'ssh'}),
   distcp: new IdGenerator({prefix: 'distcp'}),
+  fs: new IdGenerator({prefix: 'fs'}),
   fork: new IdGenerator({prefix: 'fork'}),
   decision: new IdGenerator({prefix: 'decision'}),
   join: new IdGenerator({prefix: 'join'}),
@@ -673,6 +692,90 @@ var NodeModule = function($, IdGeneratorTable) {
       };
     }
 
+    if ('deletes' in model) {
+
+        self.addDelete = function() {
+          var prop = { name: ko.observable("") };
+          prop.name.subscribe(function(value) {
+            self.deletes.valueHasMutated();
+          });
+          self.deletes.push(prop);
+        };
+
+        self.removeDelete = function(val) {
+          self.deletes.remove(val);
+        };
+      }
+
+    if ('mkdirs' in model) {
+
+        self.addMkdir = function() {
+          var prop = { name: ko.observable("") };
+          prop.name.subscribe(function(value) {
+            self.mkdirs.valueHasMutated();
+          });
+          self.mkdirs.push(prop);
+        };
+
+        self.removeMkdir = function(val) {
+          self.mkdirs.remove(val);
+        };
+      }
+
+    if ('moves' in model) {
+
+        self.addMove = function() {
+          var prop = { source: ko.observable(""), destination: ko.observable("") };
+          prop.source.subscribe(function(value) {
+            self.moves.valueHasMutated();
+          });
+          prop.destination.subscribe(function(value) {
+            self.moves.valueHasMutated();
+          });
+          self.moves.push(prop);
+        };
+
+        self.removeMove = function(val) {
+          self.moves.remove(val);
+        };
+      }
+
+    if ('chmods' in model) {
+
+        self.addChmod = function() {
+          var prop = { path: ko.observable(""), permissions: ko.observable(""), recursive: ko.observable("") };
+          prop.path.subscribe(function(value) {
+            self.chmods.valueHasMutated();
+          });
+          prop.permissions.subscribe(function(value) {
+            self.chmods.valueHasMutated();
+          });
+          prop.recursive.subscribe(function(value) {
+            self.chmods.valueHasMutated();
+          });
+          self.chmods.push(prop);
+        };
+
+        self.removeChmod = function(val) {
+          self.chmods.remove(val);
+        };
+      }
+
+    if ('touchzs' in model) {
+
+        self.addTouchz = function() {
+          var prop = { name: ko.observable("") };
+          prop.name.subscribe(function(value) {
+            self.touchzs.valueHasMutated();
+          });
+          self.touchzs.push(prop);
+        };
+
+        self.removeTouchz = function(val) {
+          self.touchzs.remove(val);
+        };
+      }
+
     self.initialize.apply(self, arguments);
 
     return self;
@@ -724,7 +827,7 @@ var NodeModule = function($, IdGeneratorTable) {
 
       // @see http://knockoutjs.com/documentation/plugins-mapping.html
       var mapping = ko.mapping.fromJS(model, {
-        ignore: ['initialize','toString'],
+        ignore: ['initialize', 'toString'],
         job_properties: {
           create: function(options) {
             var parent = options.parent;
@@ -844,7 +947,93 @@ var NodeModule = function($, IdGeneratorTable) {
 
             return map_params(options, subscribe);
           }
-        }
+        },
+        deletes: {
+          create: function(options) {
+            return map_params(options, function() {});
+          },
+          update: function(options) {
+            return map_params(options, function() {});
+          }
+        },
+        mkdirs: {
+          create: function(options) {
+            return map_params(options, function() {});
+          },
+          update: function(options) {
+            return map_params(options, function() {});
+          }
+        },
+        moves: {
+          create: function(options) {
+            var parent = options.parent;
+            var subscribe = function(mapping) {
+              mapping.source.subscribe(function(value) {
+                parent.moves.valueHasMutated();
+              });
+              mapping.destination.subscribe(function(value) {
+                parent.moves.valueHasMutated();
+              });
+            };
+
+            return map_params(options, subscribe);
+          },
+          update: function(options) {
+            var parent = options.parent;
+            var subscribe = function(mapping) {
+              mapping.source.subscribe(function(value) {
+                parent.moves.valueHasMutated();
+              });
+              mapping.destination.subscribe(function(value) {
+                parent.moves.valueHasMutated();
+              });
+            };
+
+             return map_params(options, subscribe);
+           }
+         },
+         chmods: {
+           create: function(options) {
+             var parent = options.parent;
+             var subscribe = function(mapping) {
+               mapping.path.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+               mapping.permissions.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+               mapping.recursive.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+             };
+
+             return map_params(options, subscribe);
+           },
+           update: function(options) {
+             var parent = options.parent;
+             var subscribe = function(mapping) {
+               mapping.path.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+               mapping.permissions.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+               mapping.recursive.subscribe(function(value) {
+                 parent.chmods.valueHasMutated();
+               });
+             };
+
+             return map_params(options, subscribe);
+           }
+         },
+         touchzs: {
+           create: function(options) {
+             return map_params(options, function() {});
+           },
+           update: function(options) {
+             return map_params(options, function() {});
+           }
+         },
       });
 
       $.extend(self, mapping);
@@ -1463,7 +1652,7 @@ var WorkflowModule = function($, NodeModelChooser, Node, ForkNode, DecisionNode,
 
     // @see http://knockoutjs.com/documentation/plugins-mapping.html
     var mapping = ko.mapping.fromJS(options.model, {
-      ignore: ['initialize','toString'],
+      ignore: ['initialize', 'toString'],
       job_properties: {
         create: function(options) {
           var mapping = ko.mapping.fromJS($.parseJSON(options.data) || options.data);