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:
parent
c7f71ff70b
commit
bfb99920fe
|
@ -17,3 +17,10 @@ coverage.xml
|
|||
*.log
|
||||
.testrepository
|
||||
subunit.log
|
||||
|
||||
# Django files that get created during the test runs
|
||||
.secret_key_store
|
||||
*.lock
|
||||
|
||||
# Coverage data
|
||||
.coverage.*
|
||||
|
|
|
@ -32,6 +32,7 @@ if os.path.exists(os.path.join(possible_topdir, 'freezer', '__init__.py')):
|
|||
from freezer.apiclient.backups import BackupsManager
|
||||
from freezer.apiclient.registration import RegistrationManager
|
||||
from freezer.apiclient.actions import ActionManager
|
||||
from freezer.apiclient.configs import ConfigsManager
|
||||
import exceptions
|
||||
|
||||
|
||||
|
@ -57,6 +58,7 @@ class Client(object):
|
|||
self.backups = BackupsManager(self)
|
||||
self.registration = RegistrationManager(self)
|
||||
self.actions = ActionManager(self)
|
||||
self.configs = ConfigsManager(self)
|
||||
|
||||
def _update_api_endpoint(self):
|
||||
services = self.auth.services.list()
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
"""
|
||||
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.
|
||||
|
||||
This product includes cryptographic software written by Eric Young
|
||||
(eay@cryptsoft.com). This product includes software written by Tim
|
||||
Hudson (tjh@cryptsoft.com).
|
||||
========================================================================
|
||||
"""
|
||||
|
||||
import json
|
||||
import requests
|
||||
|
||||
from freezer.apiclient import exceptions
|
||||
|
||||
|
||||
class ConfigsManager(object):
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.endpoint = self.client.endpoint + '/v1/configs/'
|
||||
|
||||
@property
|
||||
def headers(self):
|
||||
return {'X-Auth-Token': self.client.auth_token}
|
||||
|
||||
def create(self, config_file):
|
||||
r = requests.post(self.endpoint,
|
||||
data=json.dumps(config_file),
|
||||
headers=self.headers)
|
||||
if r.status_code != 201:
|
||||
raise exceptions.ConfigCreationFailure(
|
||||
"[*] Error {0}".format(r.status_code))
|
||||
config_id = r.json()['config_id']
|
||||
return config_id
|
||||
|
||||
def delete(self, config_id):
|
||||
endpoint = self.endpoint + config_id
|
||||
r = requests.delete(endpoint, headers=self.headers)
|
||||
if r.status_code != 204:
|
||||
raise exceptions.ConfigDeleteFailure(
|
||||
"[*] Error {0}".format(r.status_code))
|
||||
|
||||
def list(self, limit=10, offset=0, search=None):
|
||||
data = json.dumps(search) if search else None
|
||||
query = {'limit': int(limit), 'offset': int(offset)}
|
||||
r = requests.get(self.endpoint, headers=self.headers,
|
||||
params=query, data=data)
|
||||
if r.status_code != 200:
|
||||
raise exceptions.ConfigGetFailure(
|
||||
"[*] Error {0}".format(r.status_code))
|
||||
|
||||
return r.json()['configs']
|
||||
|
||||
def get(self, config_id):
|
||||
endpoint = self.endpoint + config_id
|
||||
r = requests.get(endpoint, headers=self.headers)
|
||||
if r.status_code == 200:
|
||||
return r.json()
|
||||
if r.status_code == 404:
|
||||
return None
|
||||
raise exceptions.ConfigGetFailure(
|
||||
"[*] Error {0}".format(r.status_code))
|
||||
|
||||
def update(self, config_id, update_doc):
|
||||
endpoint = self.endpoint + config_id
|
||||
r = requests.patch(endpoint,
|
||||
headers=self.headers,
|
||||
data=json.dumps(update_doc))
|
||||
if r.status_code != 200:
|
||||
raise exceptions.ConfigUpdateFailure(
|
||||
"[*] Error {0}: {1}".format(r.status_code, r.text))
|
||||
return r.json()['version']
|
|
@ -60,3 +60,23 @@ class AuthFailure(ApiClientException):
|
|||
class MetadataUpdateFailure(ApiClientException):
|
||||
def __init__(self, r=''):
|
||||
super(self.__class__, self).__init__(r)
|
||||
|
||||
|
||||
class ConfigCreationFailure(ApiClientException):
|
||||
def __init__(self, r=''):
|
||||
super(self.__class__, self).__init__(r)
|
||||
|
||||
|
||||
class ConfigGetFailure(ApiClientException):
|
||||
def __init__(self, r=''):
|
||||
super(self.__class__, self).__init__(r)
|
||||
|
||||
|
||||
class ConfigDeleteFailure(ApiClientException):
|
||||
def __init__(self, r=''):
|
||||
super(self.__class__, self).__init__(r)
|
||||
|
||||
|
||||
class ConfigUpdateFailure(ApiClientException):
|
||||
def __init__(self, r=''):
|
||||
super(self.__class__, self).__init__(r)
|
||||
|
|
|
@ -22,6 +22,7 @@ Hudson (tjh@cryptsoft.com).
|
|||
from freezer_api.api.v1 import backups
|
||||
from freezer_api.api.v1 import clients
|
||||
from freezer_api.api.v1 import actions
|
||||
from freezer_api.api.v1 import configs
|
||||
from freezer_api.api.v1 import homedoc
|
||||
|
||||
VERSION = {
|
||||
|
@ -60,4 +61,9 @@ def public_endpoints(storage_driver):
|
|||
('/actions/{action_id}',
|
||||
actions.ActionsResource(storage_driver)),
|
||||
|
||||
('/configs',
|
||||
configs.ConfigsCollectionResource(storage_driver)),
|
||||
|
||||
('/configs/{config_id}',
|
||||
configs.ConfigsResource(storage_driver))
|
||||
]
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
"""
|
||||
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.
|
||||
|
||||
This product includes cryptographic software written by Eric Young
|
||||
(eay@cryptsoft.com). This product includes software written by Tim
|
||||
Hudson (tjh@cryptsoft.com).
|
||||
========================================================================
|
||||
"""
|
||||
|
||||
import falcon
|
||||
from freezer_api.common import exceptions
|
||||
|
||||
|
||||
class ConfigsCollectionResource(object):
|
||||
"""
|
||||
Handler for endpoint: /v1/configs
|
||||
"""
|
||||
|
||||
def __init__(self, storage_driver):
|
||||
self.db = storage_driver
|
||||
|
||||
def on_get(self, req, resp):
|
||||
user_id = req.get_header('X-User-ID') or ''
|
||||
offset = req.get_param('offset') or ''
|
||||
limit = req.get_param_as_int('limit') or 10
|
||||
search = req.context.get('doc', {})
|
||||
obj_list = self.db.get_config(user_id=user_id, offset=offset,
|
||||
limit=limit, search=search)
|
||||
req.context['result'] = {'configs': obj_list}
|
||||
|
||||
def on_post(self, req, resp):
|
||||
try:
|
||||
doc = req.context['doc']
|
||||
except KeyError:
|
||||
raise exceptions.BadDataFormat(
|
||||
message='Missing request body',
|
||||
resp_body={'error': 'missing request body'})
|
||||
|
||||
user_name = req.get_header('X-User-Name')
|
||||
user_id = req.get_header('X-User-ID')
|
||||
config_id = self.db.add_config(
|
||||
user_id=user_id, user_name=user_name, doc=doc)
|
||||
resp.status = falcon.HTTP_201
|
||||
req.context['result'] = {'config_id': config_id}
|
||||
|
||||
|
||||
class ConfigsResource(object):
|
||||
"""
|
||||
Handler for endpoint: /v1/configs/{config_id}
|
||||
"""
|
||||
def __init__(self, storage_driver):
|
||||
self.db = storage_driver
|
||||
|
||||
def on_get(self, req, resp, config_id):
|
||||
user_id = req.get_header('X-User-ID')
|
||||
obj = self.db.get_config(user_id=user_id, config_id=config_id)
|
||||
req.context['result'] = obj
|
||||
|
||||
def on_delete(self, req, resp, config_id):
|
||||
user_id = req.get_header('X-User-ID')
|
||||
self.db.delete_config(
|
||||
user_id=user_id, config_id=config_id)
|
||||
req.context['result'] = {'config_id': config_id}
|
||||
resp.status = falcon.HTTP_204
|
||||
|
||||
def on_patch(self, req, resp, config_id):
|
||||
# PATCH /v1/configs/{config_id}
|
||||
user_id = req.get_header('X-User-ID') or ''
|
||||
patch = req.context.get('doc', {})
|
||||
new_version = self.db.update_config(user_id=user_id,
|
||||
config_id=config_id,
|
||||
patch=patch)
|
||||
req.context['result'] = {'config_id': config_id,
|
||||
'patch': patch,
|
||||
'version': new_version}
|
|
@ -19,6 +19,8 @@ Hudson (tjh@cryptsoft.com).
|
|||
========================================================================
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
class BackupMetadataDoc:
|
||||
"""
|
||||
|
@ -66,3 +68,44 @@ class BackupMetadataDoc:
|
|||
self.data['timestamp'],
|
||||
self.data['level']
|
||||
)
|
||||
|
||||
|
||||
class ConfigDoc:
|
||||
"""
|
||||
Wraps a config_file dict and adds some utility methods,
|
||||
and fields
|
||||
"""
|
||||
def __init__(self, user_id='', user_name='', data={}):
|
||||
self.user_id = user_id
|
||||
self.user_name = user_name
|
||||
self.data = data
|
||||
# self.id = str(uuid.uuid4().hex)
|
||||
|
||||
def is_valid(self):
|
||||
try:
|
||||
assert (self.config_id is not '')
|
||||
assert (self.user_id is not '')
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
|
||||
def serialize(self):
|
||||
return {'config_id': self.config_id,
|
||||
'user_id': self.user_id,
|
||||
'user_name': self.user_name,
|
||||
'config_file': self.data}
|
||||
|
||||
@staticmethod
|
||||
def un_serialize(d):
|
||||
return ConfigDoc(
|
||||
user_id=d['user_id'],
|
||||
user_name=d['user_name'],
|
||||
data=d['config_file'])
|
||||
|
||||
@property
|
||||
def config_set_id(self):
|
||||
return {'config_id': str(uuid.uuid4().hex)}
|
||||
|
||||
@property
|
||||
def config_id(self):
|
||||
return str(uuid.uuid4().hex)
|
||||
|
|
|
@ -22,6 +22,7 @@ Hudson (tjh@cryptsoft.com).
|
|||
import elasticsearch
|
||||
import logging
|
||||
from freezer_api.common.utils import BackupMetadataDoc
|
||||
from freezer_api.common.utils import ConfigDoc
|
||||
from freezer_api.common import exceptions
|
||||
|
||||
|
||||
|
@ -168,6 +169,39 @@ class ActionTypeManager(TypeManager):
|
|||
return version
|
||||
|
||||
|
||||
class ConfigTypeManager(TypeManager):
|
||||
def __init__(self, es, doc_type, index='freezer'):
|
||||
TypeManager.__init__(self, es, doc_type, index=index)
|
||||
|
||||
@staticmethod
|
||||
def get_search_query(user_id, doc_id, search={}):
|
||||
base_filter = TypeManager.get_base_search_filter(user_id, search)
|
||||
if doc_id is not None:
|
||||
base_filter.append({"term": {"config_id": doc_id}})
|
||||
query_filter = {"filter": {"bool": {"must": base_filter}}}
|
||||
return {'query': {'filtered': query_filter}}
|
||||
|
||||
def update(self, config_id, config_update_doc):
|
||||
update_doc = {'doc': config_update_doc}
|
||||
try:
|
||||
print config_update_doc
|
||||
res = self.es.update(index=self.index,
|
||||
doc_type=self.doc_type,
|
||||
id=config_id,
|
||||
body=update_doc)
|
||||
print 'here?'
|
||||
version = res['_version']
|
||||
except elasticsearch.TransportError as error:
|
||||
raise exceptions.DocumentNotFound(
|
||||
message='Unable to find configuration file to update '
|
||||
'with ID {0}'.format(config_id))
|
||||
except Exception as error:
|
||||
raise exceptions.StorageEngineError(
|
||||
message='Unable to update configuration file, '
|
||||
'config ID {0}'.format(config_id))
|
||||
return version
|
||||
|
||||
|
||||
class ElasticSearchEngine(object):
|
||||
|
||||
def __init__(self, hosts, index='freezer'):
|
||||
|
@ -177,6 +211,7 @@ class ElasticSearchEngine(object):
|
|||
self.backup_manager = BackupTypeManager(self.es, 'backups')
|
||||
self.client_manager = ClientTypeManager(self.es, 'clients')
|
||||
self.action_manager = ActionTypeManager(self.es, 'actions')
|
||||
self.config_manager = ConfigTypeManager(self.es, 'configs')
|
||||
|
||||
def get_backup(self, user_id, backup_id=None, offset=0, limit=10, search={}):
|
||||
return self.backup_manager.search(user_id,
|
||||
|
@ -271,4 +306,48 @@ class ElasticSearchEngine(object):
|
|||
format(action_id, version))
|
||||
return version
|
||||
|
||||
def add_config(self, user_id, user_name, doc):
|
||||
config_doc = ConfigDoc(user_id, user_name, doc)
|
||||
config_doc = config_doc.serialize()
|
||||
config_id = config_doc['config_id']
|
||||
|
||||
if config_id is None:
|
||||
raise exceptions.BadDataFormat(message='Missing config ID')
|
||||
|
||||
if not self.config_manager.insert(config_doc,
|
||||
doc_id=config_id):
|
||||
raise exceptions.StorageEngineError(
|
||||
message='Index operation failed, '
|
||||
' config ID: {0}'.format(config_id))
|
||||
logging.info('Config registered, config ID: {0}'.
|
||||
format(config_id))
|
||||
return config_id
|
||||
|
||||
def delete_config(self, user_id, config_id):
|
||||
return self.config_manager.delete(user_id, config_id)
|
||||
|
||||
def get_config(self, user_id, config_id=None,
|
||||
offset=0, limit=10, search={}):
|
||||
return self.config_manager.search(user_id,
|
||||
config_id,
|
||||
search=search,
|
||||
offset=offset,
|
||||
limit=limit)
|
||||
|
||||
def update_config(self, user_id, config_id, patch,
|
||||
offset=0, limit=10, search={}):
|
||||
|
||||
if 'config_id' in patch:
|
||||
raise exceptions.BadDataFormat(
|
||||
message='Config ID modification is not allowed, '
|
||||
' config ID: {0}'.format(config_id))
|
||||
config_doc = self.config_manager.search(user_id,
|
||||
config_id,
|
||||
search=search,
|
||||
offset=offset,
|
||||
limit=limit)[0]
|
||||
config_doc['config_file'].update(patch)
|
||||
version = self.config_manager.update(config_id, config_doc)
|
||||
logging.info('Configuration file {0} updated to version {1}'.
|
||||
format(config_id, version))
|
||||
return version
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'jonas'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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)
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
/* Additional CSS for mydashboard. */
|
|
@ -1 +0,0 @@
|
|||
/* Additional JavaScript for mydashboard. */
|
|
@ -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)
|
|
@ -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")
|
|
@ -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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'jonas'
|
|
@ -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
|
|
@ -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
|
|
@ -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"),
|
||||
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'jonas'
|
|
@ -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"
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
|
@ -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)
|
|
@ -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
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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')
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'jonas'
|
|
@ -0,0 +1,3 @@
|
|||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
|
@ -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)
|
|
@ -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 %}
|
|
@ -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'),
|
||||
)
|
|
@ -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")
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
}());
|
|
@ -0,0 +1 @@
|
|||
__author__ = 'jonas'
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"
|
|
@ -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)),
|
||||
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue