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
This commit is contained in:
memo 2015-11-09 16:10:36 +00:00
parent 26f1deb396
commit b08558eba4
101 changed files with 1799 additions and 1634 deletions

View File

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

View File

@ -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',
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = freezer_api.Action(request).list(json=True)
actions = json.dumps(actions)
return HttpResponse(actions,
content_type="application/json")
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")

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,82 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import 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
from horizon import workflows
import disaster_recovery.api.api as freezer_api
from disaster_recovery.backups import tables as freezer_tables
from disaster_recovery.backups.workflows import restore as restore_workflow
from disaster_recovery.utils import shield
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
name = _("Backups")
slug = "backups"
table_class = freezer_tables.BackupsTable
template_name = "disaster_recovery/backups/index.html"
@shield('Unable to retrieve backups.', redirect='backups:index')
def get_data(self):
filters = self.table.get_filter_string() or None
return freezer_api.Backup(self.request).list(search=filters)
class DetailView(generic.TemplateView):
template_name = 'disaster_recovery/backups/detail.html'
@shield('Unable to get backup.', redirect='backups:index')
def get_context_data(self, **kwargs):
backup = freezer_api.Backup(self.request).get(kwargs['backup_id'],
json=True)
return {'data': pprint.pformat(backup)}
class RestoreView(workflows.WorkflowView):
workflow_class = restore_workflow.Restore
@shield('Unable to get backup.', redirect='backups:index')
def get_object(self, *args, **kwargs):
return freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
def is_update(self):
return 'name' in self.kwargs and bool(self.kwargs['name'])
@shield('Unable to get backup.', redirect='backups:index')
def get_workflow_name(self):
backup = freezer_api.Backup(self.request).get(self.kwargs['backup_id'])
backup_date = datetime.datetime.fromtimestamp(int(backup.time_stamp))
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']}
@shield('Unable to get backup.', redirect='backups:index')
def get_workflow(self, *args, **kwargs):
workflow = super(RestoreView, self).get_workflow(*args, **kwargs)
workflow.name = self.get_workflow_name()
return workflow

View File

@ -18,11 +18,11 @@ import logging
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from horizon import forms
from horizon import exceptions
from horizon import forms
from horizon import workflows
import freezer_ui.api.api as freezer_api
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
@ -49,7 +49,7 @@ class DestinationAction(workflows.MembershipAction):
class Destination(workflows.Step):
template_name = 'freezer_ui/backups/restore.html'
template_name = 'disaster_recovery/backups/restore.html'
action_class = DestinationAction
contributes = ('client', 'path', 'backup_id')
@ -61,7 +61,7 @@ class Restore(workflows.Workflow):
slug = "restore"
name = _("Restore")
finalize_button_name = _("Restore")
success_url = "horizon:freezer_ui:backups:index"
success_url = "horizon:disaster_recovery:backups:index"
success_message = _("Restore job successfully queued. It will get "
"executed soon.")
wizard = False
@ -69,40 +69,7 @@ class Restore(workflows.Workflow):
def handle(self, request, data):
try:
backup_id = data['backup_id']
client_id = data['client']
client = freezer_api.client_get(request, client_id)
backup = freezer_api.backup_get(request, backup_id)
name = "Restore job for {0}".format(client_id)
action = {
"action": "restore",
"backup_name":
backup.data_dict['backup_metadata']['backup_name'],
"restore_abs_path": data['path'],
"container":
backup.data_dict['backup_metadata']['container'],
"restore_from_host": client.hostname,
"storage": "local"
}
action_id = freezer_api.action_create_without_job(
request, action)
job = {
"job_actions": [{
"action_id": action_id,
"freezer_action": action
}],
"client_id": client_id,
"description": name,
"job_schedule": {
"schedule_end_date": None,
"schedule_interval": None,
"schedule_start_date": None
}
}
return freezer_api.job_create(request, job)
return freezer_api.Backup(request).restore(data)
except Exception:
exceptions.handle(request)
return False

View File

@ -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 ClientsPanel(horizon.Panel):

View File

@ -0,0 +1,77 @@
# 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.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 Filter(tables.FilterAction):
filter_type = "server"
filter_choices = (("contains", "Contains text", True),)
class DeleteClient(tables.DeleteAction):
name = "delete"
classes = ("btn-danger",)
icon = "remove"
help_text = _("Delete Clients is not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Client",
u"Delete Clients",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Client",
u"Deleted Clients",
count
)
def delete(self, request, client_id):
return freezer_api.Client(request).delete(client_id)
class DeleteMultipleClients(DeleteClient):
name = "delete_multiple_clients"
def get_link(client):
return reverse('horizon:disaster_recovery:clients:client',
kwargs={'client_id': client.id})
class ClientsTable(tables.DataTable):
client_id = tables.Column('client_id', verbose_name=_("Client ID"),
link=get_link)
name = tables.Column('hostname', verbose_name=_("Hostname"))
class Meta:
name = "clients"
verbose_name = _("Clients")
row_actions = (DeleteClient,)
table_actions = (Filter, DeleteMultipleClients,)
multi_select = True

View File

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

View File

@ -0,0 +1,29 @@
# (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.clients import views
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<client_id>[^/]+)?$',
views.ClientView.as_view(),
name='client'),
)

View File

@ -0,0 +1,49 @@
# 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.utils.translation import ugettext_lazy as _
from django.views import generic
from horizon import tables
import disaster_recovery.api.api as freezer_api
from disaster_recovery.clients import tables as freezer_tables
from disaster_recovery.utils import shield
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
name = _("Clients")
slug = "clients"
table_class = freezer_tables.ClientsTable
template_name = "disaster_recovery/clients/index.html"
@shield('Unable to get clients', redirect='clients:index')
def get_data(self):
filters = self.table.get_filter_string() or None
return freezer_api.Client(self.request).list(search=filters)
class ClientView(generic.TemplateView):
template_name = 'disaster_recovery/clients/detail.html'
@shield('Unable to get client', redirect='clients:index')
def get_context_data(self, **kwargs):
client = freezer_api.Client(self.request).get(kwargs['client_id'],
json=True)
return {'data': pprint.pformat(client)}

View File

@ -20,15 +20,14 @@ import horizon
class FreezerDR(horizon.PanelGroup):
slug = "freezerdr"
name = _("Backup and Restore")
panels = ('jobs', 'sessions', 'backups', 'clients')
panels = ('jobs', 'actions', 'sessions', 'clients', 'backups')
class Freezer(horizon.Dashboard):
name = _("Disaster Recovery")
slug = "freezer_ui"
slug = "disaster_recovery"
panels = (FreezerDR,)
default_panel = 'jobs'
permissions = ('openstack.roles.admin',)
horizon.register(Freezer)

View File

@ -14,7 +14,7 @@
from django.utils.translation import ugettext_lazy as _
from freezer_ui.jobs import tables
from disaster_recovery.jobs import tables
from horizon import browsers

View File

@ -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 JobsPanel(horizon.Panel):

View File

@ -13,71 +13,39 @@
# limitations under the License.
import logging
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 freezer_ui.api.api as freezer_api
from freezer_ui.utils import timestamp_to_string
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
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 ObjectFilterAction(tables.FilterAction):
def allowed(self, request, datum):
return bool(self.table.kwargs['job_id'])
class AttachJobToSession(tables.LinkAction):
name = "attach_job_to_session"
verbose_name = _("Attach To Session")
classes = ("ajax-modal")
url = "horizon:freezer_ui:sessions:attach"
url = "horizon:disaster_recovery:sessions:attach"
def allowed(self, request, instance):
return True
def get_link_url(self, datum):
return reverse("horizon:freezer_ui:sessions:attach",
return reverse("horizon:disaster_recovery:sessions:attach",
kwargs={'job_id': datum.job_id})
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 DeleteJob(tables.DeleteAction):
name = "delete"
classes = ("btn-danger",)
@ -100,8 +68,8 @@ class DeleteJob(tables.DeleteAction):
count
)
def delete(self, request, obj_id):
return freezer_api.job_delete(request, obj_id)
def delete(self, request, job_id):
return freezer_api.Job(request).delete(job_id)
class DeleteMultipleJobs(DeleteJob):
@ -113,9 +81,9 @@ class CloneJob(tables.Action):
verbose_name = _("Clone Job")
help_text = _("Clone and edit a job file")
def single(self, table, request, obj_id):
freezer_api.job_clone(request, obj_id)
return shortcuts.redirect('horizon:freezer_ui:jobs:index')
def single(self, table, request, job_id):
freezer_api.Job(request).clone(job_id)
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
class EditJob(tables.LinkAction):
@ -125,50 +93,65 @@ class EditJob(tables.LinkAction):
icon = "pencil"
def get_link_url(self, datum=None):
return reverse("horizon:freezer_ui:jobs:configure",
kwargs={'backup_name': datum.job_id})
return reverse("horizon:disaster_recovery:jobs:configure",
kwargs={'job_id': datum.job_id})
def get_backup_configs_link(backup_config):
return reverse('horizon:freezer_ui:jobs:index',
kwargs={'job_id': backup_config.job_id})
class EditActionsInJob(tables.LinkAction):
name = "edit_actions_in_job"
verbose_name = _("Edit Actions")
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, datum=None):
return reverse("horizon:disaster_recovery:jobs:edit_action",
kwargs={'job_id': datum.job_id})
class StartJob(tables.Action):
name = "start_job"
verbose_name = _("Start Job")
def single(self, table, request, job_id):
freezer_api.Job(request).start(job_id)
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
class StopJob(tables.Action):
name = "stop_job"
verbose_name = _("Stop Job")
def single(self, table, request, job_id):
freezer_api.Job(request).stop(job_id)
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
def get_link(row):
return reverse('horizon:disaster_recovery:jobs:index',
kwargs={'job_id': row.job_id})
class CreateJob(tables.LinkAction):
name = "create"
verbose_name = _("Create Job")
url = "horizon:freezer_ui:jobs:create"
url = "horizon:disaster_recovery:jobs:create"
classes = ("ajax-modal",)
icon = "plus"
class CreateAction(tables.LinkAction):
name = "create_action"
verbose_name = _("Create Action")
url = "horizon:freezer_ui:jobs:create_action"
classes = ("ajax-modal",)
icon = "plus"
def get_link_url(self, datum=None):
return reverse("horizon:freezer_ui:jobs:create_action",
kwargs={'job_id': datum.job_id})
class ObjectFilterAction(tables.FilterAction):
def allowed(self, request, datum):
return bool(self.table.kwargs['job_id'])
class JobsTable(tables.DataTable):
job_name = tables.Column("description",
link=get_backup_configs_link,
link=get_link,
verbose_name=_("Job Name"))
event = tables.Column("event",
verbose_name=_("Job Status"))
result = tables.Column("result",
verbose_name=_("Job Result"))
def get_object_id(self, backup_config):
return backup_config.id
def get_object_id(self, row):
return row.id
class Meta(object):
name = "jobs"
@ -176,13 +159,15 @@ class JobsTable(tables.DataTable):
table_actions = (ObjectFilterAction,
CreateJob,
DeleteMultipleJobs)
footer = False
multi_select = True
row_actions = (CreateAction,
row_actions = (StartJob,
StopJob,
EditActionsInJob,
EditJob,
AttachJobToSession,
CloneJob,
DeleteJob,)
DeleteJob)
footer = False
multi_select = True
class DeleteAction(tables.DeleteAction):
@ -208,47 +193,29 @@ class DeleteAction(tables.DeleteAction):
)
def delete(self, request, obj_id):
freezer_api.action_delete(request, obj_id)
return reverse("horizon:freezer_ui:jobs:index")
freezer_api.Job(request).delete_action(obj_id)
return reverse("horizon:disaster_recovery:jobs:index")
class DeleteMultipleActions(DeleteAction):
name = "delete_multiple_actions"
class EditAction(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
classes = ("ajax-modal",)
icon = "pencil"
def get_link_url(self, datum=None):
# this is used to pass to values as an url
# TODO(m3m0): look for a way to improve this
ids = '{0}==={1}'.format(datum.action_id, datum.job_id)
return reverse("horizon:freezer_ui:jobs:create_action",
kwargs={'job_id': ids})
class ActionsTable(tables.DataTable):
action_name = tables.Column('action',
verbose_name=_("Action Type"))
action = tables.Column('action', verbose_name=_("Action Type"))
backup_name = tables.Column('backup_name',
verbose_name=_("Action Name"))
name = tables.Column('backup_name', verbose_name=_("Action Name"))
def get_object_id(self, container):
# this is used to pass to values as an url
# TODO(m3m0): look for a way to improve this
# TODO(m3m0): we should't send the ids in this way
ids = '{0}==={1}'.format(container.action_id, container.job_id)
return ids
class Meta(object):
name = "status"
verbose_name = _("Status")
name = "actions_in_job"
verbose_name = _("Actions")
table_actions = (ObjectFilterAction,
DeleteMultipleActions)
row_actions = (EditAction,
DeleteAction,)
row_actions = (DeleteAction,)
footer = False
multi_select = True

View File

@ -4,7 +4,7 @@
{% endblock %}
<style>
#sortable1, #sortable2 {
#actions_available, #actions_selected {
position: relative;
min-height: 100px;
}
@ -16,7 +16,7 @@
<div class="panel panel-default">
<div class="panel-heading">Available Actions</div>
<div class="panel-body">
<ul id="sortable1" class="connectedSortable list-group dark_stripe">
<ul id="actions_available" class="connectedSortable list-group dark_stripe">
</ul>
</div>
</div>
@ -26,7 +26,7 @@
<div class="panel panel-default">
<div class="panel-heading">Selected Actions In Order</div>
<div class="panel-body">
<ul id="sortable2" class="connectedSortable list-group dark_stripe">
<ul id="actions_selected" class="connectedSortable list-group dark_stripe">
</ul>
</div>
</div>

View File

@ -4,7 +4,7 @@
<h4>{% blocktrans %}Action{% endblocktrans %}</h4>
<p>{% blocktrans %}Specify weather this action should execute a snapshot on the client file system.
<p>{% blocktrans %}Specify whether this action should execute a snapshot on the client file system.
In Linux and it's distros it will use LVM and in Windows it will use Volume Shadow Copy{% endblocktrans %}</p>
{% endblock %}

View File

