diff --git a/README.rst b/README.rst
index a10de2e..372ba85 100644
--- a/README.rst
+++ b/README.rst
@@ -2,6 +2,13 @@
Freezer - Horizon Dashboard
===========================
+freezer-web-ui is a horizon plugin based in django aimed at providing an interaction
+with freezer
+
+* Release management: https://launchpad.net/freezer-web-ui
+* Blueprints and feature specifications: https://blueprints.launchpad.net/freezer-web-ui
+* Issue tracking: https://bugs.launchpad.net/freezer-web-ui
+
Requirements
============
@@ -73,39 +80,8 @@ To deploy freezer dashboard in production you need to do the following::
A new tab called "Disaster Recovery" will be on your panels.
-Running the unit tests
-======================
-
-1. Create a virtual environment::
-
- virtualenv --no-site-packages -p /usr/bin/python2.7 .venv
-
-2. Activate the virtual environment::
-
- . ./.venv/bin/activate
-
-3. Install the requirements::
-
- pip install -r test-requirements.txt
-
-4. Run the tests::
-
- python manage.py test . --settings=freezer_ui.tests.settings
-
-Test coverage
--------------
-
-1. Collect coverage information::
-
- coverage run --source='.' --omit='.venv/*' manage.py test . --settings=freezer_ui.tests.settings
-
-2. View coverage report::
-
- coverage report
-
-
Tox
----
+===
1. Run tox::
diff --git a/freezer_ui/__init__.py b/TODO.rst
similarity index 100%
rename from freezer_ui/__init__.py
rename to TODO.rst
diff --git a/_50_freezer.py b/_50_freezer.py
index 19aa0bb..a0f39c8 100644
--- a/_50_freezer.py
+++ b/_50_freezer.py
@@ -13,12 +13,12 @@
# limitations under the License.
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
-DASHBOARD = 'freezer_ui'
+DASHBOARD = 'disaster_recovery'
# If set to True, this dashboard will not be added to the settings.
DISABLED = False
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = [
- 'freezer_ui',
+ 'disaster_recovery',
]
diff --git a/devstack/README.rst b/devstack/README.rst
index 195f833..1d7873a 100644
--- a/devstack/README.rst
+++ b/devstack/README.rst
@@ -15,7 +15,7 @@ where::
For example::
- enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
+ enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
For more information, see:
http://docs.openstack.org/developer/devstack/plugins.html
diff --git a/devstack/local.conf.example b/devstack/local.conf.example
index fb04518..ac027a1 100644
--- a/devstack/local.conf.example
+++ b/devstack/local.conf.example
@@ -1,7 +1,7 @@
[[local|localrc]]
disable_all_services
-enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
+enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
enable_service rabbit mysql key
diff --git a/devstack/settings b/devstack/settings
index 605ec86..49f40c5 100644
--- a/devstack/settings
+++ b/devstack/settings
@@ -20,11 +20,11 @@ FREEZER_WEB_UI_DIR=$DEST/freezer-web-ui
FREEZER_WEB_UI_FILES=${FREEZER_WEB_UI_DIR}/devstack/files
# Freezer Web UI repository
-FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/stackforge/freezer-web-ui.git}
+FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/openstack/freezer-web-ui.git}
FREEZER_WEB_UI_BRANCH=${FREEZER_WEB_UI_BRANCH:-master}
# Freezer client
-FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/stackforge/freezer.git}
+FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/openstack/freezer.git}
FREEZER_CLIENT_DIR=$DEST/freezer
FREEZER_CLIENT_BRANCH=${FREEZER_CLIENT_BRANCH:-master}
diff --git a/freezer_ui/actions/__init__.py b/disaster_recovery/__init__.py
similarity index 100%
rename from freezer_ui/actions/__init__.py
rename to disaster_recovery/__init__.py
diff --git a/freezer_ui/api/__init__.py b/disaster_recovery/actions/__init__.py
similarity index 100%
rename from freezer_ui/api/__init__.py
rename to disaster_recovery/actions/__init__.py
diff --git a/disaster_recovery/actions/panel.py b/disaster_recovery/actions/panel.py
new file mode 100644
index 0000000..83a57d0
--- /dev/null
+++ b/disaster_recovery/actions/panel.py
@@ -0,0 +1,27 @@
+# (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.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+
+from disaster_recovery import dashboard
+
+
+class ActionsPanel(horizon.Panel):
+ name = _("Actions")
+ slug = "actions"
+
+
+dashboard.Freezer.register(ActionsPanel)
diff --git a/disaster_recovery/actions/tables.py b/disaster_recovery/actions/tables.py
new file mode 100644
index 0000000..53c1ccf
--- /dev/null
+++ b/disaster_recovery/actions/tables.py
@@ -0,0 +1,105 @@
+# 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 django.utils.translation import ungettext_lazy
+
+from horizon import tables
+from horizon.utils.urlresolvers import reverse
+
+import disaster_recovery.api.api as freezer_api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteAction(tables.DeleteAction):
+ name = "delete"
+ classes = ("btn-danger",)
+ icon = "remove"
+ help_text = _("Delete actions is not recoverable.")
+
+ @staticmethod
+ def action_present(count):
+ return ungettext_lazy(
+ u"Delete Action",
+ u"Delete Actions",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ungettext_lazy(
+ u"Deleted Action",
+ u"Deleted Actions",
+ count
+ )
+
+ def delete(self, request, action_id):
+ freezer_api.Action(request).delete(action_id)
+ # TODO(m3m0): this shouldnt redirect here when an action is deleted
+ # from jobs views
+ return shortcuts.redirect('horizon:disaster_recovery:actions:index')
+
+
+class DeleteMultipleActions(DeleteAction):
+ name = "delete_multiple_actions"
+
+
+class BackupFilter(tables.FilterAction):
+ filter_type = "server"
+ filter_choices = (("contains", "Contains text", True),)
+
+
+class CreateAction(tables.LinkAction):
+ name = "create_action"
+ verbose_name = _("Create Action")
+ url = "horizon:disaster_recovery:actions:create"
+ classes = ("ajax-modal",)
+ icon = "plus"
+
+
+class EditAction(tables.LinkAction):
+ name = "edit_action"
+ verbose_name = _("Edit")
+ classes = ("ajax-modal",)
+ icon = "pencil"
+
+ def get_link_url(self, datum=None):
+ return reverse("horizon:disaster_recovery:actions:create",
+ kwargs={'action_id': datum.action_id})
+
+
+def get_link(action):
+ return reverse('horizon:disaster_recovery:actions:action',
+ kwargs={'action_id': action.id})
+
+
+class ActionsTable(tables.DataTable):
+ backup_name = tables.Column('backup_name',
+
+ verbose_name=_("Action Name"),
+ link=get_link)
+ action = tables.Column('action', verbose_name=_("Action"))
+ path_to_backup = tables.Column('path_to_backup',
+ verbose_name=_("Path To Backup or Restore"))
+ storage = tables.Column('storage', verbose_name=_("Storage"))
+
+ class Meta:
+ name = "actions_table"
+ verbose_name = _("Actions")
+ row_actions = (EditAction, DeleteAction,)
+ table_actions = (BackupFilter, CreateAction, DeleteMultipleActions)
+ multi_select = True
diff --git a/disaster_recovery/actions/templates/actions/detail.html b/disaster_recovery/actions/templates/actions/detail.html
new file mode 100644
index 0000000..12b9d8f
--- /dev/null
+++ b/disaster_recovery/actions/templates/actions/detail.html
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+
+{% load i18n %}
+{% block title %}{% trans "Action" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Action") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+{% endblock %}
diff --git a/disaster_recovery/actions/templates/actions/index.html b/disaster_recovery/actions/templates/actions/index.html
new file mode 100644
index 0000000..031af49
--- /dev/null
+++ b/disaster_recovery/actions/templates/actions/index.html
@@ -0,0 +1,16 @@
+{% extends 'base.html' %}
+
+{% block css %}
+ {% include "_stylesheets.html" %}
+{% endblock css %}
+
+{% load i18n %}
+{% block title %}{% trans "Actions" %}{% endblock title %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Actions") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock main %}
diff --git a/disaster_recovery/actions/urls.py b/disaster_recovery/actions/urls.py
new file mode 100644
index 0000000..fa01880
--- /dev/null
+++ b/disaster_recovery/actions/urls.py
@@ -0,0 +1,33 @@
+# (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.
+
+
+from django.conf.urls import patterns
+from django.conf.urls import url
+
+from disaster_recovery.actions import views
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', views.IndexView.as_view(), name='index'),
+
+ url(r'^create/(?P[^/]+)?$',
+ views.ActionWorkflowView.as_view(),
+ name='create'),
+
+ url(r'^action/(?P[^/]+)?$',
+ views.ActionView.as_view(),
+ name='action'),
+)
diff --git a/disaster_recovery/actions/views.py b/disaster_recovery/actions/views.py
new file mode 100644
index 0000000..8115720
--- /dev/null
+++ b/disaster_recovery/actions/views.py
@@ -0,0 +1,76 @@
+# 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 pprint
+
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+from django.views import generic
+
+from horizon import tables
+from horizon import workflows
+
+import disaster_recovery.api.api as freezer_api
+
+from disaster_recovery.actions import tables as freezer_tables
+from disaster_recovery.actions.workflows import action as action_workflow
+from disaster_recovery.utils import shield
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tables.DataTableView):
+ name = _("Actions")
+ slug = "actions"
+ table_class = freezer_tables.ActionsTable
+ template_name = "disaster_recovery/actions/index.html"
+
+ @shield("Unable to get actions", redirect="actions:index")
+ def get_data(self):
+ filters = self.table.get_filter_string() or None
+ return freezer_api.Action(self.request).list(search=filters)
+
+
+class ActionView(generic.TemplateView):
+ template_name = 'disaster_recovery/actions/detail.html'
+
+ @shield('Unable to get action', redirect='actions:index')
+ def get_context_data(self, **kwargs):
+ action = freezer_api.Action(self.request).get(kwargs['action_id'],
+ json=True)
+ return {'data': pprint.pformat(action)}
+
+
+class ActionWorkflowView(workflows.WorkflowView):
+ workflow_class = action_workflow.ActionWorkflow
+ success_url = reverse_lazy("horizon:disaster_recovery:actions:index")
+
+ def is_update(self):
+ return 'action_id' in self.kwargs and bool(self.kwargs['action_id'])
+
+ @shield("Unable to get job", redirect="jobs:index")
+ def get_initial(self):
+ initial = super(ActionWorkflowView, self).get_initial()
+ if self.is_update():
+ initial.update({'action_id': None})
+ action = freezer_api.Action(self.request).get(
+ self.kwargs['action_id'], json=True)
+ initial.update(**action['freezer_action'])
+ initial.update({
+ "mandatory": action['mandatory'],
+ "max_retries": action['max_retries'],
+ "max_retries_interval": action['max_retries_interval']
+ })
+ initial.update({'action_id': action['action_id']})
+ return initial
diff --git a/freezer_ui/api/rest/__init__.py b/disaster_recovery/actions/workflows/__init__.py
similarity index 100%
rename from freezer_ui/api/rest/__init__.py
rename to disaster_recovery/actions/workflows/__init__.py
diff --git a/freezer_ui/jobs/workflows/action.py b/disaster_recovery/actions/workflows/action.py
similarity index 92%
rename from freezer_ui/jobs/workflows/action.py
rename to disaster_recovery/actions/workflows/action.py
index 09efe84..3baef1d 100644
--- a/freezer_ui/jobs/workflows/action.py
+++ b/disaster_recovery/actions/workflows/action.py
@@ -14,17 +14,24 @@
import logging
+from django import shortcuts
from django.utils.translation import ugettext_lazy as _
-from horizon import workflows
+
from horizon import exceptions
from horizon import forms
-import freezer_ui.api.api as freezer_api
+from horizon import workflows
+
+import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
class ActionConfigurationAction(workflows.Action):
+ action_id = forms.CharField(
+ widget=forms.HiddenInput(),
+ required=False)
+
action = forms.ChoiceField(
help_text=_("Set the action to be taken"),
required=True)
@@ -33,14 +40,6 @@ class ActionConfigurationAction(workflows.Action):
help_text=_("Choose what you want to backup"),
required=False)
- original_name = forms.CharField(
- widget=forms.HiddenInput(),
- required=False)
-
- action_id = forms.CharField(
- widget=forms.HiddenInput(),
- required=False)
-
storage = forms.ChoiceField(
help_text=_("Set storage backend for a backup"),
required=True)
@@ -224,15 +223,16 @@ class ActionConfigurationAction(workflows.Action):
class Meta(object):
name = _("Action")
- help_text_template = "freezer_ui/jobs" \
+ help_text_template = "disaster_recovery/jobs" \
"/_action.html"
class ActionConfiguration(workflows.Step):
action_class = ActionConfigurationAction
- contributes = ('action',
+ contributes = ('action_id',
+ 'action',
'mode',
- 'original_name',
+ 'storage',
'backup_name',
'mysql_conf',
'sql_server_conf',
@@ -247,11 +247,9 @@ class ActionConfiguration(workflows.Step):
'dst_file',
'remove_older_than',
'remove_from_date',
- 'action_id',
- 'storage',
- 'ssh_host',
+ 'ssh_key',
'ssh_username',
- 'ssh_key')
+ 'ssh_host')
class SnapshotConfigurationAction(workflows.Action):
@@ -270,15 +268,6 @@ class SnapshotConfigurationAction(workflows.Action):
widget=forms.CheckboxInput(),
required=False)
- vssadmin = forms.BooleanField(
- label=_("VSSAdmin"),
- help_text=_("Create a backup using a snapshot on windows "
- "using vssadmin. Options are: "
- "True and False, default is True"),
- widget=forms.CheckboxInput(),
- initial=True,
- required=False)
-
lvm_auto_snap = forms.CharField(
label=_("LVM Auto Snapshot"),
help_text=_("Automatically guess the volume group and "
@@ -321,7 +310,7 @@ class SnapshotConfigurationAction(workflows.Action):
class Meta(object):
name = _("Snapshot")
- help_text_template = "freezer_ui/jobs" \
+ help_text_template = "disaster_recovery/jobs" \
"/_snapshot.html"
@@ -329,7 +318,6 @@ class SnapshotConfiguration(workflows.Step):
action_class = SnapshotConfigurationAction
contributes = ('use_snapshot',
'is_windows',
- 'vssadmin',
'lvm_auto_snap',
'lvm_srcvol',
'lvm_snapname',
@@ -374,7 +362,6 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Upload bandwidth limit in Bytes per sec."
" Can be invoked with dimensions "
"(10K, 120M, 10G)."),
- initial=-1,
min_value=-1,
required=False)
@@ -383,17 +370,9 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Download bandwidth limit in Bytes per sec. "
"Can be invoked with dimensions"
" (10K, 120M, 10G)."),
- initial=-1,
min_value=-1,
required=False)
- optimize = forms.ChoiceField(
- choices=[('speed', _("Speed (tar)")),
- ('bandwidth', _("Bandwidth/Space (rsync)"))],
- help_text="",
- label=_('Optimize For...'),
- required=False)
-
compression = forms.ChoiceField(
choices=[
('gzip', _("Minimum Compression (GZip/Zip/Zlib)")),
@@ -409,7 +388,6 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Set the maximum file chunk size in bytes"
" to upload to swift."
" Default 67108864 bytes (64MB)"),
- initial=67108864,
min_value=1,
required=False)
@@ -453,7 +431,6 @@ class AdvancedConfigurationAction(workflows.Action):
always_level = forms.IntegerField(
label=_("Always Level"),
- initial=0,
min_value=0,
help_text=_("Set backup maximum level used with tar to"
" implement incremental backup. If a level "
@@ -467,7 +444,6 @@ class AdvancedConfigurationAction(workflows.Action):
restart_always_level = forms.IntegerField(
label=_("Restart Always Level"),
- initial=0,
min_value=0,
help_text=_("Restart the backup from level 0 after n days. "
"Valid only if --always-level option if set. "
@@ -526,30 +502,29 @@ class AdvancedConfigurationAction(workflows.Action):
class Meta(object):
name = _("Advanced")
- help_text_template = "freezer_ui/jobs" \
+ help_text_template = "disaster_recovery/jobs" \
"/_advanced.html"
class AdvancedConfiguration(workflows.Step):
action_class = AdvancedConfigurationAction
contributes = ('exclude',
- # 'log_file',
- # 'proxy',
- # 'os_auth_ver',
- # 'upload_limit',
- # 'download_limit',
- # 'optimize',
- # 'compression',
- # 'max_segment_size',
+ 'log_file',
+ 'proxy',
+ 'os_auth_ver',
+ 'upload_limit',
+ 'download_limit',
+ 'compression',
+ 'max_segment_size',
'hostname',
'encryption_password',
'no_incremental',
'max_level',
'always_level',
'restart_always_level',
- # 'insecure',
+ 'insecure',
'dereference_symlink',
- # 'dry_run',
+ 'dry_run',
'max_priority',
'quiet',)
@@ -579,7 +554,7 @@ class RulesConfigurationAction(workflows.Action):
class Meta(object):
name = _("Rules")
- help_text_template = "freezer_ui/jobs" \
+ help_text_template = "disaster_recovery/jobs" \
"/_rules.html"
@@ -590,13 +565,13 @@ class RulesConfiguration(workflows.Step):
'mandatory')
-class ConfigureAction(workflows.Workflow):
+class ActionWorkflow(workflows.Workflow):
slug = "action"
name = _("Action Configuration")
finalize_button_name = _("Save")
success_message = _('Action file saved correctly.')
failure_message = _('Unable to save action file.')
- success_url = "horizon:freezer_ui:jobs:index"
+ success_url = "horizon:disaster_recovery:actions:index"
default_steps = (ActionConfiguration,
SnapshotConfiguration,
@@ -607,18 +582,26 @@ class ConfigureAction(workflows.Workflow):
try:
if context['is_windows']:
client_os = 'Windows'
+ context.pop('is_windows')
else:
client_os = 'Linux'
+ context.pop('is_windows')
if context['use_snapshot'] and client_os == 'Windows':
context['vssadmin'] = True
+ context.pop('use_snapshot')
else:
context['vssadmin'] = False
+ context.pop('use_snapshot')
if context['action_id'] == '':
- return freezer_api.action_create(request, context)
+ freezer_api.Action(request).create(context)
else:
- return freezer_api.action_update(request, context)
+ freezer_api.Action(request).update(context,
+ context['action_id'])
+
+ return shortcuts.redirect('horizon:disaster_recovery:'
+ 'actions:index')
except Exception:
exceptions.handle(request)
return False
diff --git a/freezer_ui/backups/__init__.py b/disaster_recovery/api/__init__.py
similarity index 100%
rename from freezer_ui/backups/__init__.py
rename to disaster_recovery/api/__init__.py
diff --git a/disaster_recovery/api/api.py b/disaster_recovery/api/api.py
new file mode 100644
index 0000000..75b8e32
--- /dev/null
+++ b/disaster_recovery/api/api.py
@@ -0,0 +1,522 @@
+# 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.
+
+# Some helper functions to use the disaster_recovery client functionality
+# from horizon.
+
+import logging
+
+from django.conf import settings
+
+from horizon.utils.memoized import memoized # noqa
+
+import freezer.apiclient.client
+from disaster_recovery import utils
+
+
+LOG = logging.getLogger(__name__)
+
+
+@memoized
+def client(request):
+ """Return a freezer client object"""
+ api_url = _get_service_url(request)
+ return freezer.apiclient.client.Client(
+ token=request.user.token.id,
+ auth_url=getattr(settings, 'OPENSTACK_KEYSTONE_URL'),
+ endpoint=api_url)
+
+
+@memoized
+def _get_service_url(request):
+ """Get freezer api url"""
+ catalog = (getattr(request.user, "service_catalog", None))
+ if not catalog:
+ return _get_hardcoded_url()
+
+ for c in catalog:
+ if c['name'] == 'freezer':
+ for e in c['endpoints']:
+ return e['internalURL']
+ else:
+ return _get_hardcoded_url()
+
+
+@memoized
+def _get_hardcoded_url():
+ """In case freezer is not registered in keystone catalog, look for it in
+ local_settings.py
+ :return: freezer_api_url
+ """
+ try:
+ LOG.warn('Using hardcoded FREEZER_API_URL at {0}'
+ .format(settings.FREEZER_API_URL))
+ return getattr(settings, 'FREEZER_API_URL', None)
+ except Exception:
+ LOG.warn('No FREEZER_API_URL was found in local_settings.py')
+ raise
+
+
+class Job(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = client(request)
+
+ def list(self, json=False, limit=100, offset=0, search=None):
+ if search:
+ search = {"match": [{"_all": search}, ], }
+
+ jobs = self.client.jobs.list_all(limit=limit,
+ offset=offset,
+ search=search)
+ if json:
+ return jobs
+
+ return [utils.JobObject(
+ job.get('job_id'),
+ job.get('description'),
+ job.get('job_schedule', {}).get('result'),
+ job.get('job_schedule', {}).get('event'),
+ ) for job in jobs]
+
+ def get(self, job_id, json=False):
+ job = self.client.jobs.get(job_id)
+
+ if json:
+ return job
+
+ return utils.JobObject(
+ job.get('job_id'),
+ job.get('description'),
+ job.get('job_schedule', {}).get('result'),
+ job.get('job_schedule', {}).get('event'))
+
+ def create(self, job):
+ return self._build(job)
+
+ def update(self, job_id, job):
+ scheduling = {}
+ try:
+ if job['schedule_end_date'] != '':
+ utils.assign_and_remove(job, scheduling, 'schedule_end_date')
+ else:
+ job.pop('schedule_end_date')
+ except KeyError:
+ pass
+
+ try:
+ if job['schedule_interval'] != '':
+ utils.assign_and_remove(job, scheduling, 'schedule_interval')
+ else:
+ job.pop('schedule_interval')
+ except KeyError:
+ pass
+
+ try:
+ if job['schedule_start_date'] != '':
+ utils.assign_and_remove(job, scheduling, 'schedule_start_date')
+ else:
+ job.pop('schedule_start_date')
+ except KeyError:
+ pass
+
+ job.pop('job_actions', [])
+ job.pop('clients', None)
+ job.pop('actions', None)
+ job.pop('job_id')
+ job['job_schedule'] = scheduling
+ return self.client.jobs.update(job_id, job)
+
+ def update_actions(self, job_id, action_ids):
+ ids = utils.get_action_ids(action_ids)
+ job = self.get(job_id, json=True)
+ job.pop('job_actions', None)
+ actions = self._get_actions_in_job(ids)
+ job['job_actions'] = actions
+ return self.client.jobs.update(job_id, job)
+
+ def delete(self, job_id):
+ return self.client.jobs.delete(job_id)
+
+ def actions(self, job_id, api=False):
+ job = self.get(job_id, json=True)
+
+ if not job:
+ return []
+
+ if api:
+ return job.get('job_actions', [])
+
+ return [utils.ActionObject(
+ action_id=a.get('action_id'),
+ action=a.get('freezer_action', {}).get('action'),
+ backup_name=a.get('freezer_action', {}).get('backup_name'),
+ job_id=job_id
+ ) for a in job.get('job_actions')]
+
+ def delete_action(self, ids):
+ action_id, job_id = ids.split('===')
+ job = self.get(job_id, json=True)
+ for action in job['job_actions']:
+ if action.get('action_id') == action_id:
+ job.get('job_actions').remove(action)
+ return self.client.jobs.update(job_id, job)
+
+ def clone(self, job_id):
+ job = self.get(job_id, json=True)
+ job['description'] = '{0}_clone'.format(job['description'])
+ job.pop('job_id', None)
+ job.pop('_version', None)
+ job_id = self.client.jobs.create(job)
+ return self.stop(job_id)
+
+ def stop(self, job_id):
+ return self.client.jobs.stop_job(job_id)
+
+ def start(self, job_id):
+ return self.client.jobs.start_job(job_id)
+
+ def _build(self, job):
+ action_ids = utils.get_action_ids(job.pop('actions'))
+ job = utils.create_dict(**job)
+ clients = job.pop('clients', [])
+ scheduling = {}
+ new_job = {}
+ utils.assign_and_remove(job, scheduling, 'schedule_start_date')
+ utils.assign_and_remove(job, scheduling, 'schedule_interval')
+ utils.assign_and_remove(job, scheduling, 'schedule_end_date')
+
+ actions = self._get_actions_in_job(action_ids)
+
+ new_job['description'] = job.get('description')
+ new_job['job_actions'] = actions
+ new_job['job_schedule'] = scheduling
+
+ for client_id in clients:
+
+ search = client_id
+ client = Client(self.request).list(search=search)
+
+ new_job['client_id'] = client[0].id
+ job_id = self.client.jobs.create(new_job)
+ self.stop(job_id)
+ return True
+
+ def _get_actions_in_job(self, action_ids):
+ actions_in_job = []
+ for action_id in action_ids:
+ action = Action(self.request).get(action_id, json=True)
+ a = {
+ 'action_id': action['action_id'],
+ 'freezer_action': action['freezer_action']
+ }
+ actions_in_job.append(a)
+ return actions_in_job
+
+
+class Session(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = client(request)
+
+ def list(self, json=False, limit=30, offset=0, search=None):
+ if search:
+ search = {"match": [{"_all": search}, ], }
+
+ sessions = self.client.sessions.list_all(limit=limit,
+ offset=offset,
+ search=search)
+
+ if json:
+ return sessions
+
+ return [utils.SessionObject(
+ session.get('session_id'),
+ session.get('description'),
+ session.get('status'),
+ session.get('jobs'),
+ session.get('schedule', {}).get('schedule_start_date'),
+ session.get('schedule', {}).get('schedule_interval'),
+ session.get('schedule', {}).get('schedule_end_date')
+ ) for session in sessions]
+
+ def get(self, session_id, json=False):
+ session = self.client.sessions.get(session_id)
+
+ if json:
+ return session
+
+ return utils.SessionObject(
+ session.get('session_id'),
+ session.get('description'),
+ session.get('status'),
+ session.get('jobs'),
+ session.get('schedule', {}).get('schedule_start_date'),
+ session.get('schedule', {}).get('schedule_interval'),
+ session.get('schedule', {}).get('schedule_end_date'))
+
+ def create(self, session):
+ return self._build(session)
+
+ def update(self, session, session_id):
+ return self.client.sessions.update(session_id, session)
+
+ def delete(self, session_id):
+ return self.client.sessions.delete(session_id)
+
+ def remove_job(self, session_id, job_id):
+ try:
+ # even if the job is removed from the session the api returns an
+ # error.
+ return self.client.sessions.remove_job(session_id, job_id)
+ except Exception:
+ pass
+
+ def add_job(self, session_id, job_id):
+ return self.client.sessions.add_job(session_id, job_id)
+
+ def jobs(self, session_id):
+ session = self.get(session_id, json=True)
+ jobs = []
+ try:
+ jobs = [utils.JobsInSessionObject(k,
+ session_id,
+ v['client_id'],
+ v['result'])
+ for k, v in session['jobs'].iteritems()]
+ except AttributeError:
+ pass
+ return jobs
+
+ def _build(self, session):
+ session = utils.create_dict(**session)
+ scheduling = {}
+ utils.assign_and_remove(session, scheduling, 'schedule_start_date')
+ utils.assign_and_remove(session, scheduling, 'schedule_interval')
+ utils.assign_and_remove(session, scheduling, 'schedule_end_date')
+ session['jobs'] = {}
+ session['schedule'] = scheduling
+ return self.client.sessions.create(session)
+
+
+class Action(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = client(request)
+
+ def list(self, json=False, limit=100, offset=0, search=None):
+ if search:
+ search = {"match": [{"_all": search}, ], }
+
+ actions = self.client.actions.list(limit=limit,
+ offset=offset,
+ search=search)
+
+ if json:
+ return actions
+
+ return [utils.ActionObjectDetail(
+ action.get('action_id'),
+ action['freezer_action'].get('action'),
+ 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'),
+ ) for action in actions]
+
+ def get(self, job_id, json=False):
+
+ action = self.client.actions.get(job_id)
+
+ if json:
+ return action
+
+ return utils.ActionObjectDetail(
+ action.get('action_id'),
+ action['freezer_action'].get('action'),
+ action['freezer_action'].get('backup_name'),
+ action['freezer_action'].get('path_to_backup'),
+ action['freezer_action'].get('storage'))
+
+ def create(self, action):
+ return self._build(action)
+
+ def update(self, action, action_id):
+ updated_action = {}
+ updated_action['freezer_action'] = utils.create_dict(**action)
+ try:
+ if action['mandatory'] != '':
+ updated_action['mandatory'] = action['mandatory']
+ except KeyError:
+ pass
+
+ try:
+ if action['max_retries'] != '':
+ updated_action['max_retries'] = action['max_retries']
+ except KeyError:
+ pass
+
+ try:
+ if action['max_retries_interval'] != '':
+ updated_action['max_retries_interval'] =\
+ action['max_retries_interval']
+ except KeyError:
+ pass
+ return self.client.actions.update(action_id, updated_action)
+
+ def delete(self, action_id):
+ return self.client.actions.delete(action_id)
+
+ def _build(self, action):
+ """Get a flat action dict and convert it to a freezer action format
+ """
+ action_rules = {}
+
+ utils.assign_and_remove(action, action_rules, 'max_retries')
+ utils.assign_and_remove(action, action_rules, 'max_retries_interval')
+ utils.assign_and_remove(action, action_rules, 'mandatory')
+ action = utils.create_dict(**action)
+ action = {'freezer_action': action}
+ return self.client.actions.create(action)
+
+
+class Client(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = client(request)
+
+ def list(self, json=False, limit=100, offset=0, search=None):
+ if search:
+ search = {"match": [{"_all": search}, ], }
+
+ clients = self.client.registration.list(limit=limit,
+ offset=offset,
+ search=search)
+
+ if json:
+ return clients
+
+ return [utils.ClientObject(
+ c.get('client', {}).get('hostname'),
+ c.get('client', {}).get('client_id'),
+ c.get('uuid')
+ ) for c in clients]
+
+ def get(self, client_id, json=False):
+ c = self.client.registration.get(client_id)
+
+ if json:
+ return c
+
+ return utils.ClientObject(
+ c.get('client', {}).get('hostname'),
+ c.get('client', {}).get('client_id'),
+ c.get('uuid'))
+
+ def delete(self, client_id):
+ return self.client.registration.delete(client_id)
+
+
+class Backup(object):
+
+ def __init__(self, request):
+ self.request = request
+ self.client = client(request)
+
+ def list(self, json=False, limit=30, offset=0, search=None):
+ if search:
+ search = {"match": [{"_all": search}, ], }
+
+ backups = self.client.backups.list(limit=limit,
+ offset=offset,
+ search=search)
+
+ if json:
+ return backups
+
+ return [utils.BackupObject(
+ backup_id=b.get('backup_uuid'),
+ action=b.get('backup_metadata', {}).get('action'),
+ time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
+ backup_name=b.get('backup_metadata', {}).get('backup_name'),
+ backup_media=b.get('backup_metadata', {}).get('backup_media'),
+ path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
+ hostname=b.get('backup_metadata', {}).get('hostname'),
+ container=b.get('backup_metadata', {}).get('container'),
+ level=b.get('backup_metadata', {}).get('level'),
+ curr_backup_level=b.get('backup_metadata', {}).get(
+ 'curr_backup_level'),
+ encrypted=b.get('backup_metadata', {}).get('encrypted'),
+ total_broken_links=b.get('backup_metadata', {}).get(
+ 'total_broken_links'),
+ excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
+ ) for b in backups]
+
+ def get(self, backup_id, json=False):
+
+ search = {"match": [{"backup_uuid": backup_id}, ], }
+
+ b = self.client.backups.list(limit=1, search=search)
+ b = b[0]
+
+ if json:
+ return b
+
+ return utils.BackupObject(
+ backup_id=b.get('backup_uuid'),
+ action=b.get('backup_metadata', {}).get('action'),
+ time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
+ backup_name=b.get('backup_metadata', {}).get('backup_name'),
+ backup_media=b.get('backup_metadata', {}).get('backup_media'),
+ path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
+ hostname=b.get('backup_metadata', {}).get('hostname'),
+ container=b.get('backup_metadata', {}).get('container'),
+ level=b.get('backup_metadata', {}).get('level'),
+ curr_backup_level=b.get('backup_metadata', {}).get(
+ 'curr_backup_level'),
+ encrypted=b.get('backup_metadata', {}).get('encrypted'),
+ total_broken_links=b.get('backup_metadata', {}).get(
+ 'total_broken_links'),
+ excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
+ )
+
+ def restore(self, data):
+ backup = self.get(data['backup_id'])
+ client_id = data['client']
+ name = "Restore job for {0}".format(client_id)
+ # TODO(m3m0): change storage to be flexible
+ action = {
+ 'action': 'restore',
+ 'backup_name': backup.backup_name,
+ 'restore_abs_path': data['path'],
+ 'container': backup.container,
+ 'restore_from_host': backup.hostname,
+ 'storage': 'local'
+ }
+
+ action_id = Action(self.request).create(action)
+
+ job = {
+ 'job_actions': [{
+ 'action_id': action_id,
+ 'freezer_action': action
+ }],
+ 'client_id': client_id,
+ 'description': name,
+ 'job_schedule': {}
+ }
+ job_id = self.client.jobs.create(job)
+ return Job(self.request).start(job_id)
diff --git a/freezer_ui/backups/workflows/__init__.py b/disaster_recovery/api/rest/__init__.py
similarity index 100%
rename from freezer_ui/backups/workflows/__init__.py
rename to disaster_recovery/api/rest/__init__.py
diff --git a/freezer_ui/api/rest/rest_api.py b/disaster_recovery/api/rest/rest_api.py
similarity index 60%
rename from freezer_ui/api/rest/rest_api.py
rename to disaster_recovery/api/rest/rest_api.py
index 0534c7a..833e9dd 100644
--- a/freezer_ui/api/rest/rest_api.py
+++ b/disaster_recovery/api/rest/rest_api.py
@@ -21,7 +21,7 @@ from django.views import generic
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.api.rest.utils import JSONResponse
-import freezer_ui.api.api as freezer_api
+import disaster_recovery.api.api as freezer_api
# https://github.com/tornadoweb/tornado/issues/1009
@@ -42,38 +42,52 @@ class Clients(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
- def get(self, request):
+ def get(self, request, job_id=None):
"""Get all registered freezer clients"""
# we don't have a "get all clients" api (probably for good reason) so
# we need to resort to getting a very high number.
- clients = freezer_api.client_list_json(request)
+ clients = freezer_api.Client(request).list(json=True)
clients = json.dumps(clients)
- return HttpResponse(clients,
- content_type="application/json")
+ return HttpResponse(clients, content_type="application/json")
-class Actions(generic.View):
- """API for clients"""
-
+class ActionList(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request):
"""Get all registered freezer actions"""
- actions = freezer_api.action_list_json(request)
+
+ actions = freezer_api.Action(request).list(json=True)
actions = json.dumps(actions)
- return HttpResponse(actions,
- content_type="application/json")
+ return HttpResponse(actions, content_type="application/json")
-class ActionsInJob(generic.View):
- """API for actions in a job"""
-
+class Actions(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request, job_id=None):
- """Get all registered freezer actions"""
- actions = freezer_api.actions_in_job_json(request, job_id)
+ actions = freezer_api.Action(request).list(json=True)
+ actions_in_job = freezer_api.Job(request).actions(job_id, api=True)
+
+ action_ids = [a['action_id'] for a in actions]
+ actions_in_job_ids = [a['action_id'] for a in actions_in_job]
+
+ available = set.difference(set(action_ids), set(actions_in_job_ids))
+ selected = set.intersection(set(action_ids), set(actions_in_job_ids))
+
+ available_actions = []
+ for action in actions:
+ if action['action_id'] in available:
+ available_actions.append(action)
+
+ selected_actions = []
+ for action in actions_in_job:
+ if action['action_id'] in selected:
+ selected_actions.append(action)
+
+ actions = {'available': available_actions,
+ 'selected': selected_actions}
+
actions = json.dumps(actions)
- return HttpResponse(actions,
- content_type="application/json")
+ return HttpResponse(actions, content_type="application/json")
diff --git a/freezer_ui/api/rest/urls.py b/disaster_recovery/api/rest/urls.py
similarity index 86%
rename from freezer_ui/api/rest/urls.py
rename to disaster_recovery/api/rest/urls.py
index 410a855..ea2d124 100644
--- a/freezer_ui/api/rest/urls.py
+++ b/disaster_recovery/api/rest/urls.py
@@ -24,7 +24,7 @@ import rest_api
urlpatterns = patterns(
'',
url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"),
- url(r'^api/actions/$', rest_api.Actions.as_view(), name="api_actions"),
+ url(r'^api/actions$', rest_api.ActionList.as_view(), name="api_actions"),
url(r'^api/actions/job/(?P[^/]+)?$',
- rest_api.ActionsInJob.as_view(), name="api_actions_in_job"),
+ rest_api.Actions.as_view(), name="api_actions_in_job"),
)
diff --git a/freezer_ui/clients/__init__.py b/disaster_recovery/backups/__init__.py
similarity index 100%
rename from freezer_ui/clients/__init__.py
rename to disaster_recovery/backups/__init__.py
diff --git a/freezer_ui/backups/panel.py b/disaster_recovery/backups/panel.py
similarity index 95%
rename from freezer_ui/backups/panel.py
rename to disaster_recovery/backups/panel.py
index 3177271..8803ce3 100644
--- a/freezer_ui/backups/panel.py
+++ b/disaster_recovery/backups/panel.py
@@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
import horizon
-from freezer_ui import dashboard
+from disaster_recovery import dashboard
class BackupsPanel(horizon.Panel):
diff --git a/freezer_ui/backups/tables.py b/disaster_recovery/backups/tables.py
similarity index 86%
rename from freezer_ui/backups/tables.py
rename to disaster_recovery/backups/tables.py
index 9b7bb62..337b635 100644
--- a/freezer_ui/backups/tables.py
+++ b/disaster_recovery/backups/tables.py
@@ -17,9 +17,11 @@ import logging
from django.core.urlresolvers import reverse
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
-from horizon.utils import functions as utils
+
from horizon import tables
-from freezer_ui.utils import timestamp_to_string
+from horizon.utils import functions as utils
+
+from disaster_recovery.utils import timestamp_to_string
LOG = logging.getLogger(__name__)
@@ -32,16 +34,13 @@ class Restore(tables.LinkAction):
ajax = True
def get_link_url(self, datum=None):
- return reverse("horizon:freezer_ui:backups:restore",
+ return reverse("horizon:disaster_recovery:backups:restore",
kwargs={'backup_id': datum.id})
class BackupFilter(tables.FilterAction):
filter_type = "server"
- filter_choices = (("before", "Created before", True),
- ("after", "Created after", True),
- ("between", "Created between", True),
- ("contains", "Contains text", True))
+ filter_choices = (("contains", "Contains text", True),)
def icons(backup):
@@ -88,13 +87,14 @@ def icons(backup):
def backup_detail_view(backup):
- return reverse("horizon:freezer_ui:backups:detail",
- args=[backup.backup_id])
+ return reverse("horizon:disaster_recovery:backups:detail",
+ kwargs={'backup_id': backup.id})
class BackupsTable(tables.DataTable):
backup_name = tables.Column('backup_name',
- verbose_name=_("Backup Name"))
+ verbose_name=_("Backup Name"),
+ link=backup_detail_view)
hostname = tables.Column('hostname', verbose_name=_("Hostname"))
created = tables.Column("time_stamp",
verbose_name=_("Created At"),
diff --git a/freezer_ui/backups/templates/backups/detail.html b/disaster_recovery/backups/templates/backups/detail.html
similarity index 100%
rename from freezer_ui/backups/templates/backups/detail.html
rename to disaster_recovery/backups/templates/backups/detail.html
diff --git a/freezer_ui/backups/templates/backups/index.html b/disaster_recovery/backups/templates/backups/index.html
similarity index 100%
rename from freezer_ui/backups/templates/backups/index.html
rename to disaster_recovery/backups/templates/backups/index.html
diff --git a/freezer_ui/backups/templates/backups/restore.html b/disaster_recovery/backups/templates/backups/restore.html
similarity index 100%
rename from freezer_ui/backups/templates/backups/restore.html
rename to disaster_recovery/backups/templates/backups/restore.html
diff --git a/freezer_ui/backups/urls.py b/disaster_recovery/backups/urls.py
similarity index 95%
rename from freezer_ui/backups/urls.py
rename to disaster_recovery/backups/urls.py
index a56f703..724f4ac 100644
--- a/freezer_ui/backups/urls.py
+++ b/disaster_recovery/backups/urls.py
@@ -16,7 +16,7 @@
from django.conf.urls import patterns
from django.conf.urls import url
-from freezer_ui.backups import views
+from disaster_recovery.backups import views
urlpatterns = patterns(
diff --git a/disaster_recovery/backups/views.py b/disaster_recovery/backups/views.py
new file mode 100644
index 0000000..0eaf163
--- /dev/null
+++ b/disaster_recovery/backups/views.py
@@ -0,0 +1,82 @@
+# 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 datetime
+import logging
+import pprint
+
+from django.template.defaultfilters import date as django_date
+from django.utils.translation import ugettext_lazy as _
+from django.views import generic
+
+from horizon import tables
+from horizon import workflows
+
+import disaster_recovery.api.api as freezer_api
+
+from disaster_recovery.backups import tables as freezer_tables
+from disaster_recovery.backups.workflows import restore as restore_workflow
+from disaster_recovery.utils import shield
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tables.DataTableView):
+ name = _("Backups")
+ slug = "backups"
+ table_class = freezer_tables.BackupsTable
+ template_name = "disaster_recovery/backups/index.html"
+
+ @shield('Unable to retrieve backups.', redirect='backups:index')
+ def get_data(self):
+ filters = self.table.get_filter_string() or None
+ return freezer_api.Backup(self.request).list(search=filters)
+
+
+class DetailView(generic.TemplateView):
+ template_name = 'disaster_recovery/backups/detail.html'
+
+ @shield('Unable to get backup.', redirect='backups:index')
+ def get_context_data(self, **kwargs):
+ backup = freezer_api.Backup(self.request).get(kwargs['backup_id'],
+ json=True)
+ return {'data': pprint.pformat(backup)}
+
+
+class RestoreView(workflows.WorkflowView):
+ workflow_class = restore_workflow.Restore
+
+ @shield('Unable to get backup.', redirect='backups:index')
+ def get_object(self, *args, **kwargs):
+ return freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
+
+ def is_update(self):
+ return 'name' in self.kwargs and bool(self.kwargs['name'])
+
+ @shield('Unable to get backup.', redirect='backups:index')
+ def get_workflow_name(self):
+ backup = freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
+ backup_date = datetime.datetime.fromtimestamp(int(backup.time_stamp))
+ backup_date_str = django_date(backup_date,
+ 'SHORT_DATETIME_FORMAT')
+ return "Restore '{}' from {}".format(backup.backup_name,
+ backup_date_str)
+
+ def get_initial(self):
+ return {"backup_id": self.kwargs['backup_id']}
+
+ @shield('Unable to get backup.', redirect='backups:index')
+ def get_workflow(self, *args, **kwargs):
+ workflow = super(RestoreView, self).get_workflow(*args, **kwargs)
+ workflow.name = self.get_workflow_name()
+ return workflow
diff --git a/freezer_ui/jobs/__init__.py b/disaster_recovery/backups/workflows/__init__.py
similarity index 100%
rename from freezer_ui/jobs/__init__.py
rename to disaster_recovery/backups/workflows/__init__.py
diff --git a/freezer_ui/backups/workflows/restore.py b/disaster_recovery/backups/workflows/restore.py
similarity index 59%
rename from freezer_ui/backups/workflows/restore.py
rename to disaster_recovery/backups/workflows/restore.py
index 1405364..db49e23 100644
--- a/freezer_ui/backups/workflows/restore.py
+++ b/disaster_recovery/backups/workflows/restore.py
@@ -18,11 +18,11 @@ import logging
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
-from horizon import forms
from horizon import exceptions
+from horizon import forms
from horizon import workflows
-import freezer_ui.api.api as freezer_api
+import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ class DestinationAction(workflows.MembershipAction):
class Destination(workflows.Step):
- template_name = 'freezer_ui/backups/restore.html'
+ template_name = 'disaster_recovery/backups/restore.html'
action_class = DestinationAction
contributes = ('client', 'path', 'backup_id')
@@ -61,7 +61,7 @@ class Restore(workflows.Workflow):
slug = "restore"
name = _("Restore")
finalize_button_name = _("Restore")
- success_url = "horizon:freezer_ui:backups:index"
+ success_url = "horizon:disaster_recovery:backups:index"
success_message = _("Restore job successfully queued. It will get "
"executed soon.")
wizard = False
@@ -69,40 +69,7 @@ class Restore(workflows.Workflow):
def handle(self, request, data):
try:
- backup_id = data['backup_id']
- client_id = data['client']
- client = freezer_api.client_get(request, client_id)
- backup = freezer_api.backup_get(request, backup_id)
- name = "Restore job for {0}".format(client_id)
-
- action = {
- "action": "restore",
- "backup_name":
- backup.data_dict['backup_metadata']['backup_name'],
- "restore_abs_path": data['path'],
- "container":
- backup.data_dict['backup_metadata']['container'],
- "restore_from_host": client.hostname,
- "storage": "local"
- }
-
- action_id = freezer_api.action_create_without_job(
- request, action)
-
- job = {
- "job_actions": [{
- "action_id": action_id,
- "freezer_action": action
- }],
- "client_id": client_id,
- "description": name,
- "job_schedule": {
- "schedule_end_date": None,
- "schedule_interval": None,
- "schedule_start_date": None
- }
- }
- return freezer_api.job_create(request, job)
+ return freezer_api.Backup(request).restore(data)
except Exception:
exceptions.handle(request)
return False
diff --git a/freezer_ui/jobs/workflows/__init__.py b/disaster_recovery/clients/__init__.py
similarity index 100%
rename from freezer_ui/jobs/workflows/__init__.py
rename to disaster_recovery/clients/__init__.py
diff --git a/freezer_ui/clients/panel.py b/disaster_recovery/clients/panel.py
similarity index 95%
rename from freezer_ui/clients/panel.py
rename to disaster_recovery/clients/panel.py
index 8aee2a9..9f094bc 100644
--- a/freezer_ui/clients/panel.py
+++ b/disaster_recovery/clients/panel.py
@@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
import horizon
-from freezer_ui import dashboard
+from disaster_recovery import dashboard
class ClientsPanel(horizon.Panel):
diff --git a/disaster_recovery/clients/tables.py b/disaster_recovery/clients/tables.py
new file mode 100644
index 0000000..f563caa
--- /dev/null
+++ b/disaster_recovery/clients/tables.py
@@ -0,0 +1,77 @@
+# 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.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
+
+from horizon import tables
+from horizon.utils.urlresolvers import reverse
+
+import disaster_recovery.api.api as freezer_api
+
+
+LOG = logging.getLogger(__name__)
+
+
+class Filter(tables.FilterAction):
+ filter_type = "server"
+ filter_choices = (("contains", "Contains text", True),)
+
+
+class DeleteClient(tables.DeleteAction):
+ name = "delete"
+ classes = ("btn-danger",)
+ icon = "remove"
+ help_text = _("Delete Clients is not recoverable.")
+
+ @staticmethod
+ def action_present(count):
+ return ungettext_lazy(
+ u"Delete Client",
+ u"Delete Clients",
+ count
+ )
+
+ @staticmethod
+ def action_past(count):
+ return ungettext_lazy(
+ u"Deleted Client",
+ u"Deleted Clients",
+ count
+ )
+
+ def delete(self, request, client_id):
+ return freezer_api.Client(request).delete(client_id)
+
+
+class DeleteMultipleClients(DeleteClient):
+ name = "delete_multiple_clients"
+
+
+def get_link(client):
+ return reverse('horizon:disaster_recovery:clients:client',
+ kwargs={'client_id': client.id})
+
+
+class ClientsTable(tables.DataTable):
+ client_id = tables.Column('client_id', verbose_name=_("Client ID"),
+ link=get_link)
+ name = tables.Column('hostname', verbose_name=_("Hostname"))
+
+ class Meta:
+ name = "clients"
+ verbose_name = _("Clients")
+ row_actions = (DeleteClient,)
+ table_actions = (Filter, DeleteMultipleClients,)
+ multi_select = True
diff --git a/disaster_recovery/clients/templates/clients/detail.html b/disaster_recovery/clients/templates/clients/detail.html
new file mode 100644
index 0000000..9d8d084
--- /dev/null
+++ b/disaster_recovery/clients/templates/clients/detail.html
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+
+{% load i18n %}
+{% block title %}{% trans "Client" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Client") %}
+{% endblock page_header %}
+
+{% block main %}
+
+
+{% endblock %}
diff --git a/freezer_ui/clients/templates/clients/index.html b/disaster_recovery/clients/templates/clients/index.html
similarity index 100%
rename from freezer_ui/clients/templates/clients/index.html
rename to disaster_recovery/clients/templates/clients/index.html
diff --git a/disaster_recovery/clients/urls.py b/disaster_recovery/clients/urls.py
new file mode 100644
index 0000000..c526723
--- /dev/null
+++ b/disaster_recovery/clients/urls.py
@@ -0,0 +1,29 @@
+# (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.
+
+
+from django.conf.urls import patterns
+from django.conf.urls import url
+
+from disaster_recovery.clients import views
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', views.IndexView.as_view(), name='index'),
+
+ url(r'^(?P[^/]+)?$',
+ views.ClientView.as_view(),
+ name='client'),
+)
diff --git a/disaster_recovery/clients/views.py b/disaster_recovery/clients/views.py
new file mode 100644
index 0000000..f57d737
--- /dev/null
+++ b/disaster_recovery/clients/views.py
@@ -0,0 +1,49 @@
+# 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 pprint
+
+from django.utils.translation import ugettext_lazy as _
+from django.views import generic
+
+from horizon import tables
+
+import disaster_recovery.api.api as freezer_api
+
+from disaster_recovery.clients import tables as freezer_tables
+from disaster_recovery.utils import shield
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tables.DataTableView):
+ name = _("Clients")
+ slug = "clients"
+ table_class = freezer_tables.ClientsTable
+ template_name = "disaster_recovery/clients/index.html"
+
+ @shield('Unable to get clients', redirect='clients:index')
+ def get_data(self):
+ filters = self.table.get_filter_string() or None
+ return freezer_api.Client(self.request).list(search=filters)
+
+
+class ClientView(generic.TemplateView):
+ template_name = 'disaster_recovery/clients/detail.html'
+
+ @shield('Unable to get client', redirect='clients:index')
+ def get_context_data(self, **kwargs):
+ client = freezer_api.Client(self.request).get(kwargs['client_id'],
+ json=True)
+ return {'data': pprint.pformat(client)}
diff --git a/freezer_ui/dashboard.py b/disaster_recovery/dashboard.py
similarity index 88%
rename from freezer_ui/dashboard.py
rename to disaster_recovery/dashboard.py
index e5a9985..9ce559f 100644
--- a/freezer_ui/dashboard.py
+++ b/disaster_recovery/dashboard.py
@@ -20,15 +20,14 @@ import horizon
class FreezerDR(horizon.PanelGroup):
slug = "freezerdr"
name = _("Backup and Restore")
- panels = ('jobs', 'sessions', 'backups', 'clients')
+ panels = ('jobs', 'actions', 'sessions', 'clients', 'backups')
class Freezer(horizon.Dashboard):
name = _("Disaster Recovery")
- slug = "freezer_ui"
+ slug = "disaster_recovery"
panels = (FreezerDR,)
default_panel = 'jobs'
- permissions = ('openstack.roles.admin',)
horizon.register(Freezer)
diff --git a/freezer_ui/sessions/__init__.py b/disaster_recovery/jobs/__init__.py
similarity index 100%
rename from freezer_ui/sessions/__init__.py
rename to disaster_recovery/jobs/__init__.py
diff --git a/freezer_ui/jobs/browsers.py b/disaster_recovery/jobs/browsers.py
similarity index 95%
rename from freezer_ui/jobs/browsers.py
rename to disaster_recovery/jobs/browsers.py
index 743a65c..06c68e5 100644
--- a/freezer_ui/jobs/browsers.py
+++ b/disaster_recovery/jobs/browsers.py
@@ -14,7 +14,7 @@
from django.utils.translation import ugettext_lazy as _
-from freezer_ui.jobs import tables
+from disaster_recovery.jobs import tables
from horizon import browsers
diff --git a/freezer_ui/jobs/panel.py b/disaster_recovery/jobs/panel.py
similarity index 95%
rename from freezer_ui/jobs/panel.py
rename to disaster_recovery/jobs/panel.py
index 1ebeb6f..e0eaad0 100644
--- a/freezer_ui/jobs/panel.py
+++ b/disaster_recovery/jobs/panel.py
@@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
import horizon
-from freezer_ui import dashboard
+from disaster_recovery import dashboard
class JobsPanel(horizon.Panel):
diff --git a/freezer_ui/jobs/tables.py b/disaster_recovery/jobs/tables.py
similarity index 55%
rename from freezer_ui/jobs/tables.py
rename to disaster_recovery/jobs/tables.py
index 189136d..1178111 100644
--- a/freezer_ui/jobs/tables.py
+++ b/disaster_recovery/jobs/tables.py
@@ -13,71 +13,39 @@
# limitations under the License.
import logging
-import datetime
+
from django import shortcuts
-from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
-from horizon import messages
from horizon import tables
from horizon.utils.urlresolvers import reverse
-import freezer_ui.api.api as freezer_api
-from freezer_ui.utils import timestamp_to_string
+import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
-def format_last_backup(last_backup):
- last_backup_ts = datetime.datetime.fromtimestamp(last_backup)
- ten_days_later = last_backup_ts + datetime.timedelta(days=10)
- today = datetime.datetime.today()
-
- if last_backup is None:
- colour = 'red'
- icon = 'fire'
- text = 'Never'
- elif ten_days_later < today:
- colour = 'orange'
- icon = 'thumbs-down'
- text = timestamp_to_string(last_backup)
- else:
- colour = 'green'
- icon = 'thumbs-up'
- text = timestamp_to_string(last_backup)
-
- return safestring.mark_safe(
- ' {}'.format(colour, icon, text))
+class ObjectFilterAction(tables.FilterAction):
+ def allowed(self, request, datum):
+ return bool(self.table.kwargs['job_id'])
class AttachJobToSession(tables.LinkAction):
name = "attach_job_to_session"
verbose_name = _("Attach To Session")
classes = ("ajax-modal")
- url = "horizon:freezer_ui:sessions:attach"
+ url = "horizon:disaster_recovery:sessions:attach"
def allowed(self, request, instance):
return True
def get_link_url(self, datum):
- return reverse("horizon:freezer_ui:sessions:attach",
+ return reverse("horizon:disaster_recovery:sessions:attach",
kwargs={'job_id': datum.job_id})
-class Restore(tables.Action):
- name = "restore"
- verbose_name = _("Restore")
-
- def single(self, table, request, instance):
- messages.info(request, "Needs to be implemented")
-
- def allowed(self, request, instance):
- return True
-
-
class DeleteJob(tables.DeleteAction):
name = "delete"
classes = ("btn-danger",)
@@ -100,8 +68,8 @@ class DeleteJob(tables.DeleteAction):
count
)
- def delete(self, request, obj_id):
- return freezer_api.job_delete(request, obj_id)
+ def delete(self, request, job_id):
+ return freezer_api.Job(request).delete(job_id)
class DeleteMultipleJobs(DeleteJob):
@@ -113,9 +81,9 @@ class CloneJob(tables.Action):
verbose_name = _("Clone Job")
help_text = _("Clone and edit a job file")
- def single(self, table, request, obj_id):
- freezer_api.job_clone(request, obj_id)
- return shortcuts.redirect('horizon:freezer_ui:jobs:index')
+ def single(self, table, request, job_id):
+ freezer_api.Job(request).clone(job_id)
+ return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
class EditJob(tables.LinkAction):
@@ -125,50 +93,65 @@ class EditJob(tables.LinkAction):
icon = "pencil"
def get_link_url(self, datum=None):
- return reverse("horizon:freezer_ui:jobs:configure",
- kwargs={'backup_name': datum.job_id})
+ return reverse("horizon:disaster_recovery:jobs:configure",
+ kwargs={'job_id': datum.job_id})
-def get_backup_configs_link(backup_config):
- return reverse('horizon:freezer_ui:jobs:index',
- kwargs={'job_id': backup_config.job_id})
+class EditActionsInJob(tables.LinkAction):
+ name = "edit_actions_in_job"
+ verbose_name = _("Edit Actions")
+ classes = ("ajax-modal",)
+ icon = "pencil"
+
+ def get_link_url(self, datum=None):
+ return reverse("horizon:disaster_recovery:jobs:edit_action",
+ kwargs={'job_id': datum.job_id})
+
+
+class StartJob(tables.Action):
+ name = "start_job"
+ verbose_name = _("Start Job")
+
+ def single(self, table, request, job_id):
+ freezer_api.Job(request).start(job_id)
+ return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
+
+
+class StopJob(tables.Action):
+ name = "stop_job"
+ verbose_name = _("Stop Job")
+
+ def single(self, table, request, job_id):
+ freezer_api.Job(request).stop(job_id)
+ return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
+
+
+def get_link(row):
+ return reverse('horizon:disaster_recovery:jobs:index',
+ kwargs={'job_id': row.job_id})
class CreateJob(tables.LinkAction):
name = "create"
verbose_name = _("Create Job")
- url = "horizon:freezer_ui:jobs:create"
+ url = "horizon:disaster_recovery:jobs:create"
classes = ("ajax-modal",)
icon = "plus"
-class CreateAction(tables.LinkAction):
- name = "create_action"
- verbose_name = _("Create Action")
- url = "horizon:freezer_ui:jobs:create_action"
- classes = ("ajax-modal",)
- icon = "plus"
-
- def get_link_url(self, datum=None):
- return reverse("horizon:freezer_ui:jobs:create_action",
- kwargs={'job_id': datum.job_id})
-
-
-class ObjectFilterAction(tables.FilterAction):
- def allowed(self, request, datum):
- return bool(self.table.kwargs['job_id'])
-
-
class JobsTable(tables.DataTable):
job_name = tables.Column("description",
- link=get_backup_configs_link,
+ link=get_link,
verbose_name=_("Job Name"))
+ event = tables.Column("event",
+ verbose_name=_("Job Status"))
+
result = tables.Column("result",
verbose_name=_("Job Result"))
- def get_object_id(self, backup_config):
- return backup_config.id
+ def get_object_id(self, row):
+ return row.id
class Meta(object):
name = "jobs"
@@ -176,13 +159,15 @@ class JobsTable(tables.DataTable):
table_actions = (ObjectFilterAction,
CreateJob,
DeleteMultipleJobs)
- footer = False
- multi_select = True
- row_actions = (CreateAction,
+ row_actions = (StartJob,
+ StopJob,
+ EditActionsInJob,
EditJob,
AttachJobToSession,
CloneJob,
- DeleteJob,)
+ DeleteJob)
+ footer = False
+ multi_select = True
class DeleteAction(tables.DeleteAction):
@@ -208,47 +193,29 @@ class DeleteAction(tables.DeleteAction):
)
def delete(self, request, obj_id):
- freezer_api.action_delete(request, obj_id)
- return reverse("horizon:freezer_ui:jobs:index")
+ freezer_api.Job(request).delete_action(obj_id)
+ return reverse("horizon:disaster_recovery:jobs:index")
class DeleteMultipleActions(DeleteAction):
name = "delete_multiple_actions"
-class EditAction(tables.LinkAction):
- name = "edit"
- verbose_name = _("Edit")
- classes = ("ajax-modal",)
- icon = "pencil"
-
- def get_link_url(self, datum=None):
- # this is used to pass to values as an url
- # TODO(m3m0): look for a way to improve this
- ids = '{0}==={1}'.format(datum.action_id, datum.job_id)
- return reverse("horizon:freezer_ui:jobs:create_action",
- kwargs={'job_id': ids})
-
-
class ActionsTable(tables.DataTable):
- action_name = tables.Column('action',
- verbose_name=_("Action Type"))
+ action = tables.Column('action', verbose_name=_("Action Type"))
- backup_name = tables.Column('backup_name',
- verbose_name=_("Action Name"))
+ name = tables.Column('backup_name', verbose_name=_("Action Name"))
def get_object_id(self, container):
- # this is used to pass to values as an url
- # TODO(m3m0): look for a way to improve this
+ # TODO(m3m0): we should't send the ids in this way
ids = '{0}==={1}'.format(container.action_id, container.job_id)
return ids
class Meta(object):
- name = "status"
- verbose_name = _("Status")
+ name = "actions_in_job"
+ verbose_name = _("Actions")
table_actions = (ObjectFilterAction,
DeleteMultipleActions)
- row_actions = (EditAction,
- DeleteAction,)
+ row_actions = (DeleteAction,)
footer = False
multi_select = True
diff --git a/freezer_ui/jobs/templates/jobs/_action.html b/disaster_recovery/jobs/templates/jobs/_action.html
similarity index 100%
rename from freezer_ui/jobs/templates/jobs/_action.html
rename to disaster_recovery/jobs/templates/jobs/_action.html
diff --git a/freezer_ui/jobs/templates/jobs/_actions.html b/disaster_recovery/jobs/templates/jobs/_actions.html
similarity index 80%
rename from freezer_ui/jobs/templates/jobs/_actions.html
rename to disaster_recovery/jobs/templates/jobs/_actions.html
index 8423e83..762f2cd 100644
--- a/freezer_ui/jobs/templates/jobs/_actions.html
+++ b/disaster_recovery/jobs/templates/jobs/_actions.html
@@ -4,7 +4,7 @@
{% endblock %}