Add segment panel

Added segment panel and implemented list and create segment
functionality.Added test cases that actually not covering the
line of code but tested the list and create functionally.

Partial-Implements: blueprint masakari-dashboard

Change-Id: I1366bfdc188f4e0d53fa46f2a6ea3790c9f295fc
This commit is contained in:
nirajsingh 2018-01-30 12:59:31 +05:30
parent b09b094a2b
commit c429554def
36 changed files with 1023 additions and 43 deletions

View File

@ -53,7 +53,9 @@ Install Masakari dashboard with all dependencies in your virtual environment::
And enable it in Horizon::
ln -s ../masakari-dashboard/masakaridashboard/enabled/_90_project_MasakariDashboard.py openstack_dashboard/local/enabled
ln -s ../masakari-dashboard/masakaridashboard/local/enabled/_50_masakaridashboard.py openstack_dashboard/local/enabled
ln -s ../masakari-dashboard/masakaridashboard/local/local_settings.d/_50_masakari.py openstack_dashboard/local/local_settings.d
ln -s ../masakari-dashboard/masakaridashboard/conf/masakari_policy.json openstack_dashboard/conf
To run horizon with the newly enabled Masakari dashboard plugin run::

13
doc/requirements.txt Normal file
View File

@ -0,0 +1,13 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Order matters to the pip dependency resolver, so sorting this file
# changes how packages are installed. New dependencies should be
# added in alphabetical order, however, some dependencies may need to
# be installed in a specific order.
# Docs Requirements
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
oslosphinx>=4.7.0 # Apache-2.0
reno>=2.5.0 # Apache-2.0
docutils>=0.11 # OSI-Approved Open Source, Public Domain

View File

@ -53,7 +53,9 @@ Install Masakari dashboard with all dependencies in your virtual environment::
And enable it in Horizon::
ln -s ../masakari-dashboard/masakaridashboard/enabled/_90_project_MasakariDashboard.py openstack_dashboard/local/enabled
ln -s ../masakari-dashboard/masakaridashboard/local/enabled/_50_masakaridashboard.py openstack_dashboard/local/enabled
ln -s ../masakari-dashboard/masakaridashboard/local/local_settings.d/_50_masakari.py openstack_dashboard/local/local_settings.d
ln -s ../masakari-dashboard/masakaridashboard/conf/masakari_policy.json openstack_dashboard/conf
To run horizon with the newly enabled Masakari dashboard plugin run::

View File

@ -0,0 +1,112 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 itertools
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from horizon.utils import functions as utils
from horizon.utils import memoized
from keystoneauth1.identity.generic import token
from keystoneauth1 import session as ks_session
from openstack import connection
from masakaridashboard.handle_errors import handle_errors
@memoized.memoized
def openstack_connection(request):
auth = token.Token(
auth_url=getattr(settings, 'OPENSTACK_KEYSTONE_URL'),
token=request.user.token.id,
project_name=request.user.project_name,
project_id=request.user.tenant_id)
session = ks_session.Session(auth=auth)
conn = connection.Connection(session=session)
return conn.instance_ha
@handle_errors(_("Unable to retrieve segments"), [])
def get_segment_list(request, marker='', paginate=False, filters=None):
"""Returns segments as per page size."""
page_size = utils.get_page_size(request)
client = openstack_connection(request)
kwargs = get_request_param(marker, paginate, filters, page_size)
entities_iter = client.segments(**kwargs)
has_prev_data = has_more_data = False
if paginate:
entities, has_more_data, has_prev_data = pagination_process(
entities_iter, kwargs['limit'], page_size, marker)
else:
entities = list(entities_iter)
return entities, has_more_data, has_prev_data
def get_request_param(marker, paginate, filters, page_size):
limit = getattr(settings, 'API_RESULT_LIMIT', 100)
if paginate:
request_size = page_size + 1
else:
request_size = limit
kwargs = {"marker": marker,
"limit": request_size
}
if filters is not None:
kwargs.update(filters)
return kwargs
def pagination_process(data, request_size, page_size, marker):
"""Retrieve a listing of specific entity and handles pagination.
:param request: Request data
:param marker: Pagination marker for large data sets: entity id
:param paginate: If true will perform pagination based on settings.
Default:False
"""
prev_data = more_data = False
entities = list(itertools.islice(data, request_size))
# first and middle page condition
if len(entities) > page_size:
entities.pop()
more_data = True
# middle page condition
if marker is not None:
prev_data = True
elif marker is not None:
prev_data = True
return entities, more_data, prev_data
@handle_errors(_("Unable to retrieve segments"), [])
def segment_list(request):
return list(openstack_connection(request).segments())
def segment_create(request, data):
"""Create segment."""
return openstack_connection(request).create_segment(**data)
@handle_errors(_("Unable to retrieve segment"), [])
def get_segment(request, segment_id):
"""Returns segment by id"""
return openstack_connection(request).get_segment(segment_id)