@ -15,7 +15,7 @@
from django.conf.urls import patterns
from django.conf.urls import url
from freezer_ui.jobs import views
from disaster_recovery.jobs import views
urlpatterns = patterns(
@ -29,11 +29,11 @@ urlpatterns = patterns(
views.JobWorkflowView.as_view(),
name='create'),
url(r'^create_action/(?P<job_id>[^/]+)?$',
views.ActionWorkflowView.as_view(),
name='create_action'),
url(r'^configure/(?P<backup_name>[^/]+)?$',
url(r'^configure/(?P<job_id>[^/]+)?$',
views.JobWorkflowView.as_view(),
name='configure'),
url(r'^edit_actions/(?P<job_id>[^/]+)?$',
views.ActionsInJobView.as_view(),
name='edit_action'),
)

View File

@ -0,0 +1,84 @@
# 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 horizon import browsers
from horizon import workflows
import workflows.create as configure_workflow
import workflows.actions as actions_workflow
import disaster_recovery.api.api as freezer_api
import disaster_recovery.jobs.browsers as project_browsers
from disaster_recovery.utils import shield
LOG = logging.getLogger(__name__)
class JobsView(browsers.ResourceBrowserView):
browser_class = project_browsers.ContainerBrowser
template_name = "disaster_recovery/jobs/browser.html"
@shield("Unable to get job", redirect='jobs:index')
def get_jobs_data(self):
return freezer_api.Job(self.request).list(limit=100)
@shield("Unable to get actions for this job.", redirect='jobs:index')
def get_actions_in_job_data(self):
if self.kwargs['job_id']:
return freezer_api.Job(self.request).actions(self.kwargs['job_id'])
return []
class JobWorkflowView(workflows.WorkflowView):
workflow_class = configure_workflow.ConfigureJob
@shield("Unable to get job", redirect="jobs:index")
def get_object(self):
return freezer_api.Job(self.request).get(self.kwargs['job_id'])
def is_update(self):
return 'job_id' in self.kwargs and bool(self.kwargs['job_id'])
@shield("Unable to get job", redirect="jobs:index")
def get_initial(self):
initial = super(JobWorkflowView, self).get_initial()
if self.is_update():
initial.update({'job_id': None})
job = freezer_api.Job(self.request).get(self.kwargs['job_id'],
json=True)
initial.update(**job)
initial.update(**job['job_schedule'])
return initial
class ActionsInJobView(workflows.WorkflowView):
workflow_class = actions_workflow.ConfigureActions
@shield("Unable to get job", redirect="jobs:index")
def get_object(self):
return freezer_api.Job(self.request).get(self.kwargs['job_id'])
def is_update(self):
return 'job_id' in self.kwargs and bool(self.kwargs['job_id'])
@shield("Unable to get job", redirect="jobs:index")
def get_initial(self):
initial = super(ActionsInJobView, self).get_initial()
if self.is_update():
job = freezer_api.Job(self.request).get(self.kwargs['job_id'])
initial.update({'job_id': job.id})
return initial

View File

@ -0,0 +1,81 @@
# 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 horizon import exceptions
from horizon import forms
from horizon import workflows
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
class ActionsConfigurationAction(workflows.Action):
pass
class Meta(object):
name = _("Actions")
slug = "actions"
help_text_template = "disaster_recovery/jobs" \
"/_actions.html"
class ActionsConfiguration(workflows.Step):
action_class = ActionsConfigurationAction
contributes = ()
class InfoAction(workflows.Action):
job_id = forms.CharField(label=_("Job ID"), required=False,
widget=forms.HiddenInput(),)
actions = forms.CharField(label=_("Actions"), required=False,
widget=forms.HiddenInput(),)
def __init__(self, request, *args, **kwargs):
super(InfoAction, self).__init__(request, *args, **kwargs)
class Meta(object):
name = _("Info")
# Unusable permission so this is always hidden. However, we
# keep this step in the workflow for validation/verification purposes.
permissions = ()
class Info(workflows.Step):
action_class = InfoAction
contributes = ("job_id", "actions")
class ConfigureActions(workflows.Workflow):
slug = "job"
name = _("Actions Configuration")
finalize_button_name = _("Save")
success_message = _('Actions saved correctly.')
failure_message = _('Unable to save actions.')
success_url = "horizon:disaster_recovery:jobs:index"
default_steps = (ActionsConfiguration, Info,)
def handle(self, request, context):
try:
if context['job_id'] != '':
freezer_api.Job(request).update_actions(context['job_id'],
context['actions'])
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
except Exception:
exceptions.handle(request)
return False

View File

@ -14,18 +14,16 @@
import logging
from collections import namedtuple
import datetime
from django import shortcuts
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon import workflows
import freezer_ui.api.api as freezer_api
from freezer_ui.utils import actions_in_job
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
@ -37,7 +35,7 @@ class ActionsConfigurationAction(workflows.Action):
class Meta(object):
name = _("Actions")
slug = "actions"
help_text_template = "freezer_ui/jobs" \
help_text_template = "disaster_recovery/jobs" \
"/_actions.html"
@ -53,7 +51,7 @@ class ClientsConfigurationAction(workflows.MembershipAction):
**kwargs)
err_msg = _('Unable to retrieve client list.')
original_name = args[0].get('original_name', None)
job_id = args[0].get('job_id', None)
default_role_name = self.get_default_role_field_name()
self.fields[default_role_name] = forms.CharField(required=False)
@ -61,14 +59,14 @@ class ClientsConfigurationAction(workflows.MembershipAction):
all_clients = []
try:
all_clients = freezer_api.client_list(request)
all_clients = freezer_api.Client(request).list()
except Exception:
exceptions.handle(request, err_msg)
client_list = [(c.uuid, c.hostname)
for c in all_clients]
field_name = self.get_member_field_name('member')
if not original_name:
if not job_id:
self.fields[field_name] = forms.MultipleChoiceField(required=False)
self.fields[field_name].choices = client_list
@ -96,11 +94,23 @@ class ClientsConfiguration(workflows.UpdateMembersStep):
return context
class SchedulingConfigurationAction(workflows.Action):
class InfoConfigurationAction(workflows.Action):
actions = forms.CharField(
widget=forms.HiddenInput(),
required=False)
description = forms.CharField(
label=_("Job Name"),
help_text=_("Set a name for this job"),
required=True)
job_id = forms.CharField(
widget=forms.HiddenInput(),
required=False)
schedule_start_date = forms.CharField(
label=_("Start Date and Time"),
required=False,
help_text=_(""))
required=False)
schedule_interval = forms.CharField(
label=_("Interval"),
@ -109,17 +119,16 @@ class SchedulingConfigurationAction(workflows.Action):
schedule_end_date = forms.CharField(
label=_("End Date and Time"),
required=False,
help_text=_(""))
required=False)
def __init__(self, request, context, *args, **kwargs):
self.request = request
self.context = context
super(SchedulingConfigurationAction, self).__init__(
super(InfoConfigurationAction, self).__init__(
request, context, *args, **kwargs)
def clean(self):
cleaned_data = super(SchedulingConfigurationAction, self).clean()
cleaned_data = super(InfoConfigurationAction, self).clean()
self._check_start_datetime(cleaned_data)
self._check_end_datetime(cleaned_data)
return cleaned_data
@ -145,46 +154,21 @@ class SchedulingConfigurationAction(workflows.Action):
msg = _("End date time is not in ISO format.")
self._errors['schedule_end_date'] = self.error_class([msg])
class Meta(object):
name = _("Scheduling")
slug = "scheduling"
help_text_template = "freezer_ui/jobs" \
"/_scheduling.html"
class SchedulingConfiguration(workflows.Step):
action_class = SchedulingConfigurationAction
contributes = ('schedule_start_date',
'schedule_interval',
'schedule_end_date')
class InfoConfigurationAction(workflows.Action):
actions = forms.CharField(
widget=forms.HiddenInput(),
required=False)
description = forms.CharField(
label=_("Job Name"),
help_text=_("Set a short description for this job"),
required=True)
original_name = forms.CharField(
widget=forms.HiddenInput(),
required=False)
class Meta(object):
name = _("Job Info")
slug = "info"
help_text_template = "freezer_ui/jobs" \
"/_info.html"
help_text_template = "disaster_recovery/jobs" \
"/_scheduling.html"
class InfoConfiguration(workflows.Step):
action_class = InfoConfigurationAction
contributes = ('description',
'original_name',
'actions')
'job_id',
'actions',
'schedule_start_date',
'schedule_interval',
'schedule_end_date')
class ConfigureJob(workflows.Workflow):
@ -193,62 +177,18 @@ class ConfigureJob(workflows.Workflow):
finalize_button_name = _("Save")
success_message = _('Job created correctly.')
failure_message = _('Unable to created job.')
success_url = "horizon:freezer_ui:jobs:index"
success_url = "horizon:disaster_recovery:jobs:index"
default_steps = (InfoConfiguration,
ClientsConfiguration,
SchedulingConfiguration,
ActionsConfiguration)
def handle(self, request, context):
try:
is_edit = False
if not context['original_name'] == '':
is_edit = True
actions = actions_in_job(context.pop('actions', []))
actions_for_job = []
if is_edit:
# if this is a edit get the job and delete the action list
# TODO(m3m0) improve this to not recreate the action list
job_id = context['original_name']
job = freezer_api.job_get(request, job_id)
del job[0].data_dict['job_actions']
for action in actions:
a = freezer_api.action_get(request, action)
a = {
'action_id': a['action_id'],
'freezer_action': a['freezer_action']
}
actions_for_job.append(a)
context['job_actions'] = actions_for_job
if is_edit:
return freezer_api.job_edit(request, context)
if context['job_id'] != '':
freezer_api.Job(request).update(context['job_id'], context)
else:
if context['clients']:
# we have to query the api to get the list of clients
# because MembershipAction for clients works with uuid's
# and we need to send the client_id instead of the uuid
# for the job creation
clients = freezer_api.client_list(request)
ClientIDS = namedtuple('Client', ['client_id', 'uuid'])
client_list = [ClientIDS(c.client_id, c.uuid)
for c in clients]
for client_uuid in context['clients']:
for client_id, uuid in client_list:
if client_uuid == uuid:
context['client_id'] = client_id
freezer_api.job_create(request, context)
else:
messages.warning(request, _("At least one client is "
"required to create a job"))
return False
return True
freezer_api.Job(request).create(context)
return shortcuts.redirect('horizon:disaster_recovery:jobs:index')
except Exception:
exceptions.handle(request)
return False

View File

View File

@ -14,7 +14,7 @@
from django.utils.translation import ugettext_lazy as _
from freezer_ui.sessions import tables
from disaster_recovery.sessions import tables
from horizon import browsers

View File

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

View File

@ -21,21 +21,26 @@ from django.utils.translation import ungettext_lazy
from horizon import tables
from horizon.utils.urlresolvers import reverse
import freezer_ui.api.api as freezer_api
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
class ObjectFilterAction(tables.FilterAction):
def allowed(self, request, datum):
return bool(self.table.kwargs['session_id'])
def get_link(session):
return reverse('horizon:freezer_ui:sessions:index',
return reverse('horizon:disaster_recovery:sessions:index',
kwargs={'session_id': session.session_id})
class CreateJob(tables.LinkAction):
name = "create_session"
verbose_name = _("Create Session")
url = "horizon:freezer_ui:sessions:create"
url = "horizon:disaster_recovery:sessions:create"
classes = ("ajax-modal",)
icon = "plus"
@ -63,7 +68,7 @@ class DeleteSession(tables.DeleteAction):
)
def delete(self, request, session_id):
return freezer_api.session_delete(request, session_id)
return freezer_api.Session(request).delete(session_id)
class EditSession(tables.LinkAction):
@ -73,7 +78,7 @@ class EditSession(tables.LinkAction):
icon = "pencil"
def get_link_url(self, datum=None):
return reverse("horizon:freezer_ui:sessions:edit",
return reverse("horizon:disaster_recovery:sessions:edit",
kwargs={'session_id': datum.session_id})
@ -105,10 +110,7 @@ class DeleteJobFromSession(tables.DeleteAction):
def delete(self, request, session):
job_id, session_id = session.split('===')
return freezer_api.remove_job_from_session(
request,
session_id,
job_id)
return freezer_api.Session(request).remove_job(session_id, job_id)
class JobsTable(tables.DataTable):
@ -116,8 +118,8 @@ class JobsTable(tables.DataTable):
'client_id',
verbose_name=_("Client ID"))
status = tables.Column(
'status',
result = tables.Column(
'result',
verbose_name=_("Status"))
def get_object_id(self, job):
@ -129,7 +131,7 @@ class JobsTable(tables.DataTable):
class Meta(object):
name = "jobs"
verbose_name = _("Jobs")
table_actions = ()
table_actions = (ObjectFilterAction,)
row_actions = (DeleteJobFromSession,)
footer = False
multi_select = True
@ -143,13 +145,11 @@ class SessionsTable(tables.DataTable):
status = tables.Column('status',
verbose_name=_("Status"))
def get_object_id(self, session):
return session.session_id
class Meta(object):
name = "sessions"
verbose_name = _("Sessions")
table_actions = (CreateJob,
table_actions = (ObjectFilterAction,
CreateJob,
DeleteMultipleActions)
row_actions = (EditSession,
DeleteSession,)

View File

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

View File

@ -0,0 +1,86 @@
# (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.
import logging
from horizon import browsers
from horizon import workflows
import disaster_recovery.api.api as freezer_api
import disaster_recovery.sessions.browsers as project_browsers
from disaster_recovery.sessions.workflows import attach
from disaster_recovery.sessions.workflows import create
from disaster_recovery.utils import shield
LOG = logging.getLogger(__name__)
class SessionsView(browsers.ResourceBrowserView):
browser_class = project_browsers.SessionBrowser
template_name = "disaster_recovery/sessions/browser.html"
@shield('Unable to get sessions list.', redirect='actions:index')
def get_sessions_data(self):
return freezer_api.Session(self.request).list(limit=100)
@shield('Unable to get job list.', redirect='actions:index')
def get_jobs_data(self):
if self.kwargs['session_id']:
return freezer_api.Session(self.request).jobs(
self.kwargs['session_id'])
return []
class AttachToSessionWorkflow(workflows.WorkflowView):
workflow_class = attach.AttachJobToSession
@shield('Unable to get job', redirect='jobs:index')
def get_object(self, *args, **kwargs):
return freezer_api.Job(self.request).get(self.kwargs['job_id'])
def is_update(self):
return 'job_id' in self.kwargs and \
bool(self.kwargs['job_id'])
@shield('Unable to get job', redirect='jobs:index')
def get_initial(self):
initial = super(AttachToSessionWorkflow, self).get_initial()
job = self.get_object()
initial.update({'job_id': job.id})
return initial
class CreateSessionWorkflow(workflows.WorkflowView):
workflow_class = create.CreateSession
@shield('Unable to get session', redirect='sessions:index')
def get_object(self, *args, **kwargs):
return freezer_api.Session(self.request).get(self.kwargs['session_id'])
@shield('Unable to get session', redirect='sessions:index')
def get_initial(self):
initial = super(CreateSessionWorkflow, self).get_initial()
if self.is_update():
initial.update({'job_id': None})
session = freezer_api.Session(self.request).get(
self.kwargs['session_id'], json=True)
initial.update(**session)
initial.update(**session['schedule'])
return initial
def is_update(self):
return 'session_id' in self.kwargs and \
bool(self.kwargs['session_id'])

View File

@ -15,12 +15,13 @@
import logging
from django.utils.translation import ugettext_lazy as _
from horizon.utils.urlresolvers import reverse
from horizon import exceptions
from horizon import forms
from horizon import workflows
import freezer_ui.api.api as freezer_api
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
@ -39,7 +40,7 @@ class SessionConfigurationAction(workflows.Action):
def populate_session_id_choices(self, request, context):
sessions = []
try:
sessions = freezer_api.session_list(request)
sessions = freezer_api.Session(request).list()
except Exception:
exceptions.handle(request, _('Error getting session list'))
@ -64,16 +65,15 @@ class AttachJobToSession(workflows.Workflow):
finalize_button_name = _("Attach")
success_message = _('Job saved successfully.')
failure_message = _('Unable to attach to session.')
success_url = "horizon:freezer_ui:jobs:index"
success_url = "horizon:disaster_recovery:jobs:index"
default_steps = (SessionConfiguration,)
def handle(self, request, context):
try:
freezer_api.add_job_to_session(
request,
context['session_id'],
context['job_id'])
return True
freezer_api.Session(request).add_job(context['session_id'],
context['job_id'])
return reverse("horizon:disaster_recovery:jobs:index")
except Exception:
exceptions.handle(request)
return False

View File

@ -12,16 +12,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
import datetime
import logging
from django.utils.translation import ugettext_lazy as _
from horizon.utils.urlresolvers import reverse
from horizon import exceptions
from horizon import forms
from horizon import workflows
import freezer_ui.api.api as freezer_api
import disaster_recovery.api.api as freezer_api
LOG = logging.getLogger(__name__)
@ -37,43 +38,20 @@ class SessionConfigurationAction(workflows.Action):
widget=forms.HiddenInput(),
required=False)
class Meta:
name = _("Session Information")
slug = "sessions"
help_text_template = "freezer_ui/sessions" \
"/_info.html"
class SessionConfiguration(workflows.Step):
action_class = SessionConfigurationAction
contributes = ('description',
'session_id')
class SchedulingConfigurationAction(workflows.Action):
schedule_start_date = forms.CharField(
label=_("Start Date and Time"),
required=False,
help_text=_(""))
required=False)
schedule_interval = forms.CharField(
label=_("Interval"),
required=False,
help_text=_(""))
required=False)
schedule_end_date = forms.CharField(
label=_("End Date and Time"),
required=False,
help_text=_(""))
def __init__(self, request, context, *args, **kwargs):
self.request = request
self.context = context
super(SchedulingConfigurationAction, self).__init__(
request, context, *args, **kwargs)
required=False)
def clean(self):
cleaned_data = super(SchedulingConfigurationAction, self).clean()
cleaned_data = super(SessionConfigurationAction, self).clean()
self._check_start_datetime(cleaned_data)
self._check_end_datetime(cleaned_data)
return cleaned_data
@ -99,16 +77,18 @@ class SchedulingConfigurationAction(workflows.Action):
msg = _("End date time is not in ISO format.")
self._errors['schedule_end_date'] = self.error_class([msg])
class Meta(object):
name = _("Scheduling")
slug = "scheduling"
help_text_template = "freezer_ui/jobs" \
class Meta:
name = _("Session Information")
slug = "sessions"
help_text_template = "disaster_recovery/jobs" \
"/_scheduling.html"
class SchedulingConfiguration(workflows.Step):
action_class = SchedulingConfigurationAction
contributes = ('schedule_start_date',
class SessionConfiguration(workflows.Step):
action_class = SessionConfigurationAction
contributes = ('description',
'session_id',
'schedule_start_date',
'schedule_interval',
'schedule_end_date')
@ -116,19 +96,20 @@ class SchedulingConfiguration(workflows.Step):
class CreateSession(workflows.Workflow):
slug = "create_session"
name = _("Create Session")
finalize_button_name = _("Create")
finalize_button_name = _("Save")
success_message = _('Session created successfully.')
failure_message = _('Unable to create session.')
success_url = "horizon:freezer_ui:sessions:index"
default_steps = (SessionConfiguration,
SchedulingConfiguration)
success_url = "horizon:disaster_recovery:sessions:index"
default_steps = (SessionConfiguration,)
def handle(self, request, context):
try:
if context['session_id'] == '':
return freezer_api.session_create(request, context)
if context['session_id'] != '':
freezer_api.Session(request).update(context,
context['session_id'])
else:
return freezer_api.session_update(request, context)
freezer_api.Session(request).create(context)
return reverse("horizon:disaster_recovery:sessions:index")
except Exception:
exceptions.handle(request)
return False

View File

@ -0,0 +1,9 @@
.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%;
}

