From 7e1ba92a87ec9041fc30fab1cc8e2da7c8042524 Mon Sep 17 00:00:00 2001 From: Memo Garcia Date: Tue, 28 Jul 2015 13:31:49 +0100 Subject: [PATCH] Backup history. Enable a list of backups that you can restore immediately from the ui Change-Id: I8f9bdf85b4f476ea3af9e7c12f50a86d173a999f --- freezer_ui/api/api.py | 244 ++++++++---------- freezer_ui/api/rest/rest_api.py | 24 +- freezer_ui/backups/__init__.py | 0 freezer_ui/backups/models.py | 3 + freezer_ui/backups/panel.py | 25 ++ freezer_ui/backups/tables.py | 100 +++++++ .../backups/templates/backups/detail.html | 17 ++ .../backups/templates/backups/index.html | 17 ++ .../backups/templates/backups/restore.html | 32 +++ freezer_ui/backups/urls.py | 27 ++ freezer_ui/backups/views.py | 82 ++++++ freezer_ui/backups/workflows/__init__.py | 0 freezer_ui/backups/workflows/restore.py | 98 +++++++ freezer_ui/dashboard.py | 2 +- freezer_ui/django_utils.py | 8 - freezer_ui/jobs/tables.py | 2 +- freezer_ui/jobs/templates/jobs/_action.html | 1 - freezer_ui/jobs/templates/jobs/_actions.html | 2 +- freezer_ui/jobs/templates/jobs/_advanced.html | 1 - freezer_ui/jobs/templates/jobs/_snapshot.html | 1 - .../jobs/templates/jobs/scheduling.html | 21 -- freezer_ui/sessions/tables.py | 6 +- freezer_ui/static/freezer/js/freezer.js | 24 +- .../static/freezer/js/freezer.restore.js | 26 ++ freezer_ui/utils.py | 101 +++++++- 25 files changed, 670 insertions(+), 194 deletions(-) create mode 100644 freezer_ui/backups/__init__.py create mode 100644 freezer_ui/backups/models.py create mode 100644 freezer_ui/backups/panel.py create mode 100644 freezer_ui/backups/tables.py create mode 100644 freezer_ui/backups/templates/backups/detail.html create mode 100644 freezer_ui/backups/templates/backups/index.html create mode 100644 freezer_ui/backups/templates/backups/restore.html create mode 100644 freezer_ui/backups/urls.py create mode 100644 freezer_ui/backups/views.py create mode 100644 freezer_ui/backups/workflows/__init__.py create mode 100644 freezer_ui/backups/workflows/restore.py delete mode 100644 freezer_ui/django_utils.py delete mode 100644 freezer_ui/jobs/templates/jobs/scheduling.html create mode 100644 freezer_ui/static/freezer/js/freezer.restore.js diff --git a/freezer_ui/api/api.py b/freezer_ui/api/api.py index 07ccc3c..7e211ba 100644 --- a/freezer_ui/api/api.py +++ b/freezer_ui/api/api.py @@ -17,102 +17,23 @@ import warnings from django.conf import settings + +from horizon.utils import functions as utils from horizon.utils.memoized import memoized # noqa + import freezer.apiclient.client +from horizon_web_ui.freezer_ui.utils import Action +from horizon_web_ui.freezer_ui.utils import ActionJob +from horizon_web_ui.freezer_ui.utils import Backup +from horizon_web_ui.freezer_ui.utils import Client +from horizon_web_ui.freezer_ui.utils import Job +from horizon_web_ui.freezer_ui.utils import JobList +from horizon_web_ui.freezer_ui.utils import Session from horizon_web_ui.freezer_ui.utils import create_dict_action from horizon_web_ui.freezer_ui.utils import create_dummy_id -class Dict2Object(object): - """Makes dictionary fields accessible as if they are attributes. - - The dictionary keys become class attributes. It is possible to use one - nested dictionary by overwriting nested_dict with the key of that nested - dict. - - This class is needed because we mostly deal with objects in horizon (e.g. - for providing data to the tables) but the api only gives us json data. - """ - nested_dict = None - - def __init__(self, data_dict): - self.data_dict = data_dict - - def __getattr__(self, attr): - """Make data_dict fields available via class interface """ - if attr in self.data_dict: - return self.data_dict[attr] - elif attr in self.data_dict[self.nested_dict]: - return self.data_dict[self.nested_dict][attr] - else: - return object.__getattribute__(self, attr) - - def get_dict(self): - return self.data_dict - - -class Action(Dict2Object): - nested_dict = 'job_action' - - @property - def id(self): - return self.job_id - - -class Job(Dict2Object): - nested_dict = 'job_actions' - - @property - def id(self): - return self.job_id - - -class JobList(object): - """Create an object to be passed to horizon tables that handles - nested values - """ - def __init__(self, description, result, job_id): - self.description = description - self.result = result - self.id = job_id - self.job_id = job_id - - -class Backup(Dict2Object): - nested_dict = 'backup_metadata' - - @property - def id(self): - return self.backup_id - - -class Client(object): - def __init__(self, client, hostname): - self.client = client - self.hostname = hostname - - -class ActionJob(object): - def __init__(self, job_id, action_id, action, backup_name): - self.job_id = job_id - self.action_id = action_id - self.action = action - self.backup_name = backup_name - - -class Session(object): - def __init__(self, session_id, description, status, jobs, - start_datetime, interval, end_datetime): - self.session_id = session_id - self.description = description - self.status = status - self.jobs = jobs - self.start_datetime = start_datetime - self.interval = interval - self.end_datetime = end_datetime - - @memoized def get_service_url(request): """ Get Freezer API url from keystone catalog. @@ -151,32 +72,24 @@ def _freezerclient(request): def job_create(request, context): """Create a new job file """ - schedule = {} - if context['schedule_end_date']: - schedule['schedule_end_date'] = context.pop('schedule_end_date') - - if context['schedule_interval']: - schedule['schedule_interval'] = context.pop('schedule_interval') - - if context['schedule_start_date']: - schedule['schedule_start_date'] = context.pop('schedule_start_date') job = create_dict_action(**context) - client_id = job.pop('client_id', None) - job['description'] = job.pop('description', None) - actions = job.pop('job_actions', None) - job.pop('clients', None) schedule = {} - if context['schedule_end_date']: - schedule['schedule_end_date'] = context.pop('schedule_end_date') - if context['schedule_interval']: - schedule['schedule_interval'] = context.pop('schedule_interval') - if context['schedule_start_date']: - schedule['schedule_start_date'] = context.pop('schedule_start_date') + if job['schedule_end_date']: + schedule['schedule_end_date'] = job.pop('schedule_end_date') + + if job['schedule_interval']: + schedule['schedule_interval'] = job.pop('schedule_interval') + + if job['schedule_start_date']: + schedule['schedule_start_date'] = job.pop('schedule_start_date') job.pop('clients', None) + client_id = job.pop('client_id', None) + actions = job.pop('job_actions', []) + job['description'] = job.pop('description', None) job['job_schedule'] = schedule job['job_actions'] = actions job['client_id'] = client_id @@ -185,20 +98,23 @@ def job_create(request, context): def job_edit(request, context): """Edit an existing job file, but leave the actions to actions_edit""" - schedule = {} - if context['schedule_end_date']: - schedule['schedule_end_date'] = context.pop('schedule_end_date') - - if context['schedule_interval']: - schedule['schedule_interval'] = context.pop('schedule_interval') - - if context['schedule_start_date']: - schedule['schedule_start_date'] = context.pop('schedule_start_date') - job = create_dict_action(**context) + + schedule = {} + if job['schedule_end_date']: + schedule['schedule_end_date'] = job.pop('schedule_end_date') + + if job['schedule_interval']: + schedule['schedule_interval'] = job.pop('schedule_interval') + + if job['schedule_start_date']: + schedule['schedule_start_date'] = job.pop('schedule_start_date') + job['description'] = job.pop('description', None) - actions = job.pop('job_actions', None) + actions = job.pop('job_actions', []) + job.pop('clients', None) + job.pop('client_id', None) job['job_schedule'] = schedule job['job_actions'] = actions @@ -286,25 +202,28 @@ def actions_in_job_json(request, job_id): def actions_in_job(request, job_id): - job = _freezerclient(request).jobs.get(job_id) actions = [] - for a in job['job_actions']: - try: - action_id = a['action_id'] - except KeyError: - action_id = create_dummy_id() + try: + job = _freezerclient(request).jobs.get(job_id) + for a in job['job_actions']: + try: + action_id = a['action_id'] + except (KeyError, TypeError): + action_id = create_dummy_id() - try: - action = a['freezer_action']['action'] - except KeyError: - action = "backup" + try: + action = a['freezer_action']['action'] + except (KeyError, TypeError): + action = "backup" - try: - backup_name = a['freezer_action']['backup_name'] - except KeyError: - backup_name = "NO BACKUP NAME AVAILABLE" + try: + backup_name = a['freezer_action']['backup_name'] + except (KeyError, TypeError): + backup_name = "NO BACKUP NAME AVAILABLE" - actions.append(ActionJob(job_id, action_id, action, backup_name)) + actions.append(ActionJob(job_id, action_id, action, backup_name)) + except TypeError: + pass return actions @@ -354,6 +273,19 @@ def client_list(request): return clients +def client_list_json(request): + """Return a list of clients directly form the api in json format""" + clients = _freezerclient(request).registration.list() + return clients + + +def client_get(request, client_id): + """Get a single client""" + client = _freezerclient(request).registration.get(client_id) + client = Client(client['uuid'], client['client']['hostname']) + return client + + def add_job_to_session(request, session_id, job_id): """This function will add a job to a session and the API will handle the copy of job information to the session """ @@ -429,3 +361,47 @@ def session_get(request, session_id): session['schedule']['schedule_interval'], session['schedule']['schedule_end_date']) return session + + +def backups_list(request, offset=0, time_after=None, time_before=None, + text_match=None): + """List all backups and optionally you can provide filters and pagination + values """ + page_size = utils.get_page_size(request) + + search = {} + + if time_after: + search['time_after'] = time_after + if time_before: + search['time_before'] = time_before + + if text_match: + search['match'] = [ + { + "_all": text_match, + } + ] + + backups = _freezerclient(request).backups.list( + limit=page_size + 1, + offset=offset, + search=search) + + if len(backups) > page_size: + backups.pop() + has_more = True + else: + has_more = False + + # Wrap data in object for easier handling + backups = [Backup(data) for data in backups] + + return backups, has_more + + +def backup_get(request, backup_id): + """Get a single backup""" + backup = _freezerclient(request).backups.get(backup_id) + backup = Backup(backup) + return backup diff --git a/freezer_ui/api/rest/rest_api.py b/freezer_ui/api/rest/rest_api.py index 91bd87d..b928c34 100644 --- a/freezer_ui/api/rest/rest_api.py +++ b/freezer_ui/api/rest/rest_api.py @@ -1,8 +1,21 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + import functools +import json from django.http import HttpResponse from django.views import generic -import json + from openstack_dashboard.api.rest import utils as rest_utils from openstack_dashboard.api.rest.utils import JSONResponse @@ -32,10 +45,10 @@ class Clients(generic.View): # we don't have a "get all clients" api (probably for good reason) so # we need to resort to getting a very high number. - clients = freezer_api.client_list(request) - clients = [c.get_dict() for c in clients] - - return clients + clients = freezer_api.client_list_json(request) + clients = json.dumps(clients) + return HttpResponse(clients, + content_type="application/json") class Actions(generic.View): @@ -62,4 +75,3 @@ class ActionsInJob(generic.View): actions = json.dumps(actions) return HttpResponse(actions, content_type="application/json") - diff --git a/freezer_ui/backups/__init__.py b/freezer_ui/backups/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezer_ui/backups/models.py b/freezer_ui/backups/models.py new file mode 100644 index 0000000..1b3d5f9 --- /dev/null +++ b/freezer_ui/backups/models.py @@ -0,0 +1,3 @@ +""" +Stub file to work around django bug: https://code.djangoproject.com/ticket/7198 +""" diff --git a/freezer_ui/backups/panel.py b/freezer_ui/backups/panel.py new file mode 100644 index 0000000..919a73c --- /dev/null +++ b/freezer_ui/backups/panel.py @@ -0,0 +1,25 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +import horizon_web_ui.freezer_ui.dashboard as dashboard + + +class BackupsPanel(horizon.Panel): + name = _("Backups") + slug = "backups" + + +dashboard.Freezer.register(BackupsPanel) diff --git a/freezer_ui/backups/tables.py b/freezer_ui/backups/tables.py new file mode 100644 index 0000000..6104ecc --- /dev/null +++ b/freezer_ui/backups/tables.py @@ -0,0 +1,100 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from django.core.urlresolvers import reverse +from django.utils import safestring +from django.utils.translation import ugettext_lazy as _ +from horizon.utils import functions as utils +from horizon import tables +from horizon_web_ui.freezer_ui.utils import timestamp_to_string + + +class Restore(tables.LinkAction): + name = "restore" + verbose_name = _("Restore") + classes = ("ajax-modal", "btn-launch") + ajax = True + + def get_link_url(self, datum=None): + return reverse("horizon:freezer_ui:backups:restore", + kwargs={'backup_id': datum.id}) + + +class BackupFilter(tables.FilterAction): + filter_type = "server" + filter_choices = (("before", "Created before", True), + ("after", "Created after", True), + ("between", "Created between", True), + ("contains", "Contains text", True)) + + +def icons(backup): + result = [] + + placeholder = '' + + level_txt = "Level: {} ({} backup) out of {}".format( + backup.level, "Full" if backup.level == 0 else "Incremental", + backup.max_level) + result.append( + '{}'.format( + level_txt, backup.level)) + + if backup.encrypted: + result.append( + '') + else: + result.append(placeholder) + + if int(backup.total_broken_links) > 0: + result.append( + ''.format(backup.total_broken_links)) + else: + result.append(placeholder) + + if backup.excluded_files: + result.append( + ''.format(len(backup.excluded_files))) + else: + result.append(placeholder) + + return safestring.mark_safe("".join(result)) + + +def backup_detail_view(backup): + return reverse("horizon:freezer_ui:backups:detail", + args=[backup.id]) + + +class BackupsTable(tables.DataTable): + name = tables.Column('backup_name', + verbose_name=_("Backup Name"), + link=backup_detail_view) + hostname = tables.Column('hostname', verbose_name=_("Hostname")) + created = tables.Column("time_stamp", + verbose_name=_("Created At"), + filters=[timestamp_to_string]) + icons = tables.Column(icons, verbose_name='Info') + + def get_pagination_string(self): + page_size = utils.get_page_size(self.request) + return "=".join(['offset', str(self.offset + page_size)]) + + class Meta: + name = "backups" + verbose_name = _("Backup History") + row_actions = (Restore,) + table_actions = (BackupFilter,) + multi_select = False diff --git a/freezer_ui/backups/templates/backups/detail.html b/freezer_ui/backups/templates/backups/detail.html new file mode 100644 index 0000000..1a5e67a --- /dev/null +++ b/freezer_ui/backups/templates/backups/detail.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% load i18n %} +{% block title %}{% trans "Backups" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Backups") %} +{% endblock page_header %} + +{% block main %} +
+
+
{{ data }}
+
+
+ +{% endblock %} diff --git a/freezer_ui/backups/templates/backups/index.html b/freezer_ui/backups/templates/backups/index.html new file mode 100644 index 0000000..bb2d9e4 --- /dev/null +++ b/freezer_ui/backups/templates/backups/index.html @@ -0,0 +1,17 @@ +{% extends 'base.html' %} + +{% block css %} + {% include "_stylesheets.html" %} + +{% endblock %} + +{% load i18n %} +{% block title %}{% trans "Backup History" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Backup History") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} \ No newline at end of file diff --git a/freezer_ui/backups/templates/backups/restore.html b/freezer_ui/backups/templates/backups/restore.html new file mode 100644 index 0000000..3bfaea8 --- /dev/null +++ b/freezer_ui/backups/templates/backups/restore.html @@ -0,0 +1,32 @@ + + + +
+
+ + + + + + + + + + + + + + + + +
Hostname
+
+
+
+ {% include "horizon/common/_form_fields.html" %} + {{ table.render }} +
+
+ {{ step.get_help_text }} +
+
diff --git a/freezer_ui/backups/urls.py b/freezer_ui/backups/urls.py new file mode 100644 index 0000000..f158836 --- /dev/null +++ b/freezer_ui/backups/urls.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from django.conf.urls import patterns +from django.conf.urls import url + +from horizon_web_ui.freezer_ui.backups import views + + +urlpatterns = patterns( + '', + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P[^/]*)$', views.DetailView.as_view(), name='detail'), + url(r'^restore/(?P.*)$', + views.RestoreView.as_view(), + name='restore'), +) diff --git a/freezer_ui/backups/views.py b/freezer_ui/backups/views.py new file mode 100644 index 0000000..eb17821 --- /dev/null +++ b/freezer_ui/backups/views.py @@ -0,0 +1,82 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import datetime +import pprint + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ +from django.template.defaultfilters import date as django_date +from django.views import generic + +from horizon import exceptions +from horizon import tables +from horizon import workflows + + +from horizon_web_ui.freezer_ui.backups import tables as freezer_tables +from horizon_web_ui.freezer_ui.backups.workflows import restore as restore_workflow +import horizon_web_ui.freezer_ui.api.api as freezer_api + + +class IndexView(tables.DataTableView): + name = _("Backups") + slug = "backups" + table_class = freezer_tables.BackupsTable + template_name = "freezer_ui/backups/index.html" + + def get_data(self): + backups, self._has_more = freezer_api.backups_list(self.request) + return backups + + +class DetailView(generic.TemplateView): + template_name = 'freezer_ui/backups/detail.html' + + def get_context_data(self, **kwargs): + + backup = freezer_api.backup_get(self.request, kwargs['backup_id']) + return {'data': pprint.pformat(backup.data_dict)} + + +class RestoreView(workflows.WorkflowView): + workflow_class = restore_workflow.Restore + + def get_object(self, *args, **kwargs): + id = self.kwargs['backup_id'] + try: + return freezer_api.backup_get(self.request, id) + except Exception: + redirect = reverse("horizon:freezer_ui:backups:index") + msg = _('Unable to retrieve details.') + exceptions.handle(self.request, msg, redirect=redirect) + + def is_update(self): + return 'name' in self.kwargs and bool(self.kwargs['name']) + + def get_workflow_name(self): + backup = freezer_api.backup_get(self.request, self.kwargs['backup_id']) + backup_date = datetime.datetime.fromtimestamp( + int(backup.data_dict[0]['backup_metadata']['time_stamp'])) + backup_date_str = django_date(backup_date, 'SHORT_DATETIME_FORMAT') + return "Restore '{}' from {}".format( + backup.data_dict[0]['backup_metadata']['backup_name'], backup_date_str) + + def get_initial(self): + return {"backup_id": self.kwargs['backup_id']} + + def get_workflow(self, *args, **kwargs): + workflow = super(RestoreView, self).get_workflow(*args, **kwargs) + workflow.name = self.get_workflow_name() + + return workflow + diff --git a/freezer_ui/backups/workflows/__init__.py b/freezer_ui/backups/workflows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezer_ui/backups/workflows/restore.py b/freezer_ui/backups/workflows/restore.py new file mode 100644 index 0000000..3c2d523 --- /dev/null +++ b/freezer_ui/backups/workflows/restore.py @@ -0,0 +1,98 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms +from horizon import exceptions +from horizon import workflows + +import horizon_web_ui.freezer_ui.api.api as freezer_api + + +class DestinationAction(workflows.MembershipAction): + path = forms.CharField(label=_("Destination Path"), + help_text=_("The path in which the backup should be " + "restored"), + required=True) + backup_id = forms.CharField(widget=forms.HiddenInput()) + + def clean(self): + if 'client' in self.request.POST: + self.cleaned_data['client'] = self.request.POST['client'] + else: + raise ValidationError(_('Client is required')) + + return self.cleaned_data + + class Meta(object): + name = _("Destination") + slug = "destination" + + +class Destination(workflows.Step): + template_name = 'freezer_ui/backups/restore.html' + action_class = DestinationAction + contributes = ('client', 'path', 'backup_id') + + def has_required_fields(self): + return True + + +class Restore(workflows.Workflow): + slug = "restore" + name = _("Restore") + finalize_button_name = _("Restore") + success_url = "horizon:freezer_ui:backups:index" + success_message = _("Restore job successfully queued. It will get " + "executed soon.") + wizard = False + default_steps = (Destination,) + + def handle(self, request, data): + try: + backup_id = data['backup_id'] + client_id = data['client'] + client = freezer_api.client_get(request, client_id) + backup = freezer_api.backup_get(request, backup_id) + name = "Restore job for {0}".format(client_id) + + # 1st step is to create a job + restore_job = { + "description": name, + "client_id": client_id, + "schedule_end_date": None, + "schedule_interval": None, + "schedule_start_date": None + } + job = freezer_api.job_create(request, restore_job) + + # 2nd step is to create an action for this job + restore_action = { + "original_name": job, # this is the job_id + "action": "restore", + "backup_name": + backup.data_dict[0]['backup_metadata']['backup_name'], + "restore_abs_path": data['path'], + "container": + backup.data_dict[0]['backup_metadata']['container'], + "restore_from_host": client.hostname, + "max_retries": 3, + "max_retries_interval": 60, + "mandatory": False + } + return freezer_api.action_create(request, restore_action) + except Exception: + exceptions.handle(request) + return False diff --git a/freezer_ui/dashboard.py b/freezer_ui/dashboard.py index b6206ff..ae26964 100644 --- a/freezer_ui/dashboard.py +++ b/freezer_ui/dashboard.py @@ -18,7 +18,7 @@ import horizon class FreezerDR(horizon.PanelGroup): slug = "freezerdr" name = _("Backup and Restore") - panels = ('jobs', 'sessions') + panels = ('jobs', 'sessions', 'backups') class Freezer(horizon.Dashboard): diff --git a/freezer_ui/django_utils.py b/freezer_ui/django_utils.py deleted file mode 100644 index 76fcc53..0000000 --- a/freezer_ui/django_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -import datetime -from django.template.defaultfilters import date as django_date - - -def timestamp_to_string(ts): - return django_date( - datetime.datetime.fromtimestamp(int(ts)), - 'SHORT_DATETIME_FORMAT') diff --git a/freezer_ui/jobs/tables.py b/freezer_ui/jobs/tables.py index 0faaa96..33ec6db 100644 --- a/freezer_ui/jobs/tables.py +++ b/freezer_ui/jobs/tables.py @@ -21,7 +21,7 @@ from horizon import tables from horizon.utils.urlresolvers import reverse import horizon_web_ui.freezer_ui.api.api as freezer_api -from horizon_web_ui.freezer_ui.django_utils import timestamp_to_string +from horizon_web_ui.freezer_ui.utils import timestamp_to_string def format_last_backup(last_backup): diff --git a/freezer_ui/jobs/templates/jobs/_action.html b/freezer_ui/jobs/templates/jobs/_action.html index 84a70e8..3fce807 100644 --- a/freezer_ui/jobs/templates/jobs/_action.html +++ b/freezer_ui/jobs/templates/jobs/_action.html @@ -3,5 +3,4 @@ {% block help_message %} {% endblock %} - diff --git a/freezer_ui/jobs/templates/jobs/_actions.html b/freezer_ui/jobs/templates/jobs/_actions.html index a4ef887..8423e83 100644 --- a/freezer_ui/jobs/templates/jobs/_actions.html +++ b/freezer_ui/jobs/templates/jobs/_actions.html @@ -42,4 +42,4 @@ {{ step.get_help_text }} - \ No newline at end of file + diff --git a/freezer_ui/jobs/templates/jobs/_advanced.html b/freezer_ui/jobs/templates/jobs/_advanced.html index b514a7f..f34edd6 100644 --- a/freezer_ui/jobs/templates/jobs/_advanced.html +++ b/freezer_ui/jobs/templates/jobs/_advanced.html @@ -3,5 +3,4 @@ {% block help_message %} {% endblock %} - \ No newline at end of file diff --git a/freezer_ui/jobs/templates/jobs/_snapshot.html b/freezer_ui/jobs/templates/jobs/_snapshot.html index 36c0a71..5b83b07 100644 --- a/freezer_ui/jobs/templates/jobs/_snapshot.html +++ b/freezer_ui/jobs/templates/jobs/_snapshot.html @@ -3,5 +3,4 @@ {% block help_message %} {% endblock %} - \ No newline at end of file diff --git a/freezer_ui/jobs/templates/jobs/scheduling.html b/freezer_ui/jobs/templates/jobs/scheduling.html deleted file mode 100644 index b2f15cc..0000000 --- a/freezer_ui/jobs/templates/jobs/scheduling.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load i18n horizon humanize %} - -{% block help_message %} -

{% blocktrans %}Start and End Date Time{% endblocktrans %}

-

{% blocktrans %}Set a start date and time to execute jobs in ISO format:{% endblocktrans %}

-
    -
  • YYYY-MM-DDThh:mm:ss
  • -
- -

{% blocktrans %}Interval{% endblocktrans %}

-

{% blocktrans %}Set the interval in the following format:{% endblocktrans %}

-
    -
  • continuous
  • -
  • N weeks
  • -
  • N days
  • -
  • N hours
  • -
  • N minutes
  • -
  • N seconds
  • -
- -{% endblock %} \ No newline at end of file diff --git a/freezer_ui/sessions/tables.py b/freezer_ui/sessions/tables.py index c454169..812ce4b 100644 --- a/freezer_ui/sessions/tables.py +++ b/freezer_ui/sessions/tables.py @@ -10,18 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime -from django import shortcuts -from django.utils import safestring + from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy -from horizon import messages from horizon import tables from horizon.utils.urlresolvers import reverse import horizon_web_ui.freezer_ui.api.api as freezer_api -from horizon_web_ui.freezer_ui.django_utils import timestamp_to_string def get_link(session): diff --git a/freezer_ui/static/freezer/js/freezer.js b/freezer_ui/static/freezer/js/freezer.js index 2006fa6..c379d41 100644 --- a/freezer_ui/static/freezer/js/freezer.js +++ b/freezer_ui/static/freezer/js/freezer.js @@ -1,16 +1,14 @@ (function () { - 'use strict'; - angular.module('hz').controller('DestinationCtrl', function ($scope, $http, $location) { - $scope.query = ''; - - $http.get($location.protocol() + "://" + $location.host() + ":" + $location.port() + "/freezer_ui/api/clients"). - success(function(data, status, headers, config) { - $scope.clients = data - }); - $scope.searchComparator = function (actual, expected) { - return actual.description.indexOf(expected) > 0 - }; - }); - + 'use strict'; + angular.module('hz').controller('DestinationCtrl', function ($scope, $http, $location) { + $scope.query = ''; + $http.get($location.protocol() + "://" + $location.host() + ":" + $location.port() + "/freezer_ui/api/clients"). + success(function(data, status, headers, config) { + $scope.clients = data + }); + $scope.searchComparator = function (actual, expected) { + return actual.description.indexOf(expected) > 0 + }; + }); }()); diff --git a/freezer_ui/static/freezer/js/freezer.restore.js b/freezer_ui/static/freezer/js/freezer.restore.js new file mode 100644 index 0000000..d282958 --- /dev/null +++ b/freezer_ui/static/freezer/js/freezer.restore.js @@ -0,0 +1,26 @@ +var url = $(location).attr("origin"); +url += '/freezer_ui/api/clients'; + +$.ajax({ + url: url, + type: "GET", + cache: false, + dataType: 'json', + contentType: 'application/json; charset=utf-8' , + success: function(data) { + $.each(data, function(index, item) { + $("#available_clients").append( + '' + + '' + + '' + item["client"]["hostname"] + '' + ); + }); + }, + error: function(request, error) { + console.error(error); + $("#available_clients").append( + 'Error getting client list' + ); + } +}); + diff --git a/freezer_ui/utils.py b/freezer_ui/utils.py index 0b25b79..d7e35d6 100644 --- a/freezer_ui/utils.py +++ b/freezer_ui/utils.py @@ -13,6 +13,10 @@ import uuid +import datetime +from django.template.defaultfilters import date as django_date + + def create_dict_action(**kwargs): """Create a dict only with values that exists so we avoid send keys with None values @@ -20,8 +24,92 @@ def create_dict_action(**kwargs): return {k: v for k, v in kwargs.items() if v} +def timestamp_to_string(ts): + return django_date( + datetime.datetime.fromtimestamp(int(ts)), + 'SHORT_DATETIME_FORMAT') + + +class Dict2Object(object): + """Makes dictionary fields accessible as if they are attributes. + + The dictionary keys become class attributes. It is possible to use one + nested dictionary by overwriting nested_dict with the key of that nested + dict. + + This class is needed because we mostly deal with objects in horizon (e.g. + for providing data to the tables) but the api only gives us json data. + """ + nested_dict = None + + def __init__(self, data_dict): + self.data_dict = data_dict + + def __getattr__(self, attr): + """Make data_dict fields available via class interface """ + if attr in self.data_dict: + return self.data_dict[attr] + elif attr in self.data_dict[self.nested_dict]: + return self.data_dict[self.nested_dict][attr] + else: + return object.__getattribute__(self, attr) + + def get_dict(self): + return self.data_dict + + +class Action(Dict2Object): + nested_dict = 'job_action' + + @property + def id(self): + return self.job_id + + +class Job(Dict2Object): + nested_dict = 'job_actions' + + @property + def id(self): + return self.job_id + + +class Backup(Dict2Object): + nested_dict = 'backup_metadata' + + @property + def id(self): + return self.backup_id + + +class Client(object): + def __init__(self, client, hostname): + self.client = client + self.hostname = hostname + + +class ActionJob(object): + def __init__(self, job_id, action_id, action, backup_name): + self.job_id = job_id + self.action_id = action_id + self.action = action + self.backup_name = backup_name + + +class Session(object): + def __init__(self, session_id, description, status, jobs, + start_datetime, interval, end_datetime): + self.session_id = session_id + self.description = description + self.status = status + self.jobs = jobs + self.start_datetime = start_datetime + self.interval = interval + self.end_datetime = end_datetime + + class SessionJob(object): - """Create a session object """ + """Create a job object to work with in horizon""" def __init__(self, job_id, session_id, client_id, status): self.job_id = job_id self.session_id = session_id @@ -29,6 +117,17 @@ class SessionJob(object): self.status = status +class JobList(object): + """Create an object to be passed to horizon tables that handles + nested values + """ + def __init__(self, description, result, job_id): + self.description = description + self.result = result + self.id = job_id + self.job_id = job_id + + def create_dummy_id(): """Generate a dummy id for documents generated by the scheduler.