From b08558eba43dfc45335ad817de327048ad76cf64 Mon Sep 17 00:00:00 2001 From: memo Date: Mon, 9 Nov 2015 16:10:36 +0000 Subject: [PATCH] Improved horizon dashboard for freezer action, job, session, backup, clients are now objects in api.py shield decorator to avoid having boilerplate code in views move from freezer_ui to disaster_recovery url improved actions edition in a job javascript lint added clients panel added actions panel improved restore functionality from the dashboard Change-Id: I23aed516bcde3a40b24144b05f858d1e3a49a796 --- README.rst | 40 +- freezer_ui/__init__.py => TODO.rst | 0 _50_freezer.py | 4 +- devstack/README.rst | 2 +- devstack/local.conf.example | 2 +- devstack/settings | 4 +- .../actions => disaster_recovery}/__init__.py | 0 .../actions}/__init__.py | 0 disaster_recovery/actions/panel.py | 27 + disaster_recovery/actions/tables.py | 105 ++++ .../actions/templates/actions/detail.html | 17 + .../actions/templates/actions/index.html | 16 + disaster_recovery/actions/urls.py | 33 ++ disaster_recovery/actions/views.py | 76 +++ .../actions/workflows}/__init__.py | 0 .../actions}/workflows/action.py | 95 ++-- .../api}/__init__.py | 0 disaster_recovery/api/api.py | 522 ++++++++++++++++++ .../api/rest}/__init__.py | 0 .../api/rest/rest_api.py | 50 +- .../api/rest/urls.py | 4 +- .../backups}/__init__.py | 0 .../backups/panel.py | 2 +- .../backups/tables.py | 20 +- .../backups/templates/backups/detail.html | 0 .../backups/templates/backups/index.html | 0 .../backups/templates/backups/restore.html | 0 .../backups/urls.py | 2 +- disaster_recovery/backups/views.py | 82 +++ .../backups/workflows}/__init__.py | 0 .../backups/workflows/restore.py | 43 +- .../clients}/__init__.py | 0 .../clients/panel.py | 2 +- disaster_recovery/clients/tables.py | 77 +++ .../clients/templates/clients/detail.html | 17 + .../clients/templates/clients/index.html | 0 disaster_recovery/clients/urls.py | 29 + disaster_recovery/clients/views.py | 49 ++ .../dashboard.py | 5 +- .../jobs}/__init__.py | 0 .../jobs/browsers.py | 2 +- .../jobs/panel.py | 2 +- .../jobs/tables.py | 167 +++--- .../jobs/templates/jobs/_action.html | 0 .../jobs/templates/jobs/_actions.html | 6 +- .../jobs/templates/jobs/_advanced.html | 0 .../jobs/templates/jobs/_info.html | 0 .../jobs/templates/jobs/_rules.html | 0 .../jobs/templates/jobs/_scheduling.html | 0 .../jobs/templates/jobs/_snapshot.html | 2 +- .../jobs/_workflow_step_update_members.html | 0 .../jobs/templates/jobs/browser.html | 0 .../jobs/urls.py | 12 +- disaster_recovery/jobs/views.py | 84 +++ .../jobs}/workflows/__init__.py | 0 disaster_recovery/jobs/workflows/actions.py | 81 +++ .../jobs/workflows/create.py | 132 ++--- .../clients => disaster_recovery}/models.py | 0 disaster_recovery/sessions/__init__.py | 0 .../sessions/browsers.py | 2 +- .../sessions/panel.py | 2 +- .../sessions/tables.py | 32 +- .../sessions/templates/sessions/_info.html | 0 .../sessions/templates/sessions/browser.html | 0 .../sessions/urls.py | 2 +- disaster_recovery/sessions/views.py | 86 +++ .../sessions/workflows/__init__.py | 0 .../sessions/workflows/attach.py | 16 +- .../sessions/workflows/create.py | 67 +-- .../static/freezer/css/freezer.css | 9 + .../freezer/js/freezer.actions.action.js | 0 .../freezer/js/freezer.actions.advanced.js | 0 .../freezer/js/freezer.actions.snapshot.js | 7 - .../freezer/js/freezer.datetimepicker.js | 0 .../freezer/js/freezer.jobs.sortable.js | 57 +- .../static/freezer/js/freezer.js | 2 +- .../static/freezer/js/freezer.restore.js | 2 +- .../static/freezer/js/vendor/LICENSE | 0 .../js/vendor/bootstrap-datetimepicker.js | 0 .../static/freezer/js/vendor/moment.js | 0 .../clients => disaster_recovery}/urls.py | 6 +- disaster_recovery/utils.py | 177 ++++++ freezer_ui/api/api.py | 428 -------------- freezer_ui/backups/models.py | 3 - freezer_ui/backups/views.py | 108 ---- freezer_ui/clients/tables.py | 32 -- freezer_ui/clients/views.py | 40 -- freezer_ui/jobs/models.py | 3 - freezer_ui/jobs/views.py | 147 ----- freezer_ui/models.py | 3 - freezer_ui/sessions/views.py | 120 ---- freezer_ui/static/freezer/css/freezer.css | 57 -- freezer_ui/templates/freezer/base.html | 11 - freezer_ui/urls.py | 32 -- freezer_ui/utils.py | 156 ------ requirements.txt | 1 + setup.cfg | 4 +- test-requirements.txt | 3 +- tests/__init__.py | 1 - tests/test_api.py | 0 tox.ini | 4 +- 101 files changed, 1799 insertions(+), 1634 deletions(-) rename freezer_ui/__init__.py => TODO.rst (100%) rename {freezer_ui/actions => disaster_recovery}/__init__.py (100%) rename {freezer_ui/api => disaster_recovery/actions}/__init__.py (100%) create mode 100644 disaster_recovery/actions/panel.py create mode 100644 disaster_recovery/actions/tables.py create mode 100644 disaster_recovery/actions/templates/actions/detail.html create mode 100644 disaster_recovery/actions/templates/actions/index.html create mode 100644 disaster_recovery/actions/urls.py create mode 100644 disaster_recovery/actions/views.py rename {freezer_ui/api/rest => disaster_recovery/actions/workflows}/__init__.py (100%) rename {freezer_ui/jobs => disaster_recovery/actions}/workflows/action.py (92%) rename {freezer_ui/backups => disaster_recovery/api}/__init__.py (100%) create mode 100644 disaster_recovery/api/api.py rename {freezer_ui/backups/workflows => disaster_recovery/api/rest}/__init__.py (100%) rename {freezer_ui => disaster_recovery}/api/rest/rest_api.py (60%) rename {freezer_ui => disaster_recovery}/api/rest/urls.py (86%) rename {freezer_ui/clients => disaster_recovery/backups}/__init__.py (100%) rename {freezer_ui => disaster_recovery}/backups/panel.py (95%) rename {freezer_ui => disaster_recovery}/backups/tables.py (86%) rename {freezer_ui => disaster_recovery}/backups/templates/backups/detail.html (100%) rename {freezer_ui => disaster_recovery}/backups/templates/backups/index.html (100%) rename {freezer_ui => disaster_recovery}/backups/templates/backups/restore.html (100%) rename {freezer_ui => disaster_recovery}/backups/urls.py (95%) create mode 100644 disaster_recovery/backups/views.py rename {freezer_ui/jobs => disaster_recovery/backups/workflows}/__init__.py (100%) rename {freezer_ui => disaster_recovery}/backups/workflows/restore.py (59%) rename {freezer_ui/jobs/workflows => disaster_recovery/clients}/__init__.py (100%) rename {freezer_ui => disaster_recovery}/clients/panel.py (95%) create mode 100644 disaster_recovery/clients/tables.py create mode 100644 disaster_recovery/clients/templates/clients/detail.html rename {freezer_ui => disaster_recovery}/clients/templates/clients/index.html (100%) create mode 100644 disaster_recovery/clients/urls.py create mode 100644 disaster_recovery/clients/views.py rename {freezer_ui => disaster_recovery}/dashboard.py (88%) rename {freezer_ui/sessions => disaster_recovery/jobs}/__init__.py (100%) rename {freezer_ui => disaster_recovery}/jobs/browsers.py (95%) rename {freezer_ui => disaster_recovery}/jobs/panel.py (95%) rename {freezer_ui => disaster_recovery}/jobs/tables.py (55%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_action.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_actions.html (80%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_advanced.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_info.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_rules.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_scheduling.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_snapshot.html (77%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/_workflow_step_update_members.html (100%) rename {freezer_ui => disaster_recovery}/jobs/templates/jobs/browser.html (100%) rename {freezer_ui => disaster_recovery}/jobs/urls.py (82%) create mode 100644 disaster_recovery/jobs/views.py rename {freezer_ui/sessions => disaster_recovery/jobs}/workflows/__init__.py (100%) create mode 100644 disaster_recovery/jobs/workflows/actions.py rename freezer_ui/jobs/workflows/configure.py => disaster_recovery/jobs/workflows/create.py (62%) rename {freezer_ui/clients => disaster_recovery}/models.py (100%) create mode 100644 disaster_recovery/sessions/__init__.py rename {freezer_ui => disaster_recovery}/sessions/browsers.py (95%) rename {freezer_ui => disaster_recovery}/sessions/panel.py (95%) rename {freezer_ui => disaster_recovery}/sessions/tables.py (84%) rename {freezer_ui => disaster_recovery}/sessions/templates/sessions/_info.html (100%) rename {freezer_ui => disaster_recovery}/sessions/templates/sessions/browser.html (100%) rename {freezer_ui => disaster_recovery}/sessions/urls.py (96%) create mode 100644 disaster_recovery/sessions/views.py create mode 100644 disaster_recovery/sessions/workflows/__init__.py rename {freezer_ui => disaster_recovery}/sessions/workflows/attach.py (83%) rename freezer_ui/sessions/workflows/create_session.py => disaster_recovery/sessions/workflows/create.py (70%) create mode 100644 disaster_recovery/static/freezer/css/freezer.css rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.actions.action.js (100%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.actions.advanced.js (100%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.actions.snapshot.js (92%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.datetimepicker.js (100%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.jobs.sortable.js (61%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.js (94%) rename {freezer_ui => disaster_recovery}/static/freezer/js/freezer.restore.js (97%) rename {freezer_ui => disaster_recovery}/static/freezer/js/vendor/LICENSE (100%) rename {freezer_ui => disaster_recovery}/static/freezer/js/vendor/bootstrap-datetimepicker.js (100%) rename {freezer_ui => disaster_recovery}/static/freezer/js/vendor/moment.js (100%) rename {freezer_ui/clients => disaster_recovery}/urls.py (85%) create mode 100644 disaster_recovery/utils.py delete mode 100644 freezer_ui/api/api.py delete mode 100644 freezer_ui/backups/models.py delete mode 100644 freezer_ui/backups/views.py delete mode 100644 freezer_ui/clients/tables.py delete mode 100644 freezer_ui/clients/views.py delete mode 100644 freezer_ui/jobs/models.py delete mode 100644 freezer_ui/jobs/views.py delete mode 100644 freezer_ui/models.py delete mode 100644 freezer_ui/sessions/views.py delete mode 100644 freezer_ui/static/freezer/css/freezer.css delete mode 100644 freezer_ui/templates/freezer/base.html delete mode 100644 freezer_ui/urls.py delete mode 100644 freezer_ui/utils.py create mode 100644 tests/test_api.py 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 %} +
+
+
{{ data }}
+
+
+ +{% 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 %} +
+
+
{{ data }}
+
+
+ +{% 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 %}