View File

@ -0,0 +1,10 @@
{
"admin_api": "is_admin:True",
"context_is_admin": "role:admin",
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
"default": "rule:admin_api",
"os_masakari_api:extensions": "rule:admin_api",
"os_masakari_api:segments": "rule:admin_api",
"os_masakari_api:os-hosts": "rule:admin_api",
"os_masakari_api:notifications": "rule:admin_api"
}

View File

@ -0,0 +1,32 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 masakaridashboard.default import panel
class MasakariDashboard(horizon.Dashboard):
slug = "masakaridashboard"
name = _("Instance-ha")
panels = ('default', 'segments')
default_panel = 'default'
policy_rules = (('instance-ha', 'context_is_admin'),)
horizon.register(MasakariDashboard)
MasakariDashboard.register(panel.Default)

View File

@ -0,0 +1,24 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Default(horizon.Panel):
name = _("Default")
slug = 'default'
nav = False

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block css %}
{% include "_stylesheets.html" %}
<link href='{{ STATIC_URL }}masakaridashboard/css/style.css' type='text/css' media='screen' rel='stylesheet' />
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'masakaridashboard/default/base.html' %}
{% block main %}
<div class="masakari-wrapper">
{{ table.render }}
</div>
{% endblock %}

View File

@ -0,0 +1,75 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 functools
import inspect
import horizon.exceptions
def handle_errors(error_message, error_default=None, request_arg=None):
"""A decorator for adding default error handling to API calls.
It wraps the original method in a try-except block, with horizon's
error handling added.
Note: it should only be used on functions or methods that take request as
their argument (it has to be named "request", or ``request_arg`` has to be
provided, indicating which argument is the request).
The decorated method accepts a number of additional parameters:
:param _error_handle: whether to handle the errors in this call
:param _error_message: override the error message
:param _error_default: override the default value returned on error
:param _error_redirect: specify a redirect url for errors
:param _error_ignore: ignore known errors
"""
def decorator(func):
if request_arg is None:
_request_arg = 'request'
if _request_arg not in inspect.getargspec(func).args:
raise RuntimeError(
"The handle_errors decorator requires 'request' as "
"an argument of the function or method being decorated")
else:
_request_arg = request_arg
@functools.wraps(func)
def wrapper(*args, **kwargs):
_error_handle = kwargs.pop('_error_handle', True)
_error_message = kwargs.pop('_error_message', error_message)
_error_default = kwargs.pop('_error_default', error_default)
_error_redirect = kwargs.pop('_error_redirect', None)
_error_ignore = kwargs.pop('_error_ignore', False)
if not _error_handle:
return func(*args, **kwargs)
try:
return func(*args, **kwargs)
except Exception as e:
callargs = inspect.getcallargs(func, *args, **kwargs)
request = callargs[_request_arg]
_error_message += ': ' + str(e)
horizon.exceptions.handle(request, _error_message,
ignore=_error_ignore,
redirect=_error_redirect)
return _error_default
wrapper.wrapped = func
return wrapper
return decorator

View File

@ -0,0 +1,25 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 openstack_dashboard import exceptions
DASHBOARD = 'masakaridashboard'
ADD_INSTALLED_APPS = ['masakaridashboard']
ADD_EXCEPTIONS = {
'recoverable': exceptions.RECOVERABLE,
'not_found': exceptions.NOT_FOUND,
'unauthorized': exceptions.UNAUTHORIZED,
}

View File

@ -0,0 +1,19 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 openstack_dashboard.settings import POLICY_FILES
POLICY_FILES.update({'instance-ha': 'masakari_policy.json'})

View File

View File

