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:
parent
26f1deb396
commit
b08558eba4
40
README.rst
40
README.rst
@ -2,6 +2,13 @@
|
||||
Freezer - Horizon Dashboard
|
||||
===========================
|
||||
|
||||
freezer-web-ui is a horizon plugin based in django aimed at providing an interaction
|
||||
with freezer
|
||||
|
||||
* Release management: https://launchpad.net/freezer-web-ui
|
||||
* Blueprints and feature specifications: https://blueprints.launchpad.net/freezer-web-ui
|
||||
* Issue tracking: https://bugs.launchpad.net/freezer-web-ui
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
@ -73,39 +80,8 @@ To deploy freezer dashboard in production you need to do the following::
|
||||
A new tab called "Disaster Recovery" will be on your panels.
|
||||
|
||||
|
||||
Running the unit tests
|
||||
======================
|
||||
|
||||
1. Create a virtual environment::
|
||||
|
||||
virtualenv --no-site-packages -p /usr/bin/python2.7 .venv
|
||||
|
||||
2. Activate the virtual environment::
|
||||
|
||||
. ./.venv/bin/activate
|
||||
|
||||
3. Install the requirements::
|
||||
|
||||
pip install -r test-requirements.txt
|
||||
|
||||
4. Run the tests::
|
||||
|
||||
python manage.py test . --settings=freezer_ui.tests.settings
|
||||
|
||||
Test coverage
|
||||
-------------
|
||||
|
||||
1. Collect coverage information::
|
||||
|
||||
coverage run --source='.' --omit='.venv/*' manage.py test . --settings=freezer_ui.tests.settings
|
||||
|
||||
2. View coverage report::
|
||||
|
||||
coverage report
|
||||
|
||||
|
||||
Tox
|
||||
---
|
||||
===
|
||||
|
||||
1. Run tox::
|
||||
|
||||
|
@ -13,12 +13,12 @@
|
||||
# limitations under the License.
|
||||
|
||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
||||
DASHBOARD = 'freezer_ui'
|
||||
DASHBOARD = 'disaster_recovery'
|
||||
|
||||
# If set to True, this dashboard will not be added to the settings.
|
||||
DISABLED = False
|
||||
|
||||
# A list of applications to be added to INSTALLED_APPS.
|
||||
ADD_INSTALLED_APPS = [
|
||||
'freezer_ui',
|
||||
'disaster_recovery',
|
||||
]
|
||||
|
@ -15,7 +15,7 @@ where::
|
||||
|
||||
For example::
|
||||
|
||||
enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
|
||||
enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
|
||||
|
||||
For more information, see:
|
||||
http://docs.openstack.org/developer/devstack/plugins.html
|
||||
|
@ -1,7 +1,7 @@
|
||||
[[local|localrc]]
|
||||
disable_all_services
|
||||
|
||||
enable_plugin freezer-web-ui https://github.com/stackforge/freezer-web-ui.git master
|
||||
enable_plugin freezer-web-ui https://github.com/openstack/freezer-web-ui.git master
|
||||
|
||||
enable_service rabbit mysql key
|
||||
|
||||
|
@ -20,11 +20,11 @@ FREEZER_WEB_UI_DIR=$DEST/freezer-web-ui
|
||||
FREEZER_WEB_UI_FILES=${FREEZER_WEB_UI_DIR}/devstack/files
|
||||
|
||||
# Freezer Web UI repository
|
||||
FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/stackforge/freezer-web-ui.git}
|
||||
FREEZER_WEB_UI_REPO=${FREEZER_WEB_UI_REPO:-${GIT_BASE}/openstack/freezer-web-ui.git}
|
||||
FREEZER_WEB_UI_BRANCH=${FREEZER_WEB_UI_BRANCH:-master}
|
||||
|
||||
# Freezer client
|
||||
FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/stackforge/freezer.git}
|
||||
FREEZER_CLIENT_REPO=${FREEZER_CLIENT_REPO:-${GIT_BASE}/openstack/freezer.git}
|
||||
FREEZER_CLIENT_DIR=$DEST/freezer
|
||||
FREEZER_CLIENT_BRANCH=${FREEZER_CLIENT_BRANCH:-master}
|
||||
|
||||
|
27
disaster_recovery/actions/panel.py
Normal file
27
disaster_recovery/actions/panel.py
Normal 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)
|
105
disaster_recovery/actions/tables.py
Normal file
105
disaster_recovery/actions/tables.py
Normal 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
|
17
disaster_recovery/actions/templates/actions/detail.html
Normal file
17
disaster_recovery/actions/templates/actions/detail.html
Normal 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 %}
|
16
disaster_recovery/actions/templates/actions/index.html
Normal file
16
disaster_recovery/actions/templates/actions/index.html
Normal 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 %}
|
33
disaster_recovery/actions/urls.py
Normal file
33
disaster_recovery/actions/urls.py
Normal 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'),
|
||||
)
|
76
disaster_recovery/actions/views.py
Normal file
76
disaster_recovery/actions/views.py
Normal 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
|
@ -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
|
522
disaster_recovery/api/api.py
Normal file
522
disaster_recovery/api/api.py
Normal 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)
|
@ -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")
|
@ -24,7 +24,7 @@ import rest_api
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(r'^api/clients$', rest_api.Clients.as_view(), name="api_clients"),
|
||||
url(r'^api/actions/$', rest_api.Actions.as_view(), name="api_actions"),
|
||||
url(r'^api/actions$', rest_api.ActionList.as_view(), name="api_actions"),
|
||||
url(r'^api/actions/job/(?P<job_id>[^/]+)?$',
|
||||
rest_api.ActionsInJob.as_view(), name="api_actions_in_job"),
|
||||
rest_api.Actions.as_view(), name="api_actions_in_job"),
|
||||
)
|
@ -16,7 +16,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
from freezer_ui import dashboard
|
||||
from disaster_recovery import dashboard
|
||||
|
||||
|
||||
class BackupsPanel(horizon.Panel):
|
@ -17,9 +17,11 @@ import logging
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils import safestring
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from horizon.utils import functions as utils
|
||||
|
||||
from horizon import tables
|
||||
from freezer_ui.utils import timestamp_to_string
|
||||
from horizon.utils import functions as utils
|
||||
|
||||
from disaster_recovery.utils import timestamp_to_string
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
@ -32,16 +34,13 @@ class Restore(tables.LinkAction):
|
||||
ajax = True
|
||||
|
||||
def get_link_url(self, datum=None):
|
||||
return reverse("horizon:freezer_ui:backups:restore",
|
||||
return reverse("horizon:disaster_recovery:backups:restore",
|
||||
kwargs={'backup_id': datum.id})
|
||||
|
||||
|
||||
class BackupFilter(tables.FilterAction):
|
||||
filter_type = "server"
|
||||
filter_choices = (("before", "Created before", True),
|
||||
("after", "Created after", True),
|
||||
("between", "Created between", True),
|
||||
("contains", "Contains text", True))
|
||||
filter_choices = (("contains", "Contains text", True),)
|
||||
|
||||
|
||||
def icons(backup):
|
||||
@ -88,13 +87,14 @@ def icons(backup):
|
||||
|
||||
|
||||
def backup_detail_view(backup):
|
||||
return reverse("horizon:freezer_ui:backups:detail",
|
||||
args=[backup.backup_id])
|
||||
return reverse("horizon:disaster_recovery:backups:detail",
|
||||
kwargs={'backup_id': backup.id})
|
||||
|
||||
|
||||
class BackupsTable(tables.DataTable):
|
||||
backup_name = tables.Column('backup_name',
|
||||
verbose_name=_("Backup Name"))
|
||||
verbose_name=_("Backup Name"),
|
||||
link=backup_detail_view)
|
||||
hostname = tables.Column('hostname', verbose_name=_("Hostname"))
|
||||
created = tables.Column("time_stamp",
|
||||
verbose_name=_("Created At"),
|
@ -16,7 +16,7 @@
|
||||
from django.conf.urls import patterns
|
||||
from django.conf.urls import url
|
||||
|
||||
from freezer_ui.backups import views
|
||||
from disaster_recovery.backups import views
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
82
disaster_recovery/backups/views.py
Normal file
82
disaster_recovery/backups/views.py
Normal 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
|
@ -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
|
@ -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):
|
77
disaster_recovery/clients/tables.py
Normal file
77
disaster_recovery/clients/tables.py
Normal 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
|
17
disaster_recovery/clients/templates/clients/detail.html
Normal file
17
disaster_recovery/clients/templates/clients/detail.html
Normal 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 %}
|
29
disaster_recovery/clients/urls.py
Normal file
29
disaster_recovery/clients/urls.py
Normal 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'),
|
||||
)
|
49
disaster_recovery/clients/views.py
Normal file
49
disaster_recovery/clients/views.py
Normal 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)}
|
@ -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)
|
@ -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
|
||||
|
@ -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):
|
@ -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
|
@ -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>
|
@ -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 %}
|
@ -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'),
|
||||
)
|
84
disaster_recovery/jobs/views.py
Normal file
84
disaster_recovery/jobs/views.py
Normal 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
|
81
disaster_recovery/jobs/workflows/actions.py
Normal file
81
disaster_recovery/jobs/workflows/actions.py
Normal 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
|
@ -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
|
0
disaster_recovery/sessions/__init__.py
Normal file
0
disaster_recovery/sessions/__init__.py
Normal 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
|
||||
|
@ -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):
|
@ -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,)
|
@ -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(
|
86
disaster_recovery/sessions/views.py
Normal file
86
disaster_recovery/sessions/views.py
Normal 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'])
|
0
disaster_recovery/sessions/workflows/__init__.py
Normal file
0
disaster_recovery/sessions/workflows/__init__.py
Normal 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
|
@ -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
|
9
disaster_recovery/static/freezer/css/freezer.css
Normal file
9
disaster_recovery/static/freezer/css/freezer.css
Normal 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%;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>'
|
||||
);
|
||||
}
|
@ -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;
|
||||
});
|
@ -19,7 +19,7 @@
|
||||
"use strict";
|
||||
|
||||
var url = $(location).attr("origin");
|
||||
url += '/freezer_ui/api/clients';
|
||||
url += '/disaster_recovery/api/clients';
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
@ -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
177
disaster_recovery/utils.py
Normal 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
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
@ -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)
|
@ -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
|
@ -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)
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
@ -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
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Stub file to work around django bug: https://code.djangoproject.com/ticket/7198
|
||||
"""
|
@ -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'])
|
@ -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;
|
||||
}
|
@ -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 %}
|
||||
|
@ -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)),
|
||||
)
|
@ -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)
|
@ -0,0 +1 @@
|
||||
Django>=1.4.2,<1.8
|
@ -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
|
||||
|
@ -18,4 +18,5 @@ pytest
|
||||
pytest-cov
|
||||
pytest-xdist
|
||||
pylint>=1.3.1
|
||||
testresources
|
||||
testresources
|
||||
mock>=1.0,<1.1.0
|
@ -1 +0,0 @@
|
||||
__author__ = 'jonas'
|
0
tests/test_api.py
Normal file
0
tests/test_api.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user