Browse Source

[oozie] Add global configuration to Workflow

Add schema version to Job
Add global Workflow job-xml
Add global Workflow configuration parameters
Fix coordinator timeout format
Romain Rigaux 13 years ago
parent
commit
670d875

+ 2 - 0
apps/oozie/src/oozie/fixtures/initial_data.json

@@ -5,6 +5,7 @@
     "fields": {
     "fields": {
       "is_shared": true,
       "is_shared": true,
       "name": "SleepWorkflow",
       "name": "SleepWorkflow",
+      "schema_version": "uri:oozie:workflow:0.4",
       "deployment_dir": "/user/hue/oozie/examples/sleep",
       "deployment_dir": "/user/hue/oozie/examples/sleep",
       "schema_version": "",
       "schema_version": "",
       "last_modified": "2012-08-20 13:13:34",
       "last_modified": "2012-08-20 13:13:34",
@@ -18,6 +19,7 @@
     "fields": {
     "fields": {
       "is_shared": true,
       "is_shared": true,
       "name": "DailySleep",
       "name": "DailySleep",
+      "schema_version": "uri:oozie:coordinator:0.2",
       "deployment_dir": "/user/hue/oozie/examples/sleep",
       "deployment_dir": "/user/hue/oozie/examples/sleep",
       "schema_version": "",
       "schema_version": "",
       "last_modified": "2012-08-20 13:56:53",
       "last_modified": "2012-08-20 13:56:53",

+ 17 - 2
apps/oozie/src/oozie/forms.py

@@ -34,13 +34,22 @@ class ParameterForm(forms.Form):
 class WorkflowForm(forms.ModelForm):
 class WorkflowForm(forms.ModelForm):
   class Meta:
   class Meta:
     model = Workflow
     model = Workflow
-    exclude = ('owner', 'start', 'end', 'schema_version')
+    exclude = ('owner', 'start', 'end')
     widgets = {
     widgets = {
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'deployment_dir': forms.TextInput(attrs={'class': 'pathChooser', 'style': "width:535px"}),
       'deployment_dir': forms.TextInput(attrs={'class': 'pathChooser', 'style': "width:535px"}),
       'parameters': forms.widgets.HiddenInput(),
       'parameters': forms.widgets.HiddenInput(),
+      'job_xml': forms.widgets.HiddenInput(),
+      'job_properties': forms.widgets.HiddenInput(),
     }
     }
 
 
+  def __init__(self, *args, **kwargs):
+    super(WorkflowForm, self).__init__(*args, **kwargs)
+    self.fields['schema_version'].widget = forms.Select(choices=(('uri:oozie:workflow:0.1', '0.1'),
+                                                                 ('uri:oozie:workflow:0.2', '0.2'),
+                                                                 ('uri:oozie:workflow:0.3', '0.3'),
+                                                                 ('uri:oozie:workflow:0.4', '0.4')))
+
 
 
 class ImportJobsubDesignForm(forms.Form):
 class ImportJobsubDesignForm(forms.Form):
   """Used for specifying what oozie actions to import"""
   """Used for specifying what oozie actions to import"""
@@ -146,12 +155,18 @@ class CoordinatorForm(forms.ModelForm):
 
 
   class Meta:
   class Meta:
     model = Coordinator
     model = Coordinator
-    exclude = ('owner', 'schema_version', 'deployment_dir')
+    exclude = ('owner', 'deployment_dir')
     widgets = {
     widgets = {
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'description': forms.TextInput(attrs={'class': 'span5'}),
       'parameters': forms.widgets.HiddenInput(),
       'parameters': forms.widgets.HiddenInput(),
     }
     }
 
 
+  def __init__(self, *args, **kwargs):
+    super(CoordinatorForm, self).__init__(*args, **kwargs)
+    self.fields['schema_version'].widget = forms.Select(choices=(('uri:oozie:coordinator:0.1', '0.1'),
+                                                                 ('uri:oozie:coordinator:0.2', '0.2'),
+                                                                 ('uri:oozie:coordinator:0.3', '0.3'),
+                                                                 ('uri:oozie:coordinator:0.4', '0.4')))
 
 
 class DatasetForm(forms.ModelForm):
 class DatasetForm(forms.ModelForm):
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],
   start = forms.SplitDateTimeField(input_time_formats=[TIME_FORMAT],

+ 32 - 19
apps/oozie/src/oozie/models.py

@@ -98,14 +98,15 @@ class Job(models.Model):
       help_text=_('Name of the job, which must be unique per user.'))
       help_text=_('Name of the job, which must be unique per user.'))
   description = models.CharField(max_length=1024, blank=True, help_text=_('What is the purpose of the job.'))
   description = models.CharField(max_length=1024, blank=True, help_text=_('What is the purpose of the job.'))
   last_modified = models.DateTimeField(auto_now=True, db_index=True)
   last_modified = models.DateTimeField(auto_now=True, db_index=True)
-  schema_version = models.CharField(max_length=128, blank=True, default='')
+  schema_version = models.CharField(max_length=128,
+                                    help_text=_t('The version of the XML schema used to talk to Oozie.'))
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_('HDFS deployment directory.'),
   deployment_dir = models.CharField(max_length=1024, blank=True, verbose_name=_('HDFS deployment directory.'),
                                     help_text=_('The path on the HDFS where all the workflows and '
                                     help_text=_('The path on the HDFS where all the workflows and '
                                                 'dependencies must be uploaded.'))
                                                 'dependencies must be uploaded.'))
   is_shared = models.BooleanField(default=False, db_index=True,
   is_shared = models.BooleanField(default=False, db_index=True,
                                   help_text=_('Check if you want to have some other users to have access to this job.'))
                                   help_text=_('Check if you want to have some other users to have access to this job.'))
   parameters = models.TextField(default='[]',
   parameters = models.TextField(default='[]',
-                                help_text=_t('Set some variables of the job (e.g. market=US)'))
+                                help_text=_t('Set some parameters used at the submission time (e.g. market=US).'))
 
 
   objects = JobManager()
   objects = JobManager()
   unique_together = ('owner', 'name')
   unique_together = ('owner', 'name')
@@ -167,7 +168,7 @@ class Job(models.Model):
 
 
 class WorkflowManager(models.Manager):
 class WorkflowManager(models.Manager):
   def new_workflow(self, owner):
   def new_workflow(self, owner):
-    workflow = Workflow(owner=owner)
+    workflow = Workflow(owner=owner, schema_version='uri:oozie:workflow:0.4')
 
 
     kill = Kill(name='kill', workflow=workflow, node_type=Kill.node_type)
     kill = Kill(name='kill', workflow=workflow, node_type=Kill.node_type)
     end = End(name='end', workflow=workflow, node_type=End.node_type)
     end = End(name='end', workflow=workflow, node_type=End.node_type)
@@ -208,7 +209,14 @@ class Workflow(Job):
   is_single = models.BooleanField(default=False)
   is_single = models.BooleanField(default=False)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   start = models.ForeignKey('Start', related_name='start_workflow', blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
   end  = models.ForeignKey('End', related_name='end_workflow',  blank=True, null=True)
-  # jobxml
+  job_xml = models.CharField(max_length=PATH_MAX, default='', blank=True,
+                             help_text=_t('Refer to a Hadoop JobConf job.xml file bundled in the workflow deployment directory. '
+                                          'Properties specified in the configuration element override properties specified in the '
+                                          'files specified by any job-xml elements.'))
+  job_properties = models.TextField(default='[]',
+                                    help_text=_t('Job configuration properties used by all the actions of the workflow '
+                                                 '(e.g. mapred.job.queue.name=production)'))
+
   objects = WorkflowManager()
   objects = WorkflowManager()
 
 
   HUE_ID = 'hue-id-w'
   HUE_ID = 'hue-id-w'
@@ -216,6 +224,9 @@ class Workflow(Job):
   def get_type(self):
   def get_type(self):
     return 'workflow'
     return 'workflow'
 
 
+  def get_properties(self):
+    return json.loads(self.job_properties)
+
   def add_action(self, action, parent_action_id):
   def add_action(self, action, parent_action_id):
     parent = Node.objects.get(id=parent_action_id).get_full_node()
     parent = Node.objects.get(id=parent_action_id).get_full_node()
     error = Kill.objects.get(name='kill', workflow=self)
     error = Kill.objects.get(name='kill', workflow=self)
@@ -713,6 +724,7 @@ class Action(Node):
 # When adding a new action, also update
 # When adding a new action, also update
 #  - Action.types below
 #  - Action.types below
 #  - Node.get_full_node()
 #  - Node.get_full_node()
+#  - forms.py _node_type_TO_FORM_CLS
 
 
 class Mapreduce(Action):
 class Mapreduce(Action):
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'jar_path', 'prepares')
   PARAM_FIELDS = ('files', 'archives', 'job_properties', 'jar_path', 'prepares')
@@ -732,6 +744,10 @@ class Mapreduce(Action):
                               help_text=_t('Local or absolute path to the %(program)s jar file on HDFS') % {'program': 'MapReduce'})
                               help_text=_t('Local or absolute path to the %(program)s jar file on HDFS') % {'program': 'MapReduce'})
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
                                                          'This should be used exclusively for directory cleanup'))
                                                          'This should be used exclusively for directory cleanup'))