@ -0,0 +1,69 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from masakaridashboard.api import api
class CreateSegmentForm(forms.SelfHandlingForm):
name = forms.CharField(
label=_('Segment Name'),
widget=forms.TextInput(attrs={'maxlength': 255}),
help_text=_('The segment name.'))
recovery_method = forms.ChoiceField(
label=_('Recovery Method'),
choices=[('auto', 'auto'),
('auto_priority', 'auto_priority'),
('reserved_host', 'reserved_host'),
('rh_priority', 'rh_priority')],
widget=forms.Select(
attrs={'class': 'switchable',
'data-slug': 'recovery_method'}),
required=True,
help_text=_('Type of recovery if any host in this segment goes down.')
)
service_type = forms.CharField(
label=_('Service Type'),
help_text=_('The name of service which will be deployed in this'
' segment. As of now user can mention COMPUTE as service'
' type.'),
widget=forms.TextInput(attrs={
'readonly': 'readonly', 'value': 'compute'}))
description = forms.CharField(
label=_("Description"), widget=forms.Textarea(
attrs={'rows': 4}), required=False)
def __init__(self, *args, **kwargs):
super(CreateSegmentForm, self).__init__(*args, **kwargs)
def handle(self, request, data):
try:
api.segment_create(request, data)
msg = _('Successfully created segment')
messages.success(request, msg)
except Exception as exc:
if exc.status_code == 409:
msg = _('Segment with name "%s" already exists') % data["name"]
else:
msg = _('Failed to create segment')
redirect = reverse('horizon:masakaridashboard:segments:index')
exceptions.handle(request, msg, redirect=redirect)
return True

View File

@ -0,0 +1,28 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 masakaridashboard import dashboard
class Segment(horizon.Panel):
name = _("Segments")
slug = 'segments'
dashboard.MasakariDashboard.register(Segment)

View File

@ -0,0 +1,58 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.utils.translation import ugettext_lazy as _
from horizon import tables
class CreateSegment(tables.LinkAction):
name = "create"
verbose_name = _("Create Segment")
url = "horizon:masakaridashboard:segments:create_segment"
classes = ("ajax-modal",)
icon = "plus"
SEGMENT_FILTER_CHOICES = (
('recovery_method', _("Recovery Method ="), True),
('service_type', _("Service Type ="), True),
)
class SegmentFilterAction(tables.FilterAction):
filter_type = "server"
filter_choices = SEGMENT_FILTER_CHOICES
class FailoverSegmentTable(tables.DataTable):
name = tables.WrappingColumn('name', verbose_name=_("Name"), truncate=40)
uuid = tables.Column('uuid', verbose_name=_("UUID"))
recovery_method = tables.Column(
'recovery_method', verbose_name=_("Recovery Method"))
service_type = tables.Column(
'service_type', verbose_name=_("Service Type"))
description = tables.WrappingColumn(
'description', verbose_name=_("Description"),
truncate=40)
def get_object_id(self, datum):
return datum.uuid
class Meta(object):
name = "failover_segment"
verbose_name = _("FailoverSegment")
table_actions = (CreateSegment, SegmentFilterAction)

View File

