Upversioning platform horizon to master

This commit is to enable the stx-gui plugin to work with the vanilla horizon
master branch. It also moves whatever of our customizations that we are
keeping from horizon into the plugin.  Whatever was not moved is being dropped
as a feature.  Major changes:
- Moving the get_value template tag to the plugin
- Removing server groups panels
- Removing the custom JS related to autorefresh and the alarm banner
- Implementing the upstream feature to add the alarm banner
- Moving ceph API to the plugin, removing nova
- Django no longer supports a TIME_ZONE of None to automatically capture
  the system time, so it is now done in our setting snippet
- Move scss additions to plugin
- Removing neutron service dependancy from datanet panels and removing
  create seg range button as it isn't supported any more from platform

Known issues:
- Branding and branding SDK need to be updated
- Timezone changes now require a horizon restart to take effect, will
  need to update documentation to highlight this (if we do document that)
- Port forwarding tab, launch instance enhancements, and controller services
  tab still need to be moved over, tracked by different stories

Change-Id: Ic158d3483bb33513b64a0f8c8b5bb5a2a5e48ec5
Story: 2004765
Task: 28883
Depends-On: https://review.openstack.org/#/c/642797
Signed-off-by: Tyler Smith <tyler.smith@windriver.com>
This commit is contained in:
Tyler Smith 2019-03-12 10:47:51 -04:00
parent a02dfe050c
commit 3858e68b1f
69 changed files with 543 additions and 2332 deletions

View File

@ -11,14 +11,14 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2017-2018 Wind River Systems, Inc.
# Copyright (c) 2017-2019 Wind River Systems, Inc.
#
from starlingx_dashboard.api import base
from starlingx_dashboard.api import ceph
from starlingx_dashboard.api import dc_manager
from starlingx_dashboard.api import fm
from starlingx_dashboard.api import neutron
from starlingx_dashboard.api import nova
from starlingx_dashboard.api import patch
from starlingx_dashboard.api import sysinv
from starlingx_dashboard.api import vim
@ -26,11 +26,11 @@ from starlingx_dashboard.api import vim
__all__ = [
"base",
"ceph",
"dc_manager",
"neutron",
"nova",
"patch",
"fm",
"neutron",
"patch",
"sysinv",
"vim",
]

View File

@ -0,0 +1,181 @@
# 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.
#
# Copyright (c) 2013-2019 Wind River Systems, Inc.
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
from __future__ import absolute_import
import logging
from cephclient import wrapper
from openstack_dashboard.api import base
LOG = logging.getLogger(__name__)
def cephwrapper():
return wrapper.CephWrapper()
class Monitor(base.APIDictWrapper):
__attrs = ['host', 'rank']
def __init__(self, apidict):
super(Monitor, self).__init__(apidict)
class OSD(base.APIDictWrapper):
__attrs = ['id', 'name', 'host', 'status']
def __init__(self, apidict):
super(OSD, self).__init__(apidict)
class Cluster(base.APIDictWrapper):
_attrs = ['fsid', 'status', 'health', 'detail']
def __init__(self, apidict):
super(Cluster, self).__init__(apidict)
class Storage(base.APIDictWrapper):
_attrs = ['total', 'used', 'available',
'writes_per_sec', 'operations_per_sec']
def __init__(self, apidict):
super(Storage, self).__init__(apidict)
def _Bytes_to_MiB(value_B):
return (value_B / (1024 * 1024))
def _Bytes_to_GiB(value_B):
return (value_B / (1024 * 1024 * 1024))
def cluster_get():
# the json response doesn't give all the information
response, text_body = cephwrapper().health(body='text')
# ceph is not up, raise exception
if not response.ok:
response.raise_for_status()
health_info = text_body.split(' ', 1)
# if health is ok, there will be no details so just show HEALTH_OK
if len(health_info) > 1:
detail = health_info[1]
else:
detail = health_info[0]
response, cluster_uuid = cephwrapper().fsid(body='text')
if not response.ok:
cluster_uuid = None
cluster = {
'fsid': cluster_uuid,
'health': health_info[0],
'detail': detail,
}
return Cluster(cluster)
def storage_get():
# # Space info
response, body = cephwrapper().df(body='json')
# return no space info
if not response.ok:
response.raise_for_status()
stats = body['output']['stats']
space = {
'total': _Bytes_to_GiB(stats['total_bytes']),
'used': _Bytes_to_MiB(stats['total_used_bytes']),
'available': _Bytes_to_GiB(stats['total_avail_bytes']),
}
# # I/O info
response, body = cephwrapper().osd_pool_stats(body='json',
name='cinder-volumes')
if not response.ok:
response.raise_for_status()
stats = body['output'][0]['client_io_rate']
# not showing reads/sec at the moment
# reads_per_sec = stats['read_bytes_sec'] if (
# 'read_bytes_sec' in stats) else 0
writes_per_sec = stats['write_bytes_sec'] if (
'write_bytes_sec' in stats) else 0
operations_per_sec = stats['op_per_sec'] if ('op_per_sec' in stats) else 0
io = {
'writes_per_sec': writes_per_sec / 1024,
'operations_per_sec': operations_per_sec
}
storage = {}
storage.update(space)
storage.update(io)
return Storage(storage)
def _get_quorum_status(mon, quorums):
if mon['rank'] in quorums:
status = 'up'
else:
status = 'down'
return status
def monitor_list():
response, body = cephwrapper().mon_dump(body='json')
# return no monitors info
if not response.ok:
response.raise_for_status()
quorums = body['output']['quorum']
mons = []
for mon in body['output']['mons']:
status = _get_quorum_status(mon, quorums)
mons.append(
{'host': mon['name'], 'rank': mon['rank'], 'status': status})
return [Monitor(m) for m in mons]
def osd_list():
# would use osd_find, but it doesn't give osd's name
response, tree = cephwrapper().osd_tree(body='json')
if not response.ok:
response.raise_for_status()
osds = []
for node in tree['output']['nodes']:
# found osd
if node['type'] == 'osd':
osd = {}
osd['id'] = node['id']
osd['name'] = node['name']
osd['status'] = node['status']
# check if osd belongs to host
response, body = cephwrapper().osd_find(body='json', id=osd['id'])
if response.ok and 'host' in body['output']['crush_location']:
osd['host'] = body['output']['crush_location']['host']
# else dont set hostname
osds.append(osd)
return [OSD(o) for o in osds]

View File

@ -15,6 +15,7 @@ from django.conf import settings
from openstack_dashboard.api import base
from starlingx_dashboard.api import base as stx_base
# Fault management values
FM_ALL = 'ALL'
@ -118,7 +119,7 @@ def alarm_list(request, search_opts=None):
marker = search_opts.get('marker', None)
sort_key = search_opts.get('sort_key', None)
sort_dir = search_opts.get('sort_dir', None)
page_size = base.get_request_page_size(request, limit)
page_size = stx_base.get_request_page_size(request, limit)
if "suppression" in search_opts:
suppression = search_opts.pop('suppression')
@ -195,7 +196,7 @@ def event_log_list(request, search_opts=None):
limit = search_opts.get('limit', None)
marker = search_opts.get('marker', None)
page_size = base.get_request_page_size(request, limit)
page_size = stx_base.get_request_page_size(request, limit)
if 'paginate' in search_opts:
paginate = search_opts.pop('paginate')

View File