View File

@ -27,7 +27,6 @@ function hideOptions() {
$("#id_lvm_snapsize").closest(".form-group").hide();
$("#id_lvm_dirmount").closest(".form-group").hide();
$("#id_lvm_volgroup").closest(".form-group").hide();
$("#id_vssadmin").closest(".form-group").hide();
}
function is_windows() {
@ -40,10 +39,6 @@ function showWindowsSnapshotOptions() {
$("#id_vssadmin").closest(".form-group").show();
}
function hideWindowsSnapshotOptions() {
$("#id_vssadmin").closest(".form-group").hide();
}
function showLinuxSnapshotOptions() {
$("#id_lvm_auto_snap").closest(".form-group").show();
$("#id_lvm_srcvol").closest(".form-group").show();
@ -63,7 +58,6 @@ function hideLinuxSnapshotOptions() {
}
function hideSnapshotOptions() {
hideWindowsSnapshotOptions();
hideLinuxSnapshotOptions();
$("#id_is_windows").closest(".form-group").hide();
}
@ -74,7 +68,6 @@ function showSnapshotOptions() {
hideLinuxSnapshotOptions();
showWindowsSnapshotOptions();
} else {
hideWindowsSnapshotOptions();
showLinuxSnapshotOptions();
}
}

View File

@ -1,29 +1,15 @@
/*
# (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.
*/
/*global $, location*/
"use strict";
'use strict';
$(function () {
$("#sortable1, #sortable2").sortable({
$("#actions_available, #actions_selected").sortable({
connectWith: ".connectedSortable"
}).disableSelection();
});
var parent = $(".sortable_lists").parent();
parent.removeClass("col-sm-6");
parent.addClass("col-sm-12");
@ -33,7 +19,7 @@ siblings.remove();
$("form").submit(function (event) {
var ids = "";
$("#sortable2 li").each(function (index) {
$("#actions_selected li").each(function (index) {
ids += ($(this).attr('id'));
ids += "===";
});
@ -41,33 +27,44 @@ $("form").submit(function (event) {
});
function get_actions_url() {
var job_id = $('#id_job_id').val();
function get_url() {
var url = $(location).attr("origin");
url += '/freezer_ui/api/actions';
url += '/disaster_recovery/api/actions/job/';
url += job_id;
return url;
}
var job_id = $('#id_original_name').val();
function get_actions_url() {
var url = $(location).attr("origin");
url += '/disaster_recovery/api/actions';
return url;
}
if (job_id !== "") {
var url_available = get_actions_url();
$.ajax({
url: url_available,
url: get_url(),
type: "GET",
cache: false,
dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function (data) {
$.each(data, function (index, item) {
$("#sortable1").append(
$.each(data.available, function (index, item) {
$("#actions_available").append(
"<li class='list-group-item' id=" + item.action_id + ">" +
item.freezer_action.backup_name + "</li>"
);
});
$.each(data.selected, function (index, item) {
$("#actions_selected").append(
"<li class='list-group-item' id=" + item.action_id + ">" +
item.freezer_action.backup_name + "</li>"
);
});
},
error: function (request, error) {
$("#sortable1").append(
$("#actions_available").append(
'<tr><td>Error getting action list</td></tr>'
);
}
@ -83,7 +80,7 @@ if (job_id !== "") {
contentType: 'application/json; charset=utf-8' ,
success: function (data) {
$.each(data, function (index, item) {
$("#sortable1").append(
$("#actions_available").append(
"<li class='list-group-item' id=" + item.action_id + ">" +
item.freezer_action.backup_name +
"</li>"
@ -91,7 +88,7 @@ if (job_id !== "") {
});
},
error: function (request, error) {
$("#sortable1").append(
$("#actions_available").append(
'<tr><td>Error getting action list</td></tr>'
);
}

View File

@ -21,7 +21,7 @@
angular.module('hz').controller('DestinationCtrl', function ($scope, $http, $location) {
$scope.query = '';
$http.get($location.protocol() + "://" + $location.host() + ":" + $location.port() + "/freezer_ui/api/clients").
$http.get($location.protocol() + "://" + $location.host() + ":" + $location.port() + "/disaster_recovery/api/clients").
success(function (data) {
$scope.clients = data;
});

View File

@ -19,7 +19,7 @@
"use strict";
var url = $(location).attr("origin");
url += '/freezer_ui/api/clients';
url += '/disaster_recovery/api/clients';
$.ajax({
url: url,

View File

@ -12,14 +12,14 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from django.conf.urls import include
from django.conf.urls import patterns
from django.conf.urls import url
from freezer_ui.clients import views
import disaster_recovery.api.rest.urls as rest_urls
urlpatterns = patterns(
'',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'', include(rest_urls)),
)

177
disaster_recovery/utils.py Normal file
View File

@ -0,0 +1,177 @@
# 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 uuid
from functools import wraps
from django.core.urlresolvers import reverse
from django.template.defaultfilters import date as django_date
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
def create_dict(**kwargs):
"""Create a dict only with values that exists so we avoid send keys with
None values
"""
return {k: v for k, v in kwargs.items() if v}
def timestamp_to_string(ts):
return django_date(
datetime.datetime.fromtimestamp(int(ts)),
'SHORT_DATETIME_FORMAT')
def create_dummy_id():
"""Generate a dummy id for documents generated by the scheduler.
This is needed when the scheduler creates jobs with actions attached
directly, those actions are not registered in the db.
"""
return uuid.uuid4().hex
def get_action_ids(ids):
"""Return an ordered list of actions for a new job
"""
ids = ids.split('===')
return [i for i in ids if i]
def assign_and_remove(source_dict, dest_dict, key):
"""Assign a value to a destination dict from a source dict
if the key exists
"""
if key in source_dict:
dest_dict[key] = source_dict.pop(key)
class SessionObject(object):
def __init__(self, session_id, description, status, jobs,
start_datetime, interval, end_datetime):
self.session_id = session_id
self.id = session_id
self.description = description
self.status = status
self.jobs = jobs or []
self.schedule_start_date = start_datetime
self.schedule_end_date = end_datetime
self.schedule_interval = interval
class JobObject(object):
def __init__(self, job_id, description, result, event, client_id=None):
self.job_id = job_id
self.id = job_id
self.description = description
self.result = result or 'pending'
self.event = event or 'stop'
self.client_id = client_id
class JobsInSessionObject(object):
def __init__(self, job_id, session_id, client_id, result):
self.job_id = job_id
self.session_id = session_id
self.id = session_id
self.client_id = client_id
self.result = result or 'pending'
class ActionObject(object):
def __init__(self, action_id=None, action=None, backup_name=None,
job_id=None):
# action basic info
self.id = action_id
self.action_id = action_id or create_dummy_id()
self.action = action or 'backup'
self.backup_name = backup_name or 'no backup name available'
self.job_id = job_id
class ActionObjectDetail(object):
def __init__(self, action_id=None, action=None, backup_name=None,
path_to_backup=None, storage=None, mode=None, container=None,
mandatory=None, max_retries=None, max_retries_interval=None):
# action basic info
self.id = action_id
self.action_id = action_id or create_dummy_id()
self.action = action or 'backup'
self.backup_name = backup_name or 'no backup name available'
self.path_to_backup = path_to_backup
self.storage = storage or 'swift'
self.mode = mode or 'fs'
self.container = container
# action rules
self.mandatory = mandatory
self.max_retries = max_retries
self.max_retries_interval = max_retries_interval
class BackupObject(object):
def __init__(self, backup_id=None, action=None, time_stamp=None,
backup_name=None, backup_media=None, path_to_backup=None,
hostname=None, level=None, container=None,
curr_backup_level=None, encrypted=None,
total_broken_links=None, excluded_files=None):
self.backup_id = backup_id
self.id = backup_id
self.backup_name = backup_name
self.action = action or 'backup'
self.time_stamp = time_stamp
self.backup_media = backup_media or 'fs'
self.path_to_backup = path_to_backup
self.hostname = hostname
self.container = container
self.level = level
self.curr_backup_level = curr_backup_level or 0
self.encrypted = encrypted
self.total_broken_links = total_broken_links or 0
self.excluded_files = excluded_files
class ClientObject(object):
def __init__(self, hostname, client_id, client_uuid):
self.hostname = hostname
self.client_id = client_id
self.uuid = client_uuid
self.id = client_id
def shield(message, redirect=''):
"""decorator to reduce boilerplate try except blocks for horizon functions
:param message: a str error message
:param redirect: a str with the redirect namespace without including
horizon:disaster_recovery:
eg. @shield('error', redirect='jobs:index')
"""
def wrap(function):
@wraps(function)
def wrapped_function(request, *args, **kwargs):
try:
return function(request, *args, **kwargs)
except Exception:
namespace = "horizon:disaster_recovery:"
r = reverse("{0}{1}".format(namespace, redirect))
exceptions.handle(request, _(message), redirect=r)
return wrapped_function
return wrap

View File

@ -1,428 +0,0 @@
# (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.
# Some helper functions to use the freezer_ui client functionality
# from horizon.
import logging
from django.conf import settings
from horizon.utils import functions as utils
from horizon.utils.memoized import memoized # noqa
import freezer.apiclient.client
from freezer_ui.utils import Action
from freezer_ui.utils import ActionJob
from freezer_ui.utils import Backup
from freezer_ui.utils import Client
from freezer_ui.utils import Job
from freezer_ui.utils import JobList
from freezer_ui.utils import Session
from freezer_ui.utils import create_dict_action
from freezer_ui.utils import create_dummy_id
from freezer_ui.utils import assign_value_from_source
LOG = logging.getLogger(__name__)
@memoized
def get_hardcoded_url():
"""Get FREEZER_API_URL from local_settings.py"""
try:
LOG.warn('Using hardcoded FREEZER_API_URL at {0}'
.format(settings.FREEZER_API_URL))
return getattr(settings, 'FREEZER_API_URL', None)
except (AttributeError, TypeError):
LOG.warn('No FREEZER_API_URL was found in local_settings.py')
raise
@memoized
def get_service_url(request):
"""Get Freezer API url from keystone catalog or local_settings.py
if Freezer is not set in keystone, the fallback will be
'FREEZER_API_URL' in local_settings.py
"""
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 _freezerclient(request):
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)
def job_create(request, context):
"""Create a new job file """
job = create_dict_action(**context)
schedule = {}
assign_value_from_source(job, schedule, 'schedule_end_date')
assign_value_from_source(job, schedule, 'schedule_interval')
assign_value_from_source(job, schedule, 'schedule_start_date')
job.pop('clients', None)
client_id = job.pop('client_id', None)
actions = job.pop('job_actions', [])
job['description'] = job.pop('description', None)
job['job_schedule'] = schedule
job['job_actions'] = actions
job['client_id'] = client_id
return _freezerclient(request).jobs.create(job)
def job_edit(request, context):
"""Edit an existing job file, but leave the actions to actions_edit"""
job = create_dict_action(**context)
schedule = {}
assign_value_from_source(job, schedule, 'schedule_end_date')
assign_value_from_source(job, schedule, 'schedule_interval')
assign_value_from_source(job, schedule, 'schedule_start_date')
job['description'] = job.pop('description', None)
actions = job.pop('job_actions', [])
job.pop('clients', None)
job.pop('client_id', None)
job['job_schedule'] = schedule
job['job_actions'] = actions
job_id = job.pop('original_name', None)
return _freezerclient(request).jobs.update(job_id, job)
def job_delete(request, obj_id):
return _freezerclient(request).jobs.delete(obj_id)
def job_clone(request, job_id):
job_file = _freezerclient(request).jobs.get(job_id)
job_file['description'] = \
'{0}_clone'.format(job_file['description'])
job_file.pop('job_id', None)
job_file.pop('_version', None)
return _freezerclient(request).jobs.create(job_file)
def job_get(request, job_id):
job_file = _freezerclient(request).jobs.get(job_id)
if job_file:
job_item = [job_file]
job = [Job(data) for data in job_item]
return job
return []
def job_list(request):
jobs = _freezerclient(request).jobs.list_all(limit=100)
job_list = []
for j in jobs:
description = j['description']
job_id = j['job_id']
try:
result = j['job_schedule']['result']
except KeyError:
result = 'pending'
job_list.append(JobList(description, result, job_id))
return job_list
def action_create(request, context):
"""Create a new action for a job """
action = {}
assign_value_from_source(context, action, 'max_retries')
assign_value_from_source(context, action, 'max_retries_interval')
assign_value_from_source(context, action, 'mandatory')
job_id = context.pop('original_name')
job_action = create_dict_action(**context)
action['freezer_action'] = job_action
action_id = _freezerclient(request).actions.create(action)
action['action_id'] = action_id
job = _freezerclient(request).jobs.get(job_id)
job['job_actions'].append(action)
return _freezerclient(request).jobs.update(job_id, job)
def action_create_without_job(request, context):
"""Create an action without being attached to a job"""
action = {}
assign_value_from_source(context, action, 'max_retries')
assign_value_from_source(context, action, 'max_retries_interval')
assign_value_from_source(context, action, 'mandatory')
job_action = create_dict_action(**context)
action['freezer_action'] = job_action
return _freezerclient(request).actions.create(action)
def action_list(request):
actions = _freezerclient(request).actions.list(limit=100)
actions = [Action(data) for data in actions]
return actions
def action_list_json(request):
return _freezerclient(request).actions.list(limit=100)
def actions_in_job_json(request, job_id):
job = _freezerclient(request).jobs.get(job_id)
action_list = []
for action in job['job_actions']:
a = {
"action_id": action['action_id'],
"freezer_action": action['freezer_action']
}
action_list.append(a)
return action_list
def actions_in_job(request, job_id):
actions = []
try:
job = _freezerclient(request).jobs.get(job_id)
for a in job['job_actions']:
try:
action_id = a['action_id']
except (KeyError, TypeError):
action_id = create_dummy_id()
try:
action = a['freezer_action']['action']
except (KeyError, TypeError):
action = "backup"
try:
backup_name = a['freezer_action']['backup_name']
except (KeyError, TypeError):
backup_name = "NO BACKUP NAME AVAILABLE"
actions.append(ActionJob(job_id, action_id, action, backup_name))
except TypeError:
pass
return actions
def action_get(request, action_id):
action = _freezerclient(request).actions.get(action_id)
return action
def action_update(request, context):
job_id = context.pop('original_name')
action_id = context.pop('action_id')
job = _freezerclient(request).jobs.get(job_id)
for a in job['job_actions']:
if a['action_id'] == action_id:
assign_value_from_source(context, a, 'max_retries')
assign_value_from_source(context, a, 'max_retries_interval')
assign_value_from_source(context, a, 'mandatory')
updated_action = create_dict_action(**context)
a['freezer_action'].update(updated_action)
return _freezerclient(request).jobs.update(job_id, job)
def action_delete(request, ids):
action_id, job_id = ids.split('===')
job = _freezerclient(request).jobs.get(job_id)
for action in job['job_actions']:
if action['action_id'] == action_id:
job['job_actions'].remove(action)
return _freezerclient(request).jobs.update(job_id, job)
def client_list(request):
clients = _freezerclient(request).registration.list(limit=100)
clients = [Client(c['uuid'],
c['client']['hostname'],
c['client']['client_id'])
for c in clients]
return clients
def client_list_json(request):
"""Return a list of clients directly form the api in json format"""
clients = _freezerclient(request).registration.list(limit=100)
return clients
def client_get(request, client_id):
"""Get a single client"""
client = _freezerclient(request).registration.get(client_id)
client = Client(client['uuid'],
client['client']['hostname'],
client['client']['client_id'])
return client
def add_job_to_session(request, session_id, job_id):
"""This function will add a job to a session and the API will handle the
copy of job information to the session
"""
try:
return _freezerclient(request).sessions.add_job(session_id, job_id)
except Exception:
return False
def remove_job_from_session(request, session_id, job_id):
"""Remove a job from a session will delete the job information but not the
job itself
"""
try:
return _freezerclient(request).sessions.remove_job(session_id, job_id)
except Exception:
return False
def session_create(request, context):
"""A session is a group of jobs who share the same scheduling time. """
session = create_dict_action(**context)
session['description'] = session.pop('description', None)
schedule = {}
assign_value_from_source(session, schedule, 'schedule_start_date')
assign_value_from_source(session, schedule, 'schedule_end_date')
assign_value_from_source(session, schedule, 'schedule_interval')
session['job_schedule'] = schedule
return _freezerclient(request).sessions.create(session)
def session_update(request, context):
"""Update session information """
session = create_dict_action(**context)
session_id = session.pop('session_id', None)
session['description'] = session.pop('description', None)
schedule = {}
assign_value_from_source(session, schedule, 'schedule_start_date')
assign_value_from_source(session, schedule, 'schedule_end_date')
assign_value_from_source(session, schedule, 'schedule_interval')
session['job_schedule'] = schedule
return _freezerclient(request).sessions.update(session_id, session)
def session_delete(request, session_id):
"""Delete session from API """
return _freezerclient(request).sessions.delete(session_id)
def session_list(request):
"""List all sessions """
sessions = _freezerclient(request).sessions.list_all(limit=100)
sessions = [Session(s['session_id'],
s['description'],
s['status'],
s['jobs'],
s['job_schedule'].get('schedule_start_date'),
s['job_schedule'].get('schedule_interval'),
s['job_schedule'].get('schedule_end_date'))
for s in sessions]
return sessions
def session_get(request, session_id):
"""Get a single session """
session = _freezerclient(request).sessions.get(session_id)
session = Session(session['session_id'],
session['description'],
session['status'],
session['jobs'],
session['job_schedule'].get('schedule_start_date'),
session['job_schedule'].get('schedule_interval'),
session['job_schedule'].get('schedule_end_date'))
return session
def backups_list(request, offset=0, time_after=None, time_before=None,
text_match=None):
"""List all backups and optionally you can provide filters and pagination
values
"""
page_size = utils.get_page_size(request)
search = {}
if time_after:
search['time_after'] = time_after
if time_before:
search['time_before'] = time_before
if text_match:
search['match'] = [
{
"_all": text_match,
}
]
backups = _freezerclient(request).backups.list(
limit=page_size + 1,
offset=offset,
search=search)
if len(backups) > page_size:
backups.pop()
has_more = True
else:
has_more = False
# Wrap data in object for easier handling
backups = [Backup(data) for data in backups]
return backups, has_more
def backup_get(request, backup_id):
"""Get a single backup"""
# for a local or ssh backup, the backup_id contains the
# path of the directory to backup, so that includes "/"
# or "\" for windows.
# so we send "~~~" instead "/" from the client to avoid
# conflicts in the api endpoint
backup_id = backup_id.replace("/", "~~~")
backup_id = backup_id.replace("\\", "~~~")
backup = _freezerclient(request).backups.get(backup_id)
backup = Backup(backup)
return backup

View File

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

View File

@ -1,108 +0,0 @@
# (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.
import logging
import datetime
import pprint
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import date as django_date
from django.views import generic
from horizon import exceptions
from horizon import tables
from horizon import workflows
from freezer_ui.backups import tables as freezer_tables
from freezer_ui.backups.workflows import restore as restore_workflow
import freezer_ui.api.api as freezer_api
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
name = _("Backups")
slug = "backups"
table_class = freezer_tables.BackupsTable
template_name = "freezer_ui/backups/index.html"
def get_data(self):
try:
backups, self._has_more = freezer_api.backups_list(self.request)
return backups
except Exception:
redirect = reverse("horizon:freezer_ui:backups:index")
msg = _('Unable to retrieve backups.')
exceptions.handle(self.request, msg, redirect=redirect)
class DetailView(generic.TemplateView):
template_name = 'freezer_ui/backups/detail.html'
def get_context_data(self, **kwargs):
try:
backup = freezer_api.backup_get(self.request,
kwargs['backup_id'])
return {'data': pprint.pformat(backup.data_dict)}
except Exception:
redirect = reverse("horizon:freezer_ui:backups:index")
msg = _('Unable to retrieve backup.')
exceptions.handle(self.request, msg, redirect=redirect)
class RestoreView(workflows.WorkflowView):
workflow_class = restore_workflow.Restore
def get_object(self, *args, **kwargs):
backup_id = self.kwargs['backup_id']
try:
return freezer_api.backup_get(self.request, backup_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):
try:
backup_id = self.kwargs['backup_id']
backup = freezer_api.backup_get(self.request, backup_id)
backup_date = datetime.datetime.fromtimestamp(
int(backup.data_dict['backup_metadata']['time_stamp']))
backup_date_str = django_date(backup_date,
'SHORT_DATETIME_FORMAT')
return "Restore '{}' from {}".format(
backup.data_dict['backup_metadata']['backup_name'],
backup_date_str)
except Exception:
redirect = reverse("horizon:freezer_ui:backups:index")
msg = _('Unable to retrieve backups.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_initial(self):
return {"backup_id": self.kwargs['backup_id']}
def get_workflow(self, *args, **kwargs):
try:
workflow = super(RestoreView, self).get_workflow(*args, **kwargs)
workflow.name = self.get_workflow_name()
return workflow
except Exception:
redirect = reverse("horizon:freezer_ui:backups:index")
msg = _('Unable to retrieve backups.')
exceptions.handle(self.request, msg, redirect=redirect)

View File

@ -1,32 +0,0 @@
# (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.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tables
LOG = logging.getLogger(__name__)
class ClientsTable(tables.DataTable):
client_id = tables.Column('client_id', verbose_name=_("Client ID"))
name = tables.Column('hostname', verbose_name=_("Hostname"))
class Meta:
name = "clients"
verbose_name = _("Clients")
row_actions = ()
table_actions = ()
multi_select = False

View File

@ -1,40 +0,0 @@
# (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.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from freezer_ui.clients import tables as freezer_tables
import freezer_ui.api.api as freezer_api
LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView):
name = _("Backups")
slug = "backups"
table_class = freezer_tables.ClientsTable
template_name = "freezer_ui/clients/index.html"
def get_data(self):
try:
return freezer_api.client_list(self.request)
except Exception:
redirect = reverse("horizon:freezer_ui:clients:index")
msg = _('Unable to retrieve details.')
exceptions.handle(self.request, msg, redirect=redirect)

View File

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

View File

@ -1,147 +0,0 @@
# (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.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse_lazy
from horizon import browsers
from horizon import exceptions
from horizon import messages
from horizon import workflows
import freezer_ui.api.api as freezer_api
import freezer_ui.jobs.browsers as project_browsers
from freezer_ui.utils import create_dict_action
import workflows.configure as configure_workflow
import workflows.action as action_workflow
LOG = logging.getLogger(__name__)
class JobWorkflowView(workflows.WorkflowView):
workflow_class = configure_workflow.ConfigureJob
def get_object(self, *args, **kwargs):
job_id = self.kwargs['backup_name']
try:
return freezer_api.job_get(self.request, job_id)
except Exception:
redirect = reverse("horizon:freezer_ui:jobs:index")
msg = _('Unable to retrieve details.')
exceptions.handle(self.request, msg, redirect=redirect)
def is_update(self):
return 'backup_name' in self.kwargs and \
bool(self.kwargs['backup_name'])
def get_initial(self):
initial = super(JobWorkflowView, self).get_initial()
if self.is_update():
initial.update({'original_name': None})
job = self.get_object()[0]
d = job.get_dict()
schedule = create_dict_action(**d['job_schedule'])
initial.update(**schedule)
info = {k: v for k, v in d.items()
if not k == 'job_schedule'}
initial.update(**info)
initial.update({'original_name': d.get('job_id', None)})
return initial
class JobsView(browsers.ResourceBrowserView):
browser_class = project_browsers.ContainerBrowser
template_name = "freezer_ui/jobs/browser.html"
def get_jobs_data(self):
jobs = []
try:
jobs = freezer_api.job_list(self.request)
except Exception:
msg = _('Unable to retrieve job file list.')
exceptions.handle(self.request, msg)
return jobs
def get_status_data(self):
job = []
try:
if self.kwargs['job_id']:
job = freezer_api.actions_in_job(
self.request, self.kwargs['job_id'])
except Exception:
msg = _('Unable to retrieve instances for this job.')
exceptions.handle(self.request, msg)
return job
class ActionWorkflowView(workflows.WorkflowView):
workflow_class = action_workflow.ConfigureAction
success_url = reverse_lazy("horizon:freezer_ui:jobs:index")
def get_context_data(self, **kwargs):
context = super(ActionWorkflowView, self).get_context_data(**kwargs)
job_id = self.kwargs['job_id']
context['job_id'] = job_id
return context
def get_object(self, *args, **kwargs):
ids = self.kwargs['job_id']
try:
action_id, job_id = ids.split('===')
except ValueError:
# action_id = None
job_id = self.kwargs['job_id']
try:
return freezer_api.job_get(self.request, job_id)
except Exception:
redirect = reverse("horizon:freezer_ui:jobs:index")
msg = _('Unable to retrieve details.')
exceptions.handle(self.request, msg, redirect=redirect)
def is_update(self):
return 'job_id' in self.kwargs and \
bool(self.kwargs['job_id'])
def get_initial(self, **kwargs):
initial = super(ActionWorkflowView, self).get_initial()
try:
action_id, job_id = self.kwargs['job_id'].split('===')
except ValueError:
# job_id = self.kwargs['job_id']
action_id = None
if self.is_update():
initial.update({'original_name': None})
job = self.get_object()[0]
d = job.get_dict()
for action in d['job_actions']:
try:
if action['action_id'] == action_id:
actions = create_dict_action(**action)
rules = {k: v for k, v in action.items()
if not k == 'freezer_action'}
initial.update(**actions['freezer_action'])
initial.update(**rules)
except KeyError:
messages.warning(self.request, _("Cannot edit an action "
"created by the"
" scheduler"))
exceptions.handle(self.request, "")
initial.update({'original_name': job.id})
return initial

View File

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

View File

@ -1,120 +0,0 @@
# (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.
import logging
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import browsers
from horizon import exceptions
from horizon import workflows
import freezer_ui.sessions.browsers as project_browsers
from freezer_ui.sessions.workflows import attach
from freezer_ui.sessions.workflows import create_session
import freezer_ui.api.api as freezer_api
from freezer_ui.utils import SessionJob
LOG = logging.getLogger(__name__)
class AttachToSessionWorkflow(workflows.WorkflowView):
workflow_class = attach.AttachJobToSession
def get_object(self, *args, **kwargs):
job_id = self.kwargs['job_id']
try:
return freezer_api.job_get(self.request, job_id)
except Exception:
redirect = reverse("horizon:freezer_ui:jobs:index")
msg = _('Unable to retrieve details.')
exceptions.handle(self.request, msg, redirect=redirect)
def is_update(self):
return 'job_id' in self.kwargs and \
bool(self.kwargs['job_id'])
def get_initial(self):
initial = super(AttachToSessionWorkflow, self).get_initial()
job = self.get_object()[0]
initial.update({'job_id': job.id})
return initial
class SessionsView(browsers.ResourceBrowserView):
browser_class = project_browsers.SessionBrowser
template_name = "freezer_ui/sessions/browser.html"
def get_sessions_data(self):
sessions = []
try:
sessions = freezer_api.session_list(self.request)
except Exception:
msg = _('Unable to retrieve sessions list.')
exceptions.handle(self.request, msg)
return sessions
def get_jobs_data(self):
jobs = []
session = None
try:
if self.kwargs['session_id']:
session = freezer_api.session_get(
self.request,
self.kwargs['session_id'])
try:
jobs = [SessionJob(k,
self.kwargs['session_id'],
v['client_id'],
v['status'])
for k, v in session.jobs.iteritems()]
except AttributeError:
pass
except Exception:
msg = _('Unable to retrieve session information.')
exceptions.handle(self.request, msg)
return jobs
class CreateSessionWorkflow(workflows.WorkflowView):
workflow_class = create_session.CreateSession
def get_object(self, *args, **kwargs):
session_id = self.kwargs['session_id']
try:
return freezer_api.session_get(self.request, session_id)
except Exception:
redirect = reverse("horizon:freezer_ui:sessions:index")
msg = _('Unable to retrieve session.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_initial(self):
initial = super(CreateSessionWorkflow, self).get_initial()
if self.is_update():
session = self.get_object()
initial.update({
'description': session.description,
'session_id': session.session_id,
'schedule_start_date': session.start_datetime,
'schedule_interval': session.interval,
'schedule_end_date': session.end_datetime
})
return initial
def is_update(self):
return 'session_id' in self.kwargs and \
bool(self.kwargs['session_id'])

View File

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

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% block sidebar %}
{% include 'horizon/common/_sidebar.html' %}
{% endblock %}
{% block main %}
{% include "horizon/_messages.html" %}
{% block mydashboard_main %}{% endblock %}
{% endblock %}

View File

@ -1,32 +0,0 @@
# 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 freezer_ui.api.rest.urls as rest_urls
urlpatterns = patterns(
'',
url(r'', include(rest_urls)),
)

View File

@ -1,156 +0,0 @@
# (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.
import uuid
import datetime
from django.template.defaultfilters import date as django_date
def create_dict_action(**kwargs):
"""Create a dict only with values that exists so we avoid send keys with
None values
"""
return {k: v for k, v in kwargs.items() if v}
def timestamp_to_string(ts):
return django_date(
datetime.datetime.fromtimestamp(int(ts)),
'SHORT_DATETIME_FORMAT')
class Dict2Object(object):
"""Makes dictionary fields accessible as if they are attributes.
The dictionary keys become class attributes. It is possible to use one
nested dictionary by overwriting nested_dict with the key of that nested
dict.
This class is needed because we mostly deal with objects in horizon (e.g.
for providing data to the tables) but the api only gives us json data.
"""
nested_dict = None
def __init__(self, data_dict):
self.data_dict = data_dict
def __getattr__(self, attr):
"""Make data_dict fields available via class interface """
if attr in self.data_dict:
return self.data_dict[attr]
elif attr in self.data_dict[self.nested_dict]:
return self.data_dict[self.nested_dict][attr]
else:
return object.__getattribute__(self, attr)
def get_dict(self):
return self.data_dict
class Action(Dict2Object):
nested_dict = 'job_action'
@property
def id(self):
return self.job_id
class Job(Dict2Object):
nested_dict = 'job_actions'
@property
def id(self):
return self.job_id
class Backup(Dict2Object):
nested_dict = 'backup_metadata'
@property
def id(self):
return self.backup_id
class Client(object):
def __init__(self, uuid, hostname, client_id):
self.uuid = uuid
self.hostname = hostname
self.client_id = client_id
self.id = client_id
class ActionJob(object):
def __init__(self, job_id, action_id, action, backup_name):
self.job_id = job_id
self.action_id = action_id
self.action = action
self.backup_name = backup_name
class Session(object):
def __init__(self, session_id, description, status, jobs,
start_datetime, interval, end_datetime):
self.session_id = session_id
self.description = description
self.status = status
self.jobs = jobs
self.start_datetime = start_datetime
self.interval = interval
self.end_datetime = end_datetime
class SessionJob(object):
"""Create a job object to work with in horizon"""
def __init__(self, job_id, session_id, client_id, status):
self.job_id = job_id
self.session_id = session_id
self.client_id = client_id
self.status = status
class JobList(object):
"""Create an object to be passed to horizon tables that handles
nested values
"""
def __init__(self, description, result, job_id):
self.description = description
self.result = result
self.id = job_id
self.job_id = job_id
def create_dummy_id():
"""Generate a dummy id for documents generated by the scheduler.
This is needed when the scheduler creates jobs with actions attached
directly, those actions are not registered in the db.
"""
return uuid.uuid4().hex
def actions_in_job(ids):
"""Return an ordered list of actions for a new job
"""
ids = ids.split('===')
return [i for i in ids if i]
def assign_value_from_source(source_dict, dest_dict, key):
"""Assign a value to a destination dict from a source dict
if the key exists
"""
if key in source_dict:
dest_dict[key] = source_dict.pop(key)

View File

@ -0,0 +1 @@
Django>=1.4.2,<1.8

View File

@ -1,7 +1,7 @@
[metadata]
name = freezer-web-ui
author = Fabrizio Fresco, Fausto Marzi, Jonas Pfannschmidt, Guillermo Ramirez Garcia
author-email = openstack-dev@lists.openstack.org
author-email = memo@hpe.com
summary = Freezer - Backup as a Service User Interface
description-file = README.rst
home-page = https://github.com/openstack/freezer-web-ui
@ -27,7 +27,7 @@ keywords =
[files]
packages =
freezer_ui
disaster_recovery
[bdist_wheel]
universal = 1

View File

@ -18,4 +18,5 @@ pytest
pytest-cov
pytest-xdist
pylint>=1.3.1
testresources
testresources
mock>=1.0,<1.1.0

View File

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

0
tests/test_api.py Normal file
View File

Some files were not shown because too many files have changed in this diff Show More