summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormemo <sirmemogarcia@gmail.com>2015-11-09 16:10:36 +0000
committermemo <sirmemogarcia@gmail.com>2015-11-12 16:21:27 +0000
commitb08558eba43dfc45335ad817de327048ad76cf64 (patch)
treedda3f85aa70ee8efa775f35471408c677d4cbc15
parent26f1deb396652ddbdde078c825b7e6a7b7fa101f (diff)
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
Notes
Notes (review): Verified+2: Jenkins Code-Review+2: Fausto Marzi <fausto.marzi@hpe.com> Workflow+1: Fausto Marzi <fausto.marzi@hpe.com> Code-Review+2: Fabrizio Vanni <fabrizio.vanni@hp.com> Submitted-by: Jenkins Submitted-at: Fri, 13 Nov 2015 10:16:50 +0000 Reviewed-on: https://review.openstack.org/236175 Project: openstack/freezer-web-ui Branch: refs/heads/master
-rw-r--r--README.rst40
-rw-r--r--TODO.rst (renamed from freezer_ui/__init__.py)0
-rw-r--r--_50_freezer.py4
-rw-r--r--devstack/README.rst2
-rw-r--r--devstack/local.conf.example2
-rw-r--r--devstack/settings4
-rw-r--r--disaster_recovery/__init__.py (renamed from freezer_ui/actions/__init__.py)0
-rw-r--r--disaster_recovery/actions/__init__.py (renamed from freezer_ui/api/__init__.py)0
-rw-r--r--disaster_recovery/actions/panel.py27
-rw-r--r--disaster_recovery/actions/tables.py105
-rw-r--r--disaster_recovery/actions/templates/actions/detail.html17
-rw-r--r--disaster_recovery/actions/templates/actions/index.html16
-rw-r--r--disaster_recovery/actions/urls.py33
-rw-r--r--disaster_recovery/actions/views.py76
-rw-r--r--disaster_recovery/actions/workflows/__init__.py (renamed from freezer_ui/api/rest/__init__.py)0
-rw-r--r--disaster_recovery/actions/workflows/action.py (renamed from freezer_ui/jobs/workflows/action.py)95
-rw-r--r--disaster_recovery/api/__init__.py (renamed from freezer_ui/backups/__init__.py)0
-rw-r--r--disaster_recovery/api/api.py522
-rw-r--r--disaster_recovery/api/rest/__init__.py (renamed from freezer_ui/backups/workflows/__init__.py)0
-rw-r--r--disaster_recovery/api/rest/rest_api.py (renamed from freezer_ui/api/rest/rest_api.py)50
-rw-r--r--disaster_recovery/api/rest/urls.py (renamed from freezer_ui/api/rest/urls.py)4
-rw-r--r--disaster_recovery/backups/__init__.py (renamed from freezer_ui/clients/__init__.py)0
-rw-r--r--disaster_recovery/backups/panel.py (renamed from freezer_ui/backups/panel.py)2
-rw-r--r--disaster_recovery/backups/tables.py (renamed from freezer_ui/backups/tables.py)20
-rw-r--r--disaster_recovery/backups/templates/backups/detail.html (renamed from freezer_ui/backups/templates/backups/detail.html)0
-rw-r--r--disaster_recovery/backups/templates/backups/index.html (renamed from freezer_ui/backups/templates/backups/index.html)0
-rw-r--r--disaster_recovery/backups/templates/backups/restore.html (renamed from freezer_ui/backups/templates/backups/restore.html)0
-rw-r--r--disaster_recovery/backups/urls.py (renamed from freezer_ui/backups/urls.py)2
-rw-r--r--disaster_recovery/backups/views.py82
-rw-r--r--disaster_recovery/backups/workflows/__init__.py (renamed from freezer_ui/jobs/__init__.py)0
-rw-r--r--disaster_recovery/backups/workflows/restore.py (renamed from freezer_ui/backups/workflows/restore.py)43
-rw-r--r--disaster_recovery/clients/__init__.py (renamed from freezer_ui/jobs/workflows/__init__.py)0
-rw-r--r--disaster_recovery/clients/panel.py (renamed from freezer_ui/clients/panel.py)2
-rw-r--r--disaster_recovery/clients/tables.py77
-rw-r--r--disaster_recovery/clients/templates/clients/detail.html17
-rw-r--r--disaster_recovery/clients/templates/clients/index.html (renamed from freezer_ui/clients/templates/clients/index.html)0
-rw-r--r--disaster_recovery/clients/urls.py29
-rw-r--r--disaster_recovery/clients/views.py49
-rw-r--r--disaster_recovery/dashboard.py (renamed from freezer_ui/dashboard.py)5
-rw-r--r--disaster_recovery/jobs/__init__.py (renamed from freezer_ui/sessions/__init__.py)0
-rw-r--r--disaster_recovery/jobs/browsers.py (renamed from freezer_ui/jobs/browsers.py)2
-rw-r--r--disaster_recovery/jobs/panel.py (renamed from freezer_ui/jobs/panel.py)2
-rw-r--r--disaster_recovery/jobs/tables.py (renamed from freezer_ui/jobs/tables.py)167
-rw-r--r--disaster_recovery/jobs/templates/jobs/_action.html (renamed from freezer_ui/jobs/templates/jobs/_action.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/_actions.html (renamed from freezer_ui/jobs/templates/jobs/_actions.html)6
-rw-r--r--disaster_recovery/jobs/templates/jobs/_advanced.html (renamed from freezer_ui/jobs/templates/jobs/_advanced.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/_info.html (renamed from freezer_ui/jobs/templates/jobs/_info.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/_rules.html (renamed from freezer_ui/jobs/templates/jobs/_rules.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/_scheduling.html (renamed from freezer_ui/jobs/templates/jobs/_scheduling.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/_snapshot.html (renamed from freezer_ui/jobs/templates/jobs/_snapshot.html)2
-rw-r--r--disaster_recovery/jobs/templates/jobs/_workflow_step_update_members.html (renamed from freezer_ui/jobs/templates/jobs/_workflow_step_update_members.html)0
-rw-r--r--disaster_recovery/jobs/templates/jobs/browser.html (renamed from freezer_ui/jobs/templates/jobs/browser.html)0
-rw-r--r--disaster_recovery/jobs/urls.py (renamed from freezer_ui/jobs/urls.py)12
-rw-r--r--disaster_recovery/jobs/views.py84
-rw-r--r--disaster_recovery/jobs/workflows/__init__.py (renamed from freezer_ui/sessions/workflows/__init__.py)0
-rw-r--r--disaster_recovery/jobs/workflows/actions.py81
-rw-r--r--disaster_recovery/jobs/workflows/create.py (renamed from freezer_ui/jobs/workflows/configure.py)132
-rw-r--r--disaster_recovery/models.py (renamed from freezer_ui/clients/models.py)0
-rw-r--r--disaster_recovery/sessions/__init__.py0
-rw-r--r--disaster_recovery/sessions/browsers.py (renamed from freezer_ui/sessions/browsers.py)2
-rw-r--r--disaster_recovery/sessions/panel.py (renamed from freezer_ui/sessions/panel.py)2
-rw-r--r--disaster_recovery/sessions/tables.py (renamed from freezer_ui/sessions/tables.py)32
-rw-r--r--disaster_recovery/sessions/templates/sessions/_info.html (renamed from freezer_ui/sessions/templates/sessions/_info.html)0
-rw-r--r--disaster_recovery/sessions/templates/sessions/browser.html (renamed from freezer_ui/sessions/templates/sessions/browser.html)0
-rw-r--r--disaster_recovery/sessions/urls.py (renamed from freezer_ui/sessions/urls.py)2
-rw-r--r--disaster_recovery/sessions/views.py86
-rw-r--r--disaster_recovery/sessions/workflows/__init__.py0
-rw-r--r--disaster_recovery/sessions/workflows/attach.py (renamed from freezer_ui/sessions/workflows/attach.py)16
-rw-r--r--disaster_recovery/sessions/workflows/create.py (renamed from freezer_ui/sessions/workflows/create_session.py)67
-rw-r--r--disaster_recovery/static/freezer/css/freezer.css9
-rw-r--r--disaster_recovery/static/freezer/js/freezer.actions.action.js (renamed from freezer_ui/static/freezer/js/freezer.actions.action.js)0
-rw-r--r--disaster_recovery/static/freezer/js/freezer.actions.advanced.js (renamed from freezer_ui/static/freezer/js/freezer.actions.advanced.js)0
-rw-r--r--disaster_recovery/static/freezer/js/freezer.actions.snapshot.js (renamed from freezer_ui/static/freezer/js/freezer.actions.snapshot.js)7
-rw-r--r--disaster_recovery/static/freezer/js/freezer.datetimepicker.js (renamed from freezer_ui/static/freezer/js/freezer.datetimepicker.js)0
-rw-r--r--disaster_recovery/static/freezer/js/freezer.jobs.sortable.js (renamed from freezer_ui/static/freezer/js/freezer.jobs.sortable.js)57
-rw-r--r--disaster_recovery/static/freezer/js/freezer.js (renamed from freezer_ui/static/freezer/js/freezer.js)2
-rw-r--r--disaster_recovery/static/freezer/js/freezer.restore.js (renamed from freezer_ui/static/freezer/js/freezer.restore.js)2
-rw-r--r--disaster_recovery/static/freezer/js/vendor/LICENSE (renamed from freezer_ui/static/freezer/js/vendor/LICENSE)0
-rw-r--r--disaster_recovery/static/freezer/js/vendor/bootstrap-datetimepicker.js (renamed from freezer_ui/static/freezer/js/vendor/bootstrap-datetimepicker.js)0
-rw-r--r--disaster_recovery/static/freezer/js/vendor/moment.js (renamed from freezer_ui/static/freezer/js/vendor/moment.js)0
-rw-r--r--disaster_recovery/urls.py (renamed from freezer_ui/clients/urls.py)6
-rw-r--r--disaster_recovery/utils.py177
-rw-r--r--freezer_ui/api/api.py428
-rw-r--r--freezer_ui/backups/models.py3
-rw-r--r--freezer_ui/backups/views.py108
-rw-r--r--freezer_ui/clients/tables.py32
-rw-r--r--freezer_ui/clients/views.py40
-rw-r--r--freezer_ui/jobs/models.py3
-rw-r--r--freezer_ui/jobs/views.py147
-rw-r--r--freezer_ui/models.py3
-rw-r--r--freezer_ui/sessions/views.py120
-rw-r--r--freezer_ui/static/freezer/css/freezer.css57
-rw-r--r--freezer_ui/templates/freezer/base.html11
-rw-r--r--freezer_ui/urls.py32
-rw-r--r--freezer_ui/utils.py156
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg4
-rw-r--r--test-requirements.txt3
-rw-r--r--tests/__init__.py1
-rw-r--r--tests/test_api.py0
-rw-r--r--tox.ini4
101 files changed, 1799 insertions, 1634 deletions
diff --git a/README.rst b/README.rst
index a10de2e..372ba85 100644
--- a/README.rst
+++ b/README.rst
@@ -2,6 +2,13 @@
2Freezer - Horizon Dashboard 2Freezer - Horizon Dashboard
3=========================== 3===========================
4 4
5freezer-web-ui is a horizon plugin based in django aimed at providing an interaction
6with freezer
7
8* Release management: https://launchpad.net/freezer-web-ui
9* Blueprints and feature specifications: https://blueprints.launchpad.net/freezer-web-ui
10* Issue tracking: https://bugs.launchpad.net/freezer-web-ui
11
5Requirements 12Requirements
6============ 13============
7 14
@@ -73,39 +80,8 @@ To deploy freezer dashboard in production you need to do the following::
73A new tab called "Disaster Recovery" will be on your panels. 80A new tab called "Disaster Recovery" will be on your panels.
74 81
75 82
76Running the unit tests
77======================
78
791. Create a virtual environment::
80
81 virtualenv --no-site-packages -p /usr/bin/python2.7 .venv
82
832. Activate the virtual environment::
84
85 . ./.venv/bin/activate
86
873. Install the requirements::
88
89 pip install -r test-requirements.txt
90
914. Run the tests::
92
93 python manage.py test . --settings=freezer_ui.tests.settings
94
95Test coverage
96-------------
97
981. Collect coverage information::
99
100 coverage run --source='.' --omit='.venv/*' manage.py test . --settings=freezer_ui.tests.settings
101
1022. View coverage report::
103
104 coverage report
105
106
107Tox 83Tox
108--- 84===
109 85
1101. Run tox:: 861. Run tox::
111 87
diff --git a/freezer_ui/__init__.py b/TODO.rst
index e69de29..e69de29 100644
--- a/freezer_ui/__init__.py
+++ b/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 @@
13# limitations under the License. 13# limitations under the License.
14 14
15# The name of the dashboard to be added to HORIZON['dashboards']. Required. 15# The name of the dashboard to be added to HORIZON['dashboards']. Required.
16DASHBOARD = 'freezer_ui' 16DASHBOARD = 'disaster_recovery'
17 17
18# If set to True, this dashboard will not be added to the settings. 18# If set to True, this dashboard will not be added to the settings.
19DISABLED = False 19DISABLED = False
20 20
21# A list of applications to be added to INSTALLED_APPS. 21# A list of applications to be added to INSTALLED_APPS.
22ADD_INSTALLED_APPS = [ 22ADD_INSTALLED_APPS = [
23 'freezer_ui', 23 'disaster_recovery',
24] 24]
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::
15 15
16For example:: 16For example::
17 17
18 enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master 18 enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
19 19
20For more information, see: 20For more information, see:
21 http://docs.openstack.org/developer/devstack/plugins.html 21 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 @@
1[[local|localrc]] 1[[local|localrc]]
2disable_all_services 2disable_all_services
3 3
4enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master 4enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
5 5
6enable_service rabbit mysql key 6enable_service rabbit mysql key
7 7
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
20FREEZER_WEB_UI_FILES=${FREEZER_WEB_UI_DIR}/devstack/files 20FREEZER_WEB_UI_FILES=${FREEZER_WEB_UI_DIR}/devstack/files
21 21
22# Freezer Web UI repository 22# Freezer Web UI repository
23FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/stackforge/freezer-web-ui.git} 23FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/openstack/freezer-web-ui.git}
24FREEZER_WEB_UI_BRANCH=${FREEZER_WEB_UI_BRANCH:-master} 24FREEZER_WEB_UI_BRANCH=${FREEZER_WEB_UI_BRANCH:-master}
25 25
26# Freezer client 26# Freezer client
27FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/stackforge/freezer.git} 27FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/openstack/freezer.git}
28FREEZER_CLIENT_DIR=$DEST/freezer 28FREEZER_CLIENT_DIR=$DEST/freezer
29FREEZER_CLIENT_BRANCH=${FREEZER_CLIENT_BRANCH:-master} 29FREEZER_CLIENT_BRANCH=${FREEZER_CLIENT_BRANCH:-master}
30 30
diff --git a/freezer_ui/actions/__init__.py b/disaster_recovery/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/actions/__init__.py
+++ b/disaster_recovery/__init__.py
diff --git a/freezer_ui/api/__init__.py b/disaster_recovery/actions/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/api/__init__.py
+++ b/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 @@
1# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from django.utils.translation import ugettext_lazy as _
16
17import horizon
18
19from disaster_recovery import dashboard
20
21
22class ActionsPanel(horizon.Panel):
23 name = _("Actions")
24 slug = "actions"
25
26
27dashboard.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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from django import shortcuts
16from django.utils.translation import ugettext_lazy as _
17from django.utils.translation import ungettext_lazy
18
19from horizon import tables
20from horizon.utils.urlresolvers import reverse
21
22import disaster_recovery.api.api as freezer_api
23
24
25LOG = logging.getLogger(__name__)
26
27
28class DeleteAction(tables.DeleteAction):
29 name = "delete"
30 classes = ("btn-danger",)
31 icon = "remove"
32 help_text = _("Delete actions is not recoverable.")
33
34 @staticmethod
35 def action_present(count):
36 return ungettext_lazy(
37 u"Delete Action",
38 u"Delete Actions",
39 count
40 )
41
42 @staticmethod
43 def action_past(count):
44 return ungettext_lazy(
45 u"Deleted Action",
46 u"Deleted Actions",
47 count
48 )
49
50 def delete(self, request, action_id):
51 freezer_api.Action(request).delete(action_id)
52 # TODO(m3m0): this shouldnt redirect here when an action is deleted
53 # from jobs views
54 return shortcuts.redirect('horizon:disaster_recovery:actions:index')
55
56
57class DeleteMultipleActions(DeleteAction):
58 name = "delete_multiple_actions"
59
60
61class BackupFilter(tables.FilterAction):
62 filter_type = "server"
63 filter_choices = (("contains", "Contains text", True),)
64
65
66class CreateAction(tables.LinkAction):
67 name = "create_action"
68 verbose_name = _("Create Action")
69 url = "horizon:disaster_recovery:actions:create"
70 classes = ("ajax-modal",)
71 icon = "plus"
72
73
74class EditAction(tables.LinkAction):
75 name = "edit_action"
76 verbose_name = _("Edit")
77 classes = ("ajax-modal",)
78 icon = "pencil"
79
80 def get_link_url(self, datum=None):
81 return reverse("horizon:disaster_recovery:actions:create",
82 kwargs={'action_id': datum.action_id})
83
84
85def get_link(action):
86 return reverse('horizon:disaster_recovery:actions:action',
87 kwargs={'action_id': action.id})
88
89
90class ActionsTable(tables.DataTable):
91 backup_name = tables.Column('backup_name',
92
93 verbose_name=_("Action Name"),
94 link=get_link)
95 action = tables.Column('action', verbose_name=_("Action"))
96 path_to_backup = tables.Column('path_to_backup',
97 verbose_name=_("Path To Backup or Restore"))
98 storage = tables.Column('storage', verbose_name=_("Storage"))
99
100 class Meta:
101 name = "actions_table"
102 verbose_name = _("Actions")
103 row_actions = (EditAction, DeleteAction,)
104 table_actions = (BackupFilter, CreateAction, DeleteMultipleActions)
105 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 @@
1{% extends 'base.html' %}
2
3{% load i18n %}
4{% block title %}{% trans "Action" %}{% endblock %}
5
6{% block page_header %}
7 {% include "horizon/common/_page_header.html" with title=_("Action") %}
8{% endblock page_header %}
9
10{% block main %}
11 <div class="row">
12 <div class="col-sm-12">
13 <pre>{{ data }}</pre>
14 </div>
15 </div>
16
17{% 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 @@
1{% extends 'base.html' %}
2
3{% block css %}
4 {% include "_stylesheets.html" %}
5{% endblock css %}
6
7{% load i18n %}
8{% block title %}{% trans "Actions" %}{% endblock title %}
9
10{% block page_header %}
11 {% include "horizon/common/_page_header.html" with title=_("Actions") %}
12{% endblock page_header %}
13
14{% block main %}
15 {{ table.render }}
16{% 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 @@
1# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16from django.conf.urls import patterns
17from django.conf.urls import url
18
19from disaster_recovery.actions import views
20
21
22urlpatterns = patterns(
23 '',
24 url(r'^$', views.IndexView.as_view(), name='index'),
25
26 url(r'^create/(?P<action_id>[^/]+)?$',
27 views.ActionWorkflowView.as_view(),
28 name='create'),
29
30 url(r'^action/(?P<action_id>[^/]+)?$',
31 views.ActionView.as_view(),
32 name='action'),
33)
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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14import pprint
15
16from django.core.urlresolvers import reverse_lazy
17from django.utils.translation import ugettext_lazy as _
18from django.views import generic
19
20from horizon import tables
21from horizon import workflows
22
23import disaster_recovery.api.api as freezer_api
24
25from disaster_recovery.actions import tables as freezer_tables
26from disaster_recovery.actions.workflows import action as action_workflow
27from disaster_recovery.utils import shield
28
29
30LOG = logging.getLogger(__name__)
31
32
33class IndexView(tables.DataTableView):
34 name = _("Actions")
35 slug = "actions"
36 table_class = freezer_tables.ActionsTable
37 template_name = "disaster_recovery/actions/index.html"
38
39 @shield("Unable to get actions", redirect="actions:index")
40 def get_data(self):
41 filters = self.table.get_filter_string() or None
42 return freezer_api.Action(self.request).list(search=filters)
43
44
45class ActionView(generic.TemplateView):
46 template_name = 'disaster_recovery/actions/detail.html'
47
48 @shield('Unable to get action', redirect='actions:index')
49 def get_context_data(self, **kwargs):
50 action = freezer_api.Action(self.request).get(kwargs['action_id'],
51 json=True)
52 return {'data': pprint.pformat(action)}
53
54
55class ActionWorkflowView(workflows.WorkflowView):
56 workflow_class = action_workflow.ActionWorkflow
57 success_url = reverse_lazy("horizon:disaster_recovery:actions:index")
58
59 def is_update(self):
60 return 'action_id' in self.kwargs and bool(self.kwargs['action_id'])
61
62 @shield("Unable to get job", redirect="jobs:index")
63 def get_initial(self):
64 initial = super(ActionWorkflowView, self).get_initial()
65 if self.is_update():
66 initial.update({'action_id': None})
67 action = freezer_api.Action(self.request).get(
68 self.kwargs['action_id'], json=True)
69 initial.update(**action['freezer_action'])
70 initial.update({
71 "mandatory": action['mandatory'],
72 "max_retries": action['max_retries'],
73 "max_retries_interval": action['max_retries_interval']
74 })
75 initial.update({'action_id': action['action_id']})
76 return initial
diff --git a/freezer_ui/api/rest/__init__.py b/disaster_recovery/actions/workflows/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/api/rest/__init__.py
+++ b/disaster_recovery/actions/workflows/__init__.py
diff --git a/freezer_ui/jobs/workflows/action.py b/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 @@
14 14
15import logging 15import logging
16 16
17from django import shortcuts
17from django.utils.translation import ugettext_lazy as _ 18from django.utils.translation import ugettext_lazy as _
18from horizon import workflows 19
19from horizon import exceptions 20from horizon import exceptions
20from horizon import forms 21from horizon import forms
21import freezer_ui.api.api as freezer_api 22from horizon import workflows
23
24import disaster_recovery.api.api as freezer_api
22 25
23 26
24LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
25 28
26 29
27class ActionConfigurationAction(workflows.Action): 30class ActionConfigurationAction(workflows.Action):
31 action_id = forms.CharField(
32 widget=forms.HiddenInput(),
33 required=False)
34
28 action = forms.ChoiceField( 35 action = forms.ChoiceField(
29 help_text=_("Set the action to be taken"), 36 help_text=_("Set the action to be taken"),
30 required=True) 37 required=True)
@@ -33,14 +40,6 @@ class ActionConfigurationAction(workflows.Action):
33 help_text=_("Choose what you want to backup"), 40 help_text=_("Choose what you want to backup"),
34 required=False) 41 required=False)
35 42
36 original_name = forms.CharField(
37 widget=forms.HiddenInput(),
38 required=False)
39
40 action_id = forms.CharField(
41 widget=forms.HiddenInput(),
42 required=False)
43
44 storage = forms.ChoiceField( 43 storage = forms.ChoiceField(
45 help_text=_("Set storage backend for a backup"), 44 help_text=_("Set storage backend for a backup"),
46 required=True) 45 required=True)
@@ -224,15 +223,16 @@ class ActionConfigurationAction(workflows.Action):
224 223
225 class Meta(object): 224 class Meta(object):
226 name = _("Action") 225 name = _("Action")
227 help_text_template = "freezer_ui/jobs" \ 226 help_text_template = "disaster_recovery/jobs" \
228 "/_action.html" 227 "/_action.html"
229 228
230 229
231class ActionConfiguration(workflows.Step): 230class ActionConfiguration(workflows.Step):
232 action_class = ActionConfigurationAction 231 action_class = ActionConfigurationAction
233 contributes = ('action', 232 contributes = ('action_id',
233 'action',
234 'mode', 234 'mode',
235 'original_name', 235 'storage',
236 'backup_name', 236 'backup_name',
237 'mysql_conf', 237 'mysql_conf',
238 'sql_server_conf', 238 'sql_server_conf',
@@ -247,11 +247,9 @@ class ActionConfiguration(workflows.Step):
247 'dst_file', 247 'dst_file',
248 'remove_older_than', 248 'remove_older_than',
249 'remove_from_date', 249 'remove_from_date',
250 'action_id', 250 'ssh_key',
251 'storage',
252 'ssh_host',
253 'ssh_username', 251 'ssh_username',
254 'ssh_key') 252 'ssh_host')
255 253
256 254
257class SnapshotConfigurationAction(workflows.Action): 255class SnapshotConfigurationAction(workflows.Action):
@@ -270,15 +268,6 @@ class SnapshotConfigurationAction(workflows.Action):
270 widget=forms.CheckboxInput(), 268 widget=forms.CheckboxInput(),
271 required=False) 269 required=False)
272 270
273 vssadmin = forms.BooleanField(
274 label=_("VSSAdmin"),
275 help_text=_("Create a backup using a snapshot on windows "
276 "using vssadmin. Options are: "
277 "True and False, default is True"),
278 widget=forms.CheckboxInput(),
279 initial=True,
280 required=False)
281
282 lvm_auto_snap = forms.CharField( 271 lvm_auto_snap = forms.CharField(
283 label=_("LVM Auto Snapshot"), 272 label=_("LVM Auto Snapshot"),
284 help_text=_("Automatically guess the volume group and " 273 help_text=_("Automatically guess the volume group and "
@@ -321,7 +310,7 @@ class SnapshotConfigurationAction(workflows.Action):
321 310
322 class Meta(object): 311 class Meta(object):
323 name = _("Snapshot") 312 name = _("Snapshot")
324 help_text_template = "freezer_ui/jobs" \ 313 help_text_template = "disaster_recovery/jobs" \
325 "/_snapshot.html" 314 "/_snapshot.html"
326 315
327 316
@@ -329,7 +318,6 @@ class SnapshotConfiguration(workflows.Step):
329 action_class = SnapshotConfigurationAction 318 action_class = SnapshotConfigurationAction
330 contributes = ('use_snapshot', 319 contributes = ('use_snapshot',
331 'is_windows', 320 'is_windows',
332 'vssadmin',
333 'lvm_auto_snap', 321 'lvm_auto_snap',
334 'lvm_srcvol', 322 'lvm_srcvol',
335 'lvm_snapname', 323 'lvm_snapname',
@@ -374,7 +362,6 @@ class AdvancedConfigurationAction(workflows.Action):
374 help_text=_("Upload bandwidth limit in Bytes per sec." 362 help_text=_("Upload bandwidth limit in Bytes per sec."
375 " Can be invoked with dimensions " 363 " Can be invoked with dimensions "
376 "(10K, 120M, 10G)."), 364 "(10K, 120M, 10G)."),
377 initial=-1,
378 min_value=-1, 365 min_value=-1,
379 required=False) 366 required=False)
380 367
@@ -383,17 +370,9 @@ class AdvancedConfigurationAction(workflows.Action):
383 help_text=_("Download bandwidth limit in Bytes per sec. " 370 help_text=_("Download bandwidth limit in Bytes per sec. "
384 "Can be invoked with dimensions" 371 "Can be invoked with dimensions"
385 " (10K, 120M, 10G)."), 372 " (10K, 120M, 10G)."),
386 initial=-1,
387 min_value=-1, 373 min_value=-1,
388 required=False) 374 required=False)
389 375
390 optimize = forms.ChoiceField(
391 choices=[('speed', _("Speed (tar)")),
392 ('bandwidth', _("Bandwidth/Space (rsync)"))],
393 help_text="",
394 label=_('Optimize For...'),
395 required=False)
396
397 compression = forms.ChoiceField( 376 compression = forms.ChoiceField(
398 choices=[ 377 choices=[
399 ('gzip', _("Minimum Compression (GZip/Zip/Zlib)")), 378 ('gzip', _("Minimum Compression (GZip/Zip/Zlib)")),
@@ -409,7 +388,6 @@ class AdvancedConfigurationAction(workflows.Action):
409 help_text=_("Set the maximum file chunk size in bytes" 388 help_text=_("Set the maximum file chunk size in bytes"
410 " to upload to swift." 389 " to upload to swift."
411 " Default 67108864 bytes (64MB)"), 390 " Default 67108864 bytes (64MB)"),
412 initial=67108864,
413 min_value=1, 391 min_value=1,
414 required=False) 392 required=False)
415 393
@@ -453,7 +431,6 @@ class AdvancedConfigurationAction(workflows.Action):
453 431
454 always_level = forms.IntegerField( 432 always_level = forms.IntegerField(
455 label=_("Always Level"), 433 label=_("Always Level"),
456 initial=0,
457 min_value=0, 434 min_value=0,
458 help_text=_("Set backup maximum level used with tar to" 435 help_text=_("Set backup maximum level used with tar to"
459 " implement incremental backup. If a level " 436 " implement incremental backup. If a level "
@@ -467,7 +444,6 @@ class AdvancedConfigurationAction(workflows.Action):
467 444
468 restart_always_level = forms.IntegerField( 445 restart_always_level = forms.IntegerField(
469 label=_("Restart Always Level"), 446 label=_("Restart Always Level"),
470 initial=0,
471 min_value=0, 447 min_value=0,
472 help_text=_("Restart the backup from level 0 after n days. " 448 help_text=_("Restart the backup from level 0 after n days. "
473 "Valid only if --always-level option if set. " 449 "Valid only if --always-level option if set. "
@@ -526,30 +502,29 @@ class AdvancedConfigurationAction(workflows.Action):
526 502
527 class Meta(object): 503 class Meta(object):
528 name = _("Advanced") 504 name = _("Advanced")
529 help_text_template = "freezer_ui/jobs" \ 505 help_text_template = "disaster_recovery/jobs" \
530 "/_advanced.html" 506 "/_advanced.html"
531 507
532 508
533class AdvancedConfiguration(workflows.Step): 509class AdvancedConfiguration(workflows.Step):
534 action_class = AdvancedConfigurationAction 510 action_class = AdvancedConfigurationAction
535 contributes = ('exclude', 511 contributes = ('exclude',
536 # 'log_file', 512 'log_file',
537 # 'proxy', 513 'proxy',
538 # 'os_auth_ver', 514 'os_auth_ver',
539 # 'upload_limit', 515 'upload_limit',
540 # 'download_limit', 516 'download_limit',
541 # 'optimize', 517 'compression',
542 # 'compression', 518 'max_segment_size',
543 # 'max_segment_size',
544 'hostname', 519 'hostname',
545 'encryption_password', 520 'encryption_password',
546 'no_incremental', 521 'no_incremental',
547 'max_level', 522 'max_level',
548 'always_level', 523 'always_level',
549 'restart_always_level', 524 'restart_always_level',
550 # 'insecure', 525 'insecure',
551 'dereference_symlink', 526 'dereference_symlink',
552 # 'dry_run', 527 'dry_run',
553 'max_priority', 528 'max_priority',
554 'quiet',) 529 'quiet',)
555 530
@@ -579,7 +554,7 @@ class RulesConfigurationAction(workflows.Action):
579 554
580 class Meta(object): 555 class Meta(object):
581 name = _("Rules") 556 name = _("Rules")
582 help_text_template = "freezer_ui/jobs" \ 557 help_text_template = "disaster_recovery/jobs" \
583 "/_rules.html" 558 "/_rules.html"
584 559
585 560
@@ -590,13 +565,13 @@ class RulesConfiguration(workflows.Step):
590 'mandatory') 565 'mandatory')
591 566
592 567
593class ConfigureAction(workflows.Workflow): 568class ActionWorkflow(workflows.Workflow):
594 slug = "action" 569 slug = "action"
595 name = _("Action Configuration") 570 name = _("Action Configuration")
596 finalize_button_name = _("Save") 571 finalize_button_name = _("Save")
597 success_message = _('Action file saved correctly.') 572 success_message = _('Action file saved correctly.')
598 failure_message = _('Unable to save action file.') 573 failure_message = _('Unable to save action file.')
599 success_url = "horizon:freezer_ui:jobs:index" 574 success_url = "horizon:disaster_recovery:actions:index"
600 575
601 default_steps = (ActionConfiguration, 576 default_steps = (ActionConfiguration,
602 SnapshotConfiguration, 577 SnapshotConfiguration,
@@ -607,18 +582,26 @@ class ConfigureAction(workflows.Workflow):
607 try: 582 try:
608 if context['is_windows']: 583 if context['is_windows']:
609 client_os = 'Windows' 584 client_os = 'Windows'
585 context.pop('is_windows')
610 else: 586 else:
611 client_os = 'Linux' 587 client_os = 'Linux'
588 context.pop('is_windows')
612 589
613 if context['use_snapshot'] and client_os == 'Windows': 590 if context['use_snapshot'] and client_os == 'Windows':
614 context['vssadmin'] = True 591 context['vssadmin'] = True
592 context.pop('use_snapshot')
615 else: 593 else:
616 context['vssadmin'] = False 594 context['vssadmin'] = False
595 context.pop('use_snapshot')
617 596
618 if context['action_id'] == '': 597 if context['action_id'] == '':
619 return freezer_api.action_create(request, context) 598 freezer_api.Action(request).create(context)
620 else: 599 else:
621 return freezer_api.action_update(request, context) 600 freezer_api.Action(request).update(context,
601 context['action_id'])
602
603 return shortcuts.redirect('horizon:disaster_recovery:'
604 'actions:index')
622 except Exception: 605 except Exception:
623 exceptions.handle(request) 606 exceptions.handle(request)
624 return False 607 return False
diff --git a/freezer_ui/backups/__init__.py b/disaster_recovery/api/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/backups/__init__.py
+++ b/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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13# Some helper functions to use the disaster_recovery client functionality
14# from horizon.
15
16import logging
17
18from django.conf import settings
19
20from horizon.utils.memoized import memoized # noqa
21
22import freezer.apiclient.client
23from disaster_recovery import utils
24
25
26LOG = logging.getLogger(__name__)
27
28
29@memoized
30def client(request):
31 """Return a freezer client object"""
32 api_url = _get_service_url(request)
33 return freezer.apiclient.client.Client(
34 token=request.user.token.id,
35 auth_url=getattr(settings, 'OPENSTACK_KEYSTONE_URL'),
36 endpoint=api_url)
37
38
39@memoized
40def _get_service_url(request):
41 """Get freezer api url"""
42 catalog = (getattr(request.user, "service_catalog", None))
43 if not catalog:
44 return _get_hardcoded_url()
45
46 for c in catalog:
47 if c['name'] == 'freezer':
48 for e in c['endpoints']:
49 return e['internalURL']
50 else:
51 return _get_hardcoded_url()
52
53
54@memoized
55def _get_hardcoded_url():
56 """In case freezer is not registered in keystone catalog, look for it in
57 local_settings.py
58 :return: freezer_api_url
59 """
60 try:
61 LOG.warn('Using hardcoded FREEZER_API_URL at {0}'
62 .format(settings.FREEZER_API_URL))
63 return getattr(settings, 'FREEZER_API_URL', None)
64 except Exception:
65 LOG.warn('No FREEZER_API_URL was found in local_settings.py')
66 raise
67
68
69class Job(object):
70
71 def __init__(self, request):
72 self.request = request
73 self.client = client(request)
74
75 def list(self, json=False, limit=100, offset=0, search=None):
76 if search:
77 search = {"match": [{"_all": search}, ], }
78
79 jobs = self.client.jobs.list_all(limit=limit,
80 offset=offset,
81 search=search)
82 if json:
83 return jobs
84
85 return [utils.JobObject(
86 job.get('job_id'),
87 job.get('description'),
88 job.get('job_schedule', {}).get('result'),
89 job.get('job_schedule', {}).get('event'),
90 ) for job in jobs]
91
92 def get(self, job_id, json=False):
93 job = self.client.jobs.get(job_id)
94
95 if json:
96 return job
97
98 return utils.JobObject(
99 job.get('job_id'),
100 job.get('description'),
101 job.get('job_schedule', {}).get('result'),
102 job.get('job_schedule', {}).get('event'))
103
104 def create(self, job):
105 return self._build(job)
106
107 def update(self, job_id, job):
108 scheduling = {}
109 try:
110 if job['schedule_end_date'] != '':
111 utils.assign_and_remove(job, scheduling, 'schedule_end_date')
112 else:
113 job.pop('schedule_end_date')
114 except KeyError:
115 pass
116
117 try:
118 if job['schedule_interval'] != '':
119 utils.assign_and_remove(job, scheduling, 'schedule_interval')
120 else:
121 job.pop('schedule_interval')
122 except KeyError:
123 pass
124
125 try:
126 if job['schedule_start_date'] != '':
127 utils.assign_and_remove(job, scheduling, 'schedule_start_date')
128 else:
129 job.pop('schedule_start_date')
130 except KeyError:
131 pass
132
133 job.pop('job_actions', [])
134 job.pop('clients', None)
135 job.pop('actions', None)
136 job.pop('job_id')
137 job['job_schedule'] = scheduling
138 return self.client.jobs.update(job_id, job)
139
140 def update_actions(self, job_id, action_ids):
141 ids = utils.get_action_ids(action_ids)
142 job = self.get(job_id, json=True)
143 job.pop('job_actions', None)
144 actions = self._get_actions_in_job(ids)
145 job['job_actions'] = actions
146 return self.client.jobs.update(job_id, job)
147
148 def delete(self, job_id):
149 return self.client.jobs.delete(job_id)
150
151 def actions(self, job_id, api=False):
152 job = self.get(job_id, json=True)
153
154 if not job:
155 return []
156
157 if api:
158 return job.get('job_actions', [])
159
160 return [utils.ActionObject(
161 action_id=a.get('action_id'),
162 action=a.get('freezer_action', {}).get('action'),
163 backup_name=a.get('freezer_action', {}).get('backup_name'),
164 job_id=job_id
165 ) for a in job.get('job_actions')]
166
167 def delete_action(self, ids):
168 action_id, job_id = ids.split('===')
169 job = self.get(job_id, json=True)
170 for action in job['job_actions']:
171 if action.get('action_id') == action_id:
172 job.get('job_actions').remove(action)
173 return self.client.jobs.update(job_id, job)
174
175 def clone(self, job_id):
176 job = self.get(job_id, json=True)
177 job['description'] = '{0}_clone'.format(job['description'])
178 job.pop('job_id', None)
179 job.pop('_version', None)
180 job_id = self.client.jobs.create(job)
181 return self.stop(job_id)
182
183 def stop(self, job_id):
184 return self.client.jobs.stop_job(job_id)
185
186 def start(self, job_id):
187 return self.client.jobs.start_job(job_id)
188
189 def _build(self, job):
190 action_ids = utils.get_action_ids(job.pop('actions'))
191 job = utils.create_dict(**job)
192 clients = job.pop('clients', [])
193 scheduling = {}
194 new_job = {}
195 utils.assign_and_remove(job, scheduling, 'schedule_start_date')
196 utils.assign_and_remove(job, scheduling, 'schedule_interval')
197 utils.assign_and_remove(job, scheduling, 'schedule_end_date')
198
199 actions = self._get_actions_in_job(action_ids)
200
201 new_job['description'] = job.get('description')
202 new_job['job_actions'] = actions
203 new_job['job_schedule'] = scheduling
204
205 for client_id in clients:
206
207 search = client_id
208 client = Client(self.request).list(search=search)
209
210 new_job['client_id'] = client[0].id
211 job_id = self.client.jobs.create(new_job)
212 self.stop(job_id)
213 return True
214
215 def _get_actions_in_job(self, action_ids):
216 actions_in_job = []
217 for action_id in action_ids:
218 action = Action(self.request).get(action_id, json=True)
219 a = {
220 'action_id': action['action_id'],
221 'freezer_action': action['freezer_action']
222 }
223 actions_in_job.append(a)
224 return actions_in_job
225
226
227class Session(object):
228
229 def __init__(self, request):
230 self.request = request
231 self.client = client(request)
232
233 def list(self, json=False, limit=30, offset=0, search=None):
234 if search:
235 search = {"match": [{"_all": search}, ], }
236
237 sessions = self.client.sessions.list_all(limit=limit,
238 offset=offset,
239 search=search)
240
241 if json:
242 return sessions
243
244 return [utils.SessionObject(
245 session.get('session_id'),
246 session.get('description'),
247 session.get('status'),
248 session.get('jobs'),
249 session.get('schedule', {}).get('schedule_start_date'),
250 session.get('schedule', {}).get('schedule_interval'),
251 session.get('schedule', {}).get('schedule_end_date')
252 ) for session in sessions]
253
254 def get(self, session_id, json=False):
255 session = self.client.sessions.get(session_id)
256
257 if json:
258 return session
259
260 return utils.SessionObject(
261 session.get('session_id'),
262 session.get('description'),
263 session.get('status'),
264 session.get('jobs'),
265 session.get('schedule', {}).get('schedule_start_date'),
266 session.get('schedule', {}).get('schedule_interval'),
267 session.get('schedule', {}).get('schedule_end_date'))
268
269 def create(self, session):
270 return self._build(session)
271
272 def update(self, session, session_id):
273 return self.client.sessions.update(session_id, session)
274
275 def delete(self, session_id):
276 return self.client.sessions.delete(session_id)
277
278 def remove_job(self, session_id, job_id):
279 try:
280 # even if the job is removed from the session the api returns an
281 # error.
282 return self.client.sessions.remove_job(session_id, job_id)
283 except Exception:
284 pass
285
286 def add_job(self, session_id, job_id):
287 return self.client.sessions.add_job(session_id, job_id)
288
289 def jobs(self, session_id):
290 session = self.get(session_id, json=True)
291 jobs = []
292 try:
293 jobs = [utils.JobsInSessionObject(k,
294 session_id,
295 v['client_id'],
296 v['result'])
297 for k, v in session['jobs'].iteritems()]
298 except AttributeError:
299 pass
300 return jobs
301
302 def _build(self, session):
303 session = utils.create_dict(**session)
304 scheduling = {}
305 utils.assign_and_remove(session, scheduling, 'schedule_start_date')
306 utils.assign_and_remove(session, scheduling, 'schedule_interval')
307 utils.assign_and_remove(session, scheduling, 'schedule_end_date')
308 session['jobs'] = {}
309 session['schedule'] = scheduling
310 return self.client.sessions.create(session)
311
312
313class Action(object):
314
315 def __init__(self, request):
316 self.request = request
317 self.client = client(request)
318
319 def list(self, json=False, limit=100, offset=0, search=None):
320 if search:
321 search = {"match": [{"_all": search}, ], }
322
323 actions = self.client.actions.list(limit=limit,
324 offset=offset,
325 search=search)
326
327 if json:
328 return actions
329
330 return [utils.ActionObjectDetail(
331 action.get('action_id'),
332 action['freezer_action'].get('action'),
333 action['freezer_action'].get('backup_name'),
334 action['freezer_action'].get('path_to_backup')
335 or action['freezer_action'].get('restore_abs_path'),
336 action['freezer_action'].get('storage'),
337 ) for action in actions]
338
339 def get(self, job_id, json=False):
340
341 action = self.client.actions.get(job_id)
342
343 if json:
344 return action
345
346 return utils.ActionObjectDetail(
347 action.get('action_id'),
348 action['freezer_action'].get('action'),
349 action['freezer_action'].get('backup_name'),
350 action['freezer_action'].get('path_to_backup'),
351 action['freezer_action'].get('storage'))
352
353 def create(self, action):
354 return self._build(action)
355
356 def update(self, action, action_id):
357 updated_action = {}
358 updated_action['freezer_action'] = utils.create_dict(**action)
359 try:
360 if action['mandatory'] != '':
361 updated_action['mandatory'] = action['mandatory']
362 except KeyError:
363 pass
364
365 try:
366 if action['max_retries'] != '':
367 updated_action['max_retries'] = action['max_retries']
368 except KeyError:
369 pass
370
371 try:
372 if action['max_retries_interval'] != '':
373 updated_action['max_retries_interval'] =\
374 action['max_retries_interval']
375 except KeyError:
376 pass
377 return self.client.actions.update(action_id, updated_action)
378
379 def delete(self, action_id):
380 return self.client.actions.delete(action_id)
381
382 def _build(self, action):
383 """Get a flat action dict and convert it to a freezer action format
384 """
385 action_rules = {}
386
387 utils.assign_and_remove(action, action_rules, 'max_retries')
388 utils.assign_and_remove(action, action_rules, 'max_retries_interval')
389 utils.assign_and_remove(action, action_rules, 'mandatory')
390 action = utils.create_dict(**action)
391 action = {'freezer_action': action}
392 return self.client.actions.create(action)
393
394
395class Client(object):
396
397 def __init__(self, request):
398 self.request = request
399 self.client = client(request)
400
401 def list(self, json=False, limit=100, offset=0, search=None):
402 if search:
403 search = {"match": [{"_all": search}, ], }
404
405 clients = self.client.registration.list(limit=limit,
406 offset=offset,
407 search=search)
408
409 if json:
410 return clients
411
412 return [utils.ClientObject(
413 c.get('client', {}).get('hostname'),
414 c.get('client', {}).get('client_id'),
415 c.get('uuid')
416 ) for c in clients]
417
418 def get(self, client_id, json=False):
419 c = self.client.registration.get(client_id)
420
421 if json:
422 return c
423
424 return utils.ClientObject(
425 c.get('client', {}).get('hostname'),
426 c.get('client', {}).get('client_id'),
427 c.get('uuid'))
428
429 def delete(self, client_id):
430 return self.client.registration.delete(client_id)
431
432
433class Backup(object):
434
435 def __init__(self, request):
436 self.request = request
437 self.client = client(request)
438
439 def list(self, json=False, limit=30, offset=0, search=None):
440 if search:
441 search = {"match": [{"_all": search}, ], }
442
443 backups = self.client.backups.list(limit=limit,
444 offset=offset,
445 search=search)
446
447 if json:
448 return backups
449
450 return [utils.BackupObject(
451 backup_id=b.get('backup_uuid'),
452 action=b.get('backup_metadata', {}).get('action'),
453 time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
454 backup_name=b.get('backup_metadata', {}).get('backup_name'),
455 backup_media=b.get('backup_metadata', {}).get('backup_media'),
456 path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
457 hostname=b.get('backup_metadata', {}).get('hostname'),
458 container=b.get('backup_metadata', {}).get('container'),
459 level=b.get('backup_metadata', {}).get('level'),
460 curr_backup_level=b.get('backup_metadata', {}).get(
461 'curr_backup_level'),
462 encrypted=b.get('backup_metadata', {}).get('encrypted'),
463 total_broken_links=b.get('backup_metadata', {}).get(
464 'total_broken_links'),
465 excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
466 ) for b in backups]
467
468 def get(self, backup_id, json=False):
469
470 search = {"match": [{"backup_uuid": backup_id}, ], }
471
472 b = self.client.backups.list(limit=1, search=search)
473 b = b[0]
474
475 if json:
476 return b
477
478 return utils.BackupObject(
479 backup_id=b.get('backup_uuid'),
480 action=b.get('backup_metadata', {}).get('action'),
481 time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
482 backup_name=b.get('backup_metadata', {}).get('backup_name'),
483 backup_media=b.get('backup_metadata', {}).get('backup_media'),
484 path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
485 hostname=b.get('backup_metadata', {}).get('hostname'),
486 container=b.get('backup_metadata', {}).get('container'),
487 level=b.get('backup_metadata', {}).get('level'),
488 curr_backup_level=b.get('backup_metadata', {}).get(
489 'curr_backup_level'),
490 encrypted=b.get('backup_metadata', {}).get('encrypted'),
491 total_broken_links=b.get('backup_metadata', {}).get(
492 'total_broken_links'),
493 excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
494 )
495
496 def restore(self, data):
497 backup = self.get(data['backup_id'])
498 client_id = data['client']
499 name = "Restore job for {0}".format(client_id)
500 # TODO(m3m0): change storage to be flexible
501 action = {
502 'action': 'restore',
503 'backup_name': backup.backup_name,
504 'restore_abs_path': data['path'],
505 'container': backup.container,
506 'restore_from_host': backup.hostname,
507 'storage': 'local'
508 }
509
510 action_id = Action(self.request).create(action)
511
512 job = {
513 'job_actions': [{
514 'action_id': action_id,
515 'freezer_action': action
516 }],
517 'client_id': client_id,
518 'description': name,
519 'job_schedule': {}
520 }
521 job_id = self.client.jobs.create(job)
522 return Job(self.request).start(job_id)
diff --git a/freezer_ui/backups/workflows/__init__.py b/disaster_recovery/api/rest/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/backups/workflows/__init__.py
+++ b/disaster_recovery/api/rest/__init__.py
diff --git a/freezer_ui/api/rest/rest_api.py b/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
21from openstack_dashboard.api.rest import utils as rest_utils 21from openstack_dashboard.api.rest import utils as rest_utils
22from openstack_dashboard.api.rest.utils import JSONResponse 22from openstack_dashboard.api.rest.utils import JSONResponse
23 23
24import freezer_ui.api.api as freezer_api 24import disaster_recovery.api.api as freezer_api
25 25
26 26
27# https://github.com/tornadoweb/tornado/issues/1009 27# https://github.com/tornadoweb/tornado/issues/1009
@@ -42,38 +42,52 @@ class Clients(generic.View):
42 42
43 @prevent_json_hijacking 43 @prevent_json_hijacking
44 @rest_utils.ajax() 44 @rest_utils.ajax()
45 def get(self, request): 45 def get(self, request, job_id=None):
46 """Get all registered freezer clients""" 46 """Get all registered freezer clients"""
47 47
48 # we don't have a "get all clients" api (probably for good reason) so 48 # we don't have a "get all clients" api (probably for good reason) so
49 # we need to resort to getting a very high number. 49 # we need to resort to getting a very high number.
50 clients = freezer_api.client_list_json(request) 50 clients = freezer_api.Client(request).list(json=True)
51 clients = json.dumps(clients) 51 clients = json.dumps(clients)
52 return HttpResponse(clients, 52 return HttpResponse(clients, content_type="application/json")
53 content_type="application/json")
54 53
55 54
56class Actions(generic.View): 55class ActionList(generic.View):
57 """API for clients"""
58
59 @prevent_json_hijacking 56 @prevent_json_hijacking
60 @rest_utils.ajax() 57 @rest_utils.ajax()
61 def get(self, request): 58 def get(self, request):
62 """Get all registered freezer actions""" 59 """Get all registered freezer actions"""
63 actions = freezer_api.action_list_json(request)
64 actions = json.dumps(actions)
65 return HttpResponse(actions,
66 content_type="application/json")
67 60
61 actions = freezer_api.Action(request).list(json=True)
62 actions = json.dumps(actions)
63 return HttpResponse(actions, content_type="application/json")
68 64
69class ActionsInJob(generic.View):
70 """API for actions in a job"""
71 65
66class Actions(generic.View):
72 @prevent_json_hijacking 67 @prevent_json_hijacking
73 @rest_utils.ajax() 68 @rest_utils.ajax()
74 def get(self, request, job_id=None): 69 def get(self, request, job_id=None):
75 """Get all registered freezer actions""" 70 actions = freezer_api.Action(request).list(json=True)
76 actions = freezer_api.actions_in_job_json(request, job_id) 71 actions_in_job = freezer_api.Job(request).actions(job_id, api=True)
72
73 action_ids = [a['action_id'] for a in actions]
74 actions_in_job_ids = [a['action_id'] for a in actions_in_job]
75
76 available = set.difference(set(action_ids), set(actions_in_job_ids))
77 selected = set.intersection(set(action_ids), set(actions_in_job_ids))
78
79 available_actions = []
80 for action in actions:
81 if action['action_id'] in available:
82 available_actions.append(action)
83
84 selected_actions = []
85 for action in actions_in_job:
86 if action['action_id'] in selected:
87 selected_actions.append(action)
88
89 actions = {'available': available_actions,
90 'selected': selected_actions}
91
77 actions = json.dumps(actions) 92 actions = json.dumps(actions)
78 return HttpResponse(actions, 93 return HttpResponse(actions, content_type="application/json")
79 content_type="application/json")
diff --git a/freezer_ui/api/rest/urls.py b/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
24urlpatterns = patterns( 24urlpatterns = patterns(
25 '', 25 '',
26 url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"), 26 url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"),
27 url(r'^api/actions/$', rest_api.Actions.as_view(), name="api_actions"), 27 url(r'^api/actions$', rest_api.ActionList.as_view(), name="api_actions"),
28 url(r'^api/actions/job/(?P<job_id>[^/]+)?$', 28 url(r'^api/actions/job/(?P<job_id>[^/]+)?$',
29 rest_api.ActionsInJob.as_view(), name="api_actions_in_job"), 29 rest_api.Actions.as_view(), name="api_actions_in_job"),
30) 30)
diff --git a/freezer_ui/clients/__init__.py b/disaster_recovery/backups/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/clients/__init__.py
+++ b/disaster_recovery/backups/__init__.py
diff --git a/freezer_ui/backups/panel.py b/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 _
16 16
17import horizon 17import horizon
18 18
19from freezer_ui import dashboard 19from disaster_recovery import dashboard
20 20
21 21
22class BackupsPanel(horizon.Panel): 22class BackupsPanel(horizon.Panel):
diff --git a/freezer_ui/backups/tables.py b/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
17from django.core.urlresolvers import reverse 17from django.core.urlresolvers import reverse
18from django.utils import safestring 18from django.utils import safestring
19from django.utils.translation import ugettext_lazy as _ 19from django.utils.translation import ugettext_lazy as _
20from horizon.utils import functions as utils 20
21from horizon import tables 21from horizon import tables
22from freezer_ui.utils import timestamp_to_string 22from horizon.utils import functions as utils
23
24from disaster_recovery.utils import timestamp_to_string
23 25
24 26
25LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
@@ -32,16 +34,13 @@ class Restore(tables.LinkAction):
32 ajax = True 34 ajax = True
33 35
34 def get_link_url(self, datum=None): 36 def get_link_url(self, datum=None):
35 return reverse("horizon:freezer_ui:backups:restore", 37 return reverse("horizon:disaster_recovery:backups:restore",
36 kwargs={'backup_id': datum.id}) 38 kwargs={'backup_id': datum.id})
37 39
38 40
39class BackupFilter(tables.FilterAction): 41class BackupFilter(tables.FilterAction):
40 filter_type = "server" 42 filter_type = "server"
41 filter_choices = (("before", "Created before", True), 43 filter_choices = (("contains", "Contains text", True),)
42 ("after", "Created after", True),
43 ("between", "Created between", True),
44 ("contains", "Contains text", True))
45 44
46 45
47def icons(backup): 46def icons(backup):
@@ -88,13 +87,14 @@ def icons(backup):
88 87
89 88
90def backup_detail_view(backup): 89def backup_detail_view(backup):
91 return reverse("horizon:freezer_ui:backups:detail", 90 return reverse("horizon:disaster_recovery:backups:detail",
92 args=[backup.backup_id]) 91 kwargs={'backup_id': backup.id})
93 92
94 93
95class BackupsTable(tables.DataTable): 94class BackupsTable(tables.DataTable):
96 backup_name = tables.Column('backup_name', 95 backup_name = tables.Column('backup_name',
97 verbose_name=_("Backup Name")) 96 verbose_name=_("Backup Name"),
97 link=backup_detail_view)
98 hostname = tables.Column('hostname', verbose_name=_("Hostname")) 98 hostname = tables.Column('hostname', verbose_name=_("Hostname"))
99 created = tables.Column("time_stamp", 99 created = tables.Column("time_stamp",
100 verbose_name=_("Created At"), 100 verbose_name=_("Created At"),
diff --git a/freezer_ui/backups/templates/backups/detail.html b/disaster_recovery/backups/templates/backups/detail.html
index 1a5e67a..1a5e67a 100644
--- a/freezer_ui/backups/templates/backups/detail.html
+++ b/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
index 70795c7..70795c7 100644
--- a/freezer_ui/backups/templates/backups/index.html
+++ b/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
index 3bfaea8..3bfaea8 100644
--- a/freezer_ui/backups/templates/backups/restore.html
+++ b/disaster_recovery/backups/templates/backups/restore.html
diff --git a/freezer_ui/backups/urls.py b/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 @@
16from django.conf.urls import patterns 16from django.conf.urls import patterns
17from django.conf.urls import url 17from django.conf.urls import url
18 18
19from freezer_ui.backups import views 19from disaster_recovery.backups import views
20 20
21 21
22urlpatterns = patterns( 22urlpatterns = 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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import datetime
14import logging
15import pprint
16
17from django.template.defaultfilters import date as django_date
18from django.utils.translation import ugettext_lazy as _
19from django.views import generic
20
21from horizon import tables
22from horizon import workflows
23
24import disaster_recovery.api.api as freezer_api
25
26from disaster_recovery.backups import tables as freezer_tables
27from disaster_recovery.backups.workflows import restore as restore_workflow
28from disaster_recovery.utils import shield
29
30
31LOG = logging.getLogger(__name__)
32
33
34class IndexView(tables.DataTableView):
35 name = _("Backups")
36 slug = "backups"
37 table_class = freezer_tables.BackupsTable
38 template_name = "disaster_recovery/backups/index.html"
39
40 @shield('Unable to retrieve backups.', redirect='backups:index')
41 def get_data(self):
42 filters = self.table.get_filter_string() or None
43 return freezer_api.Backup(self.request).list(search=filters)
44
45
46class DetailView(generic.TemplateView):
47 template_name = 'disaster_recovery/backups/detail.html'
48
49 @shield('Unable to get backup.', redirect='backups:index')
50 def get_context_data(self, **kwargs):
51 backup = freezer_api.Backup(self.request).get(kwargs['backup_id'],
52 json=True)
53 return {'data': pprint.pformat(backup)}
54
55
56class RestoreView(workflows.WorkflowView):
57 workflow_class = restore_workflow.Restore
58
59 @shield('Unable to get backup.', redirect='backups:index')
60 def get_object(self, *args, **kwargs):
61 return freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
62
63 def is_update(self):
64 return 'name' in self.kwargs and bool(self.kwargs['name'])
65
66 @shield('Unable to get backup.', redirect='backups:index')
67 def get_workflow_name(self):
68 backup = freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
69 backup_date = datetime.datetime.fromtimestamp(int(backup.time_stamp))
70 backup_date_str = django_date(backup_date,
71 'SHORT_DATETIME_FORMAT')
72 return "Restore '{}' from {}".format(backup.backup_name,
73 backup_date_str)
74
75 def get_initial(self):
76 return {"backup_id": self.kwargs['backup_id']}
77
78 @shield('Unable to get backup.', redirect='backups:index')
79 def get_workflow(self, *args, **kwargs):
80 workflow = super(RestoreView, self).get_workflow(*args, **kwargs)
81 workflow.name = self.get_workflow_name()
82 return workflow
diff --git a/freezer_ui/jobs/__init__.py b/disaster_recovery/backups/workflows/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/jobs/__init__.py
+++ b/disaster_recovery/backups/workflows/__init__.py
diff --git a/freezer_ui/backups/workflows/restore.py b/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
18from django.core.exceptions import ValidationError 18from django.core.exceptions import ValidationError
19from django.utils.translation import ugettext_lazy as _ 19from django.utils.translation import ugettext_lazy as _
20 20
21from horizon import forms
22from horizon import exceptions 21from horizon import exceptions
22from horizon import forms
23from horizon import workflows 23from horizon import workflows
24 24
25import freezer_ui.api.api as freezer_api 25import disaster_recovery.api.api as freezer_api
26 26
27 27
28LOG = logging.getLogger(__name__) 28LOG = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ class DestinationAction(workflows.MembershipAction):
49 49
50 50
51class Destination(workflows.Step): 51class Destination(workflows.Step):
52 template_name = 'freezer_ui/backups/restore.html' 52 template_name = 'disaster_recovery/backups/restore.html'
53 action_class = DestinationAction 53 action_class = DestinationAction
54 contributes = ('client', 'path', 'backup_id') 54 contributes = ('client', 'path', 'backup_id')
55 55
@@ -61,7 +61,7 @@ class Restore(workflows.Workflow):
61 slug = "restore" 61 slug = "restore"
62 name = _("Restore") 62 name = _("Restore")
63 finalize_button_name = _("Restore") 63 finalize_button_name = _("Restore")
64 success_url = "horizon:freezer_ui:backups:index" 64 success_url = "horizon:disaster_recovery:backups:index"
65 success_message = _("Restore job successfully queued. It will get " 65 success_message = _("Restore job successfully queued. It will get "
66 "executed soon.") 66 "executed soon.")
67 wizard = False 67 wizard = False
@@ -69,40 +69,7 @@ class Restore(workflows.Workflow):
69 69
70 def handle(self, request, data): 70 def handle(self, request, data):
71 try: 71 try:
72 backup_id = data['backup_id'] 72 return freezer_api.Backup(request).restore(data)
73 client_id = data['client']
74 client = freezer_api.client_get(request, client_id)
75 backup = freezer_api.backup_get(request, backup_id)
76 name = "Restore job for {0}".format(client_id)
77
78 action = {
79 "action": "restore",
80 "backup_name":
81 backup.data_dict['backup_metadata']['backup_name'],
82 "restore_abs_path": data['path'],
83 "container":
84 backup.data_dict['backup_metadata']['container'],
85 "restore_from_host": client.hostname,
86 "storage": "local"
87 }
88
89 action_id = freezer_api.action_create_without_job(
90 request, action)
91
92 job = {
93 "job_actions": [{
94 "action_id": action_id,
95 "freezer_action": action
96 }],
97 "client_id": client_id,
98 "description": name,
99 "job_schedule": {
100 "schedule_end_date": None,
101 "schedule_interval": None,
102 "schedule_start_date": None
103 }
104 }
105 return freezer_api.job_create(request, job)
106 except Exception: 73 except Exception:
107 exceptions.handle(request) 74 exceptions.handle(request)
108 return False 75 return False
diff --git a/freezer_ui/jobs/workflows/__init__.py b/disaster_recovery/clients/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/jobs/workflows/__init__.py
+++ b/disaster_recovery/clients/__init__.py
diff --git a/freezer_ui/clients/panel.py b/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 _
16 16
17import horizon 17import horizon
18 18
19from freezer_ui import dashboard 19from disaster_recovery import dashboard
20 20
21 21
22class ClientsPanel(horizon.Panel): 22class 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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from django.utils.translation import ugettext_lazy as _
16from django.utils.translation import ungettext_lazy
17
18from horizon import tables
19from horizon.utils.urlresolvers import reverse
20
21import disaster_recovery.api.api as freezer_api
22
23
24LOG = logging.getLogger(__name__)
25
26
27class Filter(tables.FilterAction):
28 filter_type = "server"
29 filter_choices = (("contains", "Contains text", True),)
30
31
32class DeleteClient(tables.DeleteAction):
33 name = "delete"
34 classes = ("btn-danger",)
35 icon = "remove"
36 help_text = _("Delete Clients is not recoverable.")
37
38 @staticmethod
39 def action_present(count):
40 return ungettext_lazy(
41 u"Delete Client",
42 u"Delete Clients",
43 count
44 )
45
46 @staticmethod
47 def action_past(count):
48 return ungettext_lazy(
49 u"Deleted Client",
50 u"Deleted Clients",
51 count
52 )
53
54 def delete(self, request, client_id):
55 return freezer_api.Client(request).delete(client_id)
56
57
58class DeleteMultipleClients(DeleteClient):
59 name = "delete_multiple_clients"
60
61
62def get_link(client):
63 return reverse('horizon:disaster_recovery:clients:client',
64 kwargs={'client_id': client.id})
65
66
67class ClientsTable(tables.DataTable):
68 client_id = tables.Column('client_id', verbose_name=_("Client ID"),
69 link=get_link)
70 name = tables.Column('hostname', verbose_name=_("Hostname"))
71
72 class Meta:
73 name = "clients"
74 verbose_name = _("Clients")
75 row_actions = (DeleteClient,)
76 table_actions = (Filter, DeleteMultipleClients,)
77 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 @@
1{% extends 'base.html' %}
2
3{% load i18n %}
4{% block title %}{% trans "Client" %}{% endblock %}
5
6{% block page_header %}
7 {% include "horizon/common/_page_header.html" with title=_("Client") %}
8{% endblock page_header %}
9
10{% block main %}
11 <div class="row">
12 <div class="col-sm-12">
13 <pre>{{ data }}</pre>
14 </div>
15 </div>
16
17{% endblock %}
diff --git a/freezer_ui/clients/templates/clients/index.html b/disaster_recovery/clients/templates/clients/index.html
index 23972aa..23972aa 100644
--- a/freezer_ui/clients/templates/clients/index.html
+++ b/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 @@
1# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15
16from django.conf.urls import patterns
17from django.conf.urls import url
18
19from disaster_recovery.clients import views
20
21
22urlpatterns = patterns(
23 '',
24 url(r'^$', views.IndexView.as_view(), name='index'),
25
26 url(r'^(?P<client_id>[^/]+)?$',
27 views.ClientView.as_view(),
28 name='client'),
29)
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 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14import pprint
15
16from django.utils.translation import ugettext_lazy as _
17from django.views import generic
18
19from horizon import tables
20
21import disaster_recovery.api.api as freezer_api
22
23from disaster_recovery.clients import tables as freezer_tables
24from disaster_recovery.utils import shield
25
26
27LOG = logging.getLogger(__name__)
28
29
30class IndexView(tables.DataTableView):
31 name = _("Clients")
32 slug = "clients"
33 table_class = freezer_tables.ClientsTable
34 template_name = "disaster_recovery/clients/index.html"
35
36 @shield('Unable to get clients', redirect='clients:index')
37 def get_data(self):
38 filters = self.table.get_filter_string() or None
39 return freezer_api.Client(self.request).list(search=filters)
40
41
42class ClientView(generic.TemplateView):
43 template_name = 'disaster_recovery/clients/detail.html'
44
45 @shield('Unable to get client', redirect='clients:index')
46 def get_context_data(self, **kwargs):
47 client = freezer_api.Client(self.request).get(kwargs['client_id'],
48 json=True)
49 return {'data': pprint.pformat(client)}
diff --git a/freezer_ui/dashboard.py b/disaster_recovery/dashboard.py
index e5a9985..9ce559f 100644
--- a/freezer_ui/dashboard.py
+++ b/disaster_recovery/dashboard.py
@@ -20,15 +20,14 @@ import horizon
20class FreezerDR(horizon.PanelGroup): 20class FreezerDR(horizon.PanelGroup):
21 slug = "freezerdr" 21 slug = "freezerdr"
22 name = _("Backup and Restore") 22 name = _("Backup and Restore")
23 panels = ('jobs', 'sessions', 'backups', 'clients') 23 panels = ('jobs', 'actions', 'sessions', 'clients', 'backups')
24 24
25 25
26class Freezer(horizon.Dashboard): 26class Freezer(horizon.Dashboard):
27 name = _("Disaster Recovery") 27 name = _("Disaster Recovery")
28 slug = "freezer_ui" 28 slug = "disaster_recovery"
29 panels = (FreezerDR,) 29 panels = (FreezerDR,)
30 default_panel = 'jobs' 30 default_panel = 'jobs'
31 permissions = ('openstack.roles.admin',)
32 31
33 32
34horizon.register(Freezer) 33horizon.register(Freezer)
diff --git a/freezer_ui/sessions/__init__.py b/disaster_recovery/jobs/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/sessions/__init__.py
+++ b/disaster_recovery/jobs/__init__.py
diff --git a/freezer_ui/jobs/browsers.py b/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 @@
14 14
15from django.utils.translation import ugettext_lazy as _ 15from django.utils.translation import ugettext_lazy as _
16 16
17from freezer_ui.jobs import tables 17from disaster_recovery.jobs import tables
18 18
19from horizon import browsers 19from horizon import browsers
20 20
diff --git a/freezer_ui/jobs/panel.py b/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 _
16 16
17import horizon 17import horizon
18 18
19from freezer_ui import dashboard 19from disaster_recovery import dashboard
20 20
21 21
22class JobsPanel(horizon.Panel): 22class JobsPanel(horizon.Panel):
diff --git a/freezer_ui/jobs/tables.py b/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 @@
13# limitations under the License. 13# limitations under the License.
14 14
15import logging 15import logging
16import datetime 16
17from django import shortcuts 17from django import shortcuts
18from django.utils import safestring
19from django.utils.translation import ugettext_lazy as _ 18from django.utils.translation import ugettext_lazy as _
20from django.utils.translation import ungettext_lazy 19from django.utils.translation import ungettext_lazy
21 20
22from horizon import messages
23from horizon import tables 21from horizon import tables
24from horizon.utils.urlresolvers import reverse 22from horizon.utils.urlresolvers import reverse
25 23
26import freezer_ui.api.api as freezer_api 24import disaster_recovery.api.api as freezer_api
27from freezer_ui.utils import timestamp_to_string
28 25
29 26
30LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
31 28
32 29
33def format_last_backup(last_backup): 30class ObjectFilterAction(tables.FilterAction):
34 last_backup_ts = datetime.datetime.fromtimestamp(last_backup) 31 def allowed(self, request, datum):
35 ten_days_later = last_backup_ts + datetime.timedelta(days=10) 32 return bool(self.table.kwargs['job_id'])
36 today = datetime.datetime.today()
37
38 if last_backup is None:
39 colour = 'red'
40 icon = 'fire'
41 text = 'Never'
42 elif ten_days_later < today:
43 colour = 'orange'
44 icon = 'thumbs-down'
45 text = timestamp_to_string(last_backup)
46 else:
47 colour = 'green'
48 icon = 'thumbs-up'
49 text = timestamp_to_string(last_backup)
50
51 return safestring.mark_safe(
52 '<span style="color:{}"><span class="glyphicon glyphicon-{}" aria-hidd'
53 'en="true"></span> {}</span>'.format(colour, icon, text))
54 33
55 34
56class AttachJobToSession(tables.LinkAction): 35class AttachJobToSession(tables.LinkAction):
57 name = "attach_job_to_session" 36 name = "attach_job_to_session"
58 verbose_name = _("Attach To Session") 37 verbose_name = _("Attach To Session")
59 classes = ("ajax-modal") 38 classes = ("ajax-modal")
60 url = "horizon:freezer_ui:sessions:attach" 39 url = "horizon:disaster_recovery:sessions:attach"
61 40
62 def allowed(self, request, instance): 41 def allowed(self, request, instance):
63 return True 42 return True
64 43
65 def get_link_url(self, datum): 44 def get_link_url(self, datum):
66 return reverse("horizon:freezer_ui:sessions:attach", 45 return reverse("horizon:disaster_recovery:sessions:attach",
67 kwargs={'job_id': datum.job_id}) 46 kwargs={'job_id': datum.job_id})
68 47
69 48
70class Restore(tables.Action):
71 name = "restore"
72 verbose_name = _("Restore")
73
74 def single(self, table, request, instance):
75 messages.info(request, "Needs to be implemented")
76
77 def allowed(self, request, instance):
78 return True
79
80
81class DeleteJob(tables.DeleteAction): 49class DeleteJob(tables.DeleteAction):
82 name = "delete" 50 name = "delete"
83 classes = ("btn-danger",) 51 classes = ("btn-danger",)
@@ -100,8 +68,8 @@ class DeleteJob(tables.DeleteAction):
100 count 68 count
101 ) 69 )
102 70
103 def delete(self, request, obj_id): 71 def delete(self, request, job_id):
104 return freezer_api.job_delete(request, obj_id) 72 return freezer_api.Job(request).delete(job_id)
105 73
106 74
107class DeleteMultipleJobs(DeleteJob): 75class DeleteMultipleJobs(DeleteJob):
@@ -113,9 +81,9 @@ class CloneJob(tables.Action):
113 verbose_name = _("Clone Job") 81 verbose_name = _("Clone Job")
114 help_text = _("Clone and edit a job file") 82 help_text = _("Clone and edit a job file")
115 83
116 def single(self, table, request, obj_id): 84 def single(self, table, request, job_id):
117 freezer_api.job_clone(request, obj_id) 85 freezer_api.Job(request).clone(job_id)
118 return shortcuts.redirect('horizon:freezer_ui:jobs:index') 86 return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
119 87
120 88
121class EditJob(tables.LinkAction): 89class EditJob(tables.LinkAction):
@@ -125,50 +93,65 @@ class EditJob(tables.LinkAction):
125 icon = "pencil" 93 icon = "pencil"
126 94
127 def get_link_url(self, datum=None): 95 def get_link_url(self, datum=None):
128 return reverse("horizon:freezer_ui:jobs:configure", 96 return reverse("horizon:disaster_recovery:jobs:configure",
129 kwargs={'backup_name': datum.job_id}) 97 kwargs={'job_id': datum.job_id})
130 98
131 99
132def get_backup_configs_link(backup_config): 100class EditActionsInJob(tables.LinkAction):
133 return reverse('horizon:freezer_ui:jobs:index', 101 name = "edit_actions_in_job"
134 kwargs={'job_id': backup_config.job_id}) 102 verbose_name = _("Edit Actions")
103 classes = ("ajax-modal",)
104 icon = "pencil"
135 105
106 def get_link_url(self, datum=None):
107 return reverse("horizon:disaster_recovery:jobs:edit_action",
108 kwargs={'job_id': datum.job_id})
136 109
137class CreateJob(tables.LinkAction):
138 name = "create"
139 verbose_name = _("Create Job")
140 url = "horizon:freezer_ui:jobs:create"
141 classes = ("ajax-modal",)
142 icon = "plus"
143 110
111class StartJob(tables.Action):
112 name = "start_job"
113 verbose_name = _("Start Job")
144 114
145class CreateAction(tables.LinkAction): 115 def single(self, table, request, job_id):
146 name = "create_action" 116 freezer_api.Job(request).start(job_id)
147 verbose_name = _("Create Action") 117 return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
148 url = "horizon:freezer_ui:jobs:create_action"
149 classes = ("ajax-modal",)
150 icon = "plus"
151 118
152 def get_link_url(self, datum=None):
153 return reverse("horizon:freezer_ui:jobs:create_action",
154 kwargs={'job_id': datum.job_id})
155 119
120class StopJob(tables.Action):
121 name = "stop_job"
122 verbose_name = _("Stop Job")
156 123
157class ObjectFilterAction(tables.FilterAction): 124 def single(self, table, request, job_id):
158 def allowed(self, request, datum): 125 freezer_api.Job(request).stop(job_id)
159 return bool(self.table.kwargs['job_id']) 126 return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
127
128
129def get_link(row):
130 return reverse('horizon:disaster_recovery:jobs:index',
131 kwargs={'job_id': row.job_id})
132
133
134class CreateJob(tables.LinkAction):
135 name = "create"
136 verbose_name = _("Create Job")
137 url = "horizon:disaster_recovery:jobs:create"
138 classes = ("ajax-modal",)
139 icon = "plus"
160 140
161 141
162class JobsTable(tables.DataTable): 142class JobsTable(tables.DataTable):
163 job_name = tables.Column("description", 143 job_name = tables.Column("description",
164 link=get_backup_configs_link, 144 link=get_link,
165 verbose_name=_("Job Name")) 145 verbose_name=_("Job Name"))
166 146
147 event = tables.Column("event",
148 verbose_name=_("Job Status"))
149
167 result = tables.Column("result", 150 result = tables.Column("result",
168 verbose_name=_("Job Result")) 151 verbose_name=_("Job Result"))
169 152
170 def get_object_id(self, backup_config): 153 def get_object_id(self, row):
171 return backup_config.id 154 return row.id
172 155
173 class Meta(object): 156 class Meta(object):
174 name = "jobs" 157 name = "jobs"
@@ -176,13 +159,15 @@ class JobsTable(tables.DataTable):
176 table_actions = (ObjectFilterAction, 159 table_actions = (ObjectFilterAction,
177 CreateJob, 160 CreateJob,
178 DeleteMultipleJobs) 161 DeleteMultipleJobs)
179 footer = False 162 row_actions = (StartJob,
180 multi_select = True 163 StopJob,
181 row_actions = (CreateAction, 164 EditActionsInJob,
182 EditJob, 165 EditJob,
183 AttachJobToSession, 166 AttachJobToSession,
184 CloneJob, 167 CloneJob,
185 DeleteJob,) 168 DeleteJob)
169 footer = False
170 multi_select = True
186 171
187 172
188class DeleteAction(tables.DeleteAction): 173class DeleteAction(tables.DeleteAction):
@@ -208,47 +193,29 @@ class DeleteAction(tables.DeleteAction):
208 ) 193 )
209 194
210 def delete(self, request, obj_id): 195 def delete(self, request, obj_id):
211 freezer_api.action_delete(request, obj_id) 196 freezer_api.Job(request).delete_action(obj_id)
212 return reverse("horizon:freezer_ui:jobs:index") 197 return reverse("horizon:disaster_recovery:jobs:index")
213 198
214 199
215class DeleteMultipleActions(DeleteAction): 200class DeleteMultipleActions(DeleteAction):
216 name = "delete_multiple_actions" 201 name = "delete_multiple_actions"
217 202
218 203
219class EditAction(tables.LinkAction):
220 name = "edit"
221 verbose_name = _("Edit")
222 classes = ("ajax-modal",)
223 icon = "pencil"
224
225 def get_link_url(self, datum=None):
226 # this is used to pass to values as an url
227 # TODO(m3m0): look for a way to improve this
228 ids = '{0}==={1}'.format(datum.action_id, datum.job_id)
229 return reverse("horizon:freezer_ui:jobs:create_action",
230 kwargs={'job_id': ids})
231
232
233class ActionsTable(tables.DataTable): 204class ActionsTable(tables.DataTable):
234 action_name = tables.Column('action', 205 action = tables.Column('action', verbose_name=_("Action Type"))
235 verbose_name=_("Action Type"))
236 206
237 backup_name = tables.Column('backup_name', 207 name = tables.Column('backup_name', verbose_name=_("Action Name"))
238 verbose_name=_("Action Name"))
239 208
240 def get_object_id(self, container): 209 def get_object_id(self, container):
241 # this is used to pass to values as an url 210 # TODO(m3m0): we should't send the ids in this way
242 # TODO(m3m0): look for a way to improve this
243 ids = '{0}==={1}'.format(container.action_id, container.job_id) 211 ids = '{0}==={1}'.format(container.action_id, container.job_id)
244 return ids 212 return ids
245 213
246 class Meta(object): 214 class Meta(object):
247 name = "status" 215 name = "actions_in_job"
248 verbose_name = _("Status") 216 verbose_name = _("Actions")
249 table_actions = (ObjectFilterAction, 217 table_actions = (ObjectFilterAction,
250 DeleteMultipleActions) 218 DeleteMultipleActions)
251 row_actions = (EditAction, 219 row_actions = (DeleteAction,)
252 DeleteAction,)
253 footer = False 220 footer = False
254 multi_select = True 221 multi_select = True
diff --git a/freezer_ui/jobs/templates/jobs/_action.html b/disaster_recovery/jobs/templates/jobs/_action.html
index 6c919a3..6c919a3 100644
--- a/freezer_ui/jobs/templates/jobs/_action.html
+++ b/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
index 8423e83..762f2cd 100644
--- a/freezer_ui/jobs/templates/jobs/_actions.html
+++ b/disaster_recovery/jobs/templates/jobs/_actions.html
@@ -4,7 +4,7 @@
4{% endblock %} 4{% endblock %}
5 5
6<style> 6<style>
7 #sortable1, #sortable2 { 7 #actions_available, #actions_selected {
8 position: relative; 8 position: relative;
9 min-height: 100px; 9 min-height: 100px;
10 } 10 }
@@ -16,7 +16,7 @@
16 <div class="panel panel-default"> 16 <div class="panel panel-default">
17 <div class="panel-heading">Available Actions</div> 17 <div class="panel-heading">Available Actions</div>
18 <div class="panel-body"> 18 <div class="panel-body">
19 <ul id="sortable1" class="connectedSortable list-group dark_stripe"> 19 <ul id="actions_available" class="connectedSortable list-group dark_stripe">
20 </ul> 20 </ul>
21 </div> 21 </div>
22 </div> 22 </div>
@@ -26,7 +26,7 @@
26 <div class="panel panel-default"> 26 <div class="panel panel-default">
27 <div class="panel-heading">Selected Actions In Order</div> 27 <div class="panel-heading">Selected Actions In Order</div>
28 <div class="panel-body"> 28 <div class="panel-body">
29 <ul id="sortable2" class="connectedSortable list-group dark_stripe"> 29 <ul id="actions_selected" class="connectedSortable list-group dark_stripe">
30 </ul> 30 </ul>
31 </div> 31 </div>
32 </div> 32 </div>
diff --git a/freezer_ui/jobs/templates/jobs/_advanced.html b/disaster_recovery/jobs/templates/jobs/_advanced.html
index e6d98e7..e6d98e7 100644
--- a/freezer_ui/jobs/templates/jobs/_advanced.html
+++ b/disaster_recovery/jobs/templates/jobs/_advanced.html
diff --git a/freezer_ui/jobs/templates/jobs/_info.html b/disaster_recovery/jobs/templates/jobs/_info.html
index 773dd46..773dd46 100644
--- a/freezer_ui/jobs/templates/jobs/_info.html
+++ b/disaster_recovery/jobs/templates/jobs/_info.html
diff --git a/freezer_ui/jobs/templates/jobs/_rules.html b/disaster_recovery/jobs/templates/jobs/_rules.html
index 7a3f2b5..7a3f2b5 100644
--- a/freezer_ui/jobs/templates/jobs/_rules.html
+++ b/disaster_recovery/jobs/templates/jobs/_rules.html
diff --git a/freezer_ui/jobs/templates/jobs/_scheduling.html b/disaster_recovery/jobs/templates/jobs/_scheduling.html
index e93ac87..e93ac87 100644
--- a/freezer_ui/jobs/templates/jobs/_scheduling.html
+++ b/disaster_recovery/jobs/templates/jobs/_scheduling.html
diff --git a/freezer_ui/jobs/templates/jobs/_snapshot.html b/disaster_recovery/jobs/templates/jobs/_snapshot.html
index 1761158..8dc75cc 100644
--- a/freezer_ui/jobs/templates/jobs/_snapshot.html
+++ b/disaster_recovery/jobs/templates/jobs/_snapshot.html
@@ -4,7 +4,7 @@
4 4
5<h4>{% blocktrans %}Action{% endblocktrans %}</h4> 5<h4>{% blocktrans %}Action{% endblocktrans %}</h4>
6 6
7<p>{% blocktrans %}Specify weather this action should execute a snapshot on the client file system. 7<p>{% blocktrans %}Specify whether this action should execute a snapshot on the client file system.
8 In Linux and it's distros it will use LVM and in Windows it will use Volume Shadow Copy{% endblocktrans %}</p> 8 In Linux and it's distros it will use LVM and in Windows it will use Volume Shadow Copy{% endblocktrans %}</p>
9 9
10{% endblock %} 10{% endblock %}
diff --git a/freezer_ui/jobs/templates/jobs/_workflow_step_update_members.html b/disaster_recovery/jobs/templates/jobs/_workflow_step_update_members.html
index 0507419..0507419 100644
--- a/freezer_ui/jobs/templates/jobs/_workflow_step_update_members.html
+++ b/disaster_recovery/jobs/templates/jobs/_workflow_step_update_members.html
diff --git a/freezer_ui/jobs/templates/jobs/browser.html b/disaster_recovery/jobs/templates/jobs/browser.html
index 61646d3..61646d3 100644
--- a/freezer_ui/jobs/templates/jobs/browser.html
+++ b/disaster_recovery/jobs/templates/jobs/browser.html
diff --git a/freezer_ui/jobs/urls.py b/disaster_recovery/jobs/urls.py
index fd939e3..2c1491b 100644
--- a/freezer_ui/jobs/urls.py
+++ b/disaster_recovery/jobs/urls.py
@@ -15,7 +15,7 @@
15from django.conf.urls import patterns 15from django.conf.urls import patterns
16from django.conf.urls import url 16from django.conf.urls import url
17 17
18from freezer_ui.jobs import views 18from disaster_recovery.jobs import views
19 19
20 20
21urlpatterns = patterns( 21urlpatterns = patterns(
@@ -29,11 +29,11 @@ urlpatterns = patterns(
29 views.JobWorkflowView.as_view(), 29 views.JobWorkflowView.as_view(),
30 name='create'), 30 name='create'),
31 31
32 url(r'^create_action/(?P<job_id>[^/]+)?$', 32 url(r'^configure/(?P<job_id>[^/]+)?$',
33 views.ActionWorkflowView.as_view(),
34 name='create_action'),
35
36 url(r'^configure/(?P<backup_name>[^/]+)?$',
37 views.JobWorkflowView.as_view(), 33 views.JobWorkflowView.as_view(),
38 name='configure'), 34 name='configure'),
35
36 url(r'^edit_actions/(?P<job_id>[^/]+)?$',
37 views.ActionsInJobView.as_view(),
38 name='edit_action'),
39) 39)
diff --git a/disaster_recovery/jobs/views.py b/disaster_recovery/jobs/views.py
new file mode 100644
index 0000000..2685a83
--- /dev/null
+++ b/disaster_recovery/jobs/views.py
@@ -0,0 +1,84 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from horizon import browsers
16from horizon import workflows
17
18import workflows.create as configure_workflow
19import workflows.actions as actions_workflow
20
21import disaster_recovery.api.api as freezer_api
22import disaster_recovery.jobs.browsers as project_browsers
23
24from disaster_recovery.utils import shield
25
26
27LOG = logging.getLogger(__name__)
28
29
30class JobsView(browsers.ResourceBrowserView):
31 browser_class = project_browsers.ContainerBrowser
32 template_name = "disaster_recovery/jobs/browser.html"
33
34 @shield("Unable to get job", redirect='jobs:index')
35 def get_jobs_data(self):
36 return freezer_api.Job(self.request).list(limit=100)
37
38 @shield("Unable to get actions for this job.", redirect='jobs:index')
39 def get_actions_in_job_data(self):
40 if self.kwargs['job_id']:
41 return freezer_api.Job(self.request).actions(self.kwargs['job_id'])
42 return []
43
44
45class JobWorkflowView(workflows.WorkflowView):
46 workflow_class = configure_workflow.ConfigureJob
47
48 @shield("Unable to get job", redirect="jobs:index")
49 def get_object(self):
50 return freezer_api.Job(self.request).get(self.kwargs['job_id'])
51
52 def is_update(self):
53 return 'job_id' in self.kwargs and bool(self.kwargs['job_id'])
54
55 @shield("Unable to get job", redirect="jobs:index")
56 def get_initial(self):
57 initial = super(JobWorkflowView, self).get_initial()
58 if self.is_update():
59 initial.update({'job_id': None})
60 job = freezer_api.Job(self.request).get(self.kwargs['job_id'],
61 json=True)
62 initial.update(**job)
63 initial.update(**job['job_schedule'])
64
65 return initial
66
67
68class ActionsInJobView(workflows.WorkflowView):
69 workflow_class = actions_workflow.ConfigureActions
70
71 @shield("Unable to get job", redirect="jobs:index")
72 def get_object(self):
73 return freezer_api.Job(self.request).get(self.kwargs['job_id'])
74
75 def is_update(self):
76 return 'job_id' in self.kwargs and bool(self.kwargs['job_id'])
77
78 @shield("Unable to get job", redirect="jobs:index")
79 def get_initial(self):
80 initial = super(ActionsInJobView, self).get_initial()
81 if self.is_update():
82 job = freezer_api.Job(self.request).get(self.kwargs['job_id'])
83 initial.update({'job_id': job.id})
84 return initial
diff --git a/freezer_ui/sessions/workflows/__init__.py b/disaster_recovery/jobs/workflows/__init__.py
index e69de29..e69de29 100644
--- a/freezer_ui/sessions/workflows/__init__.py
+++ b/disaster_recovery/jobs/workflows/__init__.py
diff --git a/disaster_recovery/jobs/workflows/actions.py b/disaster_recovery/jobs/workflows/actions.py
new file mode 100644
index 0000000..847cf72
--- /dev/null
+++ b/disaster_recovery/jobs/workflows/actions.py
@@ -0,0 +1,81 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13import logging
14
15from django import shortcuts
16from django.utils.translation import ugettext_lazy as _
17
18from horizon import exceptions
19from horizon import forms
20from horizon import workflows
21
22import disaster_recovery.api.api as freezer_api
23
24
25LOG = logging.getLogger(__name__)
26
27
28class ActionsConfigurationAction(workflows.Action):
29 pass
30
31 class Meta(object):
32 name = _("Actions")
33 slug = "actions"
34 help_text_template = "disaster_recovery/jobs" \
35 "/_actions.html"
36
37
38class ActionsConfiguration(workflows.Step):
39 action_class = ActionsConfigurationAction
40 contributes = ()
41
42
43class InfoAction(workflows.Action):
44 job_id = forms.CharField(label=_("Job ID"), required=False,
45 widget=forms.HiddenInput(),)
46 actions = forms.CharField(label=_("Actions"), required=False,
47 widget=forms.HiddenInput(),)
48
49 def __init__(self, request, *args, **kwargs):
50 super(InfoAction, self).__init__(request, *args, **kwargs)
51
52 class Meta(object):
53 name = _("Info")
54 # Unusable permission so this is always hidden. However, we
55 # keep this step in the workflow for validation/verification purposes.
56 permissions = ()
57
58
59class Info(workflows.Step):
60 action_class = InfoAction
61 contributes = ("job_id", "actions")
62
63
64class ConfigureActions(workflows.Workflow):
65 slug = "job"
66 name = _("Actions Configuration")
67 finalize_button_name = _("Save")
68 success_message = _('Actions saved correctly.')
69 failure_message = _('Unable to save actions.')
70 success_url = "horizon:disaster_recovery:jobs:index"
71 default_steps = (ActionsConfiguration, Info,)
72
73 def handle(self, request, context):
74 try:
75 if context['job_id'] != '':
76 freezer_api.Job(request).update_actions(context['job_id'],
77 context['actions'])
78 return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
79 except Exception:
80 exceptions.handle(request)
81 return False
diff --git a/freezer_ui/jobs/workflows/configure.py b/disaster_recovery/jobs/workflows/create.py
index 2409610..534d504 100644
--- a/freezer_ui/jobs/workflows/configure.py
+++ b/disaster_recovery/jobs/workflows/create.py
@@ -14,18 +14,16 @@
14 14
15import logging 15import logging
16 16
17from collections import namedtuple
18import datetime 17import datetime
19 18
19from django import shortcuts
20from django.utils.translation import ugettext_lazy as _ 20from django.utils.translation import ugettext_lazy as _
21 21
22from horizon import exceptions 22from horizon import exceptions
23from horizon import forms 23from horizon import forms
24from horizon import messages
25from horizon import workflows 24from horizon import workflows
26 25
27import freezer_ui.api.api as freezer_api 26import disaster_recovery.api.api as freezer_api
28from freezer_ui.utils import actions_in_job
29 27
30 28
31LOG = logging.getLogger(__name__) 29LOG = logging.getLogger(__name__)
@@ -37,7 +35,7 @@ class ActionsConfigurationAction(workflows.Action):
37 class Meta(object): 35 class Meta(object):
38 name = _("Actions") 36 name = _("Actions")
39 slug = "actions" 37 slug = "actions"
40 help_text_template = "freezer_ui/jobs" \ 38 help_text_template = "disaster_recovery/jobs" \
41 "/_actions.html" 39 "/_actions.html"
42 40
43 41
@@ -53,7 +51,7 @@ class ClientsConfigurationAction(workflows.MembershipAction):
53 **kwargs) 51 **kwargs)
54 err_msg = _('Unable to retrieve client list.') 52 err_msg = _('Unable to retrieve client list.')
55 53
56 original_name = args[0].get('original_name', None) 54 job_id = args[0].get('job_id', None)
57 55
58 default_role_name = self.get_default_role_field_name() 56 default_role_name = self.get_default_role_field_name()
59 self.fields[default_role_name] = forms.CharField(required=False) 57 self.fields[default_role_name] = forms.CharField(required=False)
@@ -61,14 +59,14 @@ class ClientsConfigurationAction(workflows.MembershipAction):
61 59
62 all_clients = [] 60 all_clients = []
63 try: 61 try:
64 all_clients = freezer_api.client_list(request) 62 all_clients = freezer_api.Client(request).list()
65 except Exception: 63 except Exception:
66 exceptions.handle(request, err_msg) 64 exceptions.handle(request, err_msg)
67 client_list = [(c.uuid, c.hostname) 65 client_list = [(c.uuid, c.hostname)
68 for c in all_clients] 66 for c in all_clients]
69 67
70 field_name = self.get_member_field_name('member') 68 field_name = self.get_member_field_name('member')
71 if not original_name: 69 if not job_id:
72 self.fields[field_name] = forms.MultipleChoiceField(required=False) 70 self.fields[field_name] = forms.MultipleChoiceField(required=False)
73 self.fields[field_name].choices = client_list 71 self.fields[field_name].choices = client_list
74 72
@@ -96,11 +94,23 @@ class ClientsConfiguration(workflows.UpdateMembersStep):
96 return context 94 return context
97 95
98 96
99class SchedulingConfigurationAction(workflows.Action): 97class InfoConfigurationAction(workflows.Action):
98 actions = forms.CharField(
99 widget=forms.HiddenInput(),
100 required=False)
101
102 description = forms.CharField(
103 label=_("Job Name"),
104 help_text=_("Set a name for this job"),
105 required=True)
106
107 job_id = forms.CharField(
108 widget=forms.HiddenInput(),
109 required=False)
110
100 schedule_start_date = forms.CharField( 111 schedule_start_date = forms.CharField(
101 label=_("Start Date and Time"), 112 label=_("Start Date and Time"),
102 required=False, 113 required=False)
103 help_text=_(""))
104 114
105 schedule_interval = forms.CharField( 115 schedule_interval = forms.CharField(
106 label=_("Interval"), 116 label=_("Interval"),
@@ -109,17 +119,16 @@ class SchedulingConfigurationAction(workflows.Action):
109 119
110 schedule_end_date = forms.CharField( 120 schedule_end_date = forms.CharField(
111 label=_("End Date and Time"), 121 label=_("End Date and Time"),
112 required=False, 122 required=False)
113 help_text=_(""))
114 123
115 def __init__(self, request, context, *args, **kwargs): 124 def __init__(self, request, context, *args, **kwargs):
116 self.request = request 125 self.request = request
117 self.context = context 126 self.context = context
118 super(SchedulingConfigurationAction, self).__init__( 127 super(InfoConfigurationAction, self).__init__(
119 request, context, *args, **kwargs) 128 request, context, *args, **kwargs)
120 129
121 def clean(self): 130 def clean(self):
122 cleaned_data = super(SchedulingConfigurationAction, self).clean() 131 cleaned_data = super(InfoConfigurationAction, self).clean()
123 self._check_start_datetime(cleaned_data) 132 self._check_start_datetime(cleaned_data)
124 self._check_end_datetime(cleaned_data) 133 self._check_end_datetime(cleaned_data)
125 return cleaned_data 134 return cleaned_data
@@ -146,45 +155,20 @@ class SchedulingConfigurationAction(workflows.Action):
146 self._errors['schedule_end_date'] = self.error_class([msg]) 155 self._errors['schedule_end_date'] = self.error_class([msg])
147 156
148 class Meta(object): 157 class Meta(object):
149 name = _("Scheduling")
150 slug = "scheduling"
151 help_text_template = "freezer_ui/jobs" \
152 "/_scheduling.html"
153
154
155class SchedulingConfiguration(workflows.Step):
156 action_class = SchedulingConfigurationAction
157 contributes = ('schedule_start_date',
158 'schedule_interval',
159 'schedule_end_date')
160
161
162class InfoConfigurationAction(workflows.Action):
163 actions = forms.CharField(
164 widget=forms.HiddenInput(),
165 required=False)
166
167 description = forms.CharField(
168 label=_("Job Name"),
169 help_text=_("Set a short description for this job"),
170 required=True)
171
172 original_name = forms.CharField(
173 widget=forms.HiddenInput(),
174 required=False)
175
176 class Meta(object):
177 name = _("Job Info") 158 name = _("Job Info")
178 slug = "info" 159 slug = "info"
179 help_text_template = "freezer_ui/jobs" \ 160 help_text_template = "disaster_recovery/jobs" \
180 "/_info.html" 161 "/_scheduling.html"
181 162
182 163
183class InfoConfiguration(workflows.Step): 164class InfoConfiguration(workflows.Step):
184 action_class = InfoConfigurationAction 165 action_class = InfoConfigurationAction
185 contributes = ('description', 166 contributes = ('description',
186 'original_name', 167 'job_id',
187 'actions') 168 'actions',
169 'schedule_start_date',
170 'schedule_interval',
171 'schedule_end_date')
188 172
189 173
190class ConfigureJob(workflows.Workflow): 174class ConfigureJob(workflows.Workflow):
@@ -193,62 +177,18 @@ class ConfigureJob(workflows.Workflow):
193 finalize_button_name = _("Save") 177 finalize_button_name = _("Save")
194 success_message = _('Job created correctly.') 178 success_message = _('Job created correctly.')
195 failure_message = _('Unable to created job.') 179 failure_message = _('Unable to created job.')
196 success_url = "horizon:freezer_ui:jobs:index" 180 success_url = "horizon:disaster_recovery:jobs:index"
197 default_steps = (InfoConfiguration, 181 default_steps = (InfoConfiguration,
198 ClientsConfiguration, 182 ClientsConfiguration,
199 SchedulingConfiguration,
200 ActionsConfiguration) 183 ActionsConfiguration)
201 184
202 def handle(self, request, context): 185 def handle(self, request, context):
203 try: 186 try:
204 is_edit = False 187 if context['job_id'] != '':
205 if not context['original_name'] == '': 188 freezer_api.Job(request).update(context['job_id'], context)
206 is_edit = True
207
208 actions = actions_in_job(context.pop('actions', []))
209 actions_for_job = []
210
211 if is_edit:
212 # if this is a edit get the job and delete the action list
213 # TODO(m3m0) improve this to not recreate the action list
214 job_id = context['original_name']
215 job = freezer_api.job_get(request, job_id)
216 del job[0].data_dict['job_actions']
217
218 for action in actions:
219 a = freezer_api.action_get(request, action)
220 a = {
221 'action_id': a['action_id'],
222 'freezer_action': a['freezer_action']
223 }
224 actions_for_job.append(a)
225
226 context['job_actions'] = actions_for_job
227
228 if is_edit:
229 return freezer_api.job_edit(request, context)
230 else: 189 else:
231 if context['clients']: 190 freezer_api.Job(request).create(context)
232 # we have to query the api to get the list of clients 191 return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
233 # because MembershipAction for clients works with uuid's
234 # and we need to send the client_id instead of the uuid
235 # for the job creation
236 clients = freezer_api.client_list(request)
237 ClientIDS = namedtuple('Client', ['client_id', 'uuid'])
238
239 client_list = [ClientIDS(c.client_id, c.uuid)
240 for c in clients]
241
242 for client_uuid in context['clients']:
243 for client_id, uuid in client_list:
244 if client_uuid == uuid:
245 context['client_id'] = client_id
246 freezer_api.job_create(request, context)
247 else:
248 messages.warning(request, _("At least one client is "
249 "required to create a job"))
250 return False
251 return True
252 except Exception: 192 except Exception:
253 exceptions.handle(request) 193 exceptions.handle(request)
254 return False 194 return False
diff --git a/freezer_ui/clients/models.py b/disaster_recovery/models.py
index e69de29..e69de29 100644
--- a/freezer_ui/clients/models.py
+++ b/disaster_recovery/models.py
diff --git a/disaster_recovery/sessions/__init__.py b/disaster_recovery/sessions/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/disaster_recovery/sessions/__init__.py
diff --git a/freezer_ui/sessions/browsers.py b/disaster_recovery/sessions/browsers.py
index e9e7bc7..6b3da3e 100644
--- a/freezer_ui/sessions/browsers.py
+++ b/disaster_recovery/sessions/browsers.py
@@ -14,7 +14,7 @@
14 14
15from django.utils.translation import ugettext_lazy as _ 15from django.utils.translation import ugettext_lazy as _
16 16
17from freezer_ui.sessions import tables 17from disaster_recovery.sessions import tables
18 18
19from horizon import browsers 19from horizon import browsers
20 20
diff --git a/freezer_ui/sessions/panel.py b/disaster_recovery/sessions/panel.py
index 3cf6265..a009dab 100644
--- a/freezer_ui/sessions/panel.py
+++ b/disaster_recovery/sessions/panel.py
@@ -17,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _
17 17
18import horizon 18import horizon
19 19
20from freezer_ui import dashboard 20from disaster_recovery import dashboard
21 21
22 22
23class SessionsPanel(horizon.Panel): 23class SessionsPanel(horizon.Panel):
diff --git a/freezer_ui/sessions/tables.py b/disaster_recovery/sessions/tables.py
index 5074c13..569c732 100644
--- a/freezer_ui/sessions/tables.py
+++ b/disaster_recovery/sessions/tables.py
@@ -21,21 +21,26 @@ from django.utils.translation import ungettext_lazy
21from horizon import tables 21from horizon import tables
22from horizon.utils.urlresolvers import reverse 22from horizon.utils.urlresolvers import reverse
23 23
24import freezer_ui.api.api as freezer_api 24import disaster_recovery.api.api as freezer_api
25 25
26 26
27LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
28 28
29 29
30class ObjectFilterAction(tables.FilterAction):
31 def allowed(self, request, datum):
32 return bool(self.table.kwargs['session_id'])
33
34
30def get_link(session): 35def get_link(session):
31 return reverse('horizon:freezer_ui:sessions:index', 36 return reverse('horizon:disaster_recovery:sessions:index',
32 kwargs={'session_id': session.session_id}) 37 kwargs={'session_id': session.session_id})
33 38
34 39
35class CreateJob(tables.LinkAction): 40class CreateJob(tables.LinkAction):
36 name = "create_session" 41 name = "create_session"
37 verbose_name = _("Create Session") 42 verbose_name = _("Create Session")
38 url = "horizon:freezer_ui:sessions:create" 43 url = "horizon:disaster_recovery:sessions:create"
39 classes = ("ajax-modal",) 44 classes = ("ajax-modal",)
40 icon = "plus" 45 icon = "plus"
41 46
@@ -63,7 +68,7 @@ class DeleteSession(tables.DeleteAction):
63 ) 68 )
64 69
65 def delete(self, request, session_id): 70 def delete(self, request, session_id):
66 return freezer_api.session_delete(request, session_id) 71 return freezer_api.Session(request).delete(session_id)
67 72
68 73
69class EditSession(tables.LinkAction): 74class EditSession(tables.LinkAction):
@@ -73,7 +78,7 @@ class EditSession(tables.LinkAction):
73 icon = "pencil" 78 icon = "pencil"
74 79
75 def get_link_url(self, datum=None): 80 def get_link_url(self, datum=None):
76 return reverse("horizon:freezer_ui:sessions:edit", 81 return reverse("horizon:disaster_recovery:sessions:edit",
77 kwargs={'session_id': datum.session_id}) 82 kwargs={'session_id': datum.session_id})
78 83
79 84
@@ -105,10 +110,7 @@ class DeleteJobFromSession(tables.DeleteAction):
105 110
106 def delete(self, request, session): 111 def delete(self, request, session):
107 job_id, session_id = session.split('===') 112 job_id, session_id = session.split('===')
108 return freezer_api.remove_job_from_session( 113 return freezer_api.Session(request).remove_job(session_id, job_id)
109 request,
110 session_id,
111 job_id)
112 114
113 115
114class JobsTable(tables.DataTable): 116class JobsTable(tables.DataTable):
@@ -116,8 +118,8 @@ class JobsTable(tables.DataTable):
116 'client_id', 118 'client_id',
117 verbose_name=_("Client ID")) 119 verbose_name=_("Client ID"))
118 120
119 status = tables.Column( 121 result = tables.Column(
120 'status', 122 'result',
121 verbose_name=_("Status")) 123 verbose_name=_("Status"))
122 124
123 def get_object_id(self, job): 125 def get_object_id(self, job):
@@ -129,7 +131,7 @@ class JobsTable(tables.DataTable):
129 class Meta(object): 131 class Meta(object):
130 name = "jobs" 132 name = "jobs"
131 verbose_name = _("Jobs") 133 verbose_name = _("Jobs")
132 table_actions = () 134 table_actions = (ObjectFilterAction,)
133 row_actions = (DeleteJobFromSession,) 135 row_actions = (DeleteJobFromSession,)
134 footer = False 136 footer = False
135 multi_select = True 137 multi_select = True
@@ -143,13 +145,11 @@ class SessionsTable(tables.DataTable):
143 status = tables.Column('status', 145 status = tables.Column('status',
144 verbose_name=_("Status")) 146 verbose_name=_("Status"))
145 147
146 def get_object_id(self, session):
147 return session.session_id
148
149 class Meta(object): 148 class Meta(object):
150 name = "sessions" 149 name = "sessions"
151 verbose_name = _("Sessions") 150 verbose_name = _("Sessions")
152 table_actions = (CreateJob, 151 table_actions = (ObjectFilterAction,
152 CreateJob,
153 DeleteMultipleActions) 153 DeleteMultipleActions)
154 row_actions = (EditSession, 154 row_actions = (EditSession,
155 DeleteSession,) 155 DeleteSession,)
diff --git a/freezer_ui/sessions/templates/sessions/_info.html b/disaster_recovery/sessions/templates/sessions/_info.html
index 26bd441..26bd441 100644
--- a/freezer_ui/sessions/templates/sessions/_info.html
+++ b/disaster_recovery/sessions/templates/sessions/_info.html
diff --git a/freezer_ui/sessions/templates/sessions/browser.html b/disaster_recovery/sessions/templates/sessions/browser.html
index 38c20f5..38c20f5 100644
--- a/freezer_ui/sessions/templates/sessions/browser.html
+++ b/disaster_recovery/sessions/templates/sessions/browser.html
diff --git a/freezer_ui/sessions/urls.py b/disaster_recovery/sessions/urls.py
index 51f4f1d..8f5c992 100644
--- a/freezer_ui/sessions/urls.py
+++ b/disaster_recovery/sessions/urls.py
@@ -15,7 +15,7 @@
15from django.conf.urls import patterns 15from django.conf.urls import patterns
16from django.conf.urls import url 16from django.conf.urls import url
17 17
18from freezer_ui.sessions import views 18from disaster_recovery.sessions import views
19 19
20 20
21urlpatterns = patterns( 21urlpatterns = patterns(
diff --git a/disaster_recovery/sessions/views.py b/disaster_recovery/sessions/views.py
new file mode 100644
index 0000000..f14ac89
--- /dev/null
+++ b/disaster_recovery/sessions/views.py
@@ -0,0 +1,86 @@
1# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import logging
16
17from horizon import browsers
18from horizon import workflows
19
20import disaster_recovery.api.api as freezer_api
21import disaster_recovery.sessions.browsers as project_browsers
22
23from disaster_recovery.sessions.workflows import attach
24from disaster_recovery.sessions.workflows import create
25from disaster_recovery.utils import shield
26
27
28LOG = logging.getLogger(__name__)
29
30
31class SessionsView(browsers.ResourceBrowserView):
32 browser_class = project_browsers.SessionBrowser
33 template_name = "disaster_recovery/sessions/browser.html"
34
35 @shield('Unable to get sessions list.', redirect='actions:index')
36 def get_sessions_data(self):
37 return freezer_api.Session(self.request).list(limit=100)
38
39 @shield('Unable to get job list.', redirect='actions:index')
40 def get_jobs_data(self):
41 if self.kwargs['session_id']:
42 return freezer_api.Session(self.request).jobs(
43 self.kwargs['session_id'])
44 return []
45
46
47class AttachToSessionWorkflow(workflows.WorkflowView):
48 workflow_class = attach.AttachJobToSession
49
50 @shield('Unable to get job', redirect='jobs:index')
51 def get_object(self, *args, **kwargs):
52 return freezer_api.Job(self.request).get(self.kwargs['job_id'])
53
54 def is_update(self):
55 return 'job_id' in self.kwargs and \
56 bool(self.kwargs['job_id'])
57
58 @shield('Unable to get job', redirect='jobs:index')
59 def get_initial(self):
60 initial = super(AttachToSessionWorkflow, self).get_initial()
61 job = self.get_object()
62 initial.update({'job_id': job.id})
63 return initial
64
65
66class CreateSessionWorkflow(workflows.WorkflowView):
67 workflow_class = create.CreateSession
68
69 @shield('Unable to get session', redirect='sessions:index')
70 def get_object(self, *args, **kwargs):
71 return freezer_api.Session(self.request).get(self.kwargs['session_id'])
72
73 @shield('Unable to get session', redirect='sessions:index')
74 def get_initial(self):
75 initial = super(CreateSessionWorkflow, self).get_initial()
76 if self.is_update():
77 initial.update({'job_id': None})
78 session = freezer_api.Session(self.request).get(
79 self.kwargs['session_id'], json=True)
80 initial.update(**session)
81 initial.update(**session['schedule'])
82 return initial
83
84 def is_update(self):
85 return 'session_id' in self.kwargs and \
86 bool(self.kwargs['session_id'])
diff --git a/disaster_recovery/sessions/workflows/__init__.py b/disaster_recovery/sessions/workflows/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/disaster_recovery/sessions/workflows/__init__.py
diff --git a/freezer_ui/sessions/workflows/attach.py b/disaster_recovery/sessions/workflows/attach.py
index 7eaddb1..c234031 100644
--- a/freezer_ui/sessions/workflows/attach.py
+++ b/disaster_recovery/sessions/workflows/attach.py
@@ -15,12 +15,13 @@
15import logging 15import logging
16 16
17from django.utils.translation import ugettext_lazy as _ 17from django.utils.translation import ugettext_lazy as _
18from horizon.utils.urlresolvers import reverse
18 19
19from horizon import exceptions 20from horizon import exceptions
20from horizon import forms 21from horizon import forms
21from horizon import workflows 22from horizon import workflows
22 23
23import freezer_ui.api.api as freezer_api 24import disaster_recovery.api.api as freezer_api
24 25
25 26
26LOG = logging.getLogger(__name__) 27LOG = logging.getLogger(__name__)
@@ -39,7 +40,7 @@ class SessionConfigurationAction(workflows.Action):
39 def populate_session_id_choices(self, request, context): 40 def populate_session_id_choices(self, request, context):
40 sessions = [] 41 sessions = []
41 try: 42 try:
42 sessions = freezer_api.session_list(request) 43 sessions = freezer_api.Session(request).list()
43 except Exception: 44 except Exception:
44 exceptions.handle(request, _('Error getting session list')) 45 exceptions.handle(request, _('Error getting session list'))
45 46
@@ -64,16 +65,15 @@ class AttachJobToSession(workflows.Workflow):
64 finalize_button_name = _("Attach") 65 finalize_button_name = _("Attach")
65 success_message = _('Job saved successfully.') 66 success_message = _('Job saved successfully.')
66 failure_message = _('Unable to attach to session.') 67 failure_message = _('Unable to attach to session.')
67 success_url = "horizon:freezer_ui:jobs:index" 68 success_url = "horizon:disaster_recovery:jobs:index"
68 default_steps = (SessionConfiguration,) 69 default_steps = (SessionConfiguration,)
69 70
70 def handle(self, request, context): 71 def handle(self, request, context):
71 try: 72 try:
72 freezer_api.add_job_to_session( 73 freezer_api.Session(request).add_job(context['session_id'],
73 request, 74 context['job_id'])
74 context['session_id'], 75
75 context['job_id']) 76 return reverse("horizon:disaster_recovery:jobs:index")
76 return True
77 except Exception: 77 except Exception:
78 exceptions.handle(request) 78 exceptions.handle(request)
79 return False 79 return False
diff --git a/freezer_ui/sessions/workflows/create_session.py b/disaster_recovery/sessions/workflows/create.py
index 0328703..29724f5 100644
--- a/freezer_ui/sessions/workflows/create_session.py
+++ b/disaster_recovery/sessions/workflows/create.py
@@ -12,16 +12,17 @@
12# See the License for the specific language governing permissions and 12# See the License for the specific language governing permissions and
13# limitations under the License. 13# limitations under the License.
14 14
15import logging
16import datetime 15import datetime
16import logging
17 17
18from django.utils.translation import ugettext_lazy as _ 18from django.utils.translation import ugettext_lazy as _
19from horizon.utils.urlresolvers import reverse
19 20
20from horizon import exceptions 21from horizon import exceptions
21from horizon import forms 22from horizon import forms
22from horizon import workflows 23from horizon import workflows
23 24
24import freezer_ui.api.api as freezer_api 25import disaster_recovery.api.api as freezer_api
25 26
26 27
27LOG = logging.getLogger(__name__) 28LOG = logging.getLogger(__name__)
@@ -37,43 +38,20 @@ class SessionConfigurationAction(workflows.Action):
37 widget=forms.HiddenInput(), 38 widget=forms.HiddenInput(),
38 required=False) 39 required=False)
39 40
40 class Meta:
41 name = _("Session Information")
42 slug = "sessions"
43 help_text_template = "freezer_ui/sessions" \
44 "/_info.html"
45
46
47class SessionConfiguration(workflows.Step):
48 action_class = SessionConfigurationAction
49 contributes = ('description',
50 'session_id')
51
52
53class SchedulingConfigurationAction(workflows.Action):
54 schedule_start_date = forms.CharField( 41 schedule_start_date = forms.CharField(
55 label=_("Start Date and Time"), 42 label=_("Start Date and Time"),
56 required=False, 43 required=False)
57 help_text=_(""))
58 44
59 schedule_interval = forms.CharField( 45 schedule_interval = forms.CharField(
60 label=_("Interval"), 46 label=_("Interval"),
61 required=False, 47 required=False)
62 help_text=_(""))
63 48
64 schedule_end_date = forms.CharField( 49 schedule_end_date = forms.CharField(
65 label=_("End Date and Time"), 50 label=_("End Date and Time"),
66 required=False, 51 required=False)
67 help_text=_(""))
68
69 def __init__(self, request, context, *args, **kwargs):
70 self.request = request
71 self.context = context
72 super(SchedulingConfigurationAction, self).__init__(
73 request, context, *args, **kwargs)
74 52
75 def clean(self): 53 def clean(self):
76 cleaned_data = super(SchedulingConfigurationAction, self).clean() 54 cleaned_data = super(SessionConfigurationAction, self).clean()
77 self._check_start_datetime(cleaned_data) 55 self._check_start_datetime(cleaned_data)
78 self._check_end_datetime(cleaned_data) 56 self._check_end_datetime(cleaned_data)
79 return cleaned_data 57 return cleaned_data
@@ -99,16 +77,18 @@ class SchedulingConfigurationAction(workflows.Action):
99 msg = _("End date time is not in ISO format.") 77 msg = _("End date time is not in ISO format.")
100 self._errors['schedule_end_date'] = self.error_class([msg]) 78 self._errors['schedule_end_date'] = self.error_class([msg])
101 79
102 class Meta(object): 80 class Meta:
103 name = _("Scheduling") 81 name = _("Session Information")
104 slug = "scheduling" 82 slug = "sessions"
105 help_text_template = "freezer_ui/jobs" \ 83 help_text_template = "disaster_recovery/jobs" \
106 "/_scheduling.html" 84 "/_scheduling.html"
107 85
108 86
109class SchedulingConfiguration(workflows.Step): 87class SessionConfiguration(workflows.Step):
110 action_class = SchedulingConfigurationAction 88 action_class = SessionConfigurationAction
111 contributes = ('schedule_start_date', 89 contributes = ('description',
90 'session_id',
91 'schedule_start_date',
112 'schedule_interval', 92 'schedule_interval',
113 'schedule_end_date') 93 'schedule_end_date')
114 94
@@ -116,19 +96,20 @@ class SchedulingConfiguration(workflows.Step):
116class CreateSession(workflows.Workflow): 96class CreateSession(workflows.Workflow):
117 slug = "create_session" 97 slug = "create_session"
118 name = _("Create Session") 98 name = _("Create Session")
119 finalize_button_name = _("Create") 99 finalize_button_name = _("Save")
120 success_message = _('Session created successfully.') 100 success_message = _('Session created successfully.')
121 failure_message = _('Unable to create session.') 101 failure_message = _('Unable to create session.')
122 success_url = "horizon:freezer_ui:sessions:index" 102 success_url = "horizon:disaster_recovery:sessions:index"
123 default_steps = (SessionConfiguration, 103 default_steps = (SessionConfiguration,)
124 SchedulingConfiguration)
125 104
126 def handle(self, request, context): 105 def handle(self, request, context):
127 try: 106 try:
128 if context['session_id'] == '': 107 if context['session_id'] != '':
129 return freezer_api.session_create(request, context) 108 freezer_api.Session(request).update(context,
109 context['session_id'])
130 else: 110 else:
131 return freezer_api.session_update(request, context) 111 freezer_api.Session(request).create(context)
112 return reverse("horizon:disaster_recovery:sessions:index")
132 except Exception: 113 except Exception:
133 exceptions.handle(request) 114 exceptions.handle(request)
134 return False 115 return False
diff --git a/disaster_recovery/static/freezer/css/freezer.css b/disaster_recovery/static/freezer/css/freezer.css
new file mode 100644
index 0000000..3a14323
--- /dev/null
+++ b/disaster_recovery/static/freezer/css/freezer.css
@@ -0,0 +1,9 @@
1.fa-custom-number {
2 font-family: monospace;
3 line-height: 1;
4 padding: 0.1em;
5 vertical-align: baseline;
6 font-weight: bold;
7 border: 1px solid #999;
8 border-radius: 25%;
9}
diff --git a/freezer_ui/static/freezer/js/freezer.actions.action.js b/disaster_recovery/static/freezer/js/freezer.actions.action.js
index d5a96d7..d5a96d7 100644
--- a/freezer_ui/static/freezer/js/freezer.actions.action.js
+++ b/disaster_recovery/static/freezer/js/freezer.actions.action.js
diff --git a/freezer_ui/static/freezer/js/freezer.actions.advanced.js b/disaster_recovery/static/freezer/js/freezer.actions.advanced.js
index 726fc2b..726fc2b 100644
--- a/freezer_ui/static/freezer/js/freezer.actions.advanced.js
+++ b/disaster_recovery/static/freezer/js/freezer.actions.advanced.js
diff --git a/freezer_ui/static/freezer/js/freezer.actions.snapshot.js b/disaster_recovery/static/freezer/js/freezer.actions.snapshot.js
index 9d3f1fb..d501005 100644
--- a/freezer_ui/static/freezer/js/freezer.actions.snapshot.js
+++ b/disaster_recovery/static/freezer/js/freezer.actions.snapshot.js
@@ -27,7 +27,6 @@ function hideOptions() {
27 $("#id_lvm_snapsize").closest(".form-group").hide(); 27 $("#id_lvm_snapsize").closest(".form-group").hide();
28 $("#id_lvm_dirmount").closest(".form-group").hide(); 28 $("#id_lvm_dirmount").closest(".form-group").hide();
29 $("#id_lvm_volgroup").closest(".form-group").hide(); 29 $("#id_lvm_volgroup").closest(".form-group").hide();
30 $("#id_vssadmin").closest(".form-group").hide();
31} 30}
32 31
33function is_windows() { 32function is_windows() {
@@ -40,10 +39,6 @@ function showWindowsSnapshotOptions() {
40 $("#id_vssadmin").closest(".form-group").show(); 39 $("#id_vssadmin").closest(".form-group").show();
41} 40}
42 41
43function hideWindowsSnapshotOptions() {
44 $("#id_vssadmin").closest(".form-group").hide();
45}
46
47function showLinuxSnapshotOptions() { 42function showLinuxSnapshotOptions() {
48 $("#id_lvm_auto_snap").closest(".form-group").show(); 43 $("#id_lvm_auto_snap").closest(".form-group").show();
49 $("#id_lvm_srcvol").closest(".form-group").show(); 44 $("#id_lvm_srcvol").closest(".form-group").show();
@@ -63,7 +58,6 @@ function hideLinuxSnapshotOptions() {
63} 58}
64 59
65function hideSnapshotOptions() { 60function hideSnapshotOptions() {
66 hideWindowsSnapshotOptions();
67 hideLinuxSnapshotOptions(); 61 hideLinuxSnapshotOptions();
68 $("#id_is_windows").closest(".form-group").hide(); 62 $("#id_is_windows").closest(".form-group").hide();
69} 63}
@@ -74,7 +68,6 @@ function showSnapshotOptions() {
74 hideLinuxSnapshotOptions(); 68 hideLinuxSnapshotOptions();
75 showWindowsSnapshotOptions(); 69 showWindowsSnapshotOptions();
76 } else { 70 } else {
77 hideWindowsSnapshotOptions();
78 showLinuxSnapshotOptions(); 71 showLinuxSnapshotOptions();
79 } 72 }
80} 73}
diff --git a/freezer_ui/static/freezer/js/freezer.datetimepicker.js b/disaster_recovery/static/freezer/js/freezer.datetimepicker.js
index ba4a314..ba4a314 100644
--- a/freezer_ui/static/freezer/js/freezer.datetimepicker.js
+++ b/disaster_recovery/static/freezer/js/freezer.datetimepicker.js
diff --git a/freezer_ui/static/freezer/js/freezer.jobs.sortable.js b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js
index b0198a8..52987ee 100644
--- a/freezer_ui/static/freezer/js/freezer.jobs.sortable.js
+++ b/disaster_recovery/static/freezer/js/freezer.jobs.sortable.js
@@ -1,29 +1,15 @@
1/*
2# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15*/
16
17/*global $, location*/ 1/*global $, location*/
18 2
19"use strict"; 3'use strict';
4
20 5
21$(function () { 6$(function () {
22 $("#sortable1, #sortable2").sortable({ 7 $("#actions_available, #actions_selected").sortable({
23 connectWith: ".connectedSortable" 8 connectWith: ".connectedSortable"
24 }).disableSelection(); 9 }).disableSelection();
25}); 10});
26 11
12
27var parent = $(".sortable_lists").parent(); 13var parent = $(".sortable_lists").parent();
28parent.removeClass("col-sm-6"); 14parent.removeClass("col-sm-6");
29parent.addClass("col-sm-12"); 15parent.addClass("col-sm-12");
@@ -33,7 +19,7 @@ siblings.remove();
33 19
34$("form").submit(function (event) { 20$("form").submit(function (event) {
35 var ids = ""; 21 var ids = "";
36 $("#sortable2 li").each(function (index) { 22 $("#actions_selected li").each(function (index) {
37 ids += ($(this).attr('id')); 23 ids += ($(this).attr('id'));
38 ids += "==="; 24 ids += "===";
39 }); 25 });
@@ -41,33 +27,44 @@ $("form").submit(function (event) {
41}); 27});
42 28
43 29
44function get_actions_url() { 30var job_id = $('#id_job_id').val();
31
32function get_url() {
45 var url = $(location).attr("origin"); 33 var url = $(location).attr("origin");
46 url += '/freezer_ui/api/actions'; 34 url += '/disaster_recovery/api/actions/job/';
35 url += job_id;
47 return url; 36 return url;
48} 37}
49 38
50var job_id = $('#id_original_name').val(); 39function get_actions_url() {
40 var url = $(location).attr("origin");
41 url += '/disaster_recovery/api/actions';
42 return url;
43}
51 44
52if (job_id !== "") { 45if (job_id !== "") {
53 var url_available = get_actions_url();
54
55 $.ajax({ 46 $.ajax({
56 url: url_available, 47 url: get_url(),
57 type: "GET", 48 type: "GET",
58 cache: false, 49 cache: false,
59 dataType: 'json', 50 dataType: 'json',
60 contentType: 'application/json; charset=utf-8', 51 contentType: 'application/json; charset=utf-8',
61 success: function (data) { 52 success: function (data) {
62 $.each(data, function (index, item) { 53 $.each(data.available, function (index, item) {
63 $("#sortable1").append( 54 $("#actions_available").append(
55 "<li class='list-group-item' id=" + item.action_id + ">" +
56 item.freezer_action.backup_name + "</li>"
57 );
58 });
59 $.each(data.selected, function (index, item) {
60 $("#actions_selected").append(
64 "<li class='list-group-item' id=" + item.action_id + ">" + 61 "<li class='list-group-item' id=" + item.action_id + ">" +
65 item.freezer_action.backup_name + "</li>" 62 item.freezer_action.backup_name + "</li>"
66 ); 63 );
67 }); 64 });
68 }, 65 },
69 error: function (request, error) { 66 error: function (request, error) {
70 $("#sortable1").append( 67 $("#actions_available").append(
71 '<tr><td>Error getting action list</td></tr>' 68 '<tr><td>Error getting action list</td></tr>'
72 ); 69 );
73 } 70 }
@@ -83,7 +80,7 @@ if (job_id !== "") {
83 contentType: 'application/json; charset=utf-8' , 80 contentType: 'application/json; charset=utf-8' ,
84 success: function (data) { 81 success: function (data) {
85 $.each(data, function (index, item) { 82 $.each(data, function (index, item) {
86 $("#sortable1").append( 83 $("#actions_available").append(
87 "<li class='list-group-item' id=" + item.action_id + ">" + 84 "<li class='list-group-item' id=" + item.action_id + ">" +
88 item.freezer_action.backup_name + 85 item.freezer_action.backup_name +
89 "</li>" 86 "</li>"
@@ -91,7 +88,7 @@ if (job_id !== "") {
91 }); 88 });
92 }, 89 },
93 error: function (request, error) { 90 error: function (request, error) {
94 $("#sortable1").append( 91 $("#actions_available").append(
95 '<tr><td>Error getting action list</td></tr>' 92 '<tr><td>Error getting action list</td></tr>'
96 ); 93 );
97 } 94 }
diff --git a/freezer_ui/static/freezer/js/freezer.js b/disaster_recovery/static/freezer/js/freezer.js
index 08f98a8..ff1ef9b 100644
--- a/freezer_ui/static/freezer/js/freezer.js
+++ b/disaster_recovery/static/freezer/js/freezer.js
@@ -21,7 +21,7 @@
21 angular.module('hz').controller('DestinationCtrl', function ($scope, $http, $location) { 21 angular.module('hz').controller('DestinationCtrl', function ($scope, $http, $location) {