Web UI for freezer

This changes are the first implementation of
the freezer web ui.
It's a read only version that permit to view the backups
in swift made by the user.

Change-Id: I79fd98ce3fc364c7a82a8b764e1733febb16b647
This commit is contained in:
Fabrizio Fresco 2014-11-18 16:26:09 +00:00 committed by Jonas Pfannschmidt
parent dbfdee6487
commit b2c7faa37d
28 changed files with 459 additions and 109 deletions

View File

@ -13,9 +13,9 @@ To install the horizon web ui you need to do the following::
# cd freezer/horizon_web_ui
# cp -r devfreezer /opt/stack/horizon/openstack_dashboard/dashboards/
# cp -r freezer /opt/stack/horizon/openstack_dashboard/dashboards/
# cp _50_devfreezer.py /opt/stack/horizon/openstack_dashboard/enabled/
# cp _50_freezer.py /opt/stack/horizon/openstack_dashboard/enabled/
# cd /opt/stack/horizon/
# ./run_tests.sh --runserver 0.0.0.0:8878

View File

@ -1,10 +1,10 @@
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
DASHBOARD = 'devfreezer'
DASHBOARD = 'freezer'
# If set to True, this dashboard will not be added to the settings.
DISABLED = False
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = [
'openstack_dashboard.dashboards.devfreezer',
'openstack_dashboard.dashboards.freezer',
]

View File

@ -1,13 +0,0 @@
from django.utils.translation import ugettext_lazy as _
import horizon
class Devfreezer(horizon.Dashboard):
name = _("Backup as a Service")
slug = "devfreezer"
panels = ('freezerweb',) # Add your panels here.
default_panel = 'freezerweb' # Specify the slug of the dashboard's default panel.
horizon.register(Devfreezer)

View File

@ -1,13 +0,0 @@
from django.utils.translation import ugettext_lazy as _
import horizon
from openstack_dashboard.dashboards.devfreezer import dashboard
class Freezerweb(horizon.Panel):
name = _("Freezer")
slug = "freezerweb"
dashboard.Devfreezer.register(Freezerweb)

View File

@ -1,15 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class InstancesTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("Name"))
status = tables.Column("status", verbose_name=_("Status"))
zone = tables.Column('availability_zone',
verbose_name=_("Availability Zone"))
image_name = tables.Column('image_name', verbose_name=_("Image Name"))
class Meta:
name = "instances"
verbose_name = _("Instances")

View File

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

View File

@ -1,12 +0,0 @@
from django.conf.urls import patterns
from django.conf.urls import url
from .views import IndexView
from openstack_dashboard.dashboards.devfreezer.freezerweb import views
urlpatterns = patterns('',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^\?tab=mypanel_tabs_tab$',
views.IndexView.as_view(), name='mypanel_tabs')
)

View File

@ -1,16 +0,0 @@
from horizon import views
from horizon import tabs
from openstack_dashboard.dashboards.devfreezer.freezerweb \
import tabs as mydashboard_tabs
class IndexView(tabs.TabbedTableView):
tab_group_class = mydashboard_tabs.MypanelTabs
# A very simple class-based view...
template_name = 'devfreezer/freezerweb/index.html'
def get_data(self, request, context, *args, **kwargs):
# Add data to the context here...
return context

View File

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

View File

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

19
freezer/dashboard.py Normal file
View File

@ -0,0 +1,19 @@
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

@ -0,0 +1,31 @@
# 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.
from django.utils.translation import ugettext_lazy as _
from horizon import browsers
from openstack_dashboard.dashboards.freezer.freezerpanel 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"

View File

@ -0,0 +1,13 @@
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

@ -0,0 +1,220 @@
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

@ -4,36 +4,34 @@ from horizon import exceptions
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.dashboards.devfreezer.freezerweb import tables
from openstack_dashboard.dashboards.freezer.freezerpanel import tables
class InstanceTab(tabs.TableTab):
name = _("Instances Tab")
class ContainerTab(tabs.TableTab):
name = _("Backups Tab")
slug = "instances_tab"
table_classes = (tables.InstancesTable,)
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_instances_data(self):
def get_containers_data(self):
try:
marker = self.request.GET.get(
tables.InstancesTable._meta.pagination_param, None)
instances, self._has_more = api.nova.server_list(
self.request,
search_opts={'marker': marker, 'paginate': True})
return instances
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 = (InstanceTab,)
sticky = True
tabs = (ContainerTab,)
sticky = True

View File

@ -0,0 +1,12 @@
{% 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

@ -0,0 +1,15 @@
{% 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

@ -0,0 +1,15 @@
{% 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 +1,7 @@
from horizon.test import helpers as test
class FreezerwebTests(test.TestCase):
# Unit tests for freezerweb.
class MypanelTests(test.TestCase):
# Unit tests for mypanel.
def test_me(self):
self.assertTrue(1 + 1 == 2)

View File

@ -0,0 +1,15 @@
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

@ -0,0 +1,98 @@
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

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

View File

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

View File

@ -6,6 +6,6 @@
{% block main %}
{% include "horizon/_messages.html" %}
{% block devfreezer_main %}{% endblock %}
{% block mydashboard_main %}{% endblock %}
{% endblock %}