@ -0,0 +1,18 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a failover segment of hypervisor hosts." %}</p>
<p>
<strong>{% trans "Recovery methods:" %}</strong>
</p>
<p>{% trans "auto: Nova selects the new compute host for evacuation of instances running on a failed compute host" %}</p>
<p>{% trans "reserved_host : One of the reserved host configured in the segment will be used for evacuation of instances running on a failed compute host" %}</p>
<p>{% trans "auto_priority: First it will try 'auto' recovery method, if it's fails, then it will try using 'reserved_host' recovery method." %}</p>
<p>{% trans "rh_priority: It is exactly opposite of 'auto_priority' recovery method." %}</p>
<p>
<strong>{% trans "Please note: " %}</strong>
{% trans "Service Type is presently not used by Masakari but it's a mandatory field so the default value is set to 'compute' and it cannot be changed." %}
</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Validate Segment" %}{% endblock %}
{% block main %}
{% include 'masakaridashboard/segments/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'masakaridashboard/default/table.html' %}
{% load i18n %}
{% block title %}{% trans "Segments" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Segments") %}
{% endblock page_header %}

View File

@ -0,0 +1,151 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 import settings
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.utils.http import urlunquote
import mock
from masakaridashboard.segments import tables as segment_table
from masakaridashboard.test import helpers as test
INDEX_URL = reverse('horizon:masakaridashboard:segments:index')
CREATE_URL = reverse('horizon:masakaridashboard:segments:create_segment')
class SegmentTest(test.TestCase):
def test_index(self):
with mock.patch(
'masakaridashboard.api.api.get_segment_list',
return_value=[self.masakari_segment.list(),
False, False]) as mock_get_segment_list:
res = self.client.get(INDEX_URL)
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'masakaridashboard/segments/index.html')
mock_get_segment_list.assert_called_once_with(
filters={}, marker=None, paginate=True, request=mock.ANY)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, self.masakari_segment.list())
def test_create_get(self):
res = self.client.get(CREATE_URL)
self.assertTemplateUsed(res, 'masakaridashboard/segments/create.html')
def test_create_post(self):
segment = self.masakari_segment.list()[0]
form_data = {
'name': segment.name,
'recovery_method': segment.recovery_method,
'service_type': segment.service_type,
'description': segment.description
}
with mock.patch('masakaridashboard.api.api.segment_create',
return_value=segment) as mocked_create:
res = self.client.post(CREATE_URL, form_data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
self.assertRedirectsNoFollow(res, INDEX_URL)
mocked_create.assert_called_once_with(
mock.ANY,
form_data
)
def _test_segments_index_paginated(
self, filters, marker, segments, url, has_more, has_prev):
with mock.patch(
'masakaridashboard.api.api.get_segment_list',
return_value=[segments,
has_more, has_prev]) as mock_get_segment_list:
res = self.client.get(urlunquote(url))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'masakaridashboard/segments/index.html')
mock_get_segment_list.assert_called_once_with(
filters=filters, marker=marker, paginate=True, request=mock.ANY)
return res
@override_settings(API_RESULT_PAGE_SIZE=1)
@mock.patch('masakaridashboard.api.api.get_segment')
def test_segments_index_paginated(self, mock_get_segment):
mock_get_segment.return_value = self.masakari_segment.list()[0]
segment_list = self.masakari_segment.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
next = segment_table.FailoverSegmentTable._meta.pagination_param
# get first page
expected_segments = segment_list[:size]
res = self._test_segments_index_paginated(filters={}, marker=None,
segments=expected_segments,
url=base_url, has_more=True,
has_prev=False)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, expected_segments)
# get second page
expected_segments = segment_list[size:2 * size]
marker = expected_segments[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_segments_index_paginated(filters={}, marker=marker,
segments=expected_segments,
url=url, has_more=True,
has_prev=True)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, expected_segments)
# get last page
expected_segments = segment_list[-size:]
marker = expected_segments[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_segments_index_paginated(filters={}, marker=marker,
segments=expected_segments,
url=url, has_more=False,
has_prev=True)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, expected_segments)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_segments_index_paginated_prev_page(self):
segment_list = self.masakari_segment.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
prev = segment_table.FailoverSegmentTable._meta.prev_pagination_param
# prev from some page
expected_segments = segment_list[size:2 * size]
marker = expected_segments[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_segments_index_paginated(filters={}, marker=marker,
segments=expected_segments,
url=url, has_more=True,
has_prev=True)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, expected_segments)
# back to first page
expected_segments = segment_list[:size]
marker = expected_segments[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_segments_index_paginated(
filters={}, marker=marker, segments=expected_segments,
url=url, has_more=True, has_prev=False)
segments = res.context['failover_segment_table'].data
self.assertItemsEqual(segments, expected_segments)

View File

@ -0,0 +1,26 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 url
from masakaridashboard.segments import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create_segment$',
views.CreateSegmentView.as_view(),
name='create_segment'),
]

View File

