This version contains the following pages:

- Overview displays charts/reports (Currently only placeholders)
 - Configurations allows to define new backup configurations and link them to
   instances
 - Backups shows a list of all succesful backups and allows to restore them
 - Restores shows a history of all restored backups

It communicates with the api server via the client apis (freeezer.client.client).
The "glue code" between client apis and the UI lives in freezer.api.api.

Implements: Blueprint freezer-api-web-ui
Change-Id: I48cd8cba2b0169c6e64f650233c1a31b91ced34f
This commit is contained in:
Jonas Pfannschmidt 2015-03-06 12:18:05 +00:00 committed by Memo Garcia
parent b2c7faa37d
commit 008769587b
71 changed files with 2508 additions and 472 deletions

View File

@ -1,26 +1,59 @@
========================
Freezer - Horizon Web UI
=======================
========================
Freezer now has basic support for a Web UI integrated with OpenStack Horizon.
Installation
============
In the installation procedure we'll assume your main Horizon dashboard
directory is /opt/stack/horizon/openstack_dashboard/dashboards/.
To install the horizon web ui you need to do the following::
# git clone https://github.com/stackforge/freezer
# cd freezer/horizon_web_ui
# cp -r freezer /opt/stack/horizon/openstack_dashboard/dashboards/
# cp _50_freezer.py /opt/stack/horizon/openstack_dashboard/local/enabled/
# modify _50_freezer.py (line 9) and point the path to the freezer repo.
# /opt/stack/horizon/tools/with_venv.sh pip install parsedatetime
# In horizons local_settings.py add the variable FREEZER_API_URL and set it
to the url of the freezer api server. Example:
FREEZER_API_URL = 'http://127.0.0.1:9090'
# cp _50_freezer.py /opt/stack/horizon/openstack_dashboard/enabled/
# cd /opt/stack/horizon/
# ./run_tests.sh --runserver 0.0.0.0:8878
# ./run_tests.sh --runserver 0.0.0.0:8000
Now a new Tab is available in the dashboard lists on the left,
called "Backup as a Service" and a sub tab called "Freezer".
Now a new Tab is available in the dashboard lists on the left, called "Backup
Restore DR".
Running the unit tests
======================
1. Create a virtual environment:
virtualenv --no-site-packages -p /usr/bin/python2.7 .venv
2. Activate the virtual environment:
. ./.venv/bin/activate
3. Install the requirements:
pip install -r test-requirements.txt
4. Run the tests:
python manage.py test . --settings=freezer_ui.tests.settings
Test coverage
-------------
1. Collect coverage information:
coverage run --source='.' --omit='.venv/*' manage.py test . --settings=freezer_ui.tests.settings
2. View coverage report:
coverage report

View File

@ -1,10 +1,16 @@
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
DASHBOARD = 'freezer'
DASHBOARD = 'freezer_ui'
# If set to True, this dashboard will not be added to the settings.
DISABLED = False
# Until there is a more elegant SYSPATH var scheme...
import sys
sys.path.append('/opt/stack/freezer')
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = [
'openstack_dashboard.dashboards.freezer',
'horizon_web_ui.freezer_ui',
]
ADD_JS_FILES = ['freezer/js/freezer.js']

1
__init__.py Normal file
View File

@ -0,0 +1 @@
__author__ = 'jonas'

View File

@ -1,19 +0,0 @@
from django.utils.translation import ugettext_lazy as _
import horizon
class Mygroup(horizon.PanelGroup):
slug = "mygroup"
name = _("Freezer")
panels = ('freezerpanel',)
class Freezer(horizon.Dashboard):
name = _("Backup-aaS")
slug = "freezer"
panels = (Mygroup,) # Add your panels here.
default_panel = 'freezerpanel' # Specify the slug of the default panel.
horizon.register(Freezer)

View File

@ -1,13 +0,0 @@
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.dashboards.freezer import dashboard
class Freezerpanel(horizon.Panel):
name = _("Admin")
slug = "freezerpanel"
dashboard.Freezer.register(Freezerpanel)

View File

@ -1,220 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon.utils.urlresolvers import reverse # noqa
from openstack_dashboard import api
from django.template import defaultfilters as filters
from django.utils import http
from django.utils import safestring
from django import template
from horizon import tables
LOADING_IMAGE = safestring.mark_safe('<img src="/static/dashboard/img/loading.gif" />')
def get_metadata(container):
# If the metadata has not been loading, display a loading image
if not get_metadata_loaded(container):
return LOADING_IMAGE
template_name = 'freezer/freezerpanel/_container_metadata.html'
context = {"container": container}
return template.loader.render_to_string(template_name, context)
def wrap_delimiter(name):
if name and not name.endswith(api.swift.FOLDER_DELIMITER):
return name + api.swift.FOLDER_DELIMITER
return name
def get_container_link(container):
return reverse("horizon:freezer:freezerpanel:index",
args=(wrap_delimiter(container.name),))
def get_metadata_loaded(container):
# Determine if metadata has been loaded if the attribute is already set.
return hasattr(container, 'is_public') and container.is_public is not None
class ContainerAjaxUpdateRow(tables.Row):
ajax = True
def get_data(self, request, container_name):
container = api.swift.swift_get_container(request,
container_name,
with_data=False)
return container
class ContainersTable(tables.DataTable):
METADATA_LOADED_CHOICES = (
(False, None),
(True, True),
)
name = tables.Column("name", link=get_container_link,
verbose_name=_("Name"))
bytes = tables.Column(lambda x: x.container_bytes_used if get_metadata_loaded(x) else LOADING_IMAGE
, verbose_name=_("Size"))
count = tables.Column(lambda x: x.container_object_count if get_metadata_loaded(x) else LOADING_IMAGE
, verbose_name=_("Object count"))
metadata = tables.Column(get_metadata,
verbose_name=_("Container Details"),
classes=('nowrap-col', ),)
metadata_loaded = tables.Column(get_metadata_loaded,
status=True,
status_choices=METADATA_LOADED_CHOICES,
hidden=True)
def get_object_id(self, container):
return container.name
def get_absolute_url(self):
url = super(ContainersTable, self).get_absolute_url()
return http.urlquote(url)
def get_full_url(self):
"""Returns the encoded absolute URL path with its query string."""
url = super(ContainersTable, self).get_full_url()
return http.urlquote(url)
class Meta:
name = "containers"
verbose_name = _("Backups")
row_class = ContainerAjaxUpdateRow
status_columns = ['metadata_loaded', ]
class ObjectFilterAction(tables.FilterAction):
def _filtered_data(self, table, filter_string):
request = table.request
container = self.table.kwargs['container_name']
subfolder = self.table.kwargs['subfolder_path']
prefix = wrap_delimiter(subfolder) if subfolder else ''
self.filtered_data = api.swift.swift_filter_objects(request,
filter_string,
container,
prefix=prefix)
return self.filtered_data
def filter_subfolders_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type == "application/pseudo-folder"]
def filter_objects_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type != "application/pseudo-folder"]
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
def get_link_subfolder(subfolder):
container_name = subfolder.container_name
return reverse("horizon:freezer:freezerpanel:index",
args=(wrap_delimiter(container_name),
wrap_delimiter(subfolder.name)))
def sanitize_name(name):
return name.split(api.swift.FOLDER_DELIMITER)[-1]
def get_size(obj):
if obj.bytes is None:
return _("pseudo-folder")
return filters.filesizeformat(obj.bytes)
class DeleteObject(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Object",
u"Delete Objects",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Object",
u"Deleted Objects",
count
)
name = "delete_object"
allowed_data_types = ("objects", "subfolders",)
def delete(self, request, obj_id):
obj = self.table.get_object_by_id(obj_id)
container_name = obj.container_name
datum_type = getattr(obj, self.table._meta.data_type_name, None)
if datum_type == 'subfolders':
obj_id = obj_id[(len(container_name) + 1):] + "/"
api.swift.swift_delete_object(request, container_name, obj_id)
def get_success_url(self, request):
url = super(DeleteObject, self).get_success_url(request)
return http.urlquote(url)
class DeleteMultipleObjects(DeleteObject):
name = "delete_multiple_objects"
class CreatePseudoFolder(tables.FilterAction):
def _filtered_data(self, table, filter_string):
request = table.request
container = self.table.kwargs['container_name']
subfolder = self.table.kwargs['subfolder_path']
prefix = wrap_delimiter(subfolder) if subfolder else ''
self.filtered_data = api.swift.swift_filter_objects(request,
filter_string,
container,
prefix=prefix)
return self.filtered_data
def filter_subfolders_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type == "application/pseudo-folder"]
def filter_objects_data(self, table, objects, filter_string):
data = self._filtered_data(table, filter_string)
return [datum for datum in data if
datum.content_type != "application/pseudo-folder"]
def allowed(self, request, datum=None):
if self.table.kwargs.get('container_name', None):
return True
return False
class ObjectsTable(tables.DataTable):
name = tables.Column("name",
link=get_link_subfolder,
allowed_data_types=("subfolders",),
verbose_name=_("Object Name"),
filters=(sanitize_name,))
size = tables.Column(get_size, verbose_name=_('Size'))
class Meta:
name = "objects"
verbose_name = _("Objects")
table_actions = (ObjectFilterAction,
DeleteMultipleObjects)
data_types = ("subfolders", "objects")
browser_table = "content"
footer = False
def get_absolute_url(self):
url = super(ObjectsTable, self).get_absolute_url()
return http.urlquote(url)
def get_full_url(self):
"""Returns the encoded absolute URL path with its query string.
This is used for the POST action attribute on the form element
wrapping the table. We use this method to persist the
pagination marker.
"""
url = super(ObjectsTable, self).get_full_url()
return http.urlquote(url)

View File

@ -1,37 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.dashboards.freezer.freezerpanel import tables
class ContainerTab(tabs.TableTab):
name = _("Backups Tab")
slug = "instances_tab"
table_classes = (tables.ContainersTable,)
template_name = ("horizon/common/_detail_table.html")
preload = False
def has_more_data(self, table):
return self._has_more
def get_containers_data(self):
try:
marker = self.request.GET.get(
tables.ContainersTable._meta.pagination_param, None)
containers, self._has_more = api.swift.swift_get_containers(self.request, marker)
print '{}'.format(containers)
return containers
except Exception:
self._has_more = False
error_message = _('Unable to get instances')
exceptions.handle(self.request, error_message)
return []
class MypanelTabs(tabs.TabGroup):
slug = "mypanel_tabs"
tabs = (ContainerTab,)
sticky = True

View File

@ -1,12 +0,0 @@
{% load i18n %}
<ul>
<li>{% trans "Object Count: " %}{{ container.container_object_count }}</li>
<li>{% trans "Size: " %}{{ container.container_bytes_used|filesizeformat }}</li>
<li>{% trans "Access: " %}
{% if container.public_url %}
<a href="{{ container.public_url }}">{% trans "Public" %}</a>
{% else %}
{% trans "Private" %}
{% endif %}
</li>
</ul>

View File

@ -1,15 +0,0 @@
{% extends 'freezer/base.html' %}
{% load i18n %}
{% block title %}{% trans "Backups" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Freezer") %}
{% endblock page_header %}
{% block mydashboard_main %}
<div class="row">
<div class="col-sm-12">
{{ swift_browser.render }}
</div>
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends 'freezer/base.html' %}
{% load i18n %}
{% block title %}{% trans "Backups" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Freezer") %}
{% endblock page_header %}
{% block mydashboard_main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -1,7 +0,0 @@
from horizon.test import helpers as test
class MypanelTests(test.TestCase):
# Unit tests for mypanel.
def test_me(self):
self.assertTrue(1 + 1 == 2)

View File

@ -1,15 +0,0 @@
from django.conf.urls import patterns
from django.conf.urls import url
from openstack_dashboard.dashboards.freezer.freezerpanel import views
urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^((?P<container_name>.+?)/)?(?P<subfolder_path>(.+/)+)?$',
views.BackupView.as_view(), name='index'),
url(r'^\?tab=mypanel_tabs_tab$',
views.IndexView.as_view(), name='mypanel_tabs'),
)

View File

@ -1,98 +0,0 @@
from horizon import browsers
from horizon import tabs
from horizon import exceptions
from django.utils.translation import ugettext_lazy as _
from django.utils.functional import cached_property # noqa
from openstack_dashboard import api
from openstack_dashboard.dashboards.freezer.freezerpanel \
import tabs as freezer_tabs
from openstack_dashboard.dashboards.freezer.freezerpanel \
import browsers as freezer_browsers
class IndexView(tabs.TabbedTableView):
tab_group_class = freezer_tabs.MypanelTabs
template_name = 'freezer/freezerpanel/index.html'
def get_data(self, request, context, *args, **kwargs):
# Add data to the context here...
return context
class BackupView(browsers.ResourceBrowserView):
browser_class = freezer_browsers.ContainerBrowser
template_name = "freezer/freezerpanel/container.html"
def get_containers_data(self):
containers = []
self._more = None
marker = self.request.GET.get('marker', None)
try:
containers, self._more = api.swift.swift_get_containers(
self.request, marker=marker)
except Exception:
msg = _('Unable to retrieve container list.')
exceptions.handle(self.request, msg)
return containers
@cached_property
def objects(self):
"""Returns a list of objects given the subfolder's path.
The path is from the kwargs of the request.
"""
objects = []
self._more = None
marker = self.request.GET.get('marker', None)
container_name = self.kwargs['container_name']
subfolder = self.kwargs['subfolder_path']
prefix = None
if container_name:
self.navigation_selection = True
if subfolder:
prefix = subfolder
try:
objects, self._more = api.swift.swift_get_objects(
self.request,
container_name,
marker=marker,
prefix=prefix)
except Exception:
self._more = None
objects = []
msg = _('Unable to retrieve object list.')
exceptions.handle(self.request, msg)
return objects
def is_subdir(self, item):
content_type = "application/pseudo-folder"
return getattr(item, "content_type", None) == content_type
def is_placeholder(self, item):
object_name = getattr(item, "name", "")
return object_name.endswith(api.swift.FOLDER_DELIMITER)
def get_objects_data(self):
"""Returns a list of objects within the current folder."""
filtered_objects = [item for item in self.objects
if (not self.is_subdir(item) and
not self.is_placeholder(item))]
return filtered_objects
def get_subfolders_data(self):
"""Returns a list of subfolders within the current folder."""
filtered_objects = [item for item in self.objects
if self.is_subdir(item)]
return filtered_objects
def get_context_data(self, **kwargs):
context = super(BackupView, self).get_context_data(**kwargs)
context['container_name'] = self.kwargs["container_name"]
context['subfolders'] = []
if self.kwargs["subfolder_path"]:
(parent, slash, folder) = self.kwargs["subfolder_path"] \
.strip('/').rpartition('/')
while folder:
path = "%s%s%s/" % (parent, slash, folder)
context['subfolders'].insert(0, (folder, path))
(parent, slash, folder) = parent.rpartition('/')
return context

View File

@ -1 +0,0 @@
/* Additional CSS for mydashboard. */

View File

@ -1 +0,0 @@
/* Additional JavaScript for mydashboard. */

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
from horizon_web_ui.freezer_ui import dashboard
class ActionsPanel(horizon.Panel):
name = _("Jobs")
slug = "actions"
dashboard.Freezer.register(ActionsPanel)

View File

@ -0,0 +1,73 @@
# 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 _
from horizon import tables
from horizon.utils import functions as utils
from horizon_web_ui.freezer_ui.django_utils import timestamp_to_string
class ActionsTable(tables.DataTable):
METADATA_LOADED_CHOICES = (
(False, None),
(True, True),
)
STATUS_DISPLAY = (
('pending', 'Pending'),
('started', 'Started'),
('abort_req', 'Abort Requested'),
('aborting', 'Aborting'),
('aborted', 'Aborted'),
('success', 'Success'),
('fail', 'Failed')
)
TYPE_DISPLAY = (
('restore', 'Restore'),
('backup', 'Backup (Unscheduled)')
)
client_id = tables.Column("client_id", verbose_name=_("Client Id"))
type = tables.Column('action', verbose_name=_("Type"),
display_choices=TYPE_DISPLAY)
description = tables.Column("description", verbose_name=_("Description"))
status = tables.Column('status',
verbose_name=_("Status"),
display_choices=STATUS_DISPLAY)
created = tables.Column('time_created', verbose_name=_("Created"),
filters=(timestamp_to_string,))
started = tables.Column('time_started', verbose_name=_("Started"),
filters=(timestamp_to_string,))
ended = tables.Column('time_ended', verbose_name=_("Ended"),
filters=(timestamp_to_string,))
def get_object_id(self, action):
return action.id
def __init__(self, *args, **kwargs):
super(ActionsTable, self).__init__(*args, **kwargs)
if 'offset' in self.request.GET:
self.offset = self.request.GET['offset']
else:
self.offset = 0
def get_pagination_string(self):
page_size = utils.get_page_size(self.request)
return "=".join(['offset', str(self.offset + page_size)])
class Meta(object):
name = "jobs"
verbose_name = _("Jobs")

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "VMs" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Jobs") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -0,0 +1,22 @@
# 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.actions import views
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
)

