diff --git a/.pylintrc b/.pylintrc
index 72325a1..2b78dbf 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -44,7 +44,7 @@ symbols=no
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
-disable=E1002,W,C,R,method-hidden
+disable=E1002,W,C,R,method-hidden,import-error
diff --git a/disaster_recovery/actions/workflows/action.py b/disaster_recovery/actions/workflows/action.py
index 08ff273..6f797e4 100644
--- a/disaster_recovery/actions/workflows/action.py
+++ b/disaster_recovery/actions/workflows/action.py
@@ -208,7 +208,6 @@ class ActionConfigurationAction(workflows.Action):
def populate_action_choices(self, request, context):
return [
- ('', _("Select an action")),
('backup', _("Backup")),
('restore', _("Restore")),
('admin', _("Admin")),
diff --git a/disaster_recovery/api/api.py b/disaster_recovery/api/api.py
index 72a0a5b..36d6fc2 100644
--- a/disaster_recovery/api/api.py
+++ b/disaster_recovery/api/api.py
@@ -335,7 +335,8 @@ class Action(object):
action['freezer_action'].get('backup_name'),
action['freezer_action'].get('path_to_backup')
or action['freezer_action'].get('restore_abs_path'),
- action['freezer_action'].get('storage')
+ action['freezer_action'].get('storage'),
+ mode=action['freezer_action'].get('mode')
) for action in actions]
def get(self, job_id, json=False):
diff --git a/disaster_recovery/jobs/tables.py b/disaster_recovery/jobs/tables.py
index b7051d4..2a22dbc 100644
--- a/disaster_recovery/jobs/tables.py
+++ b/disaster_recovery/jobs/tables.py
@@ -19,6 +19,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
+from horizon import messages
from horizon.utils.urlresolvers import reverse
import disaster_recovery.api.api as freezer_api
@@ -93,7 +94,7 @@ class EditJob(tables.LinkAction):
icon = "pencil"
def get_link_url(self, datum=None):
- return reverse("horizon:disaster_recovery:jobs:configure",
+ return reverse("horizon:disaster_recovery:jobs:edit_job",
kwargs={'job_id': datum.job_id})
@@ -114,6 +115,7 @@ class StartJob(tables.Action):
def single(self, table, request, job_id):
freezer_api.Job(request).start(job_id)
+ messages.success(request, _("Job has started"))
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
@@ -123,6 +125,7 @@ class StopJob(tables.Action):
def single(self, table, request, job_id):
freezer_api.Job(request).stop(job_id)
+ messages.success(request, _("Job has stopped"))
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
diff --git a/disaster_recovery/jobs/templates/jobs/_actions.html b/disaster_recovery/jobs/templates/jobs/_actions.html
index 762f2cd..df68a29 100644
--- a/disaster_recovery/jobs/templates/jobs/_actions.html
+++ b/disaster_recovery/jobs/templates/jobs/_actions.html
@@ -10,6 +10,12 @@
}
+
+
+
Drag actions in the order they will be executed on the client.
+
+
+
diff --git a/disaster_recovery/jobs/urls.py b/disaster_recovery/jobs/urls.py
index 2c1491b..4d465f8 100644
--- a/disaster_recovery/jobs/urls.py
+++ b/disaster_recovery/jobs/urls.py
@@ -33,6 +33,10 @@ urlpatterns = patterns(
views.JobWorkflowView.as_view(),
name='configure'),
+ url(r'^edit/(?P[^/]+)?$',
+ views.EditJobWorkflowView.as_view(),
+ name='edit_job'),
+
url(r'^edit_actions/(?P[^/]+)?$',
views.ActionsInJobView.as_view(),
name='edit_action'),
diff --git a/disaster_recovery/jobs/views.py b/disaster_recovery/jobs/views.py
index 2685a83..5c4a2d3 100644
--- a/disaster_recovery/jobs/views.py
+++ b/disaster_recovery/jobs/views.py
@@ -16,7 +16,8 @@ from horizon import browsers
from horizon import workflows
import workflows.create as configure_workflow
-import workflows.actions as actions_workflow
+import workflows.update_job as update_job_workflow
+import workflows.update_actions as update_actions_workflow
import disaster_recovery.api.api as freezer_api
import disaster_recovery.jobs.browsers as project_browsers
@@ -65,8 +66,31 @@ class JobWorkflowView(workflows.WorkflowView):
return initial
+class EditJobWorkflowView(workflows.WorkflowView):
+ workflow_class = update_job_workflow.UpdateJob
+
+ @shield("Unable to get job", redirect="jobs:index")
+ def get_object(self):
+ return freezer_api.Job(self.request).get(self.kwargs['job_id'])
+
+ def is_update(self):
+ return 'job_id' in self.kwargs and bool(self.kwargs['job_id'])
+
+ @shield("Unable to get job", redirect="jobs:index")
+ def get_initial(self):
+ initial = super(EditJobWorkflowView, self).get_initial()
+ if self.is_update():
+ initial.update({'job_id': None})
+ job = freezer_api.Job(self.request).get(self.kwargs['job_id'],
+ json=True)
+ initial.update(**job)
+ initial.update(**job['job_schedule'])
+
+ return initial
+
+
class ActionsInJobView(workflows.WorkflowView):
- workflow_class = actions_workflow.ConfigureActions
+ workflow_class = update_actions_workflow.UpdateActions
@shield("Unable to get job", redirect="jobs:index")
def get_object(self):
diff --git a/disaster_recovery/jobs/workflows/create.py b/disaster_recovery/jobs/workflows/create.py
index eda1e59..74c9e1b 100644
--- a/disaster_recovery/jobs/workflows/create.py
+++ b/disaster_recovery/jobs/workflows/create.py
@@ -141,12 +141,24 @@ class InfoConfigurationAction(workflows.Action):
return False
def _check_start_datetime(self, cleaned_data):
- if cleaned_data.get('start_datetime') and not \
+ if cleaned_data.get('schedule_start_date') and not \
self._validate_iso_format(
cleaned_data.get('schedule_start_date')):
msg = _("Start date time is not in ISO format.")
self._errors['schedule_start_date'] = self.error_class([msg])
+ if (cleaned_data.get('schedule_start_date') and
+ cleaned_data.get('schedule_end_date')) and\
+ not cleaned_data.get('schedule_interval'):
+ msg = _("Please provide this value.")
+ self._errors['schedule_interval'] = self.error_class([msg])
+
+ if (cleaned_data.get('schedule_end_date') and
+ not cleaned_data.get('schedule_start_date')) and\
+ not cleaned_data.get('schedule_interval'):
+ msg = _("Please provide this value.")
+ self._errors['schedule_start_date'] = self.error_class([msg])
+
def _check_end_datetime(self, cleaned_data):
if cleaned_data.get('end_datetime') and not \
self._validate_iso_format(
diff --git a/disaster_recovery/jobs/workflows/update_actions.py b/disaster_recovery/jobs/workflows/update_actions.py
new file mode 100644
index 0000000..8541f5c
--- /dev/null
+++ b/disaster_recovery/jobs/workflows/update_actions.py
@@ -0,0 +1,65 @@
+# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
+#
+# Licensed 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.
+
+import logging
+
+from django import shortcuts
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+
+import disaster_recovery.api.api as freezer_api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class ActionsConfigurationAction(workflows.Action):
+ actions = forms.CharField(
+ required=False)
+
+ job_id = forms.CharField(
+ required=False)
+
+ class Meta(object):
+ name = _("Actions")
+ slug = "actions"
+ help_text_template = "disaster_recovery/jobs" \
+ "/_actions.html"
+
+
+class ActionsConfiguration(workflows.Step):
+ action_class = ActionsConfigurationAction
+ contributes = ('actions', 'job_id')
+
+
+class UpdateActions(workflows.Workflow):
+ slug = "update_actions"
+ name = _("Update Actions")
+ finalize_button_name = _("Save")
+ success_message = _('Actions updated correctly.')
+ failure_message = _('Unable to update actions.')
+ success_url = "horizon:disaster_recovery:jobs:index"
+ default_steps = (ActionsConfiguration,)
+
+ def handle(self, request, context):
+ try:
+ if context['job_id'] != '':
+ freezer_api.Job(request).update(context['job_id'], context)
+ return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
+ except Exception:
+ exceptions.handle(request)
+ return False
diff --git a/disaster_recovery/jobs/workflows/update_job.py b/disaster_recovery/jobs/workflows/update_job.py
new file mode 100644
index 0000000..28d86b5
--- /dev/null
+++ b/disaster_recovery/jobs/workflows/update_job.py
@@ -0,0 +1,122 @@
+# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
+#
+# Licensed 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.
+
+import logging
+
+import datetime
+
+from django import shortcuts
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+
+import disaster_recovery.api.api as freezer_api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class InfoConfigurationAction(workflows.Action):
+
+ description = forms.CharField(
+ label=_("Job Name"),
+ help_text=_("Set a name for this job"),
+ required=True)
+
+ job_id = forms.CharField(
+ widget=forms.HiddenInput(),
+ required=False)
+
+ schedule_start_date = forms.CharField(
+ label=_("Start Date and Time"),
+ required=False)
+
+ schedule_interval = forms.CharField(
+ label=_("Interval"),
+ required=False,
+ help_text=_("Repeat this configuration in a minutes interval."))
+
+ schedule_end_date = forms.CharField(
+ label=_("End Date and Time"),
+ required=False)
+
+ def __init__(self, request, context, *args, **kwargs):
+ self.request = request
+ self.context = context
+ super(InfoConfigurationAction, self).__init__(
+ request, context, *args, **kwargs)
+
+ def clean(self):
+ cleaned_data = super(InfoConfigurationAction, self).clean()
+ self._check_start_datetime(cleaned_data)
+ self._check_end_datetime(cleaned_data)
+ return cleaned_data
+
+ def _validate_iso_format(self, start_date):
+ try:
+ return datetime.datetime.strptime(
+ start_date, "%Y-%m-%dT%H:%M:%S")
+ except ValueError:
+ return False
+
+ def _check_start_datetime(self, cleaned_data):
+ if cleaned_data.get('start_datetime') and not \
+ self._validate_iso_format(
+ cleaned_data.get('schedule_start_date')):
+ msg = _("Start date time is not in ISO format.")
+ self._errors['schedule_start_date'] = self.error_class([msg])
+
+ def _check_end_datetime(self, cleaned_data):
+ if cleaned_data.get('end_datetime') and not \
+ self._validate_iso_format(
+ cleaned_data.get('schedule_end_date')):
+ msg = _("End date time is not in ISO format.")
+ self._errors['schedule_end_date'] = self.error_class([msg])
+
+ class Meta(object):
+ name = _("Job Info")
+ slug = "info"
+ help_text_template = "disaster_recovery/jobs" \
+ "/_scheduling.html"
+
+
+class InfoConfiguration(workflows.Step):
+ action_class = InfoConfigurationAction
+ contributes = ('description',
+ 'job_id',
+ 'actions',
+ 'schedule_start_date',
+ 'schedule_interval',
+ 'schedule_end_date')
+
+
+class UpdateJob(workflows.Workflow):
+ slug = "update_job"
+ name = _("Update Job")
+ finalize_button_name = _("Save")
+ success_message = _('Job created correctly.')
+ failure_message = _('Unable to created job.')
+ success_url = "horizon:disaster_recovery:jobs:index"
+ default_steps = (InfoConfiguration,)
+
+ def handle(self, request, context):
+ try:
+ if context['job_id'] != '':
+ freezer_api.Job(request).update(context['job_id'], context)
+ return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
+ except Exception:
+ exceptions.handle(request)
+ return False
diff --git a/disaster_recovery/static/freezer/js/freezer.actions.action.js b/disaster_recovery/static/freezer/js/freezer.actions.action.js
index d5a96d7..b641939 100644
--- a/disaster_recovery/static/freezer/js/freezer.actions.action.js
+++ b/disaster_recovery/static/freezer/js/freezer.actions.action.js
@@ -75,31 +75,33 @@ function showRestoreOptions() {
$("#id_storage").closest(".form-group").show();
}
-function showNovaOptions() {
- $("#id_mode").closest(".form-group").show();
- $("#id_nova_inst_id").closest(".form-group").show();
- $("#id_backup_name").closest(".form-group").show();
- $("#id_container").closest(".form-group").show();
-}
-
-function showCinderOptions() {
- $("#id_mode").closest(".form-group").show();
- $("#id_cinder_vol_id").closest(".form-group").show();
- $("#id_backup_name").closest(".form-group").show();
- $("#id_container").closest(".form-group").show();
-}
-
function showSSHOptions() {
$("#id_ssh_key").closest(".form-group").show();
$("#id_ssh_username").closest(".form-group").show();
$("#id_ssh_host").closest(".form-group").show();
}
-hideEverything();
+function triggerChanges() {
+ $("#id_action").trigger('change');
+ $("#id_mode").trigger('change');
+ $("#id_storage").trigger('change');
+}
+
+function hideModeOptions() {
+ $("#id_cinder_vol_id").closest(".form-group").hide();
+ $("#id_nova_inst_id").closest(".form-group").hide();
+ $("#id_mysql_conf").closest(".form-group").hide();
+ $("#id_sql_server_conf").closest(".form-group").hide();
+}
+
+function hideSSHOptions() {
+ $("#id_ssh_key").closest(".form-group").hide();
+ $("#id_ssh_username").closest(".form-group").hide();
+ $("#id_ssh_host").closest(".form-group").hide();
+}
$("#id_action").change(function () {
// Update the inputs according freezer action
-
if ($("#id_action").val() === 'backup') {
hideEverything();
showBackupOptions();
@@ -117,22 +119,18 @@ $("#id_action").change(function () {
$("#id_storage").change(function () {
// Update the inputs according freezer storage backend
-
if ($("#id_storage").val() === 'swift') {
- hideEverything();
+ //hideEverything();
showBackupOptions();
- ("#id_mode").closest(".form-group").show();
+ hideSSHOptions();
} else if ($("#id_storage").val() === 'ssh') {
- hideEverything();
+ //hideEverything();
showBackupOptions();
- $("#id_mode").closest(".form-group").show();
showSSHOptions();
} else if ($("#id_storage").val() === 'local') {
- hideEverything();
+ //hideEverything();
showBackupOptions();
- $("#id_mode").closest(".form-group").show();
- } else {
- hideEverything();
+ hideSSHOptions();
}
});
@@ -141,37 +139,27 @@ $("#id_mode").change(function () {
// Update the inputs according freezer mode
if ($("#id_action").val() === 'backup') {
if ($("#id_mode").val() === 'fs') {
- hideEverything();
- showBackupOptions();
- $("#id_advanced_configuration").closest(".form-group").show();
+ hideModeOptions();
} else if ($("#id_mode").val() === 'mysql') {
- hideEverything();
- showBackupOptions();
+ hideModeOptions();
$("#id_mysql_conf").closest(".form-group").show();
- $("#id_sql_server_conf").closest(".form-group").hide();
- $("#id_advanced_configuration").closest(".form-group").show();
} else if ($("#id_mode").val() === 'mssql') {
- hideEverything();
- showBackupOptions();
+ hideModeOptions();
$("#id_sql_server_conf").closest(".form-group").show();
- $("#id_mysql_conf").closest(".form-group").hide();
- $("#id_advanced_configuration").closest(".form-group").show();
} else if ($("#id_mode").val() === 'mongo') {
- hideEverything();
- showBackupOptions();
- $("#id_sql_server_conf").closest(".form-group").hide();
- $("#id_mysql_conf").closest(".form-group").hide();
- $("#id_advanced_configuration").closest(".form-group").show();
+ hideModeOptions();
} else if ($("#id_mode").val() === 'cinder') {
- hideEverything();
- showCinderOptions();
- $("#id_cinder_vol_id").closest(".form-group").show().addClass("required");
- $("#id_advanced_configuration").closest(".form-group").show();
+ hideModeOptions();
+ $("#id_cinder_vol_id").closest(".form-group").show();
} else if ($("#id_mode").val() === 'nova') {
- hideEverything();
- showNovaOptions();
- $("#id_nova_inst_id").closest(".form-group").show().addClass("required");
- $("#id_advanced_configuration").closest(".form-group").show();
+ hideModeOptions();
+ $("#id_nova_inst_id").closest(".form-group").show();
}
}
});
+
+
+$(function () {
+ hideEverything();
+ triggerChanges();
+});
\ No newline at end of file
diff --git a/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js
index 33919a8..8f2f1da 100644
--- a/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js
+++ b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js
@@ -14,7 +14,7 @@ var parent = $(".sortable_lists").parent();
parent.removeClass("col-sm-6");
parent.addClass("col-sm-12");
var siblings = parent.siblings();
-siblings.remove();
+siblings.hide();
$("form").submit(function (event) {
@@ -29,6 +29,7 @@ $("form").submit(function (event) {
var job_id = $('#id_job_id').val();
+
function get_url() {
var url = $(location).attr("origin");
url += '/disaster_recovery/api/actions/job/';