Backup history.

Enable a list of backups that you can restore immediately from the ui

Change-Id: I8f9bdf85b4f476ea3af9e7c12f50a86d173a999f
This commit is contained in:
Memo Garcia
2015-07-28 13:31:49 +01:00
parent a9e52f2ffe
commit 7e1ba92a87
25 changed files with 670 additions and 194 deletions

View File

@@ -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

View File

@@ -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")

View File

View File

@@ -0,0 +1,3 @@
"""
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
"""

View 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)

View 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

View 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 %}

View 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 %}

View 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>

View 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'),
)

View 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

View File

View 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

View File

@@ -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):

View File

@@ -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')

View File

@@ -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):

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 %}

View File

@@ -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):

View File

@@ -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
};
});
}());

View 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>'
);
}
});

View File

@@ -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.