@ -1,18 +1,36 @@
#
# Copyright (c) 2018 Intel Corporation
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from openstack_dashboard.api import base
from openstack_dashboard.api.neutron import NeutronAPIDictWrapper
from openstack_dashboard.api.neutron import neutronclient
from openstack_dashboard.api.neutron import PortForwardingRule
from openstack_dashboard.api.neutron import ProviderNetwork
from openstack_dashboard.api.neutron import ProviderNetworkRange
from openstack_dashboard.api.neutron import ProviderNetworkType
from openstack_dashboard.api.neutron import ProviderTenantNetwork
from openstack_dashboard.api.neutron import QoSPolicy
class PortForwardingRule(base.APIDictWrapper):
pass
class ProviderNetworkType(NeutronAPIDictWrapper):
"""Wrapper for neutron Provider Network Types."""
class ProviderNetworkRange(NeutronAPIDictWrapper):
"""Wrapper for neutron Provider Networks Id Ranges."""
class ProviderNetwork(NeutronAPIDictWrapper):
"""Wrapper for neutron Provider Networks."""
class ProviderTenantNetwork(NeutronAPIDictWrapper):
"""Wrapper for neutron Provider Tenant Networks."""
def provider_network_type_list(request, **params):
providernet_types = neutronclient(request).list_providernet_types(
**params).get(

View File

@ -1,24 +0,0 @@
#
# Copyright (c) 2018 Intel Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
from openstack_dashboard.api.nova import novaclient
def server_group_list(request, all_projects=False):
return novaclient(request).server_groups.list(all_projects)
def server_group_get(request, server_group_id):
return novaclient(request).server_groups.get(server_group_id)
def server_group_create(request, name, project_id, metadata, policies):
return novaclient(request).server_groups.create(
name, project_id, metadata, policies)
def server_group_delete(request, server_group_id):
return novaclient(request).server_groups.delete(server_group_id)

View File

@ -0,0 +1,33 @@
#
# Copyright (c) 2015-2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from six.moves import configparser
# Configuration Global used by other modules to get access to the configuration
# specified in the config file.
CONFSS = dict()
class ConfigSS(configparser.ConfigParser):
"""Override ConfigParser class to add dictionary functionality. """
def as_dict(self):
d = dict(self._sections)
for key in d:
d[key] = dict(self._defaults, **d[key])
d[key].pop('__name__', None)
return d
def load(config_file):
"""Load the configuration file into a global CONFSS variable. """
global CONFSS # noqa pylint: disable=global-statement
config = ConfigSS()
config.read(config_file)
if not CONFSS:
CONFSS = config.as_dict()
else:
CONFSS.update(config.as_dict())

View File

@ -0,0 +1,39 @@
{% load i18n %}
{% if dc_admin %}
<span class="alarmbanner">
Clouds:&nbsp;
<strong>Critical:</strong>
<span class="badge{% if critical != 0 %} badge-danger{% endif %}">{{ critical }}</span>
<strong>&nbsp;Degraded:</strong>
<span class="badge{% if degraded != 0 %} badge-warning{% endif %}">{{ degraded }}</span>
<strong>&nbsp;OK:</strong>
<span class="badge{% if OK != 0 %} badge-success{% endif %}">{{ OK }}</span>
{% if disabled != 0 %}
<strong>&nbsp;Disabled:</strong>
<span class="badge badge-danger">{{ disabled }}</span>
{% endif %}
</span>
{% else %}
<span class="alarmbanner" onclick="bannerclick()" style="cursor:pointer">
<strong>C:</strong>
<span class="badge{% if summary.critical != 0 %} badge-danger{% endif %}">{{ summary.critical }}</span>
<strong>&nbsp;M:</strong>
<span class="badge{% if summary.major != 0 %} badge-danger{% endif %}">{{ summary.major }}</span>
<strong>&nbsp;m:</strong>
<span class="badge{% if summary.minor != 0 %} badge-warning{% endif %}">{{ summary.minor }}</span>
<strong>&nbsp;W:</strong>
<span class="badge{% if summary.warnings != 0 %} badge-success{% endif %}">{{ summary.warnings }}</span>
</span>
{% endif %}
<script type="text/javascript" charset="utf-8">
function bannerclick() {
var $fm = "/admin/active_alarms";
var $dc = "/dc_admin/";
var $cur = document.location.href;
var $path = document.location.pathname;
if ($path.match($fm) === null && $path.match($dc) === null) {
document.location.href = $cur.replace($path, $fm);
}
}
</script>

View File

@ -18,9 +18,9 @@
import logging
from django.utils.translation import ugettext_lazy as _ # noqa
from django.views.generic import TemplateView
from horizon import exceptions
from horizon import views
from openstack_dashboard.api.base import is_service_enabled
from starlingx_dashboard.api import dc_manager
from starlingx_dashboard.api import fm
@ -29,8 +29,8 @@ from starlingx_dashboard.api import fm
LOG = logging.getLogger(__name__)
class BannerView(TemplateView):
template_name = 'header/_alarm_banner.html'
class BannerView(views.HorizonTemplateView):
template_name = 'admin/active_alarms/_alarm_banner.html'
def get_context_data(self, **kwargs):
@ -39,7 +39,7 @@ class BannerView(TemplateView):
if not self.request.is_ajax():
raise exceptions.NotFound()
if (not self.request.user.is_authenticated() or
if (not self.request.user.is_authenticated or
not self.request.user.is_superuser):
context["alarmbanner"] = False
elif 'dc_admin' in self.request.META.get('HTTP_REFERER'):

View File

@ -77,19 +77,6 @@ class EditDataNetwork(tables.LinkAction):
classes = ("ajax-modal", "btn-edit")
class AddDataNetworkRange(tables.LinkAction):
name = "addrange"
verbose_name = _("Create Segmentation Range")
url = "horizon:admin:datanets:datanets:addrange"
classes = ("ajax-modal", "btn-edit")
def allowed(self, request, datanet):
if datanet:
return datanet.network_type not in ('flat')
return super(AddDataNetworkRange, self).allowed(request,
datanet)
class DataNetworksFilterAction(tables.FilterAction):
def filter(self, table, datanets, filter_string):
"""Naive case-insensitive search."""
@ -119,8 +106,7 @@ class DataNetworksTable(tables.DataTable):
table_actions = (CreateDataNetwork, DeleteDataNetwork,
DataNetworksFilterAction)
row_actions = (EditDataNetwork,
DeleteDataNetwork,
AddDataNetworkRange)
DeleteDataNetwork)
def _get_link_url(datum):

View File

@ -33,8 +33,6 @@ VIEW_MOD = 'starlingx_dashboard.dashboards.admin.datanets.datanets.' \
urlpatterns = [
url(r'^create/$', views.CreateView.as_view(),
name='create'),
url(PROVIDERNETS % 'addrange',
views.CreateRangeView.as_view(), name='addrange'),
url(PROVIDERNETS % 'update', views.UpdateView.as_view(),
name='update'),
url(PROVIDERNETS % 'detail', views.DetailView.as_view(),

View File

@ -19,7 +19,6 @@
from collections import OrderedDict
import logging
from django.core.urlresolvers import reverse # noqa
from django.core.urlresolvers import reverse_lazy # noqa
from django.utils.translation import ugettext_lazy as _ # noqa
@ -34,8 +33,6 @@ from starlingx_dashboard.dashboards.admin.datanets.datanets import \
forms as datanet_forms
from starlingx_dashboard.dashboards.admin.datanets.datanets.ranges \
import tables as range_tables
from starlingx_dashboard.dashboards.admin.datanets.datanets.ranges \
import views as range_views
from starlingx_dashboard.dashboards.admin.datanets.datanets import \
tables as providernet_tables
@ -69,7 +66,7 @@ class DetailView(tables.MultiTableView):
def get_tenant_networks_data(self):
try:
# TODO(datanetworks): get tenant networks when in upstream neutron
# TODO(datanets): get tenant networks when in upstream neutron
networks = []
except Exception:
networks = []
@ -151,15 +148,3 @@ class UpdateView(forms.ModalFormView):
'mtu': datanet.mtu,
'description': datanet.description,
}
class CreateRangeView(range_views.CreateView):
template_name = 'admin/datanets/datanets/add_range.html'
success_url = 'horizon:admin:datanets:index'
failure_url = 'horizon:admin:datanets:index'
def get_success_url(self):
return reverse(self.success_url)
def get_failure_url(self):
return reverse(self.failure_url)

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2015 Wind River Systems, Inc.
# Copyright (c) 2015-2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -14,7 +14,7 @@ from openstack_dashboard.dashboards.admin import dashboard
class Datanets(horizon.Panel):
name = _("Data Networks")
slug = 'datanets'
permissions = ('openstack.services.platform', 'openstack.services.network')
permissions = ('openstack.services.platform', )
def allowed(self, context):
if context['request'].user.services_region == 'SystemController':

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}add_provider_network_range_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:datanets:datanets:addrange' providernet_id %}
{% endblock %}
{% block modal-header %}{% trans "Create Segmentation Range" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "You can create a segmentation range for the provider network. The range must not overlap with any other segmentation range."%}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a class="btn btn-default cancel" data-dismiss="modal">{% trans "Cancel" %}</a>
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Segmentation Range" %}"/>
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Segmentation Range" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create Segmentation Range") %}
{% endblock page_header %}
{% block main %}
{% include "admin/datanets/datanets/_add_range.html" %}
{% endblock %}

View File

@ -16,7 +16,7 @@ from openstack_dashboard.dashboards.admin import dashboard
class HostTopology(horizon.Panel):
name = _("Data Network Topology")
slug = 'host_topology'
permissions = ('openstack.services.platform', 'openstack.services.network')
permissions = ('openstack.services.platform', )
def allowed(self, context):
if context['request'].user.services_region == 'SystemController':

View File

@ -9,6 +9,7 @@ import json
import logging
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.http import HttpResponse # noqa
@ -217,5 +218,6 @@ class JSONView(View):
data = {'hosts': self._get_hosts(request),
'networks': self._get_dnets(request),
'alarms': self._get_alarms(request), }
json_string = json.dumps(data, ensure_ascii=False)
json_string = json.dumps(data, ensure_ascii=False,
cls=DjangoJSONEncoder)
return HttpResponse(json_string, content_type='text/json')

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2013-2015 Wind River Systems, Inc.
# Copyright (c) 2013-2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -18,6 +18,7 @@ from horizon import forms
from horizon import messages
from starlingx_dashboard.api import sysinv
from starlingx_dashboard.horizon.forms.fields import DynamicIntegerField
LOG = logging.getLogger(__name__)
@ -35,19 +36,19 @@ class UpdateCpuFunctions(forms.SelfHandlingForm):
required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
platform_processor0 = forms.DynamicIntegerField(
platform_processor0 = DynamicIntegerField(
label=_("# of Platform Physical Cores on Processor 0:"),
min_value=0, max_value=99,
required=False)
platform_processor1 = forms.DynamicIntegerField(
platform_processor1 = DynamicIntegerField(
label=_("# of Platform Physical Cores on Processor 1:"),
min_value=0, max_value=99,
required=False)
platform_processor2 = forms.DynamicIntegerField(
platform_processor2 = DynamicIntegerField(
label=_("# of Platform Physical Cores on Processor 2:"),
min_value=0, max_value=99,
required=False)
platform_processor3 = forms.DynamicIntegerField(
platform_processor3 = DynamicIntegerField(
label=_("# of Platform Physical Cores on Processor 3:"),
min_value=0, max_value=99,
required=False)
@ -56,19 +57,19 @@ class UpdateCpuFunctions(forms.SelfHandlingForm):
label=_("------------------------ Function ------------------------"),
required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
num_cores_on_processor0 = forms.DynamicIntegerField(
num_cores_on_processor0 = DynamicIntegerField(
label=_("# of vSwitch Physical Cores on Processor 0:"),
min_value=0, max_value=99,
required=False)
num_cores_on_processor1 = forms.DynamicIntegerField(
num_cores_on_processor1 = DynamicIntegerField(
label=_("# of vSwitch Physical Cores on Processor 1:"),
min_value=0, max_value=99,
required=False)
num_cores_on_processor2 = forms.DynamicIntegerField(
num_cores_on_processor2 = DynamicIntegerField(
label=_("# of vSwitch Physical Cores on Processor 2:"),
min_value=0, max_value=99,
required=False)
num_cores_on_processor3 = forms.DynamicIntegerField(
num_cores_on_processor3 = DynamicIntegerField(
label=_("# of vSwitch Physical Cores on Processor 3:"),
min_value=0, max_value=99,
required=False)
@ -77,19 +78,19 @@ class UpdateCpuFunctions(forms.SelfHandlingForm):
label=_("------------------------ Function ------------------------"),
required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'}))
num_shared_on_processor0 = forms.DynamicIntegerField(
num_shared_on_processor0 = DynamicIntegerField(
label=_("# of Shared Physical Cores on Processor 0:"),
min_value=0, max_value=99,
required=False)
num_shared_on_processor1 = forms.DynamicIntegerField(
num_shared_on_processor1 = DynamicIntegerField(
label=_("# of Shared Physical Cores on Processor 1:"),
min_value=0, max_value=99,
required=False)
num_shared_on_processor2 = forms.DynamicIntegerField(
num_shared_on_processor2 = DynamicIntegerField(
label=_("# of Shared Physical Cores on Processor 2:"),
min_value=0, max_value=99,
required=False)
num_shared_on_processor3 = forms.DynamicIntegerField(
num_shared_on_processor3 = DynamicIntegerField(
label=_("# of Shared Physical Cores on Processor 3:"),
min_value=0, max_value=99,
required=False)

View File

@ -1,4 +1,4 @@
{% load horizon i18n %}
{% load horizon i18n getvalue %}
{% for cpufunc in cpuProfile.core_assignment %}
<dt>{{ cpuFormats|get_value:cpufunc.allocated_function }}</dt>

View File

@ -1,4 +1,4 @@
{% load horizon i18n sizeformat %}
{% load horizon i18n sizeformat getvalue %}
{% block main %}
{% autoescape off %}

View File

@ -1,5 +1,5 @@
{% extends "horizon/common/_modal_form.html" %}
{% load horizon i18n %}
{% load horizon i18n getvalue %}
{% block form_id %}add_cpu_profile_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:inventory:addcpuprofile' host_id %}{% endblock %}

View File

@ -75,25 +75,6 @@
});
}
}); // horizon.addInitFunction
horizon.refresh.addRefreshFunction(function (html) {
var $sold_status = $('#active-sensor-stats');
var $snew_status = $(html).find('#active-sensor-stats');
if ($snew_status.html() != $sold_status.html()) {
$sold_status.replaceWith($snew_status);
}
var $old_table = $('#sensorgroups');
var $new_table = $(html).find('#sensorgroups');
if ($old_table.length && $new_table.length) {
var $new_table_actions = $new_table.find('.table_actions');
var $old_table_actions = $old_table.find('.table_actions');
if ($new_table_actions.html() != $old_table_actions.html())
$old_table_actions.replaceWith($new_table_actions);
}
}); // horizon.addRefreshFunction
</script>
{% endblock %}

View File

@ -12,53 +12,4 @@
{{ tab_group.render }}
</div>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript" charset="utf-8">
horizon.refresh.addRefreshFunction(function (html) {
var $old_status = $('#patching-status');
var $new_status = $(html).find('#patching-status');
if ($new_status.html() != $old_status.html()) {
$old_status.replaceWith($new_status);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_status = $('#hosts-stats');
var $new_status = $(html).find('#hosts-stats');
if ($new_status.html() != $old_status.html()) {
$old_status.replaceWith($new_status);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_table = $('#unprovisioned');
var $new_table = $(html).find('#unprovisioned');
if (($old_table.children().length == 0 && $new_table.children().length> 0) ||
($old_table.children().length > 0 && $new_table.children().length == 0)) {
$old_table.replaceWith($new_table);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_table = $('#storages');
var $new_table = $(html).find('#storages');
if (($old_table.children().length == 0 && $new_table.children().length> 0) ||
($old_table.children().length > 0 && $new_table.children().length == 0)) {
$old_table.replaceWith($new_table);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_table = $('#workers');
var $new_table = $(html).find('#workers');
if (($old_table.children().length == 0 && $new_table.children().length> 0) ||
($old_table.children().length > 0 && $new_table.children().length == 0)) {
$old_table.replaceWith($new_table);
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -1,195 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
# Copyright 2012 Nebula, Inc.
# All rights reserved.
"""
Views for managing volumes.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.instances import tables
from starlingx_dashboard.api import nova as stx_nova
class CreateForm(forms.SelfHandlingForm):
tenantP = forms.ChoiceField(label=_("Project"), required=True)
name = forms.CharField(max_length="255", label=_("Server Group Name"))
policy = forms.ChoiceField(label=_("Policy"),
required=False,
widget=forms.Select(
attrs={
'class': 'switchable',
'data-slug': 'policy_ht'}))
is_best_effort = forms.BooleanField(label=_("Best Effort"), required=False)
group_size = forms.IntegerField(
min_value=1,
label=_("Max Group Size (Instances)"),
required=False,
widget=forms.TextInput(
attrs={
'class': 'switchable switched',
'data-switch-on': 'policy_ht',
'data-policy_ht-anti-affinity': 'Max Group Size (Instances)',
'data-policy_ht-affinity': 'Max Group Size (Instances)'}))
group_size_ht = forms.IntegerField(
label=_("Max Group Size (Instances)"),
required=False,
widget=forms.TextInput(
attrs={
'readonly': 'readonly',
'class': 'switchable switched',
'data-switch-on': 'policy_ht',
'data-policy_ht-affinity-hyperthread':
'Max Group Size (Instances)'}))
def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(request, *args, **kwargs)
self.fields['policy'].choices = [("anti-affinity", "anti-affinity"),
("affinity", "affinity")]
# Populate available project_id/name choices
all_projects = []
try:
# Get list of available projects.
all_projects, has_more = api.keystone.tenant_list(request) # noqa pylint: disable=unused-variable
projects_list = [(project.id, project.name)
for project in all_projects]
except Exception:
projects_list = []
exceptions.handle(self.request,
_('Unable to retrieve list of tenants.'))
self.fields['tenantP'].choices = projects_list
def handle(self, request, data):
try:
policy = data['policy']
policies = []
if policy:
policies.append(policy)
metadata = {}
if data['is_best_effort']:
metadata['wrs-sg:best_effort'] = "true"
group_size = data['group_size']
group_size_ht = data['group_size_ht']
if group_size:
metadata['wrs-sg:group_size'] = str(group_size)
elif group_size_ht:
metadata['wrs-sg:group_size'] = str(group_size_ht)
project_id = None
if data['tenantP']:
project_id = data['tenantP']
server_group = stx_nova.server_group_create(
request, data['name'], project_id, metadata, policies)
return server_group
except ValidationError as e:
self.api_error(e.messages[0])
return False
except Exception as e:
exceptions.handle(request, ignore=True)
self.api_error(_("Unable to create server group."))
return False
class AttachForm(forms.SelfHandlingForm):
instance = forms.ChoiceField(label=_("Attach to Server Group"),
help_text=_("Select an server group to "
"attach to."))
device = forms.CharField(label=_("Device Name"))
def __init__(self, *args, **kwargs):
super(AttachForm, self).__init__(*args, **kwargs)
# Hide the device field if the hypervisor doesn't support it.
hypervisor_features = getattr(settings,
"OPENSTACK_HYPERVISOR_FEATURES", {})
can_set_mount_point = hypervisor_features.get("can_set_mount_point",
True)
if not can_set_mount_point:
self.fields['device'].widget = forms.widgets.HiddenInput()
self.fields['device'].required = False
# populate volume_id
volume = kwargs.get('initial', {}).get("volume", None)
if volume:
volume_id = volume.id
else:
volume_id = None
self.fields['volume_id'] = forms.CharField(widget=forms.HiddenInput(),
initial=volume_id)
# Populate instance choices
instance_list = kwargs.get('initial', {}).get('instances', [])
instances = []
for instance in instance_list:
if instance.status in tables.ACTIVE_STATES and \
not any(instance.id == att["server_id"]
for att in volume.attachments):
instances.append((instance.id, '%s (%s)' % (instance.name,
instance.id)))
if instances:
instances.insert(0, ("", _("Select an instance")))
else:
instances = (("", _("No instances available")),)
self.fields['instance'].choices = instances
def handle(self, request, data):
instance_choices = dict(self.fields['instance'].choices)
instance_name = instance_choices.get(data['instance'],
_("Unknown instance (None)"))
# The name of the instance in the choices list has the ID appended to
# it, so let's slice that off...
instance_name = instance_name.rsplit(" (")[0]
try:
attach = api.nova.instance_volume_attach(request,
data['volume_id'],
data['instance'],
data.get('device', ''))
volume = cinder.volume_get(request, data['volume_id'])
if not volume.display_name:
volume_name = volume.id
else:
volume_name = volume.display_name
message = _('Attaching volume %(vol)s to instance '
'%(inst)s on %(dev)s.') % {"vol": volume_name,
"inst": instance_name,
"dev": attach.device}
messages.info(request, message)
return True
except Exception:
redirect = reverse("horizon:project:volumes:index")
exceptions.handle(request,
_('Unable to attach volume.'),
redirect=redirect)

View File

@ -1,40 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.utils.translation import ugettext_lazy as _
import horizon
class ServerGroups(horizon.Panel):
name = _("Server Groups")
slug = 'server_groups'
# Server groups are wrs-specific
permissions = ('openstack.services.compute',)
policy_rules = (("compute", "context_is_admin"),)
def allowed(self, context):
if context['request'].user.services_region == 'SystemController':
return False
else:
return super(ServerGroups, self).allowed(context)
def nav(self, context):
if context['request'].user.services_region == 'SystemController':
return False
else:
return True

View File

@ -1,305 +0,0 @@
# Copyright 2014 Wind River, Inc.
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.core.urlresolvers import NoReverseMatch
from django.core.urlresolvers import reverse
from django.utils import html
from django.utils import safestring
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _ # noqa
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.tables \
import get_attachment_name
from openstack_dashboard.usage import quotas
from starlingx_dashboard.api import nova as stx_nova
DELETABLE_STATES = ("available", "error")
class DeleteServerGroup(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
"Delete Server Group",
"Delete Server Groups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Server Group",
"Deleted Server Groups",
count
)
def delete(self, request, obj_id):
obj = self.table.get_object_by_id(obj_id)
name = self.table.get_object_display(obj)
try:
stx_nova.server_group_delete(request, obj_id)
except Exception:
msg = _('Unable to delete group "%s" because it is not empty. '
'Either delete the member instances'
' or remove them from the group.')
exceptions.check_message(["group", "not", "empty."], msg % name)
raise
# maybe do a precheck to see if the group is empty first?
def allowed(self, request, server_group=None):
return True
class CreateServerGroup(tables.LinkAction):
name = "create"
verbose_name = _("Create Server Group")
url = "horizon:admin:server_groups:create"
classes = ("ajax-modal", "btn-create")
icon = "plus"
def allowed(self, request, volume=None):
usages = quotas.tenant_quota_usages(request)
if usages['server_groups']['available'] <= 0:
if "disabled" not in self.classes:
self.classes = [c for c in self.classes] + ['disabled']
self.verbose_name = string_concat(self.verbose_name, ' ',
_("(Quota exceeded)"))
else:
self.verbose_name = _("Create Server Group")
classes = [c for c in self.classes if c != "disabled"]
self.classes = classes
return True
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
url = "horizon:admin:server_groups:attach"
classes = ("ajax-modal", "btn-edit")
def allowed(self, request, server_group=None):
return True # volume.status in ("available", "in-use")
class CreateSnapshot(tables.LinkAction):
name = "snapshots"
verbose_name = _("Create Snapshot")
url = "horizon:admin:server_groups:create_snapshot"
classes = ("ajax-modal", "btn-camera")
def allowed(self, request, server_group=None):
return True # server_group.status == "available"
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, server_group_id):
server_group = stx_nova.server_group_get(request, server_group_id)
if not server_group.name:
server_group.name = server_group_id
return server_group
def get_policies(server_group):
policies = ', '.join(server_group.policies)
return policies
def get_metadata(server_group):
metadata_items = ['{}:{}'.format(x, y) for x, y in
server_group.metadata.items()]
metadata = ', '.join(metadata_items)
return metadata
def get_member_name(request, server_id):
try:
server = api.nova.server_get(request, server_id)
name = server.name
except Exception:
name = None
exceptions.handle(request, _("Unable to retrieve "
"member information."))
# try and get a URL
try:
url = reverse("horizon:admin:instances:detail", args=(server_id,))
instance = '<a href="%s">%s</a>' % (url, html.escape(name))
except NoReverseMatch:
instance = name
return instance
class ProjectNameColumn(tables.Column):
"""Customized column class
Customized column class that does complex processing on the
server group.
"""
def get_raw_data(self, server_group):
request = self.table.request
project_id = getattr(server_group, 'project_id', None)
try:
tenant = api.keystone.tenant_get(request,
project_id,
admin=True)
project_name = getattr(tenant, "name", None)
except Exception:
project_name = "(not found)"
return project_name
class MemberColumn(tables.Column):
"""Customized column class
Customized column class that does complex processing on the instances
in a server group. This was substantially copied
from the volume equivalent.
"""
def get_raw_data(self, server_group):
request = self.table.request
link = _('%(name)s (%(id)s)')
members = []
for member in server_group.members:
member_id = member
name = get_member_name(request, member)
vals = {"name": name, "id": member_id}
members.append(link % vals)
return safestring.mark_safe(", ".join(members))
def get_server_group_type(server_group):
return server_group.volume_type if server_group.volume_type != "None" \
else None
class ServerGroupsFilterAction(tables.FilterAction):
def filter(self, table, server_groups, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [group for group in server_groups
if q in group.display_name.lower()]
class ServerGroupsTable(tables.DataTable):
projectname = ProjectNameColumn("project_name",
verbose_name=_("Project"))
name = tables.Column("name",
verbose_name=_("Group Name"),
link="horizon:admin:server_groups:detail")
policies = tables.Column(get_policies,
verbose_name=_("Policies"))
members = MemberColumn("members",
verbose_name=_("Members"))
metadata = tables.Column(get_metadata,
verbose_name=_("Metadata"))
class Meta(object):
name = "server_groups"
verbose_name = _("Server Groups")
row_class = UpdateRow
table_actions = (
CreateServerGroup, DeleteServerGroup, ServerGroupsFilterAction)
row_actions = (DeleteServerGroup,)
def get_object_display(self, obj):
return obj.name
class DetachServerGroup(tables.BatchAction):
name = "detach"
action_type = 'danger'
@staticmethod
def action_present(count):
return ungettext_lazy(
"Detach Server Group",
"Detached Server Groups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Detaching Server Group",
"Detaching Server Groups",
count
)
def action(self, request, obj_id):
attachment = self.table.get_object_by_id(obj_id)
api.nova.instance_server_group_detach(request,
attachment.get('server_id',
None),
obj_id)
def get_success_url(self, request):
return reverse('horizon:admin:server_groups:index')
class AttachedInstanceColumn(tables.Column):
"""Customized column class
Customized column class that does complex processing on the attachments
for a server group.
"""
def get_raw_data(self, attachment):
request = self.table.request
return safestring.mark_safe(get_attachment_name(request, attachment,
True))
class AttachmentsTable(tables.DataTable):
instance = AttachedInstanceColumn(get_member_name,
verbose_name=_("Instance"))
device = tables.Column("device",
verbose_name=_("Device"))
def get_object_id(self, obj):
return obj['id']
def get_object_display(self, attachment):
instance_name = get_attachment_name(self.request, attachment, True)
vals = {"dev": attachment['device'],
"instance_name": html.strip_tags(instance_name)}
return _("%(dev)s on instance %(instance_name)s") % vals
def get_object_by_id(self, obj_id):
for obj in self.data:
if self.get_object_id(obj) == obj_id:
return obj
raise ValueError('No match found for the id "%s".' % obj_id)
class Meta(object):
name = "attachments"
verbose_name = _("Attachments")
table_actions = (DetachServerGroup,)
row_actions = (DetachServerGroup,)

View File

@ -1,53 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import nova
from starlingx_dashboard.api import nova as stx_nova
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("admin/server_groups/"
"_detail_overview.html")
def get_context_data(self, request):
server_group_id = self.tab_group.kwargs['server_group_id']
try:
server_group = stx_nova.server_group_get(request, server_group_id)
server_group.members_display = []
for member in server_group.members:
server_group.members_display.append(
dict(id=member, instance=nova.server_get(request, member)))
except Exception:
redirect = reverse('horizon:admin:server_groups:index')
exceptions.handle(self.request,
_('Unable to retrieve server group details.'),
redirect=redirect)
return {'server_group': server_group}
class ServerGroupDetailTabs(tabs.TabGroup):
slug = "server_group_details"
tabs = (OverviewTab,)

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}attach_volume_form{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:attach' volume.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal {% if show_attach %}split_half{% else %} no_split{% endif %}{% endblock %}
{% block modal_id %}attach_volume_modal{% endblock %}
{% block modal-header %}{% trans "Manage Volume Attachments" %}{% endblock %}
{% block modal-body %}
{% if show_attach %}
<h3>{% trans "Attach To Instance" %}</h3>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endif %}
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:project:volumes:index' %}" class="btn btn-default cancel">{% trans "Cancel" %}</a>
{% if show_attach %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Attach Volume" %}" />
{% endif %}
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:server_groups:create' %}?{{ request.GET.urlencode }}{% endblock %}
{% block modal_id %}create_server_group_modal{% endblock %}
{% block modal-header %}{% trans "Create Server Group" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can create a new server group" %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a class="btn btn-default cancel" data-dismiss="modal">{% trans "Cancel" %}</a>
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Server Group" %}" />
{% endblock %}

View File

@ -1,52 +0,0 @@
{% load i18n sizeformat parse_date %}
<h3>{% trans "Server Group Overview" %}: {{server_group.name }}</h3>
<div class="info row-fluid detail">
<h4>{% trans "Info" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ server_group.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ server_group.id }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ server_group.status|capfirst }}</dd>
</dl>
</div>
<div class="members row-fluid detail">
<h4>{% trans "Members" %}</h4>
<hr class="header_rule">
<dl>
{% for member in server_group.members_display %}
<dd>
{% url 'horizon:admin:instances:detail' member.id as instance_url%}
<a href="{{ instance_url }}">{{ member.instance.name }} ({{ member.id }})</a>
</dd>
{% empty %}
<dd><em>{% trans "No members" %}</em></dd>
{% endfor %}
</dl>
</div>
<div class="policies row-fluid detail">
<h4>{% trans "Policies" %}</h4>
<hr class="header_rule">
<dl>
{% for policy in server_group.policies %}
<dd>{{ policy }}</dd>
{% endfor %}
</dl>
</div>
<div class="status row-fluid detail">
<h4>{% trans "Metadata" %}</h4>
<hr class="header_rule">
<dl>
{% for key, value in server_group.metadata.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
</div>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Manage Volume Attachments" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Manage Volume Attachments") %}
{% endblock page_header %}
{% block main %}
{% include 'project/volumes/_attach.html' %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Server Group" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create a Server Group") %}
{% endblock page_header %}
{% block main %}
{% include 'project/server_groups/_create.html' %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% block title %}{% trans "Server Group Details" %}{% endblock %}
{% block main %}
<div class="row-fluid">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

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

View File

@ -1,32 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.conf.urls import url
from starlingx_dashboard.dashboards.admin.server_groups import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<server_group_id>[^/]+)/attach/$',
views.EditAttachmentsView.as_view(),
name='attach'),
url(r'^(?P<server_group_id>[^/]+)/$',
views.DetailView.as_view(),
name='detail')
]

View File

@ -1,148 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
"""
Views for managing server groups.
"""
from django.core.urlresolvers import reverse_lazy # noqa
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.usage import quotas
from starlingx_dashboard import api as stx_api
from starlingx_dashboard.dashboards.admin.server_groups \
import forms as admin_forms
from starlingx_dashboard.dashboards.admin.server_groups \
import tables as admin_tables
from starlingx_dashboard.dashboards.admin.server_groups \
import tabs as admin_tabs
# server groups don't currently support pagination
class IndexView(tables.DataTableView):
table_class = admin_tables.ServerGroupsTable
template_name = 'admin/server_groups/index.html'
page_title = _("Server Groups")
def get_data(self):
try:
server_groups = stx_api.nova.server_group_list(
self.request, all_projects=True)
except Exception:
server_groups = []
exceptions.handle(self.request,
_('Unable to retrieve server groups.'))
return server_groups
class DetailView(tabs.TabView):
tab_group_class = admin_tabs.ServerGroupDetailTabs
template_name = 'admin/server_groups/detail.html'
page_title = 'Server Group Details'
class CreateView(forms.ModalFormView):
form_class = admin_forms.CreateForm
template_name = 'admin/server_groups/create.html'
success_url = reverse_lazy("horizon:admin:server_groups:index")
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
try:
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request)
return context
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
table_class = admin_tables.AttachmentsTable
form_class = admin_forms.AttachForm
template_name = 'admin/server_groups/attach.html'
success_url = reverse_lazy("horizon:admin:server_groups:index")
def get_object(self):
if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id']
try:
self._object = api.cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return self._object
def get_data(self):
try:
volumes = self.get_object()
attachments = [att for att in volumes.attachments if att]
except Exception:
attachments = []
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return attachments
def get_initial(self):
try:
instances, has_more = api.nova.server_list(self.request) # noqa pylint: disable=unused-variable
except Exception:
instances = []
exceptions.handle(self.request,
_("Unable to retrieve attachment information."))
return {'volume': self.get_object(),
'instances': instances}
def get_form(self):
if not hasattr(self, "_form"):
form_class = self.get_form_class()
self._form = super(EditAttachmentsView, self).get_form(form_class)
return self._form
def get_context_data(self, **kwargs):
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
context['form'] = self.get_form()
volume = self.get_object()
if volume and volume.status == 'available':
context['show_attach'] = True
else:
context['show_attach'] = False
context['volume'] = volume
if self.request.is_ajax():
context['hide'] = True
return context
def get(self, request, *args, **kwargs):
# Table action handling
handled = self.construct_tables()
if handled:
return handled
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.get(request, *args, **kwargs)

View File

@ -42,16 +42,3 @@
</div>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript" charset="utf-8">
horizon.refresh.addRefreshFunction(function (html) {
var $old_stage_detail = $('#stage-details');
var $new_stage_detail = $(html).find('#stage-details');
if ($new_stage_detail.html() != $old_stage_detail.html()) {
$old_stage_detail.replaceWith($new_stage_detail);
}
});
</script>
{% endblock %}

View File

@ -13,33 +13,3 @@
</div>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript" charset="utf-8">
horizon.refresh.addRefreshFunction(function (html) {
var $old_strategy = $('#patch-strategy-detail');
var $new_strategy = $(html).find('#patch-strategy-detail');
if ($new_strategy.html() != $old_strategy.html()) {
$old_strategy.replaceWith($new_strategy);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_strategy = $('#upgrade-strategy-detail');
var $new_strategy = $(html).find('#upgrade-strategy-detail');
if ($new_strategy.html() != $old_strategy.html()) {
$old_strategy.replaceWith($new_strategy);
}
});
horizon.refresh.addRefreshFunction(function (html) {
var $old_status = $('#patching-status');
var $new_status = $(html).find('#patching-status');
if ($new_status.html() != $old_status.html()) {
$old_status.replaceWith($new_status);
}
});
</script>
{% endblock %}

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2016 Wind River Systems, Inc.
# Copyright (c) 2016-2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -11,9 +11,8 @@ from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import ceph
from starlingx_dashboard.api import base as stx_base
from starlingx_dashboard.api import ceph
from starlingx_dashboard.api import sysinv
from starlingx_dashboard.dashboards.admin.storage_overview import constants
from starlingx_dashboard.dashboards.admin.storage_overview import tables

View File

@ -13,16 +13,3 @@
</div>
</div>
{% endblock %}
{% block js %}
{{ block.super }}
<script type="text/javascript" charset="utf-8">
horizon.refresh.addRefreshFunction(function (html) {
var $old_strategy = $('#cloud-patch-strategy-detail');
var $new_strategy = $(html).find('#cloud-patch-strategy-detail');
if ($new_strategy.html() != $old_strategy.html()) {
$old_strategy.replaceWith($new_strategy);
}
});
</script>
{% endblock %}

View File

@ -1,180 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
# Copyright 2012 Nebula, Inc.
# All rights reserved.
"""
Views for managing volumes.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.instances import tables
from starlingx_dashboard.api import nova as stx_nova
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label=_("Server Group Name"))
policy = forms.ChoiceField(label=_("Policy"),
required=False,
widget=forms.Select(
attrs={
'class': 'switchable',
'data-slug': 'policy_ht'}))
is_best_effort = forms.BooleanField(label=_("Best Effort"), required=False)
group_size = forms.IntegerField(
min_value=1,
label=_("Max Group Size (Instances)"),
required=False,
widget=forms.TextInput(
attrs={
'class': 'switchable switched',
'data-switch-on': 'policy_ht',
'data-policy_ht-anti-affinity': 'Max Group Size (Instances)',
'data-policy_ht-affinity': 'Max Group Size (Instances)'}))
group_size_ht = forms.IntegerField(
label=_("Max Group Size (Instances)"),
required=False,
widget=forms.TextInput(
attrs={
'readonly': 'readonly',
'class': 'switchable switched',
'data-switch-on': 'policy_ht',
'data-policy_ht-affinity-hyperthread':
'Max Group Size (Instances)'}))
def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(request, *args, **kwargs)
self.fields['policy'].choices = [("anti-affinity", "anti-affinity"),
("affinity", "affinity")]
def handle(self, request, data):
try:
project_id = self.request.user.tenant_id
policy = data['policy']
policies = []
if policy:
policies.append(policy)
metadata = {}
if data['is_best_effort']:
metadata['wrs-sg:best_effort'] = "true"
group_size = data['group_size']
group_size_ht = data['group_size_ht']
if group_size:
metadata['wrs-sg:group_size'] = str(group_size)
elif group_size_ht:
metadata['wrs-sg:group_size'] = str(group_size_ht)
kwargs = {'name': data['name'],
'policies': policies,
'metadata': metadata,
'project_id': project_id}
server_group = stx_nova.server_group_create(request, **kwargs)
return server_group
except ValidationError as e:
self.api_error(e.messages[0])
return False
except Exception:
exceptions.handle(request, ignore=True)
self.api_error(_("Unable to create server group."))
return False
class AttachForm(forms.SelfHandlingForm):
instance = forms.ChoiceField(label=_("Attach to Server Group"),
help_text=_("Select an server group to "
"attach to."))
device = forms.CharField(label=_("Device Name"))
def __init__(self, *args, **kwargs):
super(AttachForm, self).__init__(*args, **kwargs)
# Hide the device field if the hypervisor doesn't support it.
hypervisor_features = getattr(settings,
"OPENSTACK_HYPERVISOR_FEATURES",
{})
can_set_mount_point = hypervisor_features.get("can_set_mount_point",
True)
if not can_set_mount_point:
self.fields['device'].widget = forms.widgets.HiddenInput()
self.fields['device'].required = False
# populate volume_id
volume = kwargs.get('initial', {}).get("volume", None)
if volume:
volume_id = volume.id
else:
volume_id = None
self.fields['volume_id'] = forms.CharField(widget=forms.HiddenInput(),
initial=volume_id)
# Populate instance choices
instance_list = kwargs.get('initial', {}).get('instances', [])
instances = []
for instance in instance_list:
if instance.status in tables.ACTIVE_STATES and \
not any(instance.id == att["server_id"]
for att in volume.attachments):
instances.append((instance.id, '%s (%s)' % (instance.name,
instance.id)))
if instances:
instances.insert(0, ("", _("Select an instance")))
else:
instances = (("", _("No instances available")),)
self.fields['instance'].choices = instances
def handle(self, request, data):
instance_choices = dict(self.fields['instance'].choices)
instance_name = instance_choices.get(data['instance'],
_("Unknown instance (None)"))
# The name of the instance in the choices list has the ID appended to
# it, so let's slice that off...
instance_name = instance_name.rsplit(" (")[0]
try:
attach = api.nova.instance_volume_attach(request,
data['volume_id'],
data['instance'],
data.get('device', ''))
volume = cinder.volume_get(request, data['volume_id'])
if not volume.display_name:
volume_name = volume.id
else:
volume_name = volume.display_name
message = _('Attaching volume %(vol)s to instance '
'%(inst)s on %(dev)s.') % {"vol": volume_name,
"inst": instance_name,
"dev": attach.device}
messages.info(request, message)
return True
except Exception:
redirect = reverse("horizon:project:volumes:index")
exceptions.handle(request,
_('Unable to attach volume.'),
redirect=redirect)

View File

@ -1,45 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.utils.translation import ugettext_lazy as _ # noqa
import horizon
from openstack_dashboard.api import base
from openstack_dashboard.dashboards.project import dashboard
class ServerGroups(horizon.Panel):
name = _("Server Groups")
slug = 'server_groups'
# Server groups are wrs-specific
permissions = ('openstack.services.platform',)
def allowed(self, context):
if not base.is_service_enabled(context['request'], 'compute'):
return False
else:
return super(ServerGroups, self).allowed(context)
def nav(self, context):
if not base.is_service_enabled(context['request'], 'compute'):
return False
else:
return True
dashboard.Project.register(ServerGroups)

View File

@ -1,283 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.core.urlresolvers import NoReverseMatch
from django.core.urlresolvers import reverse
from django.utils import html
from django.utils import safestring
from django.utils.translation import string_concat # noqa
from django.utils.translation import ugettext_lazy as _ # noqa
from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.project.volumes.tables \
import get_attachment_name
from openstack_dashboard.usage import quotas
from starlingx_dashboard.api import nova as stx_nova
DELETABLE_STATES = ("available", "error")
class DeleteServerGroup(tables.DeleteAction):
data_type_singular = _("Server Group")
data_type_plural = _("Server Groups")
@staticmethod
def action_present(count):
return ungettext_lazy(
"Delete Server Group",
"Delete Server Groups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Server Group",
"Deleted Server Groups",
count
)
def delete(self, request, obj_id):
obj = self.table.get_object_by_id(obj_id)
name = self.table.get_object_display(obj)
try:
stx_nova.server_group_delete(request, obj_id)
except Exception:
msg = _('Unable to delete group "%s" because it is not empty. '
'Either delete the member '
'instances or remove them from the group.')
exceptions.check_message(["group", "not", "empty."], msg % name)
raise
# maybe do a precheck to see if the group is empty first?
def allowed(self, request, server_group=None):
return True
class CreateServerGroup(tables.LinkAction):
name = "create"
verbose_name = _("Create Server Group")
url = "horizon:project:server_groups:create"
classes = ("ajax-modal", "btn-create")
icon = "plus"
def allowed(self, request, volume=None):
usages = quotas.tenant_quota_usages(request)
if usages['server_groups']['available'] <= 0:
if "disabled" not in self.classes:
self.classes = [c for c in self.classes] + ['disabled']
self.verbose_name = string_concat(self.verbose_name, ' ',
_("(Quota exceeded)"))
else:
self.verbose_name = _("Create Server Group")
classes = [c for c in self.classes if c != "disabled"]
self.classes = classes
return True
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
url = "horizon:project:server_groups:attach"
classes = ("ajax-modal", "btn-edit")
def allowed(self, request, server_group=None):
return True # volume.status in ("available", "in-use")
class CreateSnapshot(tables.LinkAction):
name = "snapshots"
verbose_name = _("Create Snapshot")
url = "horizon:project:server_groups:create_snapshot"
classes = ("ajax-modal", "btn-camera")
def allowed(self, request, server_group=None):
return True # server_group.status == "available"
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, server_group_id):
server_group = stx_nova.server_group_get(request, server_group_id)
if not server_group.name:
server_group.name = server_group_id
return server_group
def get_policies(server_group):
policies = ', '.join(server_group.policies)
return policies
def get_metadata(server_group):
metadata_items = ['{}:{}'.format(x, y) for x, y in
server_group.metadata.items()]
metadata = ', '.join(metadata_items)
return metadata
def get_member_name(request, server_id):
try:
server = api.nova.server_get(request, server_id)
name = server.name
except Exception:
name = None
exceptions.handle(request, _("Unable to retrieve "
"member information."))
# try and get a URL
try:
url = reverse("horizon:project:instances:detail", args=(server_id,))
instance = '<a href="%s">%s</a>' % (url, html.escape(name))
except NoReverseMatch:
instance = name
return instance
class MemberColumn(tables.Column):
"""Customized column class
Customized column class that does complex processing on the instances
in a server group. This was substantially copied
from the volume equivalent.
"""
def get_raw_data(self, server_group):
request = self.table.request
link = _('%(name)s (%(id)s)')
members = []
for member in server_group.members:
member_id = member
name = get_member_name(request, member)
vals = {"name": name, "id": member_id}
members.append(link % vals)
return safestring.mark_safe(", ".join(members))
def get_server_group_type(server_group):
return server_group.volume_type if server_group.volume_type != "None" \
else None
class ServerGroupsFilterAction(tables.FilterAction):
def filter(self, table, server_groups, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [group for group in server_groups
if q in group.display_name.lower()]
class ServerGroupsTable(tables.DataTable):
name = tables.Column("name",
verbose_name=_("Group Name"),
link="horizon:project:server_groups:detail")
policies = tables.Column(get_policies,
verbose_name=_("Policies"))
members = MemberColumn("members",
verbose_name=_("Members"))
metadata = tables.Column(get_metadata,
verbose_name=_("Metadata"))
class Meta(object):
name = "server_groups"
verbose_name = _("Server Groups")
row_class = UpdateRow
table_actions = (
CreateServerGroup, DeleteServerGroup, ServerGroupsFilterAction)
row_actions = (DeleteServerGroup,)
def get_object_display(self, obj):
return obj.name
class DetachServerGroup(tables.BatchAction):
name = "detach"
data_type_singular = _("Server Group")
data_type_plural = _("Server Groups")
classes = ('btn-danger', 'btn-detach')
@staticmethod
def action_present(count):
return ungettext_lazy(
"Server Group",
"Server Groups",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
"Deleted Group",
"Deleted Groups",
count
)
def action(self, request, obj_id):
attachment = self.table.get_object_by_id(obj_id)
api.nova.instance_server_group_detach(
request,
attachment.get('server_id', None),
obj_id)
def get_success_url(self, request):
return reverse('horizon:project:server_groups:index')
class AttachedInstanceColumn(tables.Column):
"""Customized column class
Customized column class that does complex processing on the attachments
for a server group.
"""
def get_raw_data(self, attachment):
request = self.table.request
return safestring.mark_safe(get_attachment_name(request, attachment))
class AttachmentsTable(tables.DataTable):
instance = AttachedInstanceColumn(get_member_name,
verbose_name=_("Instance"))
device = tables.Column("device",
verbose_name=_("Device"))
def get_object_id(self, obj):
return obj['id']
def get_object_display(self, attachment):
instance_name = get_attachment_name(self.request, attachment)
vals = {"dev": attachment['device'],
"instance_name": html.strip_tags(instance_name)}
return _("%(dev)s on instance %(instance_name)s") % vals
def get_object_by_id(self, obj_id):
for obj in self.data:
if self.get_object_id(obj) == obj_id:
return obj
raise ValueError('No match found for the id "%s".' % obj_id)
class Meta(object):
name = "attachments"
verbose_name = _("Attachments")
table_actions = (DetachServerGroup,)
row_actions = (DetachServerGroup,)

View File

@ -1,53 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import nova
from starlingx_dashboard.api import nova as stx_nova
class OverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = ("project/server_groups/"
"_detail_overview.html")
def get_context_data(self, request):
server_group_id = self.tab_group.kwargs['server_group_id']
try:
server_group = stx_nova.server_group_get(request, server_group_id)
server_group.members_display = []
for member in server_group.members:
server_group.members_display.append(
dict(id=member, instance=nova.server_get(request, member)))
except Exception:
redirect = reverse('horizon:project:server_groups:index')
exceptions.handle(self.request,
_('Unable to retrieve server group details.'),
redirect=redirect)
return {'server_group': server_group}
class ServerGroupDetailTabs(tabs.TabGroup):
slug = "server_group_details"
tabs = (OverviewTab,)

View File

@ -1,25 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}attach_volume_form{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:attach' volume.id %}{% endblock %}
{% block form_class %}{{ block.super }} horizontal {% if show_attach %}split_half{% else %} no_split{% endif %}{% endblock %}
{% block modal_id %}attach_volume_modal{% endblock %}
{% block modal-header %}{% trans "Manage Volume Attachments" %}{% endblock %}
{% block modal-body %}
{% if show_attach %}
<h3>{% trans "Attach To Instance" %}</h3>
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endif %}
{% endblock %}
{% block modal-footer %}
<a class="btn btn-default cancel" data-dismiss="modal">Cancel</a>
{% if show_attach %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Attach Volume" %}" />
{% endif %}
{% endblock %}

View File

@ -1,26 +0,0 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:server_groups:create' %}?{{ request.GET.urlencode }}{% endblock %}
{% block modal_id %}create_server_group_modal{% endblock %}
{% block modal-header %}{% trans "Create Server Group" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can create a new server group" %}</p>
</div>
{% endblock %}
{% block modal-footer %}
<a class="btn btn-default cancel" data-dismiss="modal">{% trans "Cancel" %}</a>
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Server Group" %}" />
{% endblock %}

View File

@ -1,52 +0,0 @@
{% load i18n sizeformat parse_date %}
<h3>{% trans "Server Group Overview" %}: {{server_group.name }}</h3>
<div class="info row-fluid detail">
<h4>{% trans "Info" %}</h4>
<hr class="header_rule">
<dl>
<dt>{% trans "Name" %}</dt>
<dd>{{ server_group.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ server_group.id }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>{{ server_group.status|capfirst }}</dd>
</dl>
</div>
<div class="members row-fluid detail">
<h4>{% trans "Members" %}</h4>
<hr class="header_rule">
<dl>
{% for member in server_group.members_display %}
<dd>
{% url 'horizon:project:instances:detail' member.id as instance_url%}
<a href="{{ instance_url }}">{{ member.instance.name }} ({{ member.id }})</a>
</dd>
{% empty %}
<dd><em>{% trans "No members" %}</em></dd>
{% endfor %}
</dl>
</div>
<div class="policies row-fluid detail">
<h4>{% trans "Policies" %}</h4>
<hr class="header_rule">
<dl>
{% for policy in server_group.policies %}
<dd>{{ policy }}</dd>
{% endfor %}
</dl>
</div>
<div class="status row-fluid detail">
<h4>{% trans "Metadata" %}</h4>
<hr class="header_rule">
<dl>
{% for key, value in server_group.metadata.items %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
</div>

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Manage Volume Attachments" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Manage Volume Attachments") %}
{% endblock page_header %}
{% block main %}
{% include 'project/volumes/_attach.html' %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Server Group" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Create an Server Group") %}
{% endblock page_header %}
{% block main %}
{% include 'project/server_groups/_create.html' %}
{% endblock %}

View File

@ -1,11 +0,0 @@
{% extends 'base.html' %}
{% load i18n breadcrumb_nav %}
{% block title %}{% trans "Server Group Details" %}{% endblock %}
{% block main %}
<div class="row-fluid">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

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

View File

@ -1,32 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
from django.conf.urls import url
from starlingx_dashboard.dashboards.project.server_groups import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<server_group_id>[^/]+)/attach/$',
views.EditAttachmentsView.as_view(),
name='attach'),
url(r'^(?P<server_group_id>[^/]+)/$',
views.DetailView.as_view(),
name='detail'),
]

View File

@ -1,148 +0,0 @@
# 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.
#
# Copyright (c) 2013-2018 Wind River Systems, Inc.
#
"""
Views for managing server groups.
"""
from django.core.urlresolvers import reverse_lazy # noqa
from django.utils.translation import ugettext_lazy as _ # noqa
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from openstack_dashboard import api
from openstack_dashboard.usage import quotas
from starlingx_dashboard import api as stx_api
from starlingx_dashboard.dashboards.project.server_groups \
import forms as project_forms
from starlingx_dashboard.dashboards.project.server_groups \
import tables as project_tables
from starlingx_dashboard.dashboards.project.server_groups \
import tabs as project_tabs
# server groups don't currently support pagination
class IndexView(tables.DataTableView):
table_class = project_tables.ServerGroupsTable
template_name = 'project/server_groups/index.html'
page_title = _("Server Groups")
def get_data(self):
try:
server_groups = stx_api.nova.server_group_list(
self.request)
except Exception:
server_groups = []
exceptions.handle(self.request,
_('Unable to retrieve server groups.'))
return server_groups
class DetailView(tabs.TabView):
tab_group_class = project_tabs.ServerGroupDetailTabs
template_name = 'project/server_groups/detail.html'
page_title = 'Server Group Details'
class CreateView(forms.ModalFormView):
form_class = project_forms.CreateForm
template_name = 'project/server_groups/create.html'
success_url = reverse_lazy("horizon:project:server_groups:index")
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
try:
context['usages'] = quotas.tenant_limit_usages(self.request)
except Exception:
exceptions.handle(self.request)
return context
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
table_class = project_tables.AttachmentsTable
form_class = project_forms.AttachForm
template_name = 'project/server_groups/attach.html'
success_url = reverse_lazy("horizon:project:server_groups:index")
def get_object(self):
if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id']
try:
self._object = api.cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return self._object
def get_data(self):
try:
volumes = self.get_object()
attachments = [att for att in volumes.attachments if att]
except Exception:
attachments = []
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return attachments
def get_initial(self):
try:
instances, has_more = api.nova.server_list(self.request) # noqa pylint: disable=unused-variable
except Exception:
instances = []
exceptions.handle(self.request,
_("Unable to retrieve attachment information."))
return {'volume': self.get_object(),
'instances': instances}
def get_form(self):
if not hasattr(self, "_form"):
form_class = self.get_form_class()
self._form = super(EditAttachmentsView, self).get_form(form_class)
return self._form
def get_context_data(self, **kwargs):
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
context['form'] = self.get_form()
volume = self.get_object()
if volume and volume.status == 'available':
context['show_attach'] = True
else:
context['show_attach'] = False
context['volume'] = volume
if self.request.is_ajax():
context['hide'] = True
return context
def get(self, request, *args, **kwargs):
# Table action handling
handled = self.construct_tables()
if handled:
return handled
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.get(request, *args, **kwargs)

View File

@ -3,4 +3,10 @@ ADD_INSTALLED_APPS = ['starlingx_dashboard']
FEATURE = ['starlingx_dashboard']
ADD_HEADER_SECTIONS = \
['starlingx_dashboard.dashboards.admin.active_alarms.views.BannerView', ]
ADD_SCSS_FILES = ['dashboard/scss/styles.scss',
'dashboard/scss/_host_topology.scss']
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -1,11 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'server_groups'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'compute'
# A list of applications to be added to INSTALLED_APPS.
# Python panel class of the PANEL to be added.
ADD_PANEL = \
'starlingx_dashboard.dashboards.project.server_groups.panel.ServerGroups'

View File

@ -1,10 +0,0 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'server_groups'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'compute'
# Python panel class of the PANEL to be added.
ADD_PANEL = \
'starlingx_dashboard.dashboards.admin.server_groups.panel.ServerGroups'

View File

@ -0,0 +1,4 @@
PANEL_DASHBOARD = 'admin'
PANEL_GROUP = 'system'
PANEL = 'defaults'
REMOVE_PANEL = True

View File

@ -0,0 +1 @@
from starlingx_dashboard.horizon.forms.fields import DynamicIntegerField # noqa

View File

@ -0,0 +1,38 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from django.core import validators
from horizon.forms import IntegerField
class DynamicIntegerField(IntegerField):
"""A subclass of ``IntegerField``.
A subclass of ``IntegerField`` with additional properties that make
dynamically updating its range easier.
"""
def set_max_value(self, max_value):
if max_value is not None:
for v in self.validators:
if isinstance(v, validators.MaxValueValidator):
self.validators.remove(v)
self.max_value = max_value
self.validators.append(validators.MaxValueValidator(max_value))
self.widget_attrs(self.widget)
self.widget.attrs['max'] = self.max_value
def set_min_value(self, min_value):
if min_value is not None:
for v in self.validators:
if isinstance(v, validators.MinValueValidator):
self.validators.remove(v)
self.min_value = min_value
self.validators.append(validators.MinValueValidator(min_value))
self.widget_attrs(self.widget)
self.widget.attrs['min'] = self.min_value

View File

@ -1,7 +1,13 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import os
from openstack_dashboard.local import configss
from openstack_dashboard.settings import HORIZON_CONFIG
from starlingx_dashboard import configss
from tsconfig.tsconfig import distributed_cloud_role
@ -312,7 +318,9 @@ LOGGING = {
},
}
# Session timeout overrides
# Session overrides
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = '/var/tmp'
# SESSION_TIMEOUT is a method to supersede the token timeout with a shorter
# horizon session timeout (in seconds). So if your token expires in 60
@ -325,8 +333,11 @@ SESSION_TIMEOUT = 3000
# value of 600 will log users out after 50 minutes.
TOKEN_TIMEOUT_MARGIN = 600
# The timezone of the server. This should correspond with the timezone
# of your entire OpenStack installation, and hopefully be in UTC.
# In this case, we set the value to None so that the interface uses the system
# timezone by default
TIME_ZONE = None
# Retrieve the current system timezone
USE_TZ = True
try:
tz = os.path.realpath('/etc/localtime')
TIME_ZONE = tz.split('zoneinfo/')[1]
except Exception:
TIME_ZONE = 'UTC'

View File

@ -0,0 +1,113 @@
#hostTopologyContainer {
#topologyCanvasContainer {
@include box-sizing(border-box);
width: 100%;
height: auto;
cursor: auto;
padding-bottom: 25px;
padding-left: 10px;
padding-right: 20px;
padding-top: 5px;
background: #ffffff;
min-height: 400px;
div.nodata {
font-size: 150%;
text-align: center;
padding-top: 150px;
display: none;
}
&.noinfo {
div.nodata {
display: block;
}
#topology_canvas {
display: none;
}
}
#canvas_container {
font-family: sans-serif;
cursor: grab;
cursor: -webkit-grab;
display: table-cell;
width: 100%;
height: 100%;
#topology_canvas {
border: 1px;
border-color: grey;
border-style: solid;
.network-name {
fill: black;
}
}
}
#top_row {
display: table;
height: 550px;
margin-bottom: 20px;
#lists_container {
display: inline-block;
min-width: 200px;
max-width: 300px;
margin-right: 20px;
vertical-align: bottom;
div {
min-height: 200px;
max-height: initial;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
a.list-group-item {
overflow-x: hidden;
text-overflow: ellipsis;
padding-right: 32px;
}
a.related {
color: #555;
background-color: #8eb9df;
}
svg.list_alarm {
overflow: visible;
width: 26px;
height: 22px;
vertical-align: middle;
margin-top: -4px;
margin-right: -29px;
float: right;
}
}
}
}
#detail_view {
.spinner {
margin-top: 100px;
}
}
}
}
#hostTopologyNavi {
overflow: hidden;
margin-top: 10px;
padding-left: 10px;
#toggleLabels {
float: left;
span.glyphicon {
margin-right: 4px;
}
input {
display: none;
}
}
}