@ -0,0 +1,94 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 import settings
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from masakaridashboard.api import api
from masakaridashboard.segments import tables as masakari_tab
from horizon import exceptions
from horizon import forms
from masakaridashboard.segments import forms as segment_forms
class IndexView(tables.DataTableView):
table_class = masakari_tab.FailoverSegmentTable
template_name = 'masakaridashboard/segments/index.html'
page_title = _("Segments")
_more = False
_prev = False
def needs_filter_first(self, table):
return self._needs_filter_first
def has_more_data(self, table):
return self._more
def has_prev_data(self, table):
return self._prev
def get_data(self):
segments = []
marker = self.request.GET.get(
masakari_tab.FailoverSegmentTable._meta.pagination_param,
None
)
if marker is not None:
segment = api.get_segment(self.request, marker)
marker = segment.id
filters = self.get_filters()
self._needs_filter_first = True
filter_first = getattr(settings, 'FILTER_DATA_FIRST', {})
if filter_first.get('masakaridashboard.segments', False) and len(
filters) == 0:
self._needs_filter_first = True
self._more = False
return segments
try:
segments, self._more, self._prev = api.get_segment_list(
request=self.request,
marker=marker,
filters=filters,
paginate=True
)
except Exception:
self._prev = False
self._more = False
msg = _('Unable to retrieve segment list.')
exceptions.handle(self.request, msg)
return segments
class CreateSegmentView(forms.ModalFormView):
template_name = 'masakaridashboard/segments/create.html'
modal_header = _("Create Segment")
form_id = "create_segment"
form_class = segment_forms.CreateSegmentForm
submit_label = _("Create")
submit_url = reverse_lazy(
"horizon:masakaridashboard:segments:create_segment")
success_url = reverse_lazy("horizon:masakaridashboard:segments:index")
page_title = _("Create Segment")
def get_form_kwargs(self):
kwargs = super(CreateSegmentView, self).get_form_kwargs()
return kwargs

View File

@ -0,0 +1,48 @@
.masakari-wrapper.list{
list-style: inherit;
}
.masakari-wrapper #actions{
width:100%;
}
.masakari-wrapper #actions a.btn{
width:initial;
}
.masakari-wrapper.detail-screen .page-breadcrumb ol li{
max-width: inherit;
}
.masakari-wrapper.detail-screen .page-breadcrumb li:last-child{
display:none;
}
.masakari-wrapper .navbar-brand{
padding: 6px 10px;
}
.boolfield{
font-style: italic;
}
.boolfield i{
padding-right: .2em;
}
.boolfield i.green{
color: green;
}
.boolfield i.red{
color: red;
}
.line-space{
margin: .3em 0;
}
.line-space dd{
display:inline-block;
margin-left: 1.5em;
}

View File

@ -0,0 +1,28 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 openstack_dashboard.test import helpers
from masakaridashboard.test.test_data import utils
class MasakariTestsMixin(object):
def _setup_test_data(self):
super(MasakariTestsMixin, self)._setup_test_data()
utils.load_test_data(self)
class TestCase(MasakariTestsMixin, helpers.TestCase):
pass

View File