+  job_xml = models.CharField(max_length=PATH_MAX, default='', blank=True,
+                             help_text=_t('Refer to a Hadoop JobConf job.xml file bundled in the workflow deployment directory. '
+                                          'Properties specified in the configuration element override properties specified in the '
+                                          'files specified by any job-xml elements.'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -803,6 +819,10 @@ class Java(Action):
                                     help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
                                     help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
                                                          'This should be used exclusively for directory cleanup'))
                                                          'This should be used exclusively for directory cleanup'))
+  job_xml = models.CharField(max_length=PATH_MAX, default='', blank=True,
+                             help_text=_t('Refer to a Hadoop JobConf job.xml file bundled in the workflow deployment directory. '
+                                          'Properties specified in the configuration element override properties specified in the '
+                                          'files specified by any job-xml elements.'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -836,6 +856,10 @@ class Pig(Action):
                                     help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
                                     help_text=_t('For the job configuration (e.g. mapred.job.queue.name=production'))
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
   prepares = models.TextField(default="[]", help_text=_t('List of paths to delete or create before starting the application. '
                                                          'This should be used exclusively for directory cleanup'))
                                                          'This should be used exclusively for directory cleanup'))
+  job_xml = models.CharField(max_length=PATH_MAX, default='', blank=True,
+                             help_text=_t('Refer to a Hadoop JobConf job.xml file bundled in the workflow deployment directory. '
+                                          'Properties specified in the configuration element override properties specified in the '
+                                          'files specified by any job-xml elements.'))
 
 
   def get_properties(self):
   def get_properties(self):
     return json.loads(self.job_properties)
     return json.loads(self.job_properties)
