Improved horizon dashboard for freezer

action, job, session, backup, clients are now objects in api.py
shield decorator to avoid having boilerplate code in views
move from freezer_ui to disaster_recovery url
improved actions edition in a job
javascript lint
added clients panel
added actions panel
improved restore functionality from the dashboard

Change-Id: I23aed516bcde3a40b24144b05f858d1e3a49a796
changes/75/236175/40
memo 8 years ago
parent 26f1deb396
commit b08558eba4

@ -2,6 +2,13 @@
Freezer - Horizon Dashboard
===========================
freezer-web-ui is a horizon plugin based in django aimed at providing an interaction
with freezer
* Release management: https://launchpad.net/freezer-web-ui
* Blueprints and feature specifications: https://blueprints.launchpad.net/freezer-web-ui
* Issue tracking: https://bugs.launchpad.net/freezer-web-ui
Requirements
============
@ -73,39 +80,8 @@ To deploy freezer dashboard in production you need to do the following::
A new tab called "Disaster Recovery" will be on your panels.
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
Tox
---
===
1. Run tox::

@ -13,12 +13,12 @@
# limitations under the License.
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
DASHBOARD = 'freezer_ui'
DASHBOARD = 'disaster_recovery'
# If set to True, this dashboard will not be added to the settings.
DISABLED = False
# A list of applications to be added to INSTALLED_APPS.
ADD_INSTALLED_APPS = [
'freezer_ui',
'disaster_recovery',
]

@ -15,7 +15,7 @@ where::
For example::
enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
For more information, see:
http://docs.openstack.org/developer/devstack/plugins.html

@ -1,7 +1,7 @@
[[local|localrc]]
disable_all_services
enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
enable_service rabbit mysql key

@ -20,11 +20,11 @@ FREEZER_WEB_UI_DIR=$DEST/freezer-web-ui
FREEZER_WEB_UI_FILES=${FREEZER_WEB_UI_DIR}/devstack/files
# Freezer Web UI repository
FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/stackforge/freezer-web-ui.git}
FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/openstack/freezer-web-ui.git}
FREEZER_WEB_UI_BRANCH=${FREEZER_WEB_UI_BRANCH:-master}
# Freezer client
FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/stackforge/freezer.git}
FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/openstack/freezer.git}
FREEZER_CLIENT_DIR=$DEST/freezer
FREEZER_CLIENT_BRANCH=${FREEZER_CLIENT_BRANCH:-master}

@ -0,0 +1,27 @@
# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
#
# 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 disaster_recovery import dashboard
class ActionsPanel(horizon.Panel):
name = _("Actions")
slug = "actions"
dashboard.Freezer.register(ActionsPanel)

@ -0,0 +1,105 @@
# 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 import shortcuts
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from horizon.utils.urlresolvers import reverse
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
class DeleteAction(tables.DeleteAction):
name = "delete"
classes = ("btn-danger",)
icon = "remove"
help_text = _("Delete actions is not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Action",
u"Delete Actions",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Action",
u"Deleted Actions",
count
)
def delete(self, request, action_id):
freezer_api.Action(request).delete(action_id)
# TODO(m3m0): this shouldnt redirect here when an action is deleted
# from jobs views
return shortcuts.redirect('horizon:disaster_recovery:actions:index')
class DeleteMultipleActions(DeleteAction):
name = "delete_multiple_actions"
class BackupFilter(tables.FilterAction):
filter_type = "server"
filter_choices = (("contains", "Contains text", True),)
class CreateAction(tables.LinkAction):
name = "create_action"
verbose_name = _("Create Action")
url = "horizon:disaster_recovery:actions:create"
classes = ("ajax-modal",)
icon = "plus"
class EditAction(tables.LinkAction):
name = "edit_action"
verbose_name = _("Edit")
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, datum=None):
return reverse("horizon:disaster_recovery:actions:create",
kwargs={'action_id': datum.action_id})
def get_link(action):
return reverse('horizon:disaster_recovery:actions:action',
kwargs={'action_id': action.id})
class ActionsTable(tables.DataTable):
backup_name = tables.Column('backup_name',
verbose_name=_("Action Name"),
link=get_link)
action = tables.Column('action', verbose_name=_("Action"))
path_to_backup = tables.Column('path_to_backup',
verbose_name=_("Path To Backup or Restore"))
storage = tables.Column('storage', verbose_name=_("Storage"))
class Meta:
name = "actions_table"
verbose_name = _("Actions")
row_actions = (EditAction, DeleteAction,)
table_actions = (BackupFilter, CreateAction, DeleteMultipleActions)
multi_select = True

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

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

@ -0,0 +1,33 @@
# (c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P.
#
# 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 disaster_recovery.actions import views
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/(?P<action_id>[^/]+)?$',
views.ActionWorkflowView.as_view(),
name='create'),
url(r'^action/(?P<action_id>[^/]+)?$',
views.ActionView.as_view(),
name='action'),
)

@ -0,0 +1,76 @@
# 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
import pprint
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import tables
from horizon import workflows
import disaster_recovery.api.api as freezer_api
from disaster_recovery.actions import tables as freezer_tables
from disaster_recovery.actions.workflows import action as action_workflow
from disaster_recovery.utils import shield
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
name = _("Actions")
slug = "actions"
table_class = freezer_tables.ActionsTable
template_name = "disaster_recovery/actions/index.html"
@shield("Unable to get actions", redirect="actions:index")
def get_data(self):
filters = self.table.get_filter_string() or None
return freezer_api.Action(self.request).list(search=filters)
class ActionView(generic.TemplateView):
template_name = 'disaster_recovery/actions/detail.html'
@shield('Unable to get action', redirect='actions:index')
def get_context_data(self, **kwargs):
action = freezer_api.Action(self.request).get(kwargs['action_id'],
json=True)
return {'data': pprint.pformat(action)}
class ActionWorkflowView(workflows.WorkflowView):
workflow_class = action_workflow.ActionWorkflow
success_url = reverse_lazy("horizon:disaster_recovery:actions:index")
def is_update(self):
return 'action_id' in self.kwargs and bool(self.kwargs['action_id'])
@shield("Unable to get job", redirect="jobs:index")
def get_initial(self):
initial = super(ActionWorkflowView, self).get_initial()
if self.is_update():
initial.update({'action_id': None})
action = freezer_api.Action(self.request).get(
self.kwargs['action_id'], json=True)
initial.update(**action['freezer_action'])
initial.update({
"mandatory": action['mandatory'],
"max_retries": action['max_retries'],
"max_retries_interval": action['max_retries_interval']
})
initial.update({'action_id': action['action_id']})
return initial

@ -14,17 +14,24 @@
import logging
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from horizon import workflows
from horizon import exceptions
from horizon import forms
import freezer_ui.api.api as freezer_api
from horizon import workflows
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
class ActionConfigurationAction(workflows.Action):
action_id = forms.CharField(
widget=forms.HiddenInput(),
required=False)
action = forms.ChoiceField(
help_text=_("Set the action to be taken"),
required=True)
@ -33,14 +40,6 @@ class ActionConfigurationAction(workflows.Action):
help_text=_("Choose what you want to backup"),
required=False)
original_name = forms.CharField(
widget=forms.HiddenInput(),
required=False)
action_id = forms.CharField(
widget=forms.HiddenInput(),
required=False)
storage = forms.ChoiceField(
help_text=_("Set storage backend for a backup"),
required=True)
@ -224,15 +223,16 @@ class ActionConfigurationAction(workflows.Action):
class Meta(object):
name = _("Action")
help_text_template = "freezer_ui/jobs" \
help_text_template = "disaster_recovery/jobs" \
"/_action.html"
class ActionConfiguration(workflows.Step):
action_class = ActionConfigurationAction
contributes = ('action',
contributes = ('action_id',
'action',
'mode',
'original_name',
'storage',
'backup_name',
'mysql_conf',
'sql_server_conf',
@ -247,11 +247,9 @@ class ActionConfiguration(workflows.Step):
'dst_file',
'remove_older_than',
'remove_from_date',
'action_id',
'storage',
'ssh_host',
'ssh_key',
'ssh_username',
'ssh_key')
'ssh_host')
class SnapshotConfigurationAction(workflows.Action):
@ -270,15 +268,6 @@ class SnapshotConfigurationAction(workflows.Action):
widget=forms.CheckboxInput(),
required=False)
vssadmin = forms.BooleanField(
label=_("VSSAdmin"),
help_text=_("Create a backup using a snapshot on windows "
"using vssadmin. Options are: "
"True and False, default is True"),
widget=forms.CheckboxInput(),
initial=True,
required=False)
lvm_auto_snap = forms.CharField(
label=_("LVM Auto Snapshot"),
help_text=_("Automatically guess the volume group and "
@ -321,7 +310,7 @@ class SnapshotConfigurationAction(workflows.Action):
class Meta(object):
name = _("Snapshot")
help_text_template = "freezer_ui/jobs" \
help_text_template = "disaster_recovery/jobs" \
"/_snapshot.html"
@ -329,7 +318,6 @@ class SnapshotConfiguration(workflows.Step):
action_class = SnapshotConfigurationAction
contributes = ('use_snapshot',
'is_windows',
'vssadmin',
'lvm_auto_snap',
'lvm_srcvol',
'lvm_snapname',
@ -374,7 +362,6 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Upload bandwidth limit in Bytes per sec."
" Can be invoked with dimensions "
"(10K, 120M, 10G)."),
initial=-1,
min_value=-1,
required=False)
@ -383,17 +370,9 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Download bandwidth limit in Bytes per sec. "
"Can be invoked with dimensions"
" (10K, 120M, 10G)."),
initial=-1,
min_value=-1,
required=False)
optimize = forms.ChoiceField(
choices=[('speed', _("Speed (tar)")),
('bandwidth', _("Bandwidth/Space (rsync)"))],
help_text="",
label=_('Optimize For...'),
required=False)
compression = forms.ChoiceField(
choices=[
('gzip', _("Minimum Compression (GZip/Zip/Zlib)")),
@ -409,7 +388,6 @@ class AdvancedConfigurationAction(workflows.Action):
help_text=_("Set the maximum file chunk size in bytes"
" to upload to swift."
" Default 67108864 bytes (64MB)"),
initial=67108864,
min_value=1,
required=False)
@ -453,7 +431,6 @@ class AdvancedConfigurationAction(workflows.Action):
always_level = forms.IntegerField(
label=_("Always Level"),
initial=0,
min_value=0,
help_text=_("Set backup maximum level used with tar to"
" implement incremental backup. If a level "
@ -467,7 +444,6 @@ class AdvancedConfigurationAction(workflows.Action):
restart_always_level = forms.IntegerField(
label=_("Restart Always Level"),
initial=0,
min_value=0,
help_text=_("Restart the backup from level 0 after n days. "
"Valid only if --always-level option if set. "
@ -526,30 +502,29 @@ class AdvancedConfigurationAction(workflows.Action):
class Meta(object):
name = _("Advanced")
help_text_template = "freezer_ui/jobs" \
help_text_template = "disaster_recovery/jobs" \
"/_advanced.html"
class AdvancedConfiguration(workflows.Step):
action_class = AdvancedConfigurationAction
contributes = ('exclude',
# 'log_file',
# 'proxy',
# 'os_auth_ver',
# 'upload_limit',
# 'download_limit',
# 'optimize',
# 'compression',
# 'max_segment_size',
'log_file',
'proxy',
'os_auth_ver',
'upload_limit',
'download_limit',
'compression',
'max_segment_size',
'hostname',
'encryption_password',
'no_incremental',
'max_level',
'always_level',
'restart_always_level',
# 'insecure',
'insecure',
'dereference_symlink',
# 'dry_run',
'dry_run',
'max_priority',
'quiet',)
@ -579,7 +554,7 @@ class RulesConfigurationAction(workflows.Action):
class Meta(object):
name = _("Rules")
help_text_template = "freezer_ui/jobs" \
help_text_template = "disaster_recovery/jobs" \
"/_rules.html"
@ -590,13 +565,13 @@ class RulesConfiguration(workflows.Step):
'mandatory')
class ConfigureAction(workflows.Workflow):
class ActionWorkflow(workflows.Workflow):
slug = "action"
name = _("Action Configuration")
finalize_button_name = _("Save")
success_message = _('Action file saved correctly.')
failure_message = _('Unable to save action file.')
success_url = "horizon:freezer_ui:jobs:index"
success_url = "horizon:disaster_recovery:actions:index"
default_steps = (ActionConfiguration,
SnapshotConfiguration,
@ -607,18 +582,26 @@ class ConfigureAction(workflows.Workflow):
try:
if context['is_windows']:
client_os = 'Windows'
context.pop('is_windows')
else:
client_os = 'Linux'
context.pop('is_windows')
if context['use_snapshot'] and client_os == 'Windows':
context['vssadmin'] = True
context.pop('use_snapshot')
else:
context['vssadmin'] = False
context.pop('use_snapshot')
if context['action_id'] == '':
return freezer_api.action_create(request, context)
freezer_api.Action(request).create(context)
else:
return freezer_api.action_update(request, context)
freezer_api.Action(request).update(context,
context['action_id'])
return shortcuts.redirect('horizon:disaster_recovery:'
'actions:index')
except Exception:
exceptions.handle(request)
return False

@ -0,0 +1,522 @@
# 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 disaster_recovery client functionality
# from horizon.
import logging
from django.conf import settings
from horizon.utils.memoized import memoized # noqa
import freezer.apiclient.client
from disaster_recovery import utils
LOG = logging.getLogger(__name__)
@memoized
def client(request):
"""Return a freezer client object"""
api_url = _get_service_url(request)
return freezer.apiclient.client.Client(
token=request.user.token.id,
auth_url=getattr(settings, 'OPENSTACK_KEYSTONE_URL'),
endpoint=api_url)
@memoized
def _get_service_url(request):
"""Get freezer api url"""
catalog = (getattr(request.user, "service_catalog", None))
if not catalog:
return _get_hardcoded_url()
for c in catalog:
if c['name'] == 'freezer':
for e in c['endpoints']:
return e['internalURL']
else:
return _get_hardcoded_url()
@memoized
def _get_hardcoded_url():
"""In case freezer is not registered in keystone catalog, look for it in
local_settings.py
:return: freezer_api_url
"""
try:
LOG.warn('Using hardcoded FREEZER_API_URL at {0}'
.format(settings.FREEZER_API_URL))
return getattr(settings, 'FREEZER_API_URL', None)
except Exception:
LOG.warn('No FREEZER_API_URL was found in local_settings.py')
raise
class Job(object):
def __init__(self, request):
self.request = request
self.client = client(request)
def list(self, json=False, limit=100, offset=0, search=None):
if search:
search = {"match": [{"_all": search}, ], }
jobs = self.client.jobs.list_all(limit=limit,
offset=offset,
search=search)
if json:
return jobs
return [utils.JobObject(
job.get('job_id'),
job.get('description'),
job.get('job_schedule', {}).get('result'),
job.get('job_schedule', {}).get('event'),
) for job in jobs]
def get(self, job_id, json=False):
job = self.client.jobs.get(job_id)
if json:
return job
return utils.JobObject(
job.get('job_id'),
job.get('description'),
job.get('job_schedule', {}).get('result'),
job.get('job_schedule', {}).get('event'))
def create(self, job):
return self._build(job)
def update(self, job_id, job):
scheduling = {}
try:
if job['schedule_end_date'] != '':
utils.assign_and_remove(job, scheduling, 'schedule_end_date')
else:
job.pop('schedule_end_date')
except KeyError:
pass
try:
if job['schedule_interval'] != '':
utils.assign_and_remove(job, scheduling, 'schedule_interval')
else:
job.pop('schedule_interval')
except KeyError:
pass
try:
if job['schedule_start_date'] != '':
utils.assign_and_remove(job, scheduling, 'schedule_start_date')
else:
job.pop('schedule_start_date')
except KeyError:
pass
job.pop('job_actions', [])
job.pop('clients', None)
job.pop('actions', None)
job.pop('job_id')
job['job_schedule'] = scheduling
return self.client.jobs.update(job_id, job)
def update_actions(self, job_id, action_ids):
ids = utils.get_action_ids(action_ids)
job = self.get(job_id, json=True)
job.pop('job_actions', None)
actions = self._get_actions_in_job(ids)
job['job_actions'] = actions
return self.client.jobs.update(job_id, job)
def delete(self, job_id):
return self.client.jobs.delete(job_id)
def actions(self, job_id, api=False):
job = self.get(job_id, json=True)
if not job:
return []
if api:
return job.get('job_actions', [])
return [utils.ActionObject(
action_id=a.get('action_id'),
action=a.get('freezer_action', {}).get('action'),
backup_name=a.get('freezer_action', {}).get('backup_name'),
job_id=job_id
) for a in job.get('job_actions')]
def delete_action(self, ids):
action_id, job_id = ids.split('===')
job = self.get(job_id, json=True)
for action in job['job_actions']:
if action.get('action_id') == action_id:
job.get('job_actions').remove(action)
return self.client.jobs.update(job_id, job)
def clone(self, job_id):
job = self.get(job_id, json=True)
job['description'] = '{0}_clone'.format(job['description'])
job.pop('job_id', None)
job.pop('_version', None)
job_id = self.client.jobs.create(job)
return self.stop(job_id)
def stop(self, job_id):
return self.client.jobs.stop_job(job_id)
def start(self, job_id):
return self.client.jobs.start_job(job_id)
def _build(self, job):
action_ids = utils.get_action_ids(job.pop('actions'))
job = utils.create_dict(**job)
clients = job.pop('clients', [])
scheduling = {}
new_job = {}
utils.assign_and_remove(job, scheduling, 'schedule_start_date')
utils.assign_and_remove(job, scheduling, 'schedule_interval')
utils.assign_and_remove(job, scheduling, 'schedule_end_date')
actions = self._get_actions_in_job(action_ids)
new_job['description'] = job.get('description')
new_job['job_actions'] = actions
new_job['job_schedule'] = scheduling
for client_id in clients:
search = client_id
client = Client(self.request).list(search=search)
new_job['client_id'] = client[0].id
job_id = self.client.jobs.create(new_job)
self.stop(job_id)
return True
def _get_actions_in_job(self, action_ids):
actions_in_job = []
for action_id in action_ids:
action = Action(self.request).get(action_id, json=True)
a = {
'action_id': action['action_id'],
'freezer_action': action['freezer_action']
}
actions_in_job.append(a)
return actions_in_job
class Session(object):
def __init__(self, request):
self.request = request
self.client = client(request)
def list(self, json=False, limit=30, offset=0, search=None):
if search:
search = {"match": [{"_all": search}, ], }
sessions = self.client.sessions.list_all(limit=limit,
offset=offset,
search=search)
if json:
return sessions
return [utils.SessionObject(
session.get('session_id'),
session.get('description'),
session.get('status'),
session.get('jobs'),
session.get('schedule', {}).get('schedule_start_date'),
session.get('schedule', {}).get('schedule_interval'),
session.get('schedule', {}).get('schedule_end_date')
) for session in sessions]
def get(self, session_id, json=False):
session = self.client.sessions.get(session_id)
if json:
return session
return utils.SessionObject(
session.get('session_id'),
session.get('description'),
session.get('status'),
session.get('jobs'),
session.get('schedule', {}).get('schedule_start_date'),
session.get('schedule', {}).get('schedule_interval'),
session.get('schedule', {}).get('schedule_end_date'))
def create(self, session):
return self._build(session)
def update(self, session, session_id):
return self.client.sessions.update(session_id, session)
def delete(self, session_id):
return self.client.sessions.delete(session_id)
def remove_job(self, session_id, job_id):
try:
# even if the job is removed from the session the api returns an
# error.
return self.client.sessions.remove_job(session_id, job_id)
except Exception:
pass
def add_job(self, session_id, job_id):
return self.client.sessions.add_job(session_id, job_id)
def jobs(self, session_id):
session = self.get(session_id, json=True)
jobs = []
try:
jobs = [utils.JobsInSessionObject(k,
session_id,
v['client_id'],
v['result'])
for k, v in session['jobs'].iteritems()]
except AttributeError:
pass
return jobs
def _build(self, session):
session = utils.create_dict(**session)
scheduling = {}
utils.assign_and_remove(session, scheduling, 'schedule_start_date')
utils.assign_and_remove(session, scheduling, 'schedule_interval')
utils.assign_and_remove(session, scheduling, 'schedule_end_date')
session['jobs'] = {}
session['schedule'] = scheduling
return self.client.sessions.create(session)
class Action(object):
def __init__(self, request):
self.request = request
self.client = client(request)
def list(self, json=False, limit=100, offset=0, search=None):
if search:
search = {"match": [{"_all": search}, ], }
actions = self.client.actions.list(limit=limit,
offset=offset,
search=search)
if json:
return actions
return [utils.ActionObjectDetail(
action.get('action_id'),
action['freezer_action'].get('action'),
action['freezer_action'].get('backup_name'),
action['freezer_action'].get('path_to_backup')
or action['freezer_action'].get('restore_abs_path'),
action['freezer_action'].get('storage'),
) for action in actions]
def get(self, job_id, json=False):
action = self.client.actions.get(job_id)
if json:
return action
return utils.ActionObjectDetail(
action.get('action_id'),
action['freezer_action'].get('action'),
action['freezer_action'].get('backup_name'),
action['freezer_action'].get('path_to_backup'),
action['freezer_action'].get('storage'))
def create(self, action):
return self._build(action)
def update(self, action, action_id):
updated_action = {}
updated_action['freezer_action'] = utils.create_dict(**action)
try:
if action['mandatory'] != '':
updated_action['mandatory'] = action['mandatory']
except KeyError:
pass
try:
if action['max_retries'] != '':
updated_action['max_retries'] = action['max_retries']
except KeyError:
pass
try:
if action['max_retries_interval'] != '':
updated_action['max_retries_interval'] =\
action['max_retries_interval']
except KeyError:
pass
return self.client.actions.update(action_id, updated_action)
def delete(self, action_id):
return self.client.actions.delete(action_id)
def _build(self, action):
"""Get a flat action dict and convert it to a freezer action format
"""
action_rules = {}
utils.assign_and_remove(action, action_rules, 'max_retries')
utils.assign_and_remove(action, action_rules, 'max_retries_interval')
utils.assign_and_remove(action, action_rules, 'mandatory')
action = utils.create_dict(**action)
action = {'freezer_action': action}
return self.client.actions.create(action)
class Client(object):
def __init__(self, request):
self.request = request
self.client = client(request)
def list(self, json=False, limit=100, offset=0, search=None):
if search:
search = {"match": [{"_all": search}, ], }
clients = self.client.registration.list(limit=limit,
offset=offset,
search=search)
if json:
return clients
return [utils.ClientObject(
c.get('client', {}).get('hostname'),
c.get('client', {}).get('client_id'),
c.get('uuid')
) for c in clients]
def get(self, client_id, json=False):
c = self.client.registration.get(client_id)
if json:
return c
return utils.ClientObject(
c.get('client', {}).get('hostname'),
c.get('client', {}).get('client_id'),
c.get('uuid'))
def delete(self, client_id):
return self.client.registration.delete(client_id)
class Backup(object):
def __init__(self, request):
self.request = request
self.client = client(request)
def list(self, json=False, limit=30, offset=0, search=None):
if search:
search = {"match": [{"_all": search}, ], }
backups = self.client.backups.list(limit=limit,
offset=offset,
search=search)
if json:
return backups
return [utils.BackupObject(
backup_id=b.get('backup_uuid'),
action=b.get('backup_metadata', {}).get('action'),
time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
backup_name=b.get('backup_metadata', {}).get('backup_name'),
backup_media=b.get('backup_metadata', {}).get('backup_media'),
path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
hostname=b.get('backup_metadata', {}).get('hostname'),
container=b.get('backup_metadata', {}).get('container'),
level=b.get('backup_metadata', {}).get('level'),
curr_backup_level=b.get('backup_metadata', {}).get(
'curr_backup_level'),
encrypted=b.get('backup_metadata', {}).get('encrypted'),
total_broken_links=b.get('backup_metadata', {}).get(
'total_broken_links'),
excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
) for b in backups]
def get(self, backup_id, json=False):
search = {"match": [{"backup_uuid": backup_id}, ], }
b = self.client.backups.list(limit=1, search=search)
b = b[0]
if json:
return b
return utils.BackupObject(
backup_id=b.get('backup_uuid'),
action=b.get('backup_metadata', {}).get('action'),
time_stamp=b.get('backup_metadata', {}).get('time_stamp'),
backup_name=b.get('backup_metadata', {}).get('backup_name'),
backup_media=b.get('backup_metadata', {}).get('backup_media'),
path_to_backup=b.get('backup_metadata', {}).get('path_to_backup'),
hostname=b.get('backup_metadata', {}).get('hostname'),
container=b.get('backup_metadata', {}).get('container'),
level=b.get('backup_metadata', {}).get('level'),
curr_backup_level=b.get('backup_metadata', {}).get(
'curr_backup_level'),
encrypted=b.get('backup_metadata', {}).get('encrypted'),
total_broken_links=b.get('backup_metadata', {}).get(
'total_broken_links'),
excluded_files=b.get('backup_metadata', {}).get('excluded_files'),
)
def restore(self, data):
backup = self.get(data['backup_id'])
client_id = data['client']
name = "Restore job for {0}".format(client_id)
# TODO(m3m0): change storage to be flexible
action = {
'action': 'restore',
'backup_name': backup.backup_name,
'restore_abs_path': data['path'],
'container': backup.container,
'restore_from_host': backup.hostname,
'storage': 'local'
}
action_id = Action(self.request).create(action)
job = {
'job_actions': [{
'action_id': action_id,
'freezer_action': action
}],
'client_id': client_id,
'description': name,
'job_schedule': {}
}
job_id = self.client.jobs.create(job)
return Job(self.request).start(job_id)

@ -21,7 +21,7 @@ from django.views import generic
from openstack_dashboard.api.rest import utils as rest_utils
from openstack_dashboard.api.rest.utils import JSONResponse
import freezer_ui.api.api as freezer_api
import disaster_recovery.api.api as freezer_api
# https://github.com/tornadoweb/tornado/issues/1009
@ -42,38 +42,52 @@ class Clients(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request):
def get(self, request, job_id=None):
"""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_json(request)
clients = freezer_api.Client(request).list(json=True)
clients = json.dumps(clients)
return HttpResponse(clients,
content_type="application/json")
return HttpResponse(clients, content_type="application/json")
class Actions(generic.View):
"""API for clients"""
class ActionList(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request):
"""Get all registered freezer actions"""
actions = freezer_api.action_list_json(request)
actions = json.dumps(actions)
return HttpResponse(actions,
content_type="application/json")
actions = freezer_api.Action(request).list(json=True)
actions = json.dumps(actions)
return HttpResponse(actions, content_type="application/json")
class ActionsInJob(generic.View):
"""API for actions in a job"""
class Actions(generic.View):
@prevent_json_hijacking
@rest_utils.ajax()
def get(self, request, job_id=None):
"""Get all registered freezer actions"""
actions = freezer_api.actions_in_job_json(request, job_id)
actions = freezer_api.Action(request).list(json=True)
actions_in_job = freezer_api.Job(request).actions(job_id, api=True)
action_ids = [a['action_id'] for a in actions]
actions_in_job_ids = [a['action_id'] for a in actions_in_job]
available = set.difference(set(action_ids), set(actions_in_job_ids))
selected = set.intersection(set(action_ids), set(actions_in_job_ids))
available_actions = []
for action in actions:
if action['action_id'] in available:
available_actions.append(action)
selected_actions = []
for action in actions_in_job:
if action['action_id'] in selected:
selected_actions.append(action)
actions = {'available': available_actions,
'selected': selected_actions}
actions = json.dumps(actions)
return HttpResponse(actions,
content_type="application/json")
return HttpResponse(actions, content_type="application/json")

@ -24,7 +24,7 @@ import rest_api
urlpatterns = patterns(
'',
url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"),
url(r'^api/actions/$', rest_api.Actions.as_view(), name="api_actions"),
url(r'^api/actions$', rest_api.ActionList.as_view(), name="api_actions"),
url(r'^api/actions/job/(?P<job_id>[^/]+)?$',
rest_api.ActionsInJob.as_view(), name="api_actions_in_job"),
rest_api.Actions.as_view(), name="api_actions_in_job"),
)

@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
import horizon
from freezer_ui import dashboard
from disaster_recovery import dashboard
class BackupsPanel(horizon.Panel):

@ -17,9 +17,11 @@ import logging
from django.core.urlresolvers import reverse
from django.utils import safestring
from django.utils.translation import ugettext_lazy as _
from horizon.utils import functions as utils
from horizon import tables
from freezer_ui.utils import timestamp_to_string
from horizon.utils import functions as utils
from disaster_recovery.utils import timestamp_to_string
LOG = logging.getLogger(__name__)
@ -32,16 +34,13 @@ class Restore(tables.LinkAction):
ajax = True
def get_link_url(self, datum=None):
return reverse("horizon:freezer_ui:backups:restore",
return reverse("horizon:disaster_recovery:backups:restore",
kwargs={'backup_id': datum.id})
class BackupFilter(tables.FilterAction):
filter_type = "server"
filter_choices = (("before", "Created before", True),
("after", "Created after", True),
("between", "Created between", True),
("contains", "Contains text", True))
filter_choices = (("contains", "Contains text", True),)
def icons(backup):
@ -88,13 +87,14 @@ def icons(backup):
def backup_detail_view(backup):
return reverse("horizon:freezer_ui:backups:detail",
args=[backup.backup_id])
return reverse("horizon:disaster_recovery:backups:detail",
kwargs={'backup_id': backup.id})
class BackupsTable(tables.DataTable):
backup_name = tables.Column('backup_name',
verbose_name=_("Backup Name"))
verbose_name=_("Backup Name"),
link=backup_detail_view)
hostname = tables.Column('hostname', verbose_name=_("Hostname"))
created = tables.Column("time_stamp",
verbose_name=_("Created At"),

@ -16,7 +16,7 @@
from django.conf.urls import patterns
from django.conf.urls import url
from freezer_ui.backups import views
from disaster_recovery.backups import views
urlpatterns = patterns(

@ -0,0 +1,82 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import logging
import pprint
from django.template.defaultfilters import date as django_date
from django.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import tables