Backup history.
Enable a list of backups that you can restore immediately from the ui Change-Id: I8f9bdf85b4f476ea3af9e7c12f50a86d173a999f
This commit is contained in:
@@ -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
|
||||
|
@@ -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")
|
||||
|
||||
|
0
freezer_ui/backups/__init__.py
Normal file
0
freezer_ui/backups/__init__.py
Normal file
3
freezer_ui/backups/models.py
Normal file
3
freezer_ui/backups/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
25
freezer_ui/backups/panel.py
Normal file
25
freezer_ui/backups/panel.py
Normal file
@@ -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)
|
100
freezer_ui/backups/tables.py
Normal file
100
freezer_ui/backups/tables.py
Normal file
@@ -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 = '<i class="fa fa-fw"></i>'
|
||||
|
||||
level_txt = "Level: {} ({} backup) out of {}".format(
|
||||
backup.level, "Full" if backup.level == 0 else "Incremental",
|
||||
backup.max_level)
|
||||
result.append(
|
||||
'<i class="fa fa-fw fa-custom-number" title="{}">{}</i>'.format(
|
||||
level_txt, backup.level))
|
||||
|
||||
if backup.encrypted:
|
||||
result.append(
|
||||
'<i class="fa fa-lock fa-fw" title="Backup is encrypted"></i>')
|
||||
else:
|
||||
result.append(placeholder)
|
||||
|
||||
if int(backup.total_broken_links) > 0:
|
||||
result.append(
|
||||
'<i class="fa fa-chain-broken fa-fw" title="There are {} broken '
|
||||
'links in this backup"></i>'.format(backup.total_broken_links))
|
||||
else:
|
||||
result.append(placeholder)
|
||||
|
||||
if backup.excluded_files:
|
||||
result.append(
|
||||
'<i class="fa fa-minus-square fa-fw" title="{} files have been exc'
|
||||
'luded from this backup"></i>'.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
|
17
freezer_ui/backups/templates/backups/detail.html
Normal file
17
freezer_ui/backups/templates/backups/detail.html
Normal file
@@ -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 %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<pre>{{ data }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
17
freezer_ui/backups/templates/backups/index.html
Normal file
17
freezer_ui/backups/templates/backups/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block css %}
|
||||
{% include "_stylesheets.html" %}
|
||||
<link href='{{ STATIC_URL }}freezer/css/freezer.css' type='text/css' media='screen' rel='stylesheet' />
|
||||
{% 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 %}
|
32
freezer_ui/backups/templates/backups/restore.html
Normal file
32
freezer_ui/backups/templates/backups/restore.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.restore.js'></script>
|
||||
|
||||
<noscript><h3>{{ step }}</h3></noscript>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
|
||||
<tr>
|
||||
<th class="multi_select_column"></th>
|
||||
<th>Hostname</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="available_clients">
|
||||
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="3" data-column="0">
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
{% include "horizon/common/_form_fields.html" %}
|
||||
{{ table.render }}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
{{ step.get_help_text }}
|
||||
</div>
|
||||
</div>
|
27
freezer_ui/backups/urls.py
Normal file
27
freezer_ui/backups/urls.py
Normal file
@@ -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<backup_id>[^/]*)$', views.DetailView.as_view(), name='detail'),
|
||||
url(r'^restore/(?P<backup_id>.*)$',
|
||||
views.RestoreView.as_view(),
|
||||
name='restore'),
|
||||
)
|
82
freezer_ui/backups/views.py
Normal file
82
freezer_ui/backups/views.py
Normal file
@@ -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
|
||||
|
0
freezer_ui/backups/workflows/__init__.py
Normal file
0
freezer_ui/backups/workflows/__init__.py
Normal file
98
freezer_ui/backups/workflows/restore.py
Normal file
98
freezer_ui/backups/workflows/restore.py
Normal file
@@ -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
|
@@ -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):
|
||||
|
@@ -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')
|
@@ -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):
|
||||
|
@@ -3,5 +3,4 @@
|
||||
{% block help_message %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- Jquery code to hide freezer inputs -->
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.actions.action.js'></script>
|
||||
|
@@ -42,4 +42,4 @@
|
||||
{{ step.get_help_text }}
|
||||
</div>
|
||||
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.jobs.sortable.js'></script>
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.jobs.sortable.js'></script>
|
||||
|
@@ -3,5 +3,4 @@
|
||||
{% block help_message %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- Jquery code to hide freezer inputs -->
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.actions.advanced.js'></script>
|
@@ -3,5 +3,4 @@
|
||||
{% block help_message %}
|
||||
{% endblock %}
|
||||
|
||||
<!-- Jquery code to hide freezer inputs -->
|
||||
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/freezer.actions.snapshot.js'></script>
|
@@ -1,21 +0,0 @@
|
||||
{% load i18n horizon humanize %}
|
||||
|
||||
{% block help_message %}
|
||||
<h4>{% blocktrans %}Start and End Date Time{% endblocktrans %}</h4>
|
||||
<p>{% blocktrans %}Set a start date and time to execute jobs in ISO format:{% endblocktrans %}</p>
|
||||
<ul>
|
||||
<li>YYYY-MM-DDThh:mm:ss</li>
|
||||
</ul>
|
||||
|
||||
<h4>{% blocktrans %}Interval{% endblocktrans %}</h4>
|
||||
<p>{% blocktrans %}Set the interval in the following format:{% endblocktrans %}</p>
|
||||
<ul>
|
||||
<li>continuous</li>
|
||||
<li>N weeks</li>
|
||||
<li>N days</li>
|
||||
<li>N hours</li>
|
||||
<li>N minutes</li>
|
||||
<li>N seconds</li>
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
@@ -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):
|
||||
|
@@ -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
|
||||
};
|
||||
});
|
||||
}());
|
||||
|
26
freezer_ui/static/freezer/js/freezer.restore.js
Normal file
26
freezer_ui/static/freezer/js/freezer.restore.js
Normal file
@@ -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(
|
||||
'<tr><td class="multi_select_column">' +
|
||||
'<input type="radio" name="client" value=' + item["client"]["client_id"] + '></td>' +
|
||||
'<td>' + item["client"]["hostname"] + '</td></tr>'
|
||||
);
|
||||
});
|
||||
},
|
||||
error: function(request, error) {
|
||||
console.error(error);
|
||||
$("#available_clients").append(
|
||||
'<tr><td>Error getting client list</td></tr>'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@@ -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.
|
||||
|
||||
|
Reference in New Issue
Block a user