@@ -973,13 +997,10 @@ class Coordinator(Job):
                              help_text=_t('When we need to start the last workflow.'))
                              help_text=_t('When we need to start the last workflow.'))
   workflow = models.ForeignKey(Workflow, null=True,
   workflow = models.ForeignKey(Workflow, null=True,
                                help_text=_t('The corresponding workflow we want to schedule repeatedly.'))
                                help_text=_t('The corresponding workflow we want to schedule repeatedly.'))
-  timeout_number = models.SmallIntegerField(default=1, choices=FREQUENCY_NUMBERS,
-                                            help_text=_t('Timeout for its coordinator actions, this is, how long the coordinator action will be in '
-                                                         'WAITING or READY status before giving up on its execution.'))
-  timeout_unit = models.CharField(max_length=20, choices=FREQUENCY_UNITS, default='days',
-                                    help_text=_t('It represents the unit of time of the timeous.'))
-
-
+  timeout = models.SmallIntegerField(null=True, blank=True,
+                                     help_text=_t('Timeout for its coordinator actions, in minutes. This is how long '
+                                                  'the coordinator action will be in '
+                                                  'WAITING or READY status before giving up on its execution.'))
   concurrency = models.PositiveSmallIntegerField(null=True, blank=True, choices=FREQUENCY_NUMBERS,
   concurrency = models.PositiveSmallIntegerField(null=True, blank=True, choices=FREQUENCY_NUMBERS,
                                  help_text=_t('Concurrency for its coordinator actions, this is, how many coordinator actions are '
                                  help_text=_t('Concurrency for its coordinator actions, this is, how many coordinator actions are '
                                               'allowed to run concurrently ( RUNNING status) before the coordinator engine '
                                               'allowed to run concurrently ( RUNNING status) before the coordinator engine '
@@ -1071,14 +1092,6 @@ class Coordinator(Job):
   def text_frequency(self):
   def text_frequency(self):
     return '%(number)d %(unit)s' % {'unit': self.frequency_unit, 'number': self.frequency_number}
     return '%(number)d %(unit)s' % {'unit': self.frequency_unit, 'number': self.frequency_number}
 
 
-  @property
-  def timeout(self):
-    return '${coord:%(unit)s(%(number)d)}' % {'unit': self.timeout_unit, 'number': self.timeout_number}
-
-  @property
-  def text_timeout(self):
-    return '%(number)d %(unit)s' % {'unit': self.timeout_unit, 'number': self.timeout_number}
-
   def find_parameters(self):
   def find_parameters(self):
     params = set()
     params = set()
 
 

+ 92 - 0
apps/oozie/src/oozie/templates/editor/coordinator_properties.mako

@@ -0,0 +1,92 @@
+## 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 django.utils.translation import ugettext as _
+%>
+
+<%namespace name="utils" file="../../utils.inc.mako" />
+
+
+<%def name="print_key_value(field, element, initial_value)">
+  <div class="control-group ko-${element}" rel="popover"
+      data-original-title="${ field.label }" data-content="${ field.help_text }">
+    <label class="control-label">${ field.label }</label>
+    <div class="controls">
+      <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
+        <thead>
+          <tr>
+            <th>${ _('Name') }</th>
+            <th>${ _('Value') }</th>
+            <th/>
+          </tr>
+        </thead>
+        <tbody data-bind="foreach: ${ element }">
+          <tr>
+            <td><input class="required" data-bind="value: name, uniqueName: false" /></td>
+            <td><input class="required" data-bind="value: value, uniqueName: false" /></td>
+            <td><a class="btn btn-small" href="#" data-bind="click: $root.remove_${ element }">${ _('Delete') }</a></td>
+          </tr>
+        </tbody>
+      </table>
+      % if field.errors:
+        <div class="row">
+          <div class="alert alert-error">
+            ${ unicode(field.errors) | n }
+          </div>
+        </div>
+      % endif
+
+      <button class="btn" data-bind="click: add_${ element }">${ _('Add') }</button>
+    </div>
+  </div>
+
+  <style>
+    .designTable th {
+      text-align:left;
+    }
+  </style>
+
+  <script type="text/javascript">
+    $(document).ready(function(){
+      var ViewModel = function(${ element }) {
+        var self = this;
+        self.${ element } = ko.observableArray(${ element });
+
+        self.add_parameters = function() {
+          self.${ element }.push({name: "", value: ""});
+        };
+
+        self.remove_parameters = function(val) {
+          self.${ element }.remove(val);
+        };
+
+        self.submit = function(form) {
+          var form = $("#jobForm");
+
+          $("<input>").attr("type", "hidden")
+              .attr("name", "${ element }")
+              .attr("value", ko.utils.stringifyJson(self.${ element }))
+              .appendTo(form);
+
+          form.submit();
+        };
+      };
+
+      window.viewModel = new ViewModel(${ initial_value });
+    });
+  </script>
+</%def>

+ 4 - 0
apps/oozie/src/oozie/templates/editor/create_coordinator.mako

@@ -51,6 +51,10 @@ ${ layout.menubar(section='coordinators') }
              ${ utils.render_field(coordinator_form['description']) }
              ${ utils.render_field(coordinator_form['description']) }
              ${ utils.render_field(coordinator_form['workflow']) }
              ${ utils.render_field(coordinator_form['workflow']) }
              ${ coordinator_form['parameters'] }
              ${ coordinator_form['parameters'] }
+             <div class="hide">
+               ${ utils.render_field(coordinator_form['timeout']) }
+               ${ utils.render_field(coordinator_form['schema_version']) }
+             </div>
            </div>
            </div>
 
 
           <hr/>
           <hr/>

+ 7 - 8
apps/oozie/src/oozie/templates/editor/create_workflow.mako

@@ -57,7 +57,14 @@ ${ layout.menubar(section='workflows') }
 
 
             <div id="advanced-container" class="hide">
             <div id="advanced-container" class="hide">
               ${ utils.render_field(workflow_form['deployment_dir']) }
               ${ utils.render_field(workflow_form['deployment_dir']) }
+              ${ utils.render_field(workflow_form['schema_version']) }
+              ${ utils.render_field(workflow_form['job_xml']) }
            </div>
            </div>
+           
+           <div class="hide">
+             ${ workflow_form['job_properties'] }
+             ${ workflow_form['parameters'] }
+         </div>           
          </fieldset>
          </fieldset>
 
 
         <div class="span2"></div>
         <div class="span2"></div>
@@ -72,14 +79,6 @@ ${ layout.menubar(section='workflows') }
     </div>
     </div>
 </div>
 </div>
 
 
-<style>
-  #advanced-btn { center:right};
-  a#advanced-btn:link {color: black; text-decoration: none; }
-  a#advanced-btn:active {color: black; text-decoration: none; }
-  a#advanced-btn:visited {color: black; text-decoration: none; }
-  a#advanced-btn:hover {color: black; text-decoration: none; }
-</style>
-
 ${ utils.path_chooser_libs(True) }
 ${ utils.path_chooser_libs(True) }
 
 
 ${ commonfooter(messages) }
 ${ commonfooter(messages) }

+ 11 - 9
apps/oozie/src/oozie/templates/editor/edit_coordinator.mako

@@ -21,7 +21,7 @@
 
 
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="layout" file="../navigation-bar.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
 <%namespace name="utils" file="../utils.inc.mako" />
-<%namespace name="properties" file="job_action_properties.mako" />
+<%namespace name="properties" file="coordinator_properties.mako" />
 <%namespace name="coordinator_data" file="create_coordinator_data.mako" />
 <%namespace name="coordinator_data" file="create_coordinator_data.mako" />
 
 
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
 ${ commonheader(_("Oozie App"), "oozie", "100px") }
@@ -71,17 +71,18 @@ ${ layout.menubar(section='coordinators') }
                </div>
                </div>
 
 
                <div id="advanced-container" class="hide">
                <div id="advanced-container" class="hide">
-                 ${ properties.print_key_value(_('Parameters'), 'parameters', coordinator_form, parameters) }
+                 ${ properties.print_key_value(coordinator_form['parameters'], 'parameters', parameters) }
+                 ${ utils.render_field(coordinator_form['timeout']) }
                  <div class="row-fluid">
                  <div class="row-fluid">
                    <div class="span6">
                    <div class="span6">
-                   ${ utils.render_field(coordinator_form['timeout_number']) }
-                 </div>
-                 <div class="span6">
-                   ${ utils.render_field(coordinator_form['timeout_unit']) }
+                     ${ utils.render_field(coordinator_form['concurrency']) }
+                   </div>
+                   <div class="span6">
+                     ${ utils.render_field(coordinator_form['throttle']) }
+                   </div>
                  </div>
                  </div>
-                 ${ utils.render_field(coordinator_form['concurrency']) }
                  ${ utils.render_field(coordinator_form['execution']) }
                  ${ utils.render_field(coordinator_form['execution']) }
-                 ${ utils.render_field(coordinator_form['throttle']) }
+                 ${ utils.render_field(coordinator_form['schema_version']) }
               </div>
               </div>
              </div>
              </div>
 
 
@@ -261,7 +262,8 @@ ${ layout.menubar(section='coordinators') }
                     <tr>
                     <tr>
                       <td>
                       <td>
                         % if can_edit_coordinator:
                         % if can_edit_coordinator:
-                          <a href="javascript:modalRequest('${ url('oozie:edit_coordinator_dataset', dataset=form.instance.id) }', '#edit-dataset-modal');" data-row-selector="true"/>
+                          <a href="javascript:modalRequest('${ url('oozie:edit_coordinator_dataset', dataset=form.instance.id) }', '#edit-dataset-modal');"
+                             data-row-selector="true"/>
                         % endif
                         % endif
                         ${ form.instance.name }
                         ${ form.instance.name }
                       </td>
                       </td>

+ 32 - 4
apps/oozie/src/oozie/templates/editor/edit_workflow.mako

@@ -135,16 +135,33 @@ ${ layout.menubar(section='workflows') }
               <fieldset>
               <fieldset>
                 ${ utils.render_field(workflow_form['name']) }
                 ${ utils.render_field(workflow_form['name']) }
                 ${ utils.render_field(workflow_form['description']) }
                 ${ utils.render_field(workflow_form['description']) }
-                ${ utils.render_field(workflow_form['deployment_dir']) }
                 ${ utils.render_field(workflow_form['is_shared']) }
                 ${ utils.render_field(workflow_form['is_shared']) }
-                ${ properties.print_key_value(_('Parameters'), 'parameters', workflow_form, parameters) }
+
+                <div class="control-group ">
+                  <label class="control-label">
+                    <a href="#" id="advanced-btn" onclick="$('#advanced-container').toggle('hide')">
+                      <i class="icon-share-alt"></i> ${ _('advanced') }</a>
+                  </label>
+                  <div class="controls"></div>
+                </div>
+
+               <div id="advanced-container" class="hide">
+                 ${ utils.render_field(workflow_form['deployment_dir']) }
+                 ${ properties.print_key_value(workflow_form['parameters'], 'parameters', parameters) }
+                 ${ workflow_form['parameters'] }
+                 ${ properties.print_key_value(workflow_form['job_properties'], 'job_properties', job_properties) }
+                 ${ workflow_form['job_properties'] }
+                 ${ utils.render_field(workflow_form['schema_version']) }
+                 ${ utils.render_field(workflow_form['job_xml']) }
+               </div>
+
              </fieldset>
              </fieldset>
            </div>
            </div>
         </div>
         </div>
         <div class="form-actions center">
         <div class="form-actions center">
           <a href="${ url('oozie:list_workflows') }" class="btn">${ _('Back') }</a>
           <a href="${ url('oozie:list_workflows') }" class="btn">${ _('Back') }</a>
           % if user_can_edit_job:
           % if user_can_edit_job:
-            <button data-bind="click: submit" class="btn btn-primary">${ _('Save') }</button>
+            <button class="btn btn-primary">${ _('Save') }</button>
           % endif
           % endif
         </div>
         </div>
         <div class="span3"></div>
         <div class="span3"></div>
@@ -239,9 +256,20 @@ modal-window .modal-content {
         }
         }
       });
       });
     });
     });
