Add Curvature topology

Replace the current network topology with the visualisation from the Curvature
user interface

Implements: blueprint curvature-network-topology

Change-Id: I4435ab9b54ce51540c3346aa709f0fa8bbcb87b4
This commit is contained in:
Bradley Jones 2014-12-03 15:00:47 +00:00
parent d83de86ab8
commit e0ac95be33
18 changed files with 1182 additions and 766 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
# Copyright 2015 Cisco Systems.
#
# 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 openstack_dashboard.dashboards.project.networks import tables
class DeleteNetwork(tables.DeleteNetwork):
redirect_url = "horizon:project:network_topology:network"
class NetworksTable(tables.NetworksTable):
class Meta(object):
name = "networks"
verbose_name = _("Networks")
table_actions = (DeleteNetwork,)
row_actions = (DeleteNetwork,)

View File

@ -0,0 +1,30 @@
# Copyright 2015 Cisco Systems.
#
# 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 openstack_dashboard.dashboards.project.networks.subnets import tables
class DeleteSubnet(tables.DeleteSubnet):
failure_url = 'horizon:project:network_topology:network'
class SubnetsTable(tables.SubnetsTable):
class Meta(object):
name = "subnets"
verbose_name = _("Subnets")
row_actions = (DeleteSubnet, )
table_actions = (DeleteSubnet, )

View File

@ -1,222 +0,0 @@
{% load i18n %}
<style type="text/css">
svg#topology_canvas {
font-family: sans-serif;
}
svg#topology_canvas .network-rect {
cursor: pointer;
}
svg#topology_canvas .network-rect.nourl {
cursor: auto;
}
svg#topology_canvas .network-name {
font-weight: bold;
font-size: 12px;
fill: #fff;
text-anchor: middle;
}
svg#topology_canvas .network-cidr {
font-size: 11px;
text-anchor: end;
}
svg#topology_canvas text.network-type {
font-family: FontAwesome;
text-anchor: end;
}
svg#topology_canvas .port_text {
font-size: 9px;
fill: #666;
}
svg#topology_canvas .port_text.left {
text-anchor: end;
}
svg#topology_canvas .base_bg_normal {
fill: #333;
}
svg#topology_canvas .loading_bg_normal {
fill: #555;
}
svg#topology_canvas .base_bg_small,
svg#topology_canvas .loading_bg_small {
fill: #fff;
}
svg#topology_canvas .active {
fill: #45B035;
}
svg#topology_canvas .icon polygon {
fill: #333;
}
svg#topology_canvas .instance_small .frame,
svg#topology_canvas .router_small .frame {
fill: url(#device_small_bg);
stroke: #333;
stroke-width: 3;
}
svg#topology_canvas .instance_small .port_text,
svg#topology_canvas .router_small .port_text {
display: none;
}
svg#topology_canvas .router_normal .frame,
svg#topology_canvas .instance_normal .frame {
fill: #fff;
stroke: #333;
stroke-width: 4;
}
svg#topology_canvas .router_normal .icon_bg,
svg#topology_canvas .instance_normal .icon_bg {
fill: #fff;
stroke: #333;
stroke-width: 4;
}
svg#topology_canvas .router_normal .texts_bg,
svg#topology_canvas .instance_normal .texts_bg {
fill: url('#device_normal_bg');
}
svg#topology_canvas .router_normal .texts .name,
svg#topology_canvas .instance_normal .texts .name {
text-anchor: middle;
fill: #333;
font-size: 12px;
}
svg#topology_canvas .router_normal .texts .type,
svg#topology_canvas .instance_normal .texts .type {
text-anchor: middle;
fill: #fff;
font-size: 12px;
}
svg#topology_canvas .router_normal .instance_bg,
svg#topology_canvas .instance_normal .instance_bg {
fill: #333;
}
svg#topology_canvas g.loading .active {
fill: #555;
}
svg#topology_canvas g.loading .icon polygon {
fill: #555;
}
svg#topology_canvas g.loading .instance_bg {
fill: #555;
}
svg#topology_canvas g.loading .instance_small .frame,
svg#topology_canvas g.loading .router_small .frame {
stroke: #555;
fill: url(#device_small_bg_loading);
}
svg#topology_canvas g.loading .router_normal .frame,
svg#topology_canvas g.loading .instance_normal .frame {
stroke: #555;
}
svg#topology_canvas g.loading .router_normal .name,
svg#topology_canvas g.loading .instance_normal .name {
fill: #999;
}
svg#topology_canvas g.loading .router_normal .texts_bg,
svg#topology_canvas g.loading .instance_normal .texts_bg {
fill: url(#device_normal_bg_loading);
}
svg#topology_canvas g.loading .router_normal .icon_bg,
svg#topology_canvas g.loading .instance_normal .icon_bg {
stroke: #555;
}
</style>
<svg width="400" height="400" id="topology_canvas">
<defs>
<pattern id="device_normal_bg" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="base_bg_normal"></rect>
</g>
</pattern>
<pattern id="device_normal_bg_loading" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="loading_bg_normal"></rect>
<path d='M0 20L20 0ZM22 18L18 22ZM-2 2L2 -2Z' stroke-linecap="square" stroke='rgba(0, 0, 0, 0.3)' stroke-width="7"></path>
</g>
<animate attributeName="x" attributeType="XML" begin="0s" dur="0.5s" from="0" to="-20" repeatCount="indefinite"></animate>
</pattern>
<pattern id="device_small_bg" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<g>
<rect width="20" height="20" class="base_bg_small"></rect>
</g>
</pattern>
<pattern id="device_small_bg_loading" patternUnits="userSpaceOnUse" x="0" y="0" width="5" height="5">
<g>
<rect width="5" height="5" class="loading_bg_small"></rect>
<path d='M0 5L5 0ZM6 4L4 6ZM-1 1L1 -1Z' stroke-linecap="square" stroke='rgba(0, 0, 0, 0.4)' stroke-width="1.5"></path>
</g>
<animate attributeName="x" attributeType="XML" begin="0s" dur="0.5s" from="0" to="-5" repeatCount="indefinite"></animate>
</pattern>
</defs>
</svg>
<svg id="topology_template" display="none">
<g class="router_small device_body">
<g class="ports" pointer-events="none"></g>
<rect rx="3" ry="3" width="20" height="20" class="frame"></rect>
<g transform="translate(3.5,3)" class="icon">
<polygon points="12.51,4.23 12.51,0.49 8.77,0.49 9.68,1.4 6.92,4.16 8.84,6.08 11.6,3.32 "></polygon>
<polygon points="0.49,8.77 0.49,12.51 4.23,12.51 3.32,11.6 6.08,8.83 4.17,6.92 1.4,9.68 "></polygon>
<polygon points="1.85,5.59 5.59,5.59 5.59,1.85 4.68,2.76 1.92,0 0,1.92 2.76,4.68 "></polygon>
<polygon points="11.15,7.41 7.41,7.41 7.41,11.15 8.32,10.24 11.08,13 13,11.08 10.24,8.32 "></polygon>
</g>
</g>
<g class="instance_small device_body">
<g class="ports" pointer-events="none"></g>
<rect rx="3" ry="3" width="20" height="20" class="frame"></rect>
<g transform="translate(5,3)" class="icon">
<rect class="instance_bg" width="10" height="13"></rect>
<rect x="2" y="1" fill="#FFFFFF" width="6" height="2"></rect>
<rect x="2" y="4" fill="#FFFFFF" width="6" height="2"></rect>
<circle class="active" cx="3" cy="10" r="1.3"></circle>
</g>
</g>
<g class="network_container_small">
<rect rx="7" ry="7" width="15" height="200" style="fill: #8541B5;" class="network-rect"></rect>
<text x="250" y="-3" class="network-name" transform="rotate(90 0 0)" pointer-events="none">Network</text>
<text x="0" y="-20" class="network-cidr" transform="rotate(90 0 0)">0.0.0.0</text>
<text x="0" y="-20" class="network-type" transform="rotate(90 0 0)"
data-toggle="tooltip" data-placement="bottom" title="{% trans 'External Network' %}"></text>
</g>
<g class="router_normal device_body">
<g class="ports" pointer-events="none"></g>
<rect class="frame" x="0" y="0" rx="6" ry="6" width="90" height="50"></rect>
<g class="texts" pointer-events="none">
<rect class="texts_bg" x="1.5" y="32" width="87" height="17"></rect>
<text x="45" y="46" class="type">{% trans "Router" %}</text>
<text x="45" y="22" class="name">router</text>
</g>
<g class="icon" transform="translate(6,6)" pointer-events="none">
<circle class="icon_bg" cx="0" cy="0" r="12"></circle>
<g transform="translate(-6.5,-6.5)">
<polygon points="12.51,4.23 12.51,0.49 8.77,0.49 9.68,1.4 6.92,4.16 8.84,6.08 11.6,3.32 "></polygon>
<polygon points="0.49,8.77 0.49,12.51 4.23,12.51 3.32,11.6 6.08,8.83 4.17,6.92 1.4,9.68 "></polygon>
<polygon points="1.85,5.59 5.59,5.59 5.59,1.85 4.68,2.76 1.92,0 0,1.92 2.76,4.68 "></polygon>
<polygon points="11.15,7.41 7.41,7.41 7.41,11.15 8.32,10.24 11.08,13 13,11.08 10.24,8.32 "></polygon>
</g>
</g>
</g>
<g class="instance_normal device_body">
<g class="ports" pointer-events="none"></g>
<rect class="frame" x="0" y="0" rx="6" ry="6" width="90" height="50"></rect>
<g class="texts">
<rect class="texts_bg" x="1.5" y="32" width="87" height="17"></rect>
<text x="45" y="46" class="type">{% trans "Instance" %}</text>
<text x="45" y="22" class="name">instance</text>
</g>
<g class="icon" transform="translate(6,6)">
<circle class="icon_bg" cx="0" cy="0" r="12"></circle>
<g transform="translate(-5,-6.5)">
<rect class="instance_bg" width="10" height="13"></rect>
<rect x="2" y="1" fill="#FFFFFF" width="6" height="2"></rect>
<rect x="2" y="4" fill="#FFFFFF" width="6" height="2"></rect>
<circle class="active" cx="3" cy="10" r="1.3"></circle>
</g>
</g>
</g>
<g class="network_container_normal">
<rect rx="9" ry="9" width="17" height="500" style="fill: #8541B5;" class="network-rect"></rect>
<text x="250" y="-4" class="network-name" transform="rotate(90 0 0)" pointer-events="none">Network</text>
<text x="490" y="-20" class="network-cidr" transform="rotate(90 0 0)">0.0.0.0</text>
<text x="490" y="-20" class="network-type" transform="rotate(90 0 0)"
data-toggle="tooltip" data-placement="bottom" title="{% trans 'External Network' %}"></text>
</g>
</svg>