View File

@ -0,0 +1,34 @@
# 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 _
from horizon import tables
from horizon_web_ui.freezer_ui.actions import tables as actions_tables
from horizon_web_ui.freezer_ui.api import api as freezer_api
class IndexView(tables.DataTableView):
name = _("Jobs")
slug = "actions"
table_class = actions_tables.ActionsTable
template_name = ("freezer_ui/actions/index.html")
def has_more_data(self, table):
return self._has_more
def get_data(self):
backups, self._has_more = freezer_api.actions_list(
self.request,
offset=self.table.offset)
return backups

View File

@ -0,0 +1 @@
__author__ = 'jonas'

293
freezer_ui/api/api.py Normal file
View File

@ -0,0 +1,293 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Some helper functions to use the freezer_ui client functionality
from horizon.
"""
from django.conf import settings
import warnings
import freezer.apiclient.client
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized # noqa
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'
@property
def id(self):
return self.action_id
class Configuration(Dict2Object):
nested_dict = 'config_file'
@property
def id(self):
return self.config_id
class Backup(Dict2Object):
nested_dict = 'backup_metadata'
@property
def id(self):
return self.backup_id
class Client(Dict2Object):
nested_dict = 'client'
@property
def id(self):
return self.client_id
@property
def name(self):
return self.client_id
class ConfigClient(object):
def __init__(self, name, last_backup):
self.id = name
self.name = name
self.last_backup = last_backup
@memoized
def _freezerclient(request):
warnings.warn('Endpoint discovery not implemented yet. Using hard-coded: {'
'}'.format(settings.FREEZER_API_URL))
return freezer.apiclient.client.Client(
token=request.user.token.id,
auth_url=getattr(settings, 'OPENSTACK_KEYSTONE_URL'),
endpoint=settings.FREEZER_API_URL)
def configuration_create(request, name=None, container_name=None,
src_file=None, levels=None, optimize=None,
compression=None, encryption_password=None,
clients=[], start_datetime=None, interval=None,
exclude=None, log_file=None, proxy=None,
max_priority=False):
"""Create a new configuration file """
data = {
"name": name,
"container_name": container_name,
"src_file": src_file,
"levels": levels,
"optimize": optimize,
"compression": compression,
"encryption_password": encryption_password,
"clients": clients,
"start_datetime": start_datetime,
"interval": interval,
"exclude": exclude,
"log_file": log_file,
"proxy": proxy,
"max_priority": max_priority
}
return _freezerclient(request).configs.create(data)
def configuration_update(request, config_id=None, name=None,
src_file=None, levels=None, optimize=None,
compression=None, encryption_password=None,
clients=[], start_datetime=None, interval=None,
exclude=None, log_file=None, proxy=None,
max_priority=False, container_name=None,):
"""Update a new configuration file """
data = {
"name": name,
"container_name": container_name,
"src_file": src_file,
"levels": levels,
"optimize": optimize,
"compression": compression,
"encryption_password": encryption_password,
"clients": clients,
"start_datetime": start_datetime,
"interval": interval,
"exclude": exclude,
"log_file": log_file,
"proxy": proxy,
"max_priority": max_priority
}
return _freezerclient(request).configs.update(config_id, data)
def configuration_delete(request, obj_id):
return _freezerclient(request).configs.delete(obj_id)
def configuration_clone(request, config_id):
config_file = _freezerclient(request).configs.get(config_id)
data = config_file[0]['config_file']
data['name'] = '{0}_clone'.format(data['name'])
return _freezerclient(request).configs.create(data)
def configuration_get(request, config_id):
config_file = _freezerclient(request).configs.get(config_id)
if config_file:
return [Configuration(data) for data in config_file]
return []
def configuration_list(request):
configurations = _freezerclient(request).configs.list()
configurations = [Configuration(data) for data in configurations]
return configurations
def clients_in_config(request, config_id):
configuration = configuration_get(request, config_id)
clients = []
last_backup = None
clients_dict = [c.get_dict() for c in configuration]
for client in clients_dict:
for client_id in client['config_file']['clients']:
backups, has_more = backups_list(request, text_match=client_id)
backups = [Backup(data) for data in backups]
backups = [b.get_dict() for b in backups]
for backup in backups:
last_backup = backup.data_dict['backup_metadata']['timestamp']
clients.append(ConfigClient(client_id, last_backup))
return clients
def client_list(request, limit=20):
clients = _freezerclient(request).registration.list(limit=limit)
clients = [Client(client) for client in clients]
return clients
def backups_list(request, offset=0, time_after=None, time_before=None,
text_match=None):
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):
data = _freezerclient(request).backups.get(backup_id)
if data:
return Backup(data[0])
def restore_action_create(request,
backup_id,
destination_client_id,
destination_path,
description=None,
dry_run=False,
max_prio=False):
c = _freezerclient(request)
backup = c.backups.get(backup_id)[0]
action = {
"job": {
"action": "restore",
"container_name": backup['backup_metadata']['container'],
"restore-abs-path": destination_path,
"backup-name": backup['backup_metadata']['backup_name'],
"restore-from-host": backup['backup_metadata']['host_name'],
"max_cpu_priority": max_prio,
"dry_run": dry_run
},
"description": description,
"client_id": destination_client_id
}
c.actions.create(action)
def actions_list(request, offset=0):
page_size = utils.get_page_size(request)
actions = _freezerclient(request).actions.list(
limit=page_size + 1,
offset=offset)
if len(actions) > page_size:
actions.pop()
has_more = True
else:
has_more = False
# Wrap data in object for easier handling
actions = [Action(data['action']) for data in actions]
return actions, has_more

View File

View File

@ -0,0 +1,37 @@
import functools
from django.views import generic
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.api.rest.utils import JSONResponse
import horizon_web_ui.freezer_ui.api.api as freezer_api
# https://github.com/tornadoweb/tornado/issues/1009
# http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx/
def prevent_json_hijacking(function):
@functools.wraps(function)
def wrapper(*args, **kwargs):
response = function(*args, **kwargs)
if isinstance(response, JSONResponse) and response.content:
response.content = ")]}',\n" + response.content
return response
return wrapper
class Clients(generic.View):
"""API for nova limits."""
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request):
"""Get all registered freezer clients"""
# we don't have a "get all clients" api (probably for good reason) so
# we need to resort to getting a very high number.
clients = freezer_api.client_list(request, limit=9999)
clients = [c.get_dict() for c in clients]
return clients

View File

@ -0,0 +1,31 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# 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.
"""
URL patterns for the OpenStack Dashboard.
"""
from django.conf.urls import patterns
from django.conf.urls import url
import rest_api
urlpatterns = patterns(
'',
url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"),
)

View File

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,107 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from horizon import forms
from horizon import workflows
import horizon_web_ui.freezer_ui.api.api as freezer_api
LOG = logging.getLogger(__name__)
class DestinationAction(workflows.MembershipAction):
path = forms.CharField(label=_("Destination Path"),
initial='/home/',
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 OptionsAction(workflows.Action):
description = forms.CharField(widget=forms.Textarea,
label="Description",
required=False,
help_text="Free text description of this "
"restore.")
dry_run = forms.BooleanField(label=_("Dry Run"),
required=False)
max_prio = forms.BooleanField(label=_("Max Process Priority"),
required=False)
class Meta(object):
name = _("Options")
class Options(workflows.Step):
action_class = OptionsAction
contributes = ('description', 'dry_run', 'max_prio')
after = Destination
class ConfigureBackups(workflows.Workflow):
slug = "restore"
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, Options)
def __init__(self, *args, **kwargs):
super(ConfigureBackups, self).__init__(*args, **kwargs)
pass
def handle(self, request, data):
freezer_api.restore_action_create(
request,
backup_id=data['backup_id'],
destination_client_id=data['client'],
destination_path=data['path'],
description=data['description'],
dry_run=data['dry_run'],
max_prio=data['max_prio']
)
return True

View File

@ -0,0 +1,111 @@
# 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 import tables
from horizon.utils import functions as utils
from horizon_web_ui.freezer_ui.django_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})
def allowed(self, request, instance):
return True # is_loaded(instance)
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):
backup_name = tables.Column('backup_name', verbose_name=_("Backup Name"),
link=backup_detail_view)
host_name = tables.Column('host_name', verbose_name=_("Host Name"))
created_by = tables.Column("user_name", verbose_name=_("Created By"))
created = tables.Column("timestamp",
verbose_name=_("Created At"),
filters=[timestamp_to_string])
icons = tables.Column(icons, verbose_name='Info')
def __init__(self, *args, **kwargs):
super(BackupsTable, self).__init__(*args, **kwargs)
if 'offset' in self.request.GET:
self.offset = self.request.GET['offset']
else:
self.offset = 0
def get_object_id(self, backup):
return backup.id
def get_pagination_string(self):
page_size = utils.get_page_size(self.request)
return "=".join(['offset', str(self.offset + page_size)])
class Meta(object):
name = "vms"
verbose_name = _("Backups")
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 "VMs" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Backups") %}
{% endblock page_header %}
{% block main %}
{{ table.render }}
{% endblock %}

View File

@ -0,0 +1,46 @@
<noscript><h3>{{ step }}</h3></noscript>
<div class="row" ng-controller="DestinationCtrl">
<div class="col-sm-12">
<table class="table table-bordered table-striped">
<thead>
<tr class="table_caption">
<th class="table_header" colspan="3" data-column="0">
<div class="table_actions clearfix">
<div class="table_search">
<input class="form-control" ng-model="query">
</div>
</div>
</th>
</tr>
<tr ng-hide="filtered.length <= 0">
<th class="multi_select_column"></th>
<th>Hostname</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="client in filtered = (clients | filter: { client : { $ : query } } | limitTo:10)">
<td class="multi_select_column">
<input type="radio" name="client" value="{$ client['client']['client_id'] $}">
</td>
<td>{$ client['client']['client_id'] $}</td>
<td>{$ client['client']['description'] $}</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" data-column="0">
{$ filtered.length ? filtered.length : 0 $} out of {$ clients.length $} displayed. Use the filter field to limit the number of results.
</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,26 @@
# 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'),
)

141
freezer_ui/backups/views.py Normal file
View File

@ -0,0 +1,141 @@
# 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
import time
from django.core.urlresolvers import reverse
from django.template.defaultfilters import date as django_date
from django.utils.translation import ugettext_lazy as _
from django.views import generic
import parsedatetime as pdt
from horizon import exceptions
from horizon import messages
from horizon import tables
from horizon import workflows
from horizon_web_ui.freezer_ui.backups import tables as freezer_tables
import horizon_web_ui.freezer_ui.api.api as freezer_api
import restore_workflow
class IndexView(tables.DataTableView):
name = _("Backups")
slug = "backups"
table_class = freezer_tables.BackupsTable
template_name = ("freezer_ui/backups/index.html")
def has_more_data(self, table):
return self._has_more
def get_data(self):
filter = self.get_filters(self.request,
self.table.get_filter_field(),
self.table.get_filter_string())
backups, self._has_more = freezer_api.backups_list(
self.request,
offset=self.table.offset,
time_after=filter['from'],
time_before=filter['to'],
text_match=filter['contains']
)
return backups
def get_filters(self, request, filter_field, filter_string):
cal = pdt.Calendar()
filters = {}
filters['from'] = None
filters['to'] = None
filters['contains'] = None
if filter_field == 'between':
result_range = cal.nlp(filter_string)
if result_range and len(result_range) == 2:
filters['from'] = int(
time.mktime(result_range[0][0].timetuple()))
filters['to'] = int(
time.mktime(result_range[1][0].timetuple()))
else:
messages.warning(
request,
"Please enter two dates. E.g: '01/01/2014 - 05/09/2015'.")
elif filter_field in ['before', 'after']:
result, what = cal.parse(filter_string)
if what == 0:
messages.warning(
self.table.request,
"Please enter a date/time. E.g: '01/01/2014 12pm' or '1 we"
"ek ago'.")
else:
field = 'to' if filter_field == 'before' else 'from'
dt = datetime.datetime(*result[:6])
if what == 1: # a date without time
# use .date() to remove time part
filters[field] = int(time.mktime(dt.date().timetuple()))
elif what in [2, 3]: # date and time or time with current date
filters[field] = int(time.mktime(dt.timetuple()))
else:
raise Exception(
'Unknown result when parsing date: {}'.format(what))
elif filter_field == 'contains':
filters['contains'] = filter_string.lower()
return filters
class DetailView(generic.TemplateView):
template_name = 'freezer_ui/backups/detail.html'
def get_context_data(self, **kwargs):
backup = freezer_api.get_backup(self.request, kwargs['backup_id'])
return {'data': pprint.pformat(backup.data_dict)}
class RestoreView(workflows.WorkflowView):
workflow_class = restore_workflow.ConfigureBackups
def get_object(self, *args, **kwargs):
id = self.kwargs['backup_id']
try:
return freezer_api.get_backup(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.timestamp))
backup_date_str = django_date(backup_date, 'SHORT_DATETIME_FORMAT')
return "Restore '{}' from {}".format(
backup.backup_name, backup_date_str)
def get_initial(self):
return {"backup_id": self.kwargs['backup_id']}
def get_workflow(self, *args, **kwargs):
workflow = super(RestoreView, self).get_workflow(*args, **kwargs)
workflow.name = self.get_workflow_name()
return workflow

View File

@ -0,0 +1 @@
__author__ = 'jonas'

View File

@ -16,16 +16,13 @@ from django.utils.translation import ugettext_lazy as _
from horizon import browsers
from openstack_dashboard.dashboards.freezer.freezerpanel import tables
from horizon_web_ui.freezer_ui.configurations import tables
class ContainerBrowser(browsers.ResourceBrowser):
name = "swift"
verbose_name = _("Swift")
navigation_table_class = tables.ContainersTable
content_table_class = tables.ObjectsTable
navigable_item_name = _("Container")
navigation_kwarg_name = "container_name"
content_kwarg_name = "subfolder_path"
has_breadcrumb = True
breadcrumb_url = "horizon:freezer:freezerpanel:index"
name = "backup_configuration"
verbose_name = _("Backup Configuration")
navigation_table_class = tables.BackupConfigsTable
content_table_class = tables.InstancesTable
navigable_item_name = _("Backup Configuration")
navigation_kwarg_name = "name"

View File

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

View File

@ -0,0 +1,24 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
from horizon_web_ui.freezer_ui import dashboard
class BackupConfigsPanel(horizon.Panel):
name = _("Configurations")
slug = "configurations"
dashboard.Freezer.register(BackupConfigsPanel)

View File

@ -0,0 +1,161 @@
# 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
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 format_last_backup(last_backup):
last_backup_ts = datetime.datetime.fromtimestamp(last_backup)
ten_days_later = last_backup_ts + datetime.timedelta(days=10)
today = datetime.datetime.today()
if last_backup is None:
colour = 'red'
icon = 'fire'
text = 'Never'
elif ten_days_later < today:
colour = 'orange'
icon = 'thumbs-down'
text = timestamp_to_string(last_backup)
else:
colour = 'green'
icon = 'thumbs-up'
text = timestamp_to_string(last_backup)
return safestring.mark_safe(
'<span style="color:{}"><span class="glyphicon glyphicon-{}" aria-hidd'
'en="true"></span> {}</span>'.format(colour, icon, text))
class Restore(tables.Action):
name = "restore"
verbose_name = _("Restore")
def single(self, table, request, instance):
messages.info(request, "Needs to be implemented")
def allowed(self, request, instance):
return True
class DeleteConfig(tables.DeleteAction):
name = "delete"
classes = ("btn-danger",)
icon = "remove"
help_text = _("Delete configurations are not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Configuration File",
u"Delete Configuration Files",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Configuration File",
u"Deleted Configuration Files",
count
)
def delete(self, request, obj_id):
return freezer_api.configuration_delete(request, obj_id)
class CloneConfig(tables.Action):
name = "clone"
verbose_name = _("Clone")
# classes = ("ajax-modal",)
help_text = _("Clone and edit a configuration file")
def single(self, table, request, obj_id):
freezer_api.configuration_clone(request, obj_id)
return shortcuts.redirect('horizon:freezer_ui:configurations:index')
class EditConfig(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
classes = ("ajax-modal",)
def get_link_url(self, datum=None):
return reverse("horizon:freezer_ui:configurations:configure",
kwargs={'name': datum.config_id})
def get_backup_configs_link(backup_config):
return reverse('horizon:freezer_ui:configurations:index',
kwargs={'config_id': backup_config.config_id})
class CreateConfig(tables.LinkAction):
name = "create"
verbose_name = _("Create Configuration")
url = "horizon:freezer_ui:configurations:create"
classes = ("ajax-modal",)
icon = "plus"
class BackupConfigsTable(tables.DataTable):
name = tables.Column("name", link=get_backup_configs_link,
verbose_name=_("Configuration Name"))
def get_object_id(self, backup_config):
return backup_config.id
class Meta(object):
name = "backup_configuration"
verbose_name = _("Backup Configurations")
table_actions = (CreateConfig,)
footer = False
multi_select = False
row_actions = (EditConfig,
CloneConfig,
DeleteConfig, )
class ObjectFilterAction(tables.FilterAction):
def allowed(self, request, datum):
return bool(self.table.kwargs['config_id'])
class InstancesTable(tables.DataTable):
client = tables.Column('name', verbose_name=_("Client Name"))
created = tables.Column('last_backup',
filters=(format_last_backup,),
verbose_name=_("Last backup"))
def get_object_id(self, container):
return container.name
class Meta(object):
name = "clients"
verbose_name = _("Clients")
table_actions = (ObjectFilterAction,)
row_actions = (Restore,)
footer = False
multi_select = False

View File

@ -0,0 +1,61 @@
{% load i18n %}
<noscript><h3>{{ step }}</h3></noscript>
<div class="membership {{ step.slug }}_membership dropdown_fix" data-show-roles="{{ step.show_roles }}">
<div class="header">
<div class="help_text">{{ step.help_text }}</div>
<div class="row">
<div class="col-xs-6">
<div class="fake_table fake_table_header fake_{{ step.slug }}_table clearfix">
<span class="members_title">{{ step.available_list_title }}</span>
<div class="form-group has-feedback">
<input type="text" name="available_{{ step.slug }}_filter" id="available_{{ step.slug }}" class="filter {{ step.slug }}_filter form-control input-sm" placeholder="{% trans "Filter" %}">
<span class="fa fa-search search-icon form-control-feedback"></span>
</div>
</div>
</div>
<div class="col-xs-6">
<div class="fake_table fake_table_header fake_{{ step.slug }}_table clearfix">
<span class="members_title">{{ step.members_list_title }}</span>
<div class="form-group has-feedback">
<input type="text" name="{{ step.slug }}_members_filter" id="{{ step.slug }}_members" class="filter {{ step.slug }}_filter form-control input-sm" placeholder="{% trans "Filter" %}">
<span class="fa fa-search search-icon form-control-feedback"></span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-6 filterable {{ step.slug }}_filterable">
<div class="fake_table fake_{{ step.slug }}_table" id="available_{{ step.slug }}">
<ul class="available_members available_{{ step.slug }}"></ul>
<ul class="no_results" id="no_available_{{ step.slug }}"><li>{{ step.no_available_text }}</li></ul>
</div>
</div>
<div class="col-xs-6 filterable {{ step.slug }}_filterable">
<div class="fake_table fake_{{ step.slug }}_table" id="{{ step.slug }}_members">
<ul class="members {{ step.slug }}_members"></ul>
<ul class="no_results" id="no_{{ step.slug }}_members"><li>{{ step.no_members_text }}</li></ul>
</div>
</div>
</div>
</div>
<div class="hide">
{% include "horizon/common/_form_fields.html" %}
</div>
<script>
if (typeof horizon.membership !== 'undefined') {
horizon.membership.workflow_init($(".workflow"), "{{ step.slug }}", "{{ step.get_id }}");
} else {
addHorizonLoadEvent(function() {
horizon.membership.workflow_init($(".workflow"), "{{ step.slug }}", "{{ step.get_id }}");
});
}
</script>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Backup Configurations" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Backup Configurations") %}
{% endblock page_header %}
{% block main %}
{{ backup_configuration_browser.render }}
{% endblock %}

View File

@ -0,0 +1,34 @@
# 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.configurations import views
urlpatterns = patterns(
'',
# url(r'^$', views.BackupConfigsView.as_view(), name='test'),
url(r'^(?P<config_id>[^/]+)?$',
views.BackupConfigsView.as_view(),
name='index'),
url(r'^create/$',
views.ConfigureWorkflowView.as_view(),
name='create'),
url(r'^configure/(?P<name>[^/]+)?$',
views.ConfigureWorkflowView.as_view(),
name='configure'),
)

View File

@ -0,0 +1,40 @@
# Copyright 2014 Hewlett-Packard
#
# 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.
class Configuration(object):
def __init__(self, data_dict):
self.data_dict = data_dict
@property
def id(self):
return self.config_id
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['config_file']:
return self.data_dict['config_file'][attr]
else:
return object.__getattribute__(self, attr)
class Client(object):
"""Aggregate clients and metadata """
def __init__(self, client):
self.name = client
self.clients = client
self.client_id = client

View File

@ -0,0 +1,94 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import workflows
from horizon import browsers
from horizon import exceptions
import horizon_web_ui.freezer_ui.api.api as freezer_api
import horizon_web_ui.freezer_ui.configurations.browsers as project_browsers
import workflows.configure as configure_workflow
LOG = logging.getLogger(__name__)
class ConfigureWorkflowView(workflows.WorkflowView):
workflow_class = configure_workflow.ConfigureBackups
def get_object(self, *args, **kwargs):
config_id = self.kwargs['name']
try:
return freezer_api.configuration_get(self.request, config_id)[0]
except Exception:
redirect = reverse("horizon:freezer_ui:configurations: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_initial(self):
initial = super(ConfigureWorkflowView, self).get_initial()
if self.is_update():
initial.update({'original_name': None})
config = self.get_object()
initial['name'] = config.name
initial['container_name'] = config.container_name
initial['config_id'] = config.config_id
initial['src_file'] = config.src_file
initial['levels'] = config.levels
initial['optimize'] = config.optimize
initial['compression'] = config.compression
initial['encryption_password'] = config.encryption_password
initial['start_datetime'] = config.start_datetime
initial['interval'] = config.interval
initial['exclude'] = config.exclude
initial['log_file'] = config.log_file
initial['encryption_password'] = config.encryption_password
initial['proxy'] = config.proxy
initial['max_priority'] = config.max_priority
initial['clients'] = config.clients
initial['original_name'] = config.config_id
initial.update({'original_name': config.config_id})
return initial
class BackupConfigsView(browsers.ResourceBrowserView):
browser_class = project_browsers.ContainerBrowser
template_name = "freezer_ui/configurations/browser.html"
def get_backup_configuration_data(self):
configurations = []
try:
configurations = freezer_api.configuration_list(self.request)
except Exception:
msg = _('Unable to retrieve configuration file list.')
exceptions.handle(self.request, msg)
return configurations
def get_clients_data(self):
configuration = []
try:
if self.kwargs['config_id']:
configuration = freezer_api.clients_in_config(
self.request, self.kwargs['config_id'])
except Exception:
msg = _('Unable to retrieve instances for this configuration.')
exceptions.handle(self.request, msg)
return configuration

View File

@ -0,0 +1,315 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.debug import sensitive_variables # noqa
from horizon import exceptions
from horizon import forms
from horizon import workflows
import horizon_web_ui.freezer_ui.api.api as freezer_api
LOG = logging.getLogger(__name__)
class BackupConfigurationAction(workflows.Action):
original_name = forms.CharField(
widget=forms.HiddenInput(),
required=False)
name = forms.CharField(
label=_("Configuration Name"),
required=True)
container_name = forms.CharField(
label=_("Swift Container Name"),
required=True)
mode = forms.ChoiceField(
help_text="Choose what you want to backup",
required=True)
src_file = forms.CharField(
label=_("Source File/Directory"),
help_text="The file or directory you want to back up to Swift",
required=True)
def populate_mode_choices(self, request, context):
return [
('fs', _("File system")),
('snapshot', _("Snapshot")),
('mongo', _("MongoDB")),
('mysql', _("MySQL")),
('mssql', _("Microsoft SQL Server")),
('elastic', _("ElasticSearch")),
('postgres', _("Postgres")),
]
class Meta(object):
name = _("Backup")
class BackupConfiguration(workflows.Step):
action_class = BackupConfigurationAction
contributes = ('mode',
'name',
'container_name',
'src_file',
'original_name')
class OptionsConfigurationAction(workflows.Action):
levels = forms.IntegerField(
label=_("Number of incremental backups"),
initial=0,
min_value=0,
required=False,
help_text="Set the backup level used with tar"
" to implement incremental backup. "
"If a level 1 is specified but no "
"level 0 is already available, a "
"level 0 will be done and "
"subsequently backs to level 1. "
"Default 0 (No Incremental)")
optimize = forms.ChoiceField(
choices=[('speed', _("Speed (tar)")),
('bandwith', "Bandwith/Space (rsync)")],
help_text="",
label='Optimize for...',
required=False)
compression = forms.ChoiceField(
choices=[('gzip', _("Minimum Compression (GZip/Zip)")),
('bzip', _("Medium Compression (BZip2")),
('xz', _("Maximum Compression (XZ)"))],
help_text="",
label='Compression Level',
required=False)
encryption_password = forms.CharField(
label=_("Encryption Password"), # encryption key
widget=forms.PasswordInput(),
help_text="",
required=False)
class Meta(object):
name = _("Options")
class OptionsConfiguration(workflows.Step):
action_class = OptionsConfigurationAction
contributes = ('levels',
'optimize',
'compression',
'encryption_password',)
class ClientsConfigurationAction(workflows.MembershipAction):
def __init__(self, request, *args, **kwargs):
super(ClientsConfigurationAction, self).__init__(request,
*args,
**kwargs)
err_msg_configured = 'Unable to retrieve list of configured clients.'
err_msg_all = 'Unable to retrieve list of clients.'
default_role_field_name = self.get_default_role_field_name()
self.fields[default_role_field_name] = forms.CharField(required=False)
self.fields[default_role_field_name].initial = 'member'
field_name = self.get_member_field_name('member')
self.fields[field_name] = forms.MultipleChoiceField(required=False)
all_clients = []
try:
all_clients = freezer_api.client_list(request)
except Exception:
exceptions.handle(request, err_msg_all)
clients = [(c.client_id, c.name) for c in all_clients]
self.fields[field_name].choices = clients
if request.method == 'POST':
return
initial_clients = []
try:
original_name = args[0].get('original_name', None)
if original_name:
configured_clients = \
freezer_api.clients_in_config(request, original_name)
initial_clients = [client.id for client in configured_clients]
except Exception:
exceptions.handle(request, err_msg_configured)
self.fields[field_name].initial = initial_clients
class Meta(object):
name = _("Clients")
slug = "configure_clients"
class ClientsConfiguration(workflows.UpdateMembersStep):
action_class = ClientsConfigurationAction
help_text = _(
"Select the clients that will be backed up using this configuration.")
available_list_title = _("All Clients")
members_list_title = _("Selected Clients")
no_available_text = _("No clients found.")
no_members_text = _("No clients selected.")
show_roles = False
contributes = ("clients",)
def contribute(self, data, context):
if data:
member_field_name = self.get_member_field_name('member')
context['clients'] = data.get(member_field_name, [])
return context
class SchedulingConfigurationAction(workflows.Action):
start_datetime = forms.CharField(
label=_("Start Date and Time"),
required=False,
help_text=_("Set a start date and time for backups"))
interval = forms.CharField(
label=_("Interval"),
required=False,
help_text=_("Repeat this configuration in an interval. e.g. 24 hours"))
class Meta(object):
name = _("Scheduling")
class SchedulingConfiguration(workflows.Step):
action_class = SchedulingConfigurationAction
contributes = ('start_datetime',
'interval',)
class AdvancedConfigurationAction(workflows.Action):
exclude = forms.CharField(
label=_("Exclude Files"),
help_text="Exclude files, given as a PATTERN.Ex:"
" --exclude '*.log' will exclude any "
"file with name ending with .log. "
"Default no exclude",
required=False)
log_file = forms.CharField(
label=_("Log File Path"),
help_text="Set log file. By default logs to "
"/var/log/freezer.log If that file "
"is not writable, freezer tries to "
"log to ~/.freezer/freezer.log",
required=False)
proxy = forms.CharField(
label=_("Proxy URL"),
help_text="Enforce proxy that alters system "
"HTTP_PROXY and HTTPS_PROXY",
widget=forms.URLInput(),
required=False)
max_priority = forms.BooleanField(
label=_("Max Priority"),
help_text="Set the cpu process to the "
"highest priority (i.e. -20 "
"on Linux) and real-time for "
"I/O. The process priority "
"will be set only if nice and "
"ionice are installed Default "
"disabled. Use with caution.",
widget=forms.CheckboxInput(),
required=False)
class Meta(object):
name = _("Advanced Configuration")
class AdvancedConfiguration(workflows.Step):
action_class = AdvancedConfigurationAction
contributes = ('exclude',
'log_file',
'proxy',
'max_priority')
class ConfigureBackups(workflows.Workflow):
slug = "configuration"
name = _("Configuration")
finalize_button_name = _("Save")
success_message = _('Configuration file saved correctly.')
failure_message = _('Unable to save configuration file.')
success_url = "horizon:freezer_ui:configurations:index"
default_steps = (BackupConfiguration,
OptionsConfiguration,
ClientsConfiguration,
SchedulingConfiguration,
AdvancedConfiguration)
@sensitive_variables('encryption_password',
'confirm_encryption_password')
def handle(self, request, context):
try:
if context['original_name'] == '':
freezer_api.configuration_create(
request,
name=context['name'],
container_name=context['container_name'],
src_file=context['src_file'],
levels=context['levels'], # if empty save 0 not null
optimize=context['optimize'],
compression=context['compression'],
encryption_password=context['encryption_password'],
clients=context['clients'], # save the name of the client
start_datetime=context['start_datetime'],
interval=context['interval'],
exclude=context['exclude'],
log_file=context['log_file'],
proxy=context['proxy'],
max_priority=context['max_priority'],
)
else:
freezer_api.configuration_update(
request,
config_id=context['original_name'],
name=context['name'],
container_name=context['container_name'],
src_file=context['src_file'],
levels=context['levels'], # if empty save 0 not null
optimize=context['optimize'],
compression=context['compression'],
encryption_password=context['encryption_password'],
clients=context['clients'], # save the name of the client
start_datetime=context['start_datetime'],
interval=context['interval'],
exclude=context['exclude'],
log_file=context['log_file'],
proxy=context['proxy'],
max_priority=context['max_priority'],
)
return True
except Exception:
exceptions.handle(request)
return False

31
freezer_ui/dashboard.py Normal file
View File

@ -0,0 +1,31 @@
# 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
class Mygroup(horizon.PanelGroup):
slug = "mygroup"
name = _("Freezer")
panels = ('overview', 'configurations', 'backups', 'actions')
class Freezer(horizon.Dashboard):
name = _("Backup Restore DR")
slug = "freezer_ui"
panels = (Mygroup,)
default_panel = 'overview'
horizon.register(Freezer)

View File

@ -0,0 +1,8 @@
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')

3
freezer_ui/models.py Normal 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 @@
__author__ = 'jonas'

View File

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

View File

@ -0,0 +1,24 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
from horizon_web_ui.freezer_ui import dashboard
class OverviewPanel(horizon.Panel):
name = _("Overview")
slug = "overview"
dashboard.Freezer.register(OverviewPanel)

View File

@ -0,0 +1,112 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Backup Restore DR Overview" %}{% endblock %}
{% block css %}
{% include "_stylesheets.html" %}
<link href='{{ STATIC_URL }}freezer/css/freezer.css' type='text/css' media='screen' rel='stylesheet' />
{% endblock %}
<noscript>
{% trans "This pane needs javascript support." %}
</noscript>
{% block main %}
<div class="quota-dynamic">
<h3 class="quota-heading">{% trans "Overview" %}</h3>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="30"></div>
<strong>{% trans "Nodes without backup in the last week" %}<br />
18 of 50
</strong>
</div>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="70"></div>
<strong>{% trans "Total space used for backups" %}<br />
150gb of 200gb
</strong>
</div>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="0"></div>
<strong>{% trans "Storage price per month" %}<br />
60 dls
</strong>
</div>
</div>
<div class="quota-dynamic">
<h3 class="quota-heading">{% trans "Backup Summary" %}</h3>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="50"></div>
<strong>{% trans "Backups older than 1 week" %}<br />
60 of 120
</strong>
</div>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="7"></div>
<strong>{% trans "Average backup size" %}<br />
7gb for 18 backups
</strong>
</div>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="18"></div>
<strong>{% trans "Average backup time" %}<br />
18 min
</strong>
</div>
</div>
<div class="quota-dynamic">
<h3 class="quota-heading">{% trans "Restore Summary" %}</h3>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="7"></div>
<strong>{% trans "Average restore time" %}<br />
15 min
</strong>
</div>
<div class="d3_quota_bar">
<div class="d3_pie_chart_usage" data-used="0"></div>
<strong>{% trans "Restore ratio success to failure" %}<br />
100% success
</strong>
</div>
</div>
<div class="quota-dynamic">
<h3 class="quota-heading">{% trans "OS Summary" %}</h3>
</div>
<div id='dashboard'>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type='text/javascript' src='{{ STATIC_URL }}freezer/js/dashboard.js'></script>
<script type="text/javascript">
var freqData=[
{State:'Linux',freq:{binary:6902, text:12085, media:500}}
,{State:'Windows',freq:{binary:6500, text:5200, media:100}}
,{State:'Mac OS',freq:{binary:200, text:1000, media:1500}}
];
dashboard('#dashboard',freqData);
</script>
{% endblock %}

View File

@ -0,0 +1,22 @@
# 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.overview import views
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
)

View File

@ -0,0 +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.
from django.utils.translation import ugettext_lazy as _
from django.views import generic
class IndexView(generic.TemplateView):
name = _("Overview")
slug = "overview"
template_name = ("freezer_ui/overview/overview.html")

View File

@ -0,0 +1,43 @@
.fa-custom-number {
font-family: monospace;
line-height: 1;
padding: 0.1em;
vertical-align: baseline;
font-weight: bold;
border: 1px solid #999;
border-radius: 25%;
}
/* d3 css */
path { stroke: #fff; }
path:hover { opacity:0.9; }
rect:hover { fill:#006CCF; }
.axis { font: 10px sans-serif; }
.legend tr{ border-bottom:1px solid grey; }
.legend tr:first-child{ border-top:1px solid grey; }
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path { display: none; }
.legend{
margin-bottom:76px;
display:inline-block;
border-collapse: collapse;
border-spacing: 0px;
}
.legend td{
padding:4px 5px;
vertical-align:bottom;
}
.legendFreq, .legendPerc{
align:right;
width:50px;
}

View File

@ -0,0 +1,196 @@
function dashboard(id, fData){
var barColor = '#006CCF';
function segColor(c){ return {binary:"#807dba", text:"#e08214",media:"#41ab5d"}[c]; }
// compute total for each state.
fData.forEach(function(d){d.total=d.freq.binary+d.freq.text+d.freq.media;});
// function to handle histogram.
function histoGram(fD){
var hG={}, hGDim = {t: 60, r: 0, b: 30, l: 0};
hGDim.w = 500 - hGDim.l - hGDim.r,
hGDim.h = 300 - hGDim.t - hGDim.b;
//create svg for histogram.
var hGsvg = d3.select(id).append("svg")
.attr("width", hGDim.w + hGDim.l + hGDim.r)
.attr("height", hGDim.h + hGDim.t + hGDim.b).append("g")
.attr("transform", "translate(" + hGDim.l + "," + hGDim.t + ")");
// create function for x-axis mapping.
var x = d3.scale.ordinal().rangeRoundBands([0, hGDim.w], 0.1)
.domain(fD.map(function(d) { return d[0]; }));
// Add x-axis to the histogram svg.
hGsvg.append("g").attr("class", "x axis")
.attr("transform", "translate(0," + hGDim.h + ")")
.call(d3.svg.axis().scale(x).orient("bottom"));
// Create function for y-axis map.
var y = d3.scale.linear().range([hGDim.h, 0])
.domain([0, d3.max(fD, function(d) { return d[1]; })]);
// Create bars for histogram to contain rectangles and freq labels.
var bars = hGsvg.selectAll(".bar").data(fD).enter()
.append("g").attr("class", "bar");
//create the rectangles.
bars.append("rect")
.attr("x", function(d) { return x(d[0]); })
.attr("y", function(d) { return y(d[1]); })
.attr("width", x.rangeBand())
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr('fill',barColor)
.on("mouseover",mouseover)// mouseover is defined below.
.on("mouseout",mouseout);// mouseout is defined below.
//Create the frequency labels above the rectangles.
bars.append("text").text(function(d){ return d3.format(",")(d[1])})
.attr("x", function(d) { return x(d[0])+x.rangeBand()/2; })
.attr("y", function(d) { return y(d[1])-5; })
.attr("text-anchor", "middle");
function mouseover(d){ // utility function to be called on mouseover.
// filter for selected state.
var st = fData.filter(function(s){ return s.State == d[0];})[0],
nD = d3.keys(st.freq).map(function(s){ return {type:s, freq:st.freq[s]};});
// call update functions of pie-chart and legend.
pC.update(nD);
leg.update(nD);
}
function mouseout(d){ // utility function to be called on mouseout.
// reset the pie-chart and legend.
pC.update(tF);
leg.update(tF);
}
// create function to update the bars. This will be used by pie-chart.
hG.update = function(nD, color){
// update the domain of the y-axis map to reflect change in frequencies.
y.domain([0, d3.max(nD, function(d) { return d[1]; })]);
// Attach the new data to the bars.
var bars = hGsvg.selectAll(".bar").data(nD);
// transition the height and color of rectangles.
bars.select("rect").transition().duration(500)
.attr("y", function(d) {return y(d[1]); })
.attr("height", function(d) { return hGDim.h - y(d[1]); })
.attr("fill", color);
// transition the frequency labels location and change value.
bars.select("text").transition().duration(500)
.text(function(d){ return d3.format(",")(d[1])})
.attr("y", function(d) {return y(d[1])-5; });
}
return hG;
}
// function to handle pieChart.
function pieChart(pD){
var pC ={}, pieDim ={w:250, h: 250};
pieDim.r = Math.min(pieDim.w, pieDim.h) / 2;
// create svg for pie chart.
var piesvg = d3.select(id).append("svg")
.attr("width", pieDim.w).attr("height", pieDim.h).append("g")
.attr("transform", "translate("+pieDim.w/2+","+pieDim.h/2+")");
// create function to draw the arcs of the pie slices.
var arc = d3.svg.arc().outerRadius(pieDim.r - 10).innerRadius(0);
// create a function to compute the pie slice angles.
var pie = d3.layout.pie().sort(null).value(function(d) { return d.freq; });
// Draw the pie slices.
piesvg.selectAll("path").data(pie(pD)).enter().append("path").attr("d", arc)
.each(function(d) { this._current = d; })
.style("fill", function(d) { return segColor(d.data.type); })
.on("mouseover",mouseover).on("mouseout",mouseout);
// create function to update pie-chart. This will be used by histogram.
pC.update = function(nD){
piesvg.selectAll("path").data(pie(nD)).transition().duration(500)
.attrTween("d", arcTween);
}
// Utility function to be called on mouseover a pie slice.
function mouseover(d){
// call the update function of histogram with new data.
hG.update(fData.map(function(v){
return [v.State,v.freq[d.data.type]];}),segColor(d.data.type));
}
//Utility function to be called on mouseout a pie slice.
function mouseout(d){
// call the update function of histogram with all data.
hG.update(fData.map(function(v){
return [v.State,v.total];}), barColor);
}
// Animating the pie-slice requiring a custom function which specifies
// how the intermediate paths should be drawn.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return arc(i(t)); };
}
return pC;
}
// function to handle legend.
function legend(lD){
var leg = {};
// create table for legend.
var legend = d3.select(id).append("table").attr('class','legend');
// create one row per segment.
var tr = legend.append("tbody").selectAll("tr").data(lD).enter().append("tr");
// create the first column for each segment.
tr.append("td").append("svg").attr("width", '16').attr("height", '16').append("rect")
.attr("width", '16').attr("height", '16')
.attr("fill",function(d){ return segColor(d.type); });
// create the second column for each segment.
tr.append("td").text(function(d){ return d.type;});
// create the third column for each segment.
tr.append("td").attr("class",'legendFreq')
.text(function(d){ return d3.format(",")(d.freq);});
// create the fourth column for each segment.
tr.append("td").attr("class",'legendPerc')
.text(function(d){ return getLegend(d,lD);});
// Utility function to be used to update the legend.
leg.update = function(nD){
// update the data attached to the row elements.
var l = legend.select("tbody").selectAll("tr").data(nD);
// update the frequencies.
l.select(".legendFreq").text(function(d){ return d3.format(",")(d.freq);});
// update the percentage column.
l.select(".legendPerc").text(function(d){ return getLegend(d,nD);});
}
function getLegend(d,aD){ // Utility function to compute percentage.
return d3.format("%")(d.freq/d3.sum(aD.map(function(v){ return v.freq; })));
}
return leg;
}
// calculate total frequency by segment for all state.
var tF = ['binary','text','media'].map(function(d){
return {type:d, freq: d3.sum(fData.map(function(t){ return t.freq[d];}))};
});
// calculate total frequency by state for all segment.
var sF = fData.map(function(d){return [d.State,d.total];});
var hG = histoGram(sF), // create the histogram.
pC = pieChart(tF), // create the pie-chart.
leg= legend(tF); // create the legend.
}

View File

@ -0,0 +1,16 @@
(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
};
});
}());

View File

@ -0,0 +1 @@
__author__ = 'jonas'

View File

@ -0,0 +1,80 @@
from django.conf import settings
from horizon_web_ui.freezer_ui.api import api
from mock import patch
from openstack_auth import utils
import openstack_dashboard.test.helpers as helpers
@patch('freezer.apiclient.client')
class TestApi(helpers.TestCase):
CONFIG = {u'user_name': u'admin',
u'config_id': u'053a62e0-66a9-4a1c-ba58-6b7348d22166',
u'config_file': {u'start_datetime': u'1432736797',
u'repeat': u'1440',
u'max_priority': False,
u'encryption_password': u'secret',
u'src_file': u'fdsfsdds',
u'clients': [u'test-client'],
u'levels': 0,
u'proxy': u'',
u'container_name': u'dummy_container',
u'exclude': u'/tmp',
u'compression': u'gzip',
u'log_file': u'',
u'optimize': u'speed',
u'name': u'fdsfs'},
u'user_id': u'13c2b15308c04cdf86989ee7335eb504'}
def setUp(self):
super(TestApi, self).setUp()
# Usually this monkey patching happens in urls.py. This doesn't work
# here because we never invoke urls.py in this test. So we have to do
# it manually.
utils.patch_middleware_get_user()
def _setup_request(self):
super(helpers.TestCase, self)._setup_request()
# For some strange reason, Horizon sets the token to the token id
# rather than the token object. This fixes it.
self.request.session['token'] = self.token
def assert_client_got_created(self, client_mock):
client_mock.Client.assert_called_with(
token=self.request.session['token'].id,
auth_url=settings.OPENSTACK_KEYSTONE_URL,
endpoint=settings.FREEZER_API_URL)
def test_configuration_delete(self, client_mock):
api.configuration_delete(
self.request, u'053a62e0-66a9-4a1c-ba58-6b7348d22166')
self.assert_client_got_created(client_mock)
client_mock.Client().configs.delete.\
assert_called_once_with(u'053a62e0-66a9-4a1c-ba58-6b7348d22166')
def test_configuration_clone(self, client_mock):
client_mock.Client().configs.get.return_value = [self.CONFIG]
client_mock.Client().configs.\
create.return_value = u'28124cf0-6cd3-4b38-a0e9-b6f41568fa37'
result = api.configuration_clone(
self.request, u'053a62e0-66a9-4a1c-ba58-6b7348d22166')
self.assertEqual(result, u'28124cf0-6cd3-4b38-a0e9-b6f41568fa37')
self.assert_client_got_created(client_mock)
data = self.CONFIG[u'config_file']
data['name'] = 'fdsfs_clone'
client_mock.Client().configs.create.assert_called_once_with(data)
def test_configuration_get(self, client_mock):
client_mock.Client().configs.get.return_value = [self.CONFIG]
result = api.configuration_get(
self.request, u'053a62e0-66a9-4a1c-ba58-6b7348d22166')
self.assertEqual(1, len(result))
# Test if properties are accessible via object properties
self.assertEqual(u'admin', result[0].user_name)
# Test if nested properties are accessible via object properties
self.assertEqual(u'1432736797', result[0].start_datetime)

View File

@ -0,0 +1,30 @@
import json
from mock import patch
from horizon.utils.urlresolvers import reverse
import openstack_dashboard.test.helpers as helpers
@patch('freezer.apiclient.client')
class TestRestApi(helpers.TestCase):
CLIENT_1 = {u'client': {u'hostname': u'jonas',
u'description': u'client description',
u'client_id': u'test-client',
u'config_ids': [u'fdaf2fwf2', u'fdsfdsfdsfs']},
u'user_id': u'13c2b15308c04cdf86989ee7335eb504'}
JSON_PREFIX = ')]}\',\n'
def test_clients_get(self, client_mock):
client_mock.Client().registration.list.return_value = [self.CLIENT_1]
url = reverse("horizon:freezer_ui:api_clients")
res = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(200, res.status_code)
self.assertEqual('application/json', res['content-type'])
self.assertEqual(self.JSON_PREFIX + json.dumps([self.CLIENT_1]),
res.content)
# there is no get ALL api at the moment, so we just fetch a big number
client_mock.Client().registration.list.assert_called_once_with(
limit=9999)

View File

@ -0,0 +1,18 @@
# 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 horizon.test.settings import * # noqa
INSTALLED_APPS = ()
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"
FREEZER_API_URL = "test"

32
freezer_ui/urls.py Normal file
View File

@ -0,0 +1,32 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# 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.
"""
URL patterns for the OpenStack Dashboard.
"""
from django.conf.urls import include
from django.conf.urls import patterns
from django.conf.urls import url
import horizon_web_ui.freezer_ui.api.rest.urls as rest_urls
urlpatterns = patterns(
'',
url(r'', include(rest_urls)),
)

23
manage.py Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# 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 os
import sys
from django.core.management import execute_from_command_line # noqa
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"freezer_ui.tests.settings")
execute_from_command_line(sys.argv)

8
test-requirements.txt Normal file
View File

@ -0,0 +1,8 @@
-e git+https://github.com/openstack/horizon.git#egg=horizon
django-nose
nose-exclude
mock
python-openstackclient
mox
parsedatetime
coverage>=3.6

19
tox.ini Normal file
View File

@ -0,0 +1,19 @@
[tox]
envlist = flake8
skipsdist = True
[testenv]
deps =
install_command = pip install -U {opts} {packages}
setenv = VIRTUAL_ENV={envdir}
commands =
[testenv:flake8]
deps = hacking
commands = flake8
[flake8]
max-line-length = 80
show-source = True
exclude = .venv,.git,.tox,*egg,.ropeproject
max-complexity = 19