-
+    /*
+    var viewModel = function(){};
+    window.viewModel = new viewModel();
     ko.applyBindings(window.viewModel);
     ko.applyBindings(window.viewModel);
 
 
+     */
+     ko.applyBindings(window.viewModelparameters, $("#parameters")[0]);
+     ko.applyBindings(window.viewModeljob_properties, $("#job_properties")[0]);
+
+    $('#jobForm').submit(function() {
+    window.viewModelparameters.pre_submit();
+    window.viewModeljob_properties.pre_submit();
+  })
+
     $("a[data-row-selector='true']").jHueRowSelector();
     $("a[data-row-selector='true']").jHueRowSelector();
 
 
     $("*[rel=popover]").popover({
     $("*[rel=popover]").popover({

+ 3 - 3
apps/oozie/src/oozie/templates/editor/gen/coordinator.xml.mako

@@ -18,10 +18,10 @@
 <coordinator-app name="${ coord.name }"
 <coordinator-app name="${ coord.name }"
   frequency="${ coord.frequency }"
   frequency="${ coord.frequency }"
   start="${ coord.start_utc }" end="${ coord.end_utc }" timezone="${ coord.timezone }"
   start="${ coord.start_utc }" end="${ coord.end_utc }" timezone="${ coord.timezone }"
-  xmlns="uri:oozie:coordinator:0.1">
-  % if (coord.timeout_number and coord.timeout_unit) or coord.concurrency or coord.execution or coord.throttle:
+  xmlns="${ coord.schema_version }">
+  % if coord.timeout or coord.concurrency or coord.execution or coord.throttle:
   <controls>
   <controls>
-    % if coord.timeout_number and coord.timeout_unit:
+    % if coord.timeout:
     <timeout>${ coord.timeout }</timeout>
     <timeout>${ coord.timeout }</timeout>
     % endif
     % endif
     % if coord.concurrency:
     % if coord.concurrency:

+ 11 - 1
apps/oozie/src/oozie/templates/editor/gen/workflow.xml.mako

@@ -14,8 +14,18 @@
 ## See the License for the specific language governing permissions and
 ## See the License for the specific language governing permissions and
 ## limitations under the License.
 ## limitations under the License.
 
 
+<%namespace name="common" file="workflow-common.xml.mako" />
 
 
-<workflow-app name="${ workflow.name }" xmlns="uri:oozie:workflow:0.2">
+
+<workflow-app name="${ workflow.name }" xmlns="${ workflow.schema_version }">
+  % if workflow.job_xml or workflow.get_properties():
+  <global>
+    % if workflow.job_xml:
+      <job-xml>${ workflow.job_xml }</job-xml>
+      ${ common.configuration(workflow.get_properties()) }
+    % endif
+  </global>
+  % endif
   % for node in workflow.node_list:
   % for node in workflow.node_list:
       ${ node.to_xml() }
       ${ node.to_xml() }
   % endfor
   % endfor

+ 15 - 22
apps/oozie/src/oozie/templates/editor/job_action_properties.mako

@@ -21,10 +21,10 @@
 <%namespace name="utils" file="../../utils.inc.mako" />
 <%namespace name="utils" file="../../utils.inc.mako" />
 
 
 
 
-<%def name="print_key_value(label, element, form, initial_parameters)">
-  <div class="control-group ko-${element}" rel="popover"
-      data-original-title="${ label }" data-content="${ _('Set some variables of the job (e.g. market=US)') }">
-    <label class="control-label">${ label }</label>
+<%def name="print_key_value(field, element, initial_value)">
+  <div id="${ element }" class="control-group ko-${element}" rel="popover"
+      data-original-title="${ field.label }" data-content="${ field.help_text }">
+    <label class="control-label">${ field.label }</label>
     <div class="controls">
     <div class="controls">
       <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
       <table class="table-condensed designTable" data-bind="visible: ${ element }().length > 0">
         <thead>
         <thead>
@@ -42,10 +42,10 @@
           </tr>
           </tr>
         </tbody>
         </tbody>
       </table>
       </table>
-      % if form[element].errors:
+      % if field.errors:
         <div class="row">
         <div class="row">
           <div class="alert alert-error">
           <div class="alert alert-error">
-            ${ unicode(form[element].errors) | n }
+            ${ unicode(field.errors) | n }
           </div>
           </div>
         </div>
         </div>
       % endif
       % endif
@@ -62,31 +62,24 @@
 
 
   <script type="text/javascript">
   <script type="text/javascript">
     $(document).ready(function(){
     $(document).ready(function(){
-      var ViewModel = function(parameters) {
+      var ViewModel = function(${ element }) {
         var self = this;
         var self = this;
-        self.parameters = ko.observableArray(parameters);
+        self.${ element } = ko.observableArray(${ element });
 
 
-        self.add_parameters = function() {
-          self.parameters.push({name: "", value: ""});
+        self.add_${ element } = function() {
+          self.${ element }.push({name: "", value: ""});
         };
         };
 
 
-        self.remove_parameters = function(val) {
-          self.parameters.remove(val);
+        self.remove_${ element } = function(val) {
+          self.${ element }.remove(val);
         };
         };
 
 
-        self.submit = function(form) {
-          var form = $("#jobForm");
-
-          $("<input>").attr("type", "hidden")
-              .attr("name", "parameters")
-              .attr("value", ko.utils.stringifyJson(self.parameters))
-              .appendTo(form);
-
-          form.submit();
+        self.pre_submit = function(form) {
+          $("#id_${ element }").attr("value", ko.utils.stringifyJson(self.${ element }));
         };
         };
       };
       };
 
 
-      window.viewModel = new ViewModel(${ initial_parameters });
+      window.viewModel${ element } = new ViewModel(${ initial_value });
     });
     });
   </script>
   </script>
 </%def>
 </%def>

+ 17 - 4
apps/oozie/src/oozie/tests.py

@@ -339,6 +339,15 @@ class TestEditor:
   def test_workflow_gen_xml(self):
   def test_workflow_gen_xml(self):
     assert_equal(
     assert_equal(
         '<workflow-app name="wf-name-1" xmlns="uri:oozie:workflow:0.2">\n'
         '<workflow-app name="wf-name-1" xmlns="uri:oozie:workflow:0.2">\n'
+        '    <global>\n'
+        '        <job-xml>jobconf.xml</job-xml>\n'
+        '        <configuration>\n'
+        '            <property>\n'
+        '                <name>sleep-all</name>\n'
+        '                <value>${SLEEP}</value>\n'
+        '            </property>\n'
+        '         </configuration>\n'
+        '    </global>\n'
         '    <start to="action-name-1"/>\n'
         '    <start to="action-name-1"/>\n'
         '    <action name="action-name-1">\n'
         '    <action name="action-name-1">\n'
         '        <map-reduce>\n'
         '        <map-reduce>\n'
@@ -792,7 +801,7 @@ class TestEditor:
         '  start="2012-07-01T00:00Z" end="2012-07-04T00:00Z" timezone="America/Los_Angeles"\n'
         '  start="2012-07-01T00:00Z" end="2012-07-04T00:00Z" timezone="America/Los_Angeles"\n'
         '  xmlns="uri:oozie:coordinator:0.1">\n'
         '  xmlns="uri:oozie:coordinator:0.1">\n'
         '  <controls>\n'
         '  <controls>\n'
-        '    <timeout>${coord:hours(2)}</timeout>\n'
+        '    <timeout>100</timeout>\n'
         '    <concurrency>3</concurrency>\n'
         '    <concurrency>3</concurrency>\n'
         '    <execution>FIFO</execution>\n'
         '    <execution>FIFO</execution>\n'
         '    <throttle>10</throttle>\n'
         '    <throttle>10</throttle>\n'
@@ -825,7 +834,10 @@ class TestEditor:
 
 
 # Utils
 # Utils
 WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u''],
 WORKFLOW_DICT = {u'deployment_dir': [u''], u'name': [u'wf-name-1'], u'description': [u''],
-                 u'parameters': [u'[{"name":"market","value":"US"}]']}
+                 u'schema_version': [u'uri:oozie:workflow:0.2'],
+                 u'parameters': [u'[{"name":"market","value":"US"}]'],
+                 u'job_xml': [u'jobconf.xml'],
+                 u'job_properties': [u'[{"name":"sleep-all","value":"${SLEEP}"}]']}
 
 
 
 
 # Beware: client not consistent with self.c in TestEditor
 # Beware: client not consistent with self.c in TestEditor
@@ -875,10 +887,11 @@ def create_coordinator(workflow):
                         u'end_0': [u'07/04/2012'], u'end_1': [u'12:00 AM'],
                         u'end_0': [u'07/04/2012'], u'end_1': [u'12:00 AM'],
                         u'timezone': [u'America/Los_Angeles'],
                         u'timezone': [u'America/Los_Angeles'],
                         u'parameters': [u'[{"name":"market","value":"US,France"}]'],
                         u'parameters': [u'[{"name":"market","value":"US,France"}]'],
-                        u'timeout_number': [u'2'], u'timeout_unit': [u'hours'],
+                        u'timeout': [u'100'],
                         u'concurrency': [u'3'],
                         u'concurrency': [u'3'],
                         u'execution': [u'FIFO'],
                         u'execution': [u'FIFO'],
-                        u'throttle': [u'10']
+                        u'throttle': [u'10'],
+                        u'schema_version': [u'uri:oozie:coordinator:0.1']
   })
   })
   assert_equal(coord_count + 1, Coordinator.objects.count(), response)
   assert_equal(coord_count + 1, Coordinator.objects.count(), response)
 
 

+ 10 - 5
apps/oozie/src/oozie/views/editor.py

@@ -29,7 +29,6 @@ from django.forms.models import inlineformset_factory, modelformset_factory
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 from django.utils.functional import curry, wraps
 from django.utils.functional import curry, wraps
-from django.utils.safestring import mark_safe
 from django.utils.translation import ugettext as _
 from django.utils.translation import ugettext as _
 
 
 from desktop.lib.django_util import render, PopupException, extract_field_data
 from desktop.lib.django_util import render, PopupException, extract_field_data
@@ -218,6 +217,8 @@ def create_workflow(request):
       wf = workflow_form.save()
       wf = workflow_form.save()
       Workflow.objects.initialize(wf, request.fs)
       Workflow.objects.initialize(wf, request.fs)
       return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': workflow.id}))
       return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': workflow.id}))
+    else:
+      request.error(_('Errors on the form: %s') % workflow_form.errors)
   else:
   else:
     workflow_form = WorkflowForm(instance=workflow)
     workflow_form = WorkflowForm(instance=workflow)
 
 
@@ -249,7 +250,8 @@ def edit_workflow(request, workflow):
         if workflow.has_cycle():
         if workflow.has_cycle():
           raise PopupException(_('Sorry, this operation is not creating a cycle which would break the workflow.'))
           raise PopupException(_('Sorry, this operation is not creating a cycle which would break the workflow.'))
 
 
-        return redirect(reverse('oozie:list_workflows'))
+        request.info(_("Workflow saved!"))
+        return redirect(reverse('oozie:edit_workflow', kwargs={'workflow': workflow.id}))
     except Exception, e:
     except Exception, e:
       request.error(_('Sorry, this operation is not supported: %(error)s') % {'error': e})
       request.error(_('Sorry, this operation is not supported: %(error)s') % {'error': e})
 
 
@@ -270,7 +272,8 @@ def edit_workflow(request, workflow):
     'graph': graph,
     'graph': graph,
     'history': history,
     'history': history,
     'user_can_edit_job': user_can_edit_job,
     'user_can_edit_job': user_can_edit_job,
-    'parameters': extract_field_data(workflow_form['parameters'])
+    'parameters': extract_field_data(workflow_form['parameters']),
+    'job_properties': extract_field_data(workflow_form['job_properties'])
   })
   })
 
 
 
 
@@ -547,9 +550,9 @@ def move_down_action(request, action):
 @check_job_access_permission
 @check_job_access_permission
 def create_coordinator(request, workflow=None):
 def create_coordinator(request, workflow=None):
   if workflow is not None:
   if workflow is not None:
-    coordinator = Coordinator(owner=request.user, workflow=workflow)
+    coordinator = Coordinator(owner=request.user, schema_version="uri:oozie:coordinator:0.1", workflow=workflow)
   else:
   else:
-    coordinator = Coordinator(owner=request.user)
+    coordinator = Coordinator(owner=request.user, schema_version="uri:oozie:coordinator:0.1")
 
 
   if request.method == 'POST':
   if request.method == 'POST':
     coordinator_form = CoordinatorForm(request.POST, instance=coordinator)
     coordinator_form = CoordinatorForm(request.POST, instance=coordinator)
@@ -557,6 +560,8 @@ def create_coordinator(request, workflow=None):
     if coordinator_form.is_valid():
     if coordinator_form.is_valid():
       coordinator = coordinator_form.save()
       coordinator = coordinator_form.save()
       return redirect(reverse('oozie:edit_coordinator', kwargs={'coordinator': coordinator.id}))
       return redirect(reverse('oozie:edit_coordinator', kwargs={'coordinator': coordinator.id}))
+    else:
+      request.error(_('Errors on the form: %s') % coordinator_form.errors)
   else:
   else:
     coordinator_form = CoordinatorForm(instance=coordinator)
     coordinator_form = CoordinatorForm(instance=coordinator)
 
 

+ 24 - 0
desktop/core/static/css/hue2.css

@@ -947,3 +947,27 @@ div.box {
 .jHueNotify .close {
 .jHueNotify .close {
     right: -27px;
     right: -27px;
 }
 }
+
+/*
+ * Advanced options
+ */
+
+#advanced-btn {
+  center:right
+}
+
+a#advanced-btn:link {
+  color: black; text-decoration: none;
+}
+
+a#advanced-btn:active {
+  color: black; text-decoration: none;
+}
+
+a#advanced-btn:visited {
+  color: black; text-decoration: none;
+}
+
+a#advanced-btn:hover {
+  color: black; text-decoration: none;
+}