View File

@ -0,0 +1,33 @@
// Data List for sysinv
.dl-horizontal-wide {
@extend .dl-horizontal;
dt {
width: 22em;
}
dd {
margin-left: 23em;
}
}
// Fault management summary formatting
#active-alarm-stats {
margin: 10px 5px;
& > span {
margin: 0px 5px;
}
}
// System patch summary formatting
#patching-status {
margin-top: 10px;
}
// Hosts summary formatting
#hosts-stats {
margin-top: 10px;
}
// Sensor summary formatting
#active-sensor-stats {
margin-top: 10px;
}

View File

@ -1,74 +0,0 @@
/**
* Copyright (c) 2019 Wind River Systems, Inc.
*
* SPDX-License-Identifier: Apache-2.0
*
*/
/* Core functionality related to alam-banner. */
horizon.alarmbanner = {
enabled: false,
refresh: function(data) {
var $old = $(location).attr('pathname');
var $url = $(location).attr('href');
$url = $url.replace($old, "/admin/active_alarms/banner");
horizon.ajax.queue({
url: $url,
success: function(data, textStatus, jqXHR) {
var $new = $(data);
var $old = $('.alarmbanner');
if ($new.html() !== $old.html()) {
$old.html($new.html());
}
// start periodic refresh
if (horizon.alarmbanner.enabled === false) {
horizon.refresh.addRefreshFunction(
horizon.alarmbanner.refresh);
horizon.alarmbanner.enabled = true;
}
},
error: function(jqXHR, textStatus, error) {
if (jqXHR.status !== 401 && jqXHR.status !== 403) {
// error is raised with status of 0 when ajax query is cancelled
// due to new page request
if (jqXHR.status !== 0) {
horizon.clearInfoMessages();
horizon.clearErrorMessages();
horizon.alert("info", gettext("Failed to refresh alarm banner, retrying on next interval."));
}
// start periodic refresh
if (horizon.alarmbanner.enabled === false) {
horizon.refresh.addRefreshFunction(
horizon.alarmbanner.refresh);
horizon.alarmbanner.enabled = true;
}
}
},
complete: function(jqXHR, textStatus) {
}
});
return true;
},
onclick: function() {
var $fm = "/admin/active_alarms";
var $dc = "/dc_admin/";
var $cur = document.location.href;
var $path = document.location.pathname;
if ($path.match($fm) === null && $path.match($dc) === null) {
document.location.href = $cur.replace($path, $fm);
}
},
};
horizon.addInitFunction(function() {
// trigger alarm banner refresh on page load and enable
// periodic refresh after the first one
horizon.alarmbanner.refresh(null);
});

View File

@ -1,30 +0,0 @@
/* Functionality related to detail panes */
horizon.details = {
};
horizon.details.refresh = function(html) {
var $new_details = $(html).find('.detail > dl').parent();
var $old_details = $('.detail > dl').parent();
if ($new_details.length != $old_details.length) {
// Page structure has changed, abort refresh
return false;
}
$new_details.each(function(index, elm) {
var $new_detail = $(this);
var $old_detail = $($old_details.get(index));
if ($new_detail.html() != $old_detail.html()) {
$old_detail.replaceWith($new_detail);
}
});
return true;
};
horizon.addInitFunction(function() {
if ($('.detail > dl').length > 0) {
// Register callback handler to update the detail panels on page refresh
horizon.refresh.addRefreshFunction(horizon.details.refresh);
}
});

View File

@ -1,135 +0,0 @@
/* Core functionality related to page auto refresh. */
horizon.refresh = {
refresh_interval: 5000,
_refresh_functions: [],
timeout: null,
init: function() {
// Add page refresh time to page header
horizon.refresh.datetime();
$('#navbar-collapse > div.context_selection > ul > li > ul li a ').click(function (){
clearTimeout(horizon.refresh.timeout);
clearTimeout(horizon.datatables.timeout);
});
// Setup next refresh interval
horizon.refresh.timeout = setTimeout(horizon.refresh.update, horizon.refresh.refresh_interval);
},
update: function() {
if (!horizon.refresh._refresh_functions.length) {
// No refresh handlers registered
return;
}
// Ignore any tabs which react badly to auto-refresh
var ignore = ["instance_details__console"];
// Figure out which tabs have been loaded
loaded = "";
$(".nav-tabs a[data-target]").each(function(index){
var slug = $(this).attr('data-target').replace('#','');
if ($(this).attr('data-loaded') == 'true' && ignore.indexOf(slug) == -1){
if (loaded){loaded+=','}
loaded+=slug;
}
});
// Grab current href (for refresh) but remove the tab parameter ( if exists )
var currentHREF = $(location).attr('href');
var qryStrObj = jQuery.query.load( currentHREF );
var oldQryStr = qryStrObj.toString();
var newQryStr = qryStrObj.SET("tab", null).SET("loaded", loaded).COMPACT().toString();
if (oldQryStr){
var $href=currentHREF.replace(oldQryStr, newQryStr);
}else {
var $href=currentHREF += newQryStr;
}
horizon.ajax.queue({
url: $href,
success: function(data, textStatus, jqXHR) {
var refreshed = true;
if (jqXHR.responseText.length > 0) {
$(horizon.refresh._refresh_functions).each(function (index, f) {
refreshed = f(data);
return refreshed;
});
if (refreshed) {
horizon.refresh.datetime();
}
} else {
// assume that the entity has been deleted
location.href = $('#sidebar-accordion a.openstack-panel.active').attr('href');
}
},
error: function(jqXHR, textStatus, error) {
// Zero-status usually seen when user navigates away
if (jqXHR.status != 0) {
horizon.refresh.alert();
}
},
complete: function(jqXHR, textStatus) {
// Check for redirect condition
var redirect = false
switch (jqXHR.status) {
case 401:
// Authorization error, likely redirect to login page
redirect = jqXHR.getResponseHeader("X-Horizon-Location");
break;
case 404:
// The object seems to be gone, redirect back to main nav
redirect = $('#sidebar-accordion a.openstack-panel.active').attr('href');
break;
case 302:
// Object is likely gone and are being redirect to index page
redirect = jqXHR.getResponseHeader("Location");
break;
}
if (redirect) {
location.href = redirect;
}
horizon.autoDismissAlerts();
// Check for error condition
var messages = $.parseJSON(horizon.ajax.get_messages(jqXHR));
var errors = $(messages).filter(function (index, item) {
return item[0] == 'error';
});
if (errors.length > 0) {
horizon.refresh.alert();
}
// Reset for next refresh interval
horizon.refresh.timeout = setTimeout(horizon.refresh.update, horizon.refresh.refresh_interval);
}
});
},
alert: function() {
horizon.clearInfoMessages();
horizon.clearErrorMessages();
horizon.clearWarningMessages();
horizon.alert("info", gettext("Failed to auto refresh page, retrying on next interval."));
},
datetime: function() {
// Update page refresh time
var location = $('.datetime');
if (location !== null) {
var datetime = new Date();
location.html(datetime.toLocaleString());
}
},
addRefreshFunction: function(f) {
horizon.refresh._refresh_functions.push(f);
}
};
horizon.addInitFunction(function() {
horizon.refresh.init();
});

View File

@ -0,0 +1,14 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from django import template
register = template.Library()
@register.filter(name="get_value")
def get_value(dictionary, key):
return dictionary.get(key)