View File

@ -22,9 +22,11 @@
[[/console_id]]
{% endif %}
</div>
[[#type]]
<div class="cell delete">
<button class="delete-device btn btn-danger btn-xs [[type]]" data-type="[[type]]" data-device-id="[[id]]">[[delete_label]]</button>
</div>
[[/type]]
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
{% extends "horizon/client_side/template.html" %}
{% load horizon %}
{% block id %}balloon_instance{% endblock %}
{% block template %}
{% jstemplate %}
<div class="portTableHeader">
<div class="title">[[ips_label]]</div>
</div>
<table class="detailInfoTable">
<caption></caption>
<tbody>
[[#ips]]
<tr>
<th>[[ip_address]]</th>
<td>[[subnet_id]]</td>
</tr>
[[/ips]]
</tbody>
</table>
{% endjstemplate %}
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends "horizon/client_side/template.html" %}
{% load horizon %}
{% block id %}balloon_net{% endblock %}
{% block template %}
{% jstemplate %}
<div class="portTableHeader">
<div class="title">[[subnets_label]]</div>
<div class="action">
<a class="add-subnet btn btn-default btn-xs ajax-modal [[type]]" href="[[add_subnet_url]]">
<span class="fa fa-plus"></span>
[[add_subnet_label]]
</a>
</div>
</div>
<table class="detailInfoTable">
<caption></caption>
<tbody>
[[#subnet]]
<tr>
<th>
<span title="[[id]]">
<a href="[[url]]">[[id]]</a>
</span>
</th>
<td>[[cidr]]</td>
<td class="delete">
<button class="delete-port btn btn-danger btn-xs"
data-router-id="[[router_id]]" data-network-id="[[network_id]]"
data-port-id="[[id]]">[[delete_subnet_label]]</button>
</td>
</tr>
[[/subnet]]
</tbody>
</table>
{% endjstemplate %}
{% endblock %}

View File

@ -31,7 +31,9 @@
</td>
<td class="delete">
[[#is_interface]]
<button class="delete-port btn btn-danger btn-xs" data-router-id="[[router_id]]" data-port-id="[[id]]">[[delete_interface_label]]</button>
<button class="delete-port btn btn-danger btn-xs"
data-router-id="[[router_id]]" data-network-id="[[network_id]]"
data-port-id="[[id]]">[[delete_interface_label]]</button>
[[/is_interface]]
</td>
</tr>

View File

@ -8,7 +8,7 @@
</head>
<body>
{% include "horizon/_messages.html" %}
{% firstof table.render interfaces_table.render tab_group.render %}
{% firstof subnets_table.render table.render interfaces_table.render tab_group.render %}
{% include "project/network_topology/_post_massage.html" %}
</body>
</html>

View File

@ -12,19 +12,20 @@
{% include "project/network_topology/client_side/_balloon_container.html" %}
{% include "project/network_topology/client_side/_balloon_device.html" %}
{% include "project/network_topology/client_side/_balloon_port.html" %}
{% include "project/network_topology/client_side/_balloon_net.html" %}
{% include "project/network_topology/client_side/_balloon_instance.html" %}
<div class='description'>
{% blocktrans %}
Resize the canvas by scrolling up/down with your mouse/trackpad on the topology.
Pan around the canvas by clicking and dragging the space behind the topology.
{% endblocktrans %}
</div>
<div class="topologyNavi">
<div class="toggleView btn-group" data-toggle="buttons">
<label class="btn btn-default" data-value="small">
<input type="radio" name="options" id="option1" checked>
<span class="fa fa-th"></span>
{% trans "Small" %}
</label>
<label class="btn btn-default" data-value="normal">
<input type="radio" name="options" id="option2">
<span class="fa fa-th-large"></span>
{% trans "Normal" %}
</label>
<div class="toggleView btn-group" data-toggle="buttons-radio">
<button type="button" class="btn btn-default" id="toggle_labels"><span class="fa
fa-th-large"></span> {%trans "Toggle labels" %}</button>
<button type="button" class="btn btn-default" id="toggle_networks"><span class="fa
fa-th"></span> {%trans "Toggle Network Collapse" %}</button>
</div>
<div class="launchButtons">
{% if launch_instance_allowed %}
@ -47,7 +48,6 @@
<div id="topologyCanvasContainer">
<div class="nodata">{% blocktrans %}There are no networks, routers, or connected instances to display.{% endblocktrans %}</div>
{% include "project/network_topology/_svg_element.html" %}
</div>
<span data-networktopology="{% url 'horizon:project:network_topology:json' %}" id="networktopology"></span>
<div id="topologyMessages"></div>

View File

@ -110,17 +110,22 @@ class NetworkTopologyTests(test.TestCase):
expect_net_urls = []
if router_enable:
expect_net_urls += [{'id': net.id,
'url': None,
'url': '/project/networks/%s/detail' % net.id,
'name': net.name,
'router:external': net.router__external,
'subnets': [{'cidr': subnet.cidr}
for subnet in net.subnets]}
'status': net.status,
'subnets': []}
for net in external_networks]
expect_net_urls += [{'id': net.id,
'url': '/project/networks/%s/detail' % net.id,
'name': net.name,
'router:external': net.router__external,
'subnets': [{'cidr': subnet.cidr}
'status': net.status,
'subnets': [{'cidr': subnet.cidr,
'id': subnet.id,
'url':
'/project/networks/subnets/%s/detail'
% subnet.id}
for subnet in net.subnets]}
for net in tenant_networks]
for exp_net in expect_net_urls:

View File

@ -27,9 +27,16 @@ urlpatterns = patterns(
'openstack_dashboard.dashboards.project.network_topology.views',
url(r'^$', views.NetworkTopologyView.as_view(), name='index'),
url(r'^router$', views.RouterView.as_view(), name='router'),
url(r'^network$', views.NetworkView.as_view(), name='network'),
url(r'^instance$', views.InstanceView.as_view(), name='instance'),
url(r'^router/(?P<router_id>[^/]+)/$', views.RouterDetailView.as_view(),
name='detail'),
url(r'^router/(?P<router_id>[^/]+)/addinterface$',
views.NTAddInterfaceView.as_view(), name='interface'),
url(r'^network/(?P<network_id>[^/]+)/$', views.NetworkDetailView.as_view(),
name='detail'),
url(r'^network/(?P<network_id>[^/]+)/subnet/create$',
views.NTCreateSubnetView.as_view(), name='subnet'),
url(r'^json$', views.JSONView.as_view(), name='json'),
url(r'^launchinstance$', views.NTLaunchInstanceView.as_view(),
name='launchinstance'),

View File

@ -33,10 +33,14 @@ from openstack_dashboard.usage import quotas
from openstack_dashboard.dashboards.project.network_topology.instances \
import tables as instances_tables
from openstack_dashboard.dashboards.project.network_topology.networks \
import tables as networks_tables
from openstack_dashboard.dashboards.project.network_topology.ports \
import tables as ports_tables
from openstack_dashboard.dashboards.project.network_topology.routers \
import tables as routers_tables
from openstack_dashboard.dashboards.project.network_topology.subnets \
import tables as subnets_tables
from openstack_dashboard.dashboards.project.instances import\
console as i_console
@ -44,14 +48,33 @@ from openstack_dashboard.dashboards.project.instances import\
views as i_views
from openstack_dashboard.dashboards.project.instances.workflows import\
create_instance as i_workflows
from openstack_dashboard.dashboards.project.networks.subnets import\
views as s_views
from openstack_dashboard.dashboards.project.networks.subnets import\
workflows as s_workflows
from openstack_dashboard.dashboards.project.networks import\
views as n_views
from openstack_dashboard.dashboards.project.networks import\
workflows as n_workflows
from openstack_dashboard.dashboards.project.routers.ports import\
views as p_views
from openstack_dashboard.dashboards.project.routers import\
views as r_views
class NTAddInterfaceView(p_views.AddInterfaceView):
success_url = "horizon:project:network_topology:index"
failure_url = "horizon:project:network_topology:index"
def get_success_url(self):
return reverse("horizon:project:network_topology:index")
def get_context_data(self, **kwargs):
context = super(NTAddInterfaceView, self).get_context_data(**kwargs)
context['form_url'] = 'horizon:project:network_topology:interface'
return context
class NTCreateRouterView(r_views.CreateView):
template_name = 'project/network_topology/create_router.html'
success_url = reverse_lazy("horizon:project:network_topology:index")
@ -78,6 +101,18 @@ class NTLaunchInstanceView(i_views.LaunchInstanceView):
workflow_class = NTLaunchInstance
class NTCreateSubnet(s_workflows.CreateSubnet):
def get_success_url(self):
return reverse("horizon:project:network_topology:index")
def get_failure_url(self):
return reverse("horizon:project:network_topology:index")
class NTCreateSubnetView(s_views.CreateView):
workflow_class = NTCreateSubnet
class InstanceView(i_views.IndexView):
table_class = instances_tables.InstancesTable
template_name = 'project/network_topology/iframe.html'
@ -88,6 +123,11 @@ class RouterView(r_views.IndexView):
template_name = 'project/network_topology/iframe.html'
class NetworkView(n_views.IndexView):
table_class = networks_tables.NetworksTable
template_name = 'project/network_topology/iframe.html'
class RouterDetailView(r_views.DetailView):
table_classes = (ports_tables.PortsTable, )
template_name = 'project/network_topology/iframe.html'
@ -96,6 +136,11 @@ class RouterDetailView(r_views.DetailView):
pass
class NetworkDetailView(n_views.DetailView):
table_classes = (subnets_tables.SubnetsTable, )
template_name = 'project/network_topology/iframe.html'
class NetworkTopologyView(views.HorizonTemplateView):
template_name = 'project/network_topology/index.html'
page_title = _("Network Topology")
@ -197,14 +242,18 @@ class JSONView(View):
request.user.tenant_id)
except Exception:
neutron_networks = []
networks = [{'name': network.name,
'id': network.id,
'subnets': [{'cidr': subnet.cidr}
for subnet in network.subnets],
'router:external': network['router:external']}
for network in neutron_networks]
self.add_resource_url('horizon:project:networks:detail',
networks)
networks = []
for network in neutron_networks:
obj = {'name': network.name,
'id': network.id,
'subnets': [{'id': subnet.id,
'cidr': subnet.cidr}
for subnet in network.subnets],
'status': network.status,
'router:external': network['router:external']}
self.add_resource_url('horizon:project:networks:subnets:detail',
obj['subnets'])
networks.append(obj)
# Add public networks to the networks list
if self.is_router_enabled:
@ -219,16 +268,25 @@ class JSONView(View):
if publicnet.id in my_network_ids:
continue
try:
subnets = [{'cidr': subnet.cidr}
for subnet in publicnet.subnets]
subnets = []
for subnet in publicnet.subnets:
snet = {'id': subnet.id,
'cidr': subnet.cidr}
self.add_resource_url(
'horizon:project:networks:subnets:detail', snet)
subnets.append(snet)
except Exception:
subnets = []
networks.append({
'name': publicnet.name,
'id': publicnet.id,
'subnets': subnets,
'status': publicnet.status,
'router:external': publicnet['router:external']})
self.add_resource_url('horizon:project:networks:detail',
networks)
return sorted(networks,
key=lambda x: x.get('router:external'),
reverse=True)

View File

@ -52,6 +52,7 @@ class AddInterfaceView(forms.ModalFormView):
def get_context_data(self, **kwargs):
context = super(AddInterfaceView, self).get_context_data(**kwargs)
context['router'] = self.get_object()
context['form_url'] = 'horizon:project:routers:addinterface'
return context
def get_initial(self):

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}add_interface_form{% endblock %}
{% block form_action %}{% url 'horizon:project:routers:addinterface' router.id %}
{% block form_action %}{% url form_url router.id %}
{% endblock %}
{% block modal-header %}{% trans "Add Interface" %}{% endblock %}

View File

@ -4,7 +4,7 @@
height: auto;
padding: 25px;
padding-left: 50px;
background: #efefef;
background: #ffffff;
min-height: 400px;
div.nodata {
font-size: 150%;