@ -1,4 +1,5 @@
# Copyright (c) 2018 NTT DATA
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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
@ -22,13 +23,13 @@ HORIZON_CONFIG.pop('dashboards', None)
HORIZON_CONFIG.pop('default_dashboard', None)
# Update the dashboards with masakaridashboard
import masakaridashboard.enabled
from masakaridashboard.local import enabled
import openstack_dashboard.enabled
from openstack_dashboard.utils import settings
settings.update_dashboards(
[
masakaridashboard.enabled,
enabled,
openstack_dashboard.enabled,
],
HORIZON_CONFIG,
@ -36,4 +37,9 @@ settings.update_dashboards(
)
# Ensure any duplicate apps are removed after the update_dashboards call
INSTALLED_APPS = list(set(INSTALLED_APPS))
NOSE_ARGS = ['--nocapture',
'--nologcapture',
'--cover-package=masakaridashboard',
'--cover-inclusive']

View File

@ -0,0 +1,38 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 openstack.instance_ha.v1 import segment
from openstack_dashboard.test.test_data import utils as test_data_utils
from masakaridashboard.test import uuidsentinel
def data(TEST):
TEST.masakari_segment = test_data_utils.TestDataContainer()
segment1 = segment.Segment(uuid=uuidsentinel.segment1, name='test',
recovery_method='auto',
service_type='service', description='demo')
segment2 = segment.Segment(uuid=uuidsentinel.segment2,
name='test2', recovery_method='auto',
service_type='service', description='demo')
segment3 = segment.Segment(uuid=uuidsentinel.segment3, name='test3',
recovery_method='auto',
service_type='service', description='demo')
TEST.masakari_segment.add(segment1)
TEST.masakari_segment.add(segment2)
TEST.masakari_segment.add(segment3)

View File

@ -0,0 +1,33 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 openstack_dashboard.test.test_data import utils
def load_test_data(load_onto=None):
from masakaridashboard.test.test_data import masakari_data
from openstack_dashboard.test.test_data import exceptions
# The order of these loaders matters, some depend on others.
loaders = (
exceptions.data,
masakari_data.data,
)
if load_onto:
for data_func in loaders:
data_func(load_onto)
return load_onto
else:
return utils.TestData(*loaders)

View File

@ -0,0 +1,33 @@
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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 sys
class UUIDSentinels(object):
def __init__(self):
from oslo_utils import uuidutils
self._uuid_module = uuidutils
self._sentinels = {}
def __getattr__(self, name):
if name.startswith('_'):
raise ValueError('Sentinels must not start with _')
if name not in self._sentinels:
self._sentinels[name] = self._uuid_module.generate_uuid()
return self._sentinels[name]
sys.modules[__name__] = UUIDSentinels()

View File

@ -1,16 +1,18 @@
# Copyright (c) 2018 NTT DATA
# Copyright (C) 2018 NTT DATA
# All Rights Reserved.
#
# 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
# 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
# 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.
# 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 pbr.version
version_info = pbr.version.VersionInfo('masakaridashboard')

View File

@ -7,11 +7,9 @@
# be installed in a specific order.
#
# PBR should always appear first
pbr>=2.0.0,!=2.1.0 # Apache-2.0
pbr!=2.1.0,>=2.0.0 # Apache-2.0
Babel>=2.3.4,!=2.4.0 # BSD
Django>=1.8,<2.0 # BSD
django-babel>=0.5.1 # BSD
django-compressor>=2.0 # MIT
django-pyscss>=2.0.2 # BSD License (2 clause)
# Horizon Core Requirements
horizon>=13.0.0 # Apache-2.0
openstacksdk>=0.13.0
PyYAML>=3.12 # MIT

View File

@ -7,22 +7,18 @@
# be installed in a specific order.
#
# Hacking should appear first in case something else depends on pep8
hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0
#
coverage>=4.0,!=4.4 # Apache-2.0
django-nose>=1.4.4 # BSD
mock>=2.0.0 # BSD
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
# Testing Requirements
coverage!=4.4,>=4.0 # Apache-2.0
django-nose>=1.4.4 # BSD
mock>=2.0.0 # BSD
mox3>=0.20.0 # Apache-2.0
nodeenv>=0.9.4 # BSD
nose>=1.3.7 # LGPL
nose-exclude>=0.3.0 # LGPL
nosehtmloutput>=0.0.3 # Apache-2.0
nosexcover>=1.0.10 # BSD
openstack.nose-plugin>=0.7 # Apache-2.0
openstackdocstheme>=1.18.1 # Apache-2.0
reno>=2.5.0 # Apache-2.0
selenium>=2.50.1 # Apache-2.0
sphinx>=1.6.2,!=1.6.6,!=1.6.7 # BSD
testtools>=2.2.0 # MIT
# This also needs xvfb library installed on your OS
xvfbwrapper>=0.1.3 #license: MIT
nodeenv>=0.9.4 # BSD
nose>=1.3.7 # LGPL
nose-exclude>=0.3.0 # LGPL
nosexcover>=1.0.10 # BSD
openstack.nose-plugin>=0.7 # Apache-2.0
nosehtmloutput>=0.0.3 # Apache-2.0
selenium>=2.50.1 # Apache-2.0
xvfbwrapper>=0.1.3 #license: MIT

20
tox.ini
View File

@ -17,7 +17,7 @@ deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = python manage.py test {posargs} --settings=masakaridashboard.test.settings
commands = python manage.py test {posargs} --settings=masakaridashboard.test.settings --ignore-files=masakaridashboard/test/uuidsentinel.py
[testenv:pep8]
commands = flake8 {posargs}
@ -36,7 +36,7 @@ commands =
basepython = python2.7
commands =
pip install django>=1.8,<1.9
python manage.py test {posargs} --settings=masakaridashboard.test.settings
python manage.py test {posargs} --settings=masakaridashboard.test.settings --ignore-files=masakaridashboard/test/uuidsentinel.py
[testenv:eslint]
whitelist_externals = npm
@ -59,10 +59,22 @@ commands =
echo "nexecute `npm run test`"
[testenv:docs]
commands = python setup.py build_sphinx
# We need to install horizon dependencies to build module references
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -W -b html doc/source doc/build/html
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
# There is no need to install horizon.
usedevelop = False
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/doc/requirements.txt
commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[flake8]
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules