summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAkihiro MOTOKI <motoki@da.jp.nec.com>2012-07-23 14:26:30 +0900
committerAkihiro MOTOKI <motoki@da.jp.nec.com>2012-08-13 02:54:42 +0900
commit05cf90049244ea0694d853f9c1e2aff14d753c9a (patch)
tree36feb6e467b5ca92d05df63cf03ad777c7db3777
parentb0066309fd57c0d20f754d3ecea94b617235f40e (diff)
downloadtuskar-ui-05cf90049244ea0694d853f9c1e2aff14d753c9a.tar.gz
Initial support of Quantum V2.
Implementes blueprint quantum-horizon and blueprint readd-quantum-support. This commit also covers blueprint quantum-workflow-integration. - Added quantum API layer, - Added network/subnet/port CRUD operations, - Added 'Network' user panel, - Added 'Network' system panel, - Added 'Networking' tab in instance creation workflow. - Supported launching an instance with specified network(s) Change-Id: I7ad608e17cb6fb4f0de02721888e96a68cf926e8
-rw-r--r--.gitignore1
-rw-r--r--horizon/api/__init__.py1
-rw-r--r--horizon/api/nova.py4
-rw-r--r--horizon/api/quantum.py239
-rw-r--r--horizon/dashboards/nova/dashboard.py3
-rw-r--r--horizon/dashboards/nova/instances/tests.py25
-rw-r--r--horizon/dashboards/nova/instances/workflows.py54
-rw-r--r--horizon/dashboards/nova/networks/__init__.py0
-rw-r--r--horizon/dashboards/nova/networks/forms.py55
-rw-r--r--horizon/dashboards/nova/networks/panel.py28
-rw-r--r--horizon/dashboards/nova/networks/ports/__init__.py0
-rw-r--r--horizon/dashboards/nova/networks/ports/tables.py53
-rw-r--r--horizon/dashboards/nova/networks/ports/tabs.py46
-rw-r--r--horizon/dashboards/nova/networks/ports/urls.py24
-rw-r--r--horizon/dashboards/nova/networks/ports/views.py28
-rw-r--r--horizon/dashboards/nova/networks/subnets/__init__.py0
-rw-r--r--horizon/dashboards/nova/networks/subnets/forms.py138
-rw-r--r--horizon/dashboards/nova/networks/subnets/tables.py79
-rw-r--r--horizon/dashboards/nova/networks/subnets/tabs.py48
-rw-r--r--horizon/dashboards/nova/networks/subnets/urls.py24
-rw-r--r--horizon/dashboards/nova/networks/subnets/views.py109
-rw-r--r--horizon/dashboards/nova/networks/tables.py94
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/_create.html24
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/_detail_overview.html18
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/_network_ips.html10
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/_update.html24
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/create.html11
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/detail.html18
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/index.html11
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html41
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html7
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/ports/detail.html15
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/_create.html25
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html29
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/_update.html33
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/create.html11
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/detail.html15
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/index.html11
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/subnets/update.html11
-rw-r--r--horizon/dashboards/nova/networks/templates/networks/update.html11
-rw-r--r--horizon/dashboards/nova/networks/tests.py753
-rw-r--r--horizon/dashboards/nova/networks/urls.py37
-rw-r--r--horizon/dashboards/nova/networks/views.py146
-rw-r--r--horizon/dashboards/nova/networks/workflows.py162
-rw-r--r--horizon/dashboards/syspanel/dashboard.py2
-rw-r--r--horizon/dashboards/syspanel/instances/tests.py6
-rw-r--r--horizon/dashboards/syspanel/networks/__init__.py0
-rw-r--r--horizon/dashboards/syspanel/networks/forms.py67
-rw-r--r--horizon/dashboards/syspanel/networks/panel.py28
-rw-r--r--horizon/dashboards/syspanel/networks/ports/__init__.py0
-rw-r--r--horizon/dashboards/syspanel/networks/ports/forms.py92
-rw-r--r--horizon/dashboards/syspanel/networks/ports/tables.py85
-rw-r--r--horizon/dashboards/syspanel/networks/ports/tabs.py46
-rw-r--r--horizon/dashboards/syspanel/networks/ports/urls.py24
-rw-r--r--horizon/dashboards/syspanel/networks/ports/views.py98
-rw-r--r--horizon/dashboards/syspanel/networks/subnets/__init__.py0
-rw-r--r--horizon/dashboards/syspanel/networks/subnets/forms.py52
-rw-r--r--horizon/dashboards/syspanel/networks/subnets/tables.py82
-rw-r--r--horizon/dashboards/syspanel/networks/subnets/urls.py24
-rw-r--r--horizon/dashboards/syspanel/networks/subnets/views.py101
-rw-r--r--horizon/dashboards/syspanel/networks/tables.py79
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/_create.html25
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/_update.html24
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/create.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/index.html21
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html25
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html29
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/ports/create.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/ports/update.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html25
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html33
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html11
-rw-r--r--horizon/dashboards/syspanel/networks/templates/networks/update.html11
-rw-r--r--horizon/dashboards/syspanel/networks/tests.py801
-rw-r--r--horizon/dashboards/syspanel/networks/urls.py45
-rw-r--r--horizon/dashboards/syspanel/networks/views.py133
-rw-r--r--horizon/tables/base.py2
-rw-r--r--horizon/templates/horizon/common/_data_table.html2
-rw-r--r--horizon/templates/horizon/common/_workflow_step.html6
-rw-r--r--horizon/test.py10
-rw-r--r--horizon/tests/api_tests/quantum_tests.py205
-rw-r--r--horizon/tests/test_data/exceptions.py4
-rw-r--r--horizon/tests/test_data/quantum_data.py109
-rw-r--r--horizon/tests/test_data/utils.py2
-rw-r--r--openstack_dashboard/exceptions.py13
-rw-r--r--openstack_dashboard/static/dashboard/less/horizon.less6
-rw-r--r--tools/pip-requires1
89 files changed, 4849 insertions, 11 deletions
diff --git a/.gitignore b/.gitignore
index d353785e..daec31fc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
*.pyc
*.swp
+*.sqlite3
.environment_version
.selenium_log
.coverage*
diff --git a/horizon/api/__init__.py b/horizon/api/__init__.py
index 3e37214d..b2b1d243 100644
--- a/horizon/api/__init__.py
+++ b/horizon/api/__init__.py
@@ -36,3 +36,4 @@ from horizon.api.glance import *
from horizon.api.keystone import *
from horizon.api.nova import *
from horizon.api.swift import *
+from horizon.api.quantum import *
diff --git a/horizon/api/nova.py b/horizon/api/nova.py
index e4315a64..ef367680 100644
--- a/horizon/api/nova.py
+++ b/horizon/api/nova.py
@@ -292,11 +292,13 @@ def keypair_list(request):
def server_create(request, name, image, flavor, key_name, user_data,
- security_groups, block_device_mapping, instance_count=1):
+ security_groups, block_device_mapping, nics=None,
+ instance_count=1):
return Server(novaclient(request).servers.create(
name, image, flavor, userdata=user_data,
security_groups=security_groups,
key_name=key_name, block_device_mapping=block_device_mapping,
+ nics=nics,
min_count=instance_count), request)
diff --git a/horizon/api/quantum.py b/horizon/api/quantum.py
new file mode 100644
index 00000000..45742573
--- /dev/null
+++ b/horizon/api/quantum.py
@@ -0,0 +1,239 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Copyright 2012 Cisco Systems, Inc.
+# Copyright 2012 NEC Corporation
+#
+# 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 __future__ import absolute_import
+
+import logging
+
+from quantumclient.v2_0 import client as quantum_client
+from django.utils.datastructures import SortedDict
+
+from horizon.api.base import APIDictWrapper, url_for
+
+
+LOG = logging.getLogger(__name__)
+
+
+class QuantumAPIDictWrapper(APIDictWrapper):
+
+ def set_id_as_name_if_empty(self, length=8):
+ try:
+ if not self._apidict['name']:
+ id = self._apidict['id']
+ if length:
+ id = id[:length]
+ self._apidict['name'] = '(%s)' % id
+ except KeyError:
+ pass
+
+ def items(self):
+ return self._apidict.items()
+
+
+class Network(QuantumAPIDictWrapper):
+ """Wrapper for quantum Networks"""
+ _attrs = ['name', 'id', 'subnets', 'tenant_id', 'status', 'admin_state_up']
+
+ def __init__(self, apiresource):
+ apiresource['admin_state'] = \
+ 'UP' if apiresource['admin_state_up'] else 'DOWN'
+ super(Network, self).__init__(apiresource)
+
+
+class Subnet(QuantumAPIDictWrapper):
+ """Wrapper for quantum subnets"""
+ _attrs = ['name', 'id', 'cidr', 'network_id', 'tenant_id',
+ 'ip_version', 'ipver_str']
+
+ def __init__(self, apiresource):
+ apiresource['ipver_str'] = get_ipver_str(apiresource['ip_version'])
+ super(Subnet, self).__init__(apiresource)
+
+
+class Port(QuantumAPIDictWrapper):
+ """Wrapper for quantum ports"""
+ _attrs = ['name', 'id', 'network_id', 'tenant_id',
+ 'admin_state_up', 'status', 'mac_address',
+ 'fixed_ips', 'host_routes', 'device_id']
+
+ def __init__(self, apiresource):
+ apiresource['admin_state'] = \
+ 'UP' if apiresource['admin_state_up'] else 'DOWN'
+ super(Port, self).__init__(apiresource)
+
+
+IP_VERSION_DICT = {4: 'IPv4', 6: 'IPv6'}
+
+
+def get_ipver_str(ip_version):
+ """Convert an ip version number to a human-friendly string"""
+ return IP_VERSION_DICT.get(ip_version, '')
+
+
+def quantumclient(request):
+ LOG.debug('quantumclient connection created using token "%s" and url "%s"'
+ % (request.user.token.id, url_for(request, 'network')))
+ LOG.debug('user_id=%(user)s, tenant_id=%(tenant)s' %
+ {'user': request.user.id, 'tenant': request.user.tenant_id})
+ c = quantum_client.Client(token=request.user.token.id,
+ endpoint_url=url_for(request, 'network'))
+ return c
+
+
+def network_list(request, **params):
+ LOG.debug("network_list(): params=%s" % (params))
+ networks = quantumclient(request).list_networks(**params).get('networks')
+ # Get subnet list to expand subnet info in network list.
+ subnets = subnet_list(request)
+ subnet_dict = SortedDict([(s['id'], s) for s in subnets])
+ # Expand subnet list from subnet_id to values.
+ for n in networks:
+ n['subnets'] = [subnet_dict[s] for s in n['subnets']]
+ return [Network(n) for n in networks]
+
+
+def network_get(request, network_id, **params):
+ LOG.debug("network_get(): netid=%s, params=%s" % (network_id, params))
+ network = quantumclient(request).show_network(network_id,
+ **params).get('network')
+ # Since the number of subnets per network must be small,
+ # call subnet_get() for each subnet instead of calling
+ # subnet_list() once.
+ network['subnets'] = [subnet_get(request, sid)
+ for sid in network['subnets']]
+ return Network(network)
+
+
+def network_create(request, **kwargs):
+ """
+ Create a subnet on a specified network.
+ :param request: request context
+ :param tenant_id: (optional) tenant id of the network created
+ :param name: (optional) name of the network created
+ :returns: Subnet object
+ """
+ LOG.debug("network_create(): kwargs = %s" % kwargs)
+ body = {'network': kwargs}
+ network = quantumclient(request).create_network(body=body).get('network')
+ return Network(network)
+
+
+def network_modify(request, network_id, **kwargs):
+ LOG.debug("network_modify(): netid=%s, params=%s" % (network_id, kwargs))
+ body = {'network': kwargs}
+ network = quantumclient(request).update_network(network_id,
+ body=body).get('network')
+ return Network(network)
+
+
+def network_delete(request, network_id):
+ LOG.debug("network_delete(): netid=%s" % network_id)
+ quantumclient(request).delete_network(network_id)
+
+
+def subnet_list(request, **params):
+ LOG.debug("subnet_list(): params=%s" % (params))
+ subnets = quantumclient(request).list_subnets(**params).get('subnets')
+ return [Subnet(s) for s in subnets]
+
+
+def subnet_get(request, subnet_id, **params):
+ LOG.debug("subnet_get(): subnetid=%s, params=%s" % (subnet_id, params))
+ subnet = quantumclient(request).show_subnet(subnet_id,
+ **params).get('subnet')
+ return Subnet(subnet)
+
+
+def subnet_create(request, network_id, cidr, ip_version, **kwargs):
+ """
+ Create a subnet on a specified network.
+ :param request: request context
+ :param network_id: network id a subnet is created on
+ :param cidr: subnet IP address range
+ :param ip_version: IP version (4 or 6)
+ :param gateway_ip: (optional) IP address of gateway
+ :param tenant_id: (optional) tenant id of the subnet created
+ :param name: (optional) name of the subnet created
+ :returns: Subnet object
+ """
+ LOG.debug("subnet_create(): netid=%s, cidr=%s, ipver=%d, kwargs=%s"
+ % (network_id, cidr, ip_version, kwargs))
+ body = {'subnet':
+ {'network_id': network_id,
+ 'ip_version': ip_version,
+ 'cidr': cidr}}
+ body['subnet'].update(kwargs)
+ subnet = quantumclient(request).create_subnet(body=body).get('subnet')
+ return Subnet(subnet)
+
+
+def subnet_modify(request, subnet_id, **kwargs):
+ LOG.debug("subnet_modify(): subnetid=%s, kwargs=%s" % (subnet_id, kwargs))
+ body = {'subnet': kwargs}
+ subnet = quantumclient(request).update_subnet(subnet_id,
+ body=body).get('subnet')
+ return Subnet(subnet)
+
+
+def subnet_delete(request, subnet_id):
+ LOG.debug("subnet_delete(): subnetid=%s" % subnet_id)
+ quantumclient(request).delete_subnet(subnet_id)
+
+
+def port_list(request, **params):
+ LOG.debug("port_list(): params=%s" % (params))
+ ports = quantumclient(request).list_ports(**params).get('ports')
+ return [Port(p) for p in ports]
+
+
+def port_get(request, port_id, **params):
+ LOG.debug("port_get(): portid=%s, params=%s" % (port_id, params))
+ port = quantumclient(request).show_port(port_id, **params).get('port')
+ return Port(port)
+
+
+def port_create(request, network_id, **kwargs):
+ """
+ Create a port on a specified network.
+ :param request: request context
+ :param network_id: network id a subnet is created on
+ :param device_id: (optional) device id attached to the port
+ :param tenant_id: (optional) tenant id of the port created
+ :param name: (optional) name of the port created
+ :returns: Port object
+ """
+ LOG.debug("port_create(): netid=%s, kwargs=%s" % (network_id, kwargs))
+ body = {'port': {'network_id': network_id}}
+ body['port'].update(kwargs)
+ port = quantumclient(request).create_port(body=body).get('port')
+ return Port(port)
+
+
+def port_delete(request, port_id):
+ LOG.debug("port_delete(): portid=%s" % port_id)
+ quantumclient(request).delete_port(port_id)
+
+
+def port_modify(request, port_id, **kwargs):
+ LOG.debug("port_modify(): portid=%s, kwargs=%s" % (port_id, kwargs))
+ body = {'port': kwargs}
+ port = quantumclient(request).update_port(port_id, body=body).get('port')
+ return Port(port)
diff --git a/horizon/dashboards/nova/dashboard.py b/horizon/dashboards/nova/dashboard.py
index e950873c..822f9262 100644
--- a/horizon/dashboards/nova/dashboard.py
+++ b/horizon/dashboards/nova/dashboard.py
@@ -26,7 +26,8 @@ class BasePanels(horizon.PanelGroup):
'instances',
'volumes',
'images_and_snapshots',
- 'access_and_security')
+ 'access_and_security',
+ 'networks')
class ObjectStorePanels(horizon.PanelGroup):
diff --git a/horizon/dashboards/nova/instances/tests.py b/horizon/dashboards/nova/instances/tests.py
index 38f24aa6..d093d19d 100644
--- a/horizon/dashboards/nova/instances/tests.py
+++ b/horizon/dashboards/nova/instances/tests.py
@@ -589,6 +589,7 @@ class InstanceTests(test.TestCase):
'security_group_list',
'volume_snapshot_list',
'volume_list',),
+ api.quantum: ('network_list',),
api.glance: ('image_list_detailed',)})
def test_launch_instance_get(self):
quota_usages = self.quota_usages.first()
@@ -604,6 +605,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(quota_usages)
api.nova.flavor_list(IsA(http.HttpRequest)) \
@@ -631,10 +634,12 @@ class InstanceTests(test.TestCase):
self.assertQuerysetEqual(workflow.steps,
['<SetInstanceDetails: setinstancedetailsaction>',
'<SetAccessControls: setaccesscontrolsaction>',
+ '<SetNetwork: setnetworkaction>',
'<VolumeOptions: volumeoptionsaction>',
'<PostCreationStep: customizeaction>'])
@test.create_stubs({api.glance: ('image_list_detailed',),
+ api.quantum: ('network_list',),
api.nova: ('flavor_list',
'keypair_list',
'security_group_list',
@@ -653,6 +658,7 @@ class InstanceTests(test.TestCase):
device_name = u'vda'
volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice}
+ nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
@@ -666,6 +672,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
@@ -677,6 +685,7 @@ class InstanceTests(test.TestCase):
customization_script,
[sec_group.name],
block_device_mapping,
+ nics=nics,
instance_count=IsA(int))
self.mox.ReplayAll()
@@ -693,6 +702,7 @@ class InstanceTests(test.TestCase):
'volume_type': 'volume_id',
'volume_id': volume_choice,
'device_name': device_name,
+ 'network': self.networks.first().id,
'count': 1}
url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data)
@@ -702,6 +712,7 @@ class InstanceTests(test.TestCase):
reverse('horizon:nova:instances:index'))
@test.create_stubs({api.glance: ('image_list_detailed',),
+ api.quantum: ('network_list',),
api.nova: ('flavor_list',
'keypair_list',
'security_group_list',
@@ -727,6 +738,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
api.nova.keypair_list(IsA(http.HttpRequest)) \
@@ -762,6 +775,7 @@ class InstanceTests(test.TestCase):
'nova/instances/launch.html')
@test.create_stubs({api.glance: ('image_list_detailed',),
+ api.quantum: ('network_list',),
api.nova: ('tenant_quota_usages',
'flavor_list',
'keypair_list',
@@ -779,6 +793,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(self.quota_usages.first())
api.nova.flavor_list(IsA(http.HttpRequest)) \
@@ -799,6 +815,7 @@ class InstanceTests(test.TestCase):
'nova/instances/launch.html')
@test.create_stubs({api.glance: ('image_list_detailed',),
+ api.quantum: ('network_list',),
api.nova: ('flavor_list',
'keypair_list',
'security_group_list',
@@ -812,6 +829,7 @@ class InstanceTests(test.TestCase):
server = self.servers.first()
sec_group = self.security_groups.first()
customization_script = 'userData'
+ nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
@@ -825,6 +843,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
api.nova.server_create(IsA(http.HttpRequest),
server.name,
@@ -834,6 +854,7 @@ class InstanceTests(test.TestCase):
customization_script,
[sec_group.name],
None,
+ nics=nics,
instance_count=IsA(int)) \
.AndRaise(self.exceptions.keystone)
@@ -849,6 +870,7 @@ class InstanceTests(test.TestCase):
'user_id': self.user.id,
'groups': sec_group.name,
'volume_type': '',
+ 'network': self.networks.first().id,
'count': 1}
url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data)
@@ -856,6 +878,7 @@ class InstanceTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.glance: ('image_list_detailed',),
+ api.quantum: ('network_list',),
api.nova: ('flavor_list',
'keypair_list',
'security_group_list',
@@ -885,6 +908,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
diff --git a/horizon/dashboards/nova/instances/workflows.py b/horizon/dashboards/nova/instances/workflows.py
index c7155d6b..eb04e64d 100644
--- a/horizon/dashboards/nova/instances/workflows.py
+++ b/horizon/dashboards/nova/instances/workflows.py
@@ -18,6 +18,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+
from django.utils.text import normalize_newlines
from django.utils.translation import ugettext as _
@@ -28,6 +30,9 @@ from horizon import workflows
from horizon.openstack.common import jsonutils
+LOG = logging.getLogger(__name__)
+
+
class SelectProjectUserAction(workflows.Action):
project_id = forms.ChoiceField(label=_("Project"))
user_id = forms.ChoiceField(label=_("User"))
@@ -400,6 +405,46 @@ class PostCreationStep(workflows.Step):
contributes = ("customization_script",)
+class SetNetworkAction(workflows.Action):
+ network = forms.MultipleChoiceField(label=_("Networks"),
+ required=True,
+ widget=forms.CheckboxSelectMultiple(),
+ help_text=_("Launch instance with"
+ "these networks"))
+
+ class Meta:
+ name = _("Networking")
+ permissions = ('openstack.services.network',)
+ help_text = _("Select networks for your instance.")
+
+ def populate_network_choices(self, request, context):
+ try:
+ networks = api.quantum.network_list(request)
+ for n in networks:
+ n.set_id_as_name_if_empty()
+ network_list = [(network.id, network.name) for network in networks]
+ except:
+ network_list = []
+ exceptions.handle(request,
+ _('Unable to retrieve networks.'))
+ return network_list
+
+
+class SetNetwork(workflows.Step):
+ action_class = SetNetworkAction
+ contributes = ("network_id",)
+
+ def contribute(self, data, context):
+ if data:
+ networks = self.workflow.request.POST.getlist("network")
+ # If no networks are explicitly specified, network list
+ # contains an empty string, so remove it.
+ networks = [n for n in networks if n != '']
+ if networks:
+ context['network_id'] = networks
+ return context
+
+
class LaunchInstance(workflows.Workflow):
slug = "launch_instance"
name = _("Launch Instance")
@@ -410,6 +455,7 @@ class LaunchInstance(workflows.Workflow):
default_steps = (SelectProjectUser,
SetInstanceDetails,
SetAccessControls,
+ SetNetwork,
VolumeOptions,
PostCreationStep)
@@ -437,6 +483,13 @@ class LaunchInstance(workflows.Workflow):
else:
dev_mapping = None
+ netids = context.get('network_id', None)
+ if netids:
+ nics = [{"net-id": netid, "v4-fixed-ip": ""}
+ for netid in netids]
+ else:
+ nics = None
+
try:
api.nova.server_create(request,
context['name'],
@@ -446,6 +499,7 @@ class LaunchInstance(workflows.Workflow):
normalize_newlines(custom_script),
context['security_group_ids'],
dev_mapping,
+ nics=nics,
instance_count=int(context['count']))
return True
except:
diff --git a/horizon/dashboards/nova/networks/__init__.py b/horizon/dashboards/nova/networks/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/nova/networks/__init__.py
diff --git a/horizon/dashboards/nova/networks/forms.py b/horizon/dashboards/nova/networks/forms.py
new file mode 100644
index 00000000..0ca47975
--- /dev/null
+++ b/horizon/dashboards/nova/networks/forms.py
@@ -0,0 +1,55 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+
+LOG = logging.getLogger(__name__)
+
+
+class UpdateNetwork(forms.SelfHandlingForm):
+ name = forms.CharField(label=_("Name"), required=False)
+ tenant_id = forms.CharField(widget=forms.HiddenInput)
+ network_id = forms.CharField(label=_("ID"),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ failure_url = 'horizon:nova:networks:index'
+
+ def handle(self, request, data):
+ try:
+ network = api.quantum.network_modify(request, data['network_id'],
+ name=data['name'])
+ msg = _('Network %s was successfully updated.') % data['name']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return network
+ except:
+ msg = _('Failed to update network %s') % data['name']
+ LOG.info(msg)
+ redirect = reverse(self.failure_url)
+ exceptions.handle(request, msg, redirect=redirect)
diff --git a/horizon/dashboards/nova/networks/panel.py b/horizon/dashboards/nova/networks/panel.py
new file mode 100644
index 00000000..e4270bec
--- /dev/null
+++ b/horizon/dashboards/nova/networks/panel.py
@@ -0,0 +1,28 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+from horizon.dashboards.nova import dashboard
+
+
+class Networks(horizon.Panel):
+ name = _("Networks")
+ slug = 'networks'
+ permissions = ('openstack.services.network',)
+
+dashboard.Nova.register(Networks)
diff --git a/horizon/dashboards/nova/networks/ports/__init__.py b/horizon/dashboards/nova/networks/ports/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/nova/networks/ports/__init__.py
diff --git a/horizon/dashboards/nova/networks/ports/tables.py b/horizon/dashboards/nova/networks/ports/tables.py
new file mode 100644
index 00000000..0483d15e
--- /dev/null
+++ b/horizon/dashboards/nova/networks/ports/tables.py
@@ -0,0 +1,53 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django import template
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+def get_fixed_ips(port):
+ template_name = 'nova/networks/ports/_port_ips.html'
+ context = {"ips": port.fixed_ips}
+ return template.loader.render_to_string(template_name, context)
+
+
+def get_attached(port):
+ return _('Attached') if port['device_id'] else _('Detached')
+
+
+class PortsTable(tables.DataTable):
+ name = tables.Column("name",
+ verbose_name=_("Name"),
+ link="horizon:nova:networks:ports:detail")
+ fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs"))
+ attached = tables.Column(get_attached, verbose_name=_("Device Attached"))
+ status = tables.Column("status", verbose_name=_("Status"))
+ admin_state = tables.Column("admin_state",
+ verbose_name=_("Admin State"))
+
+ def get_object_display(self, port):
+ return port.id
+
+ class Meta:
+ name = "ports"
+ verbose_name = _("Ports")
diff --git a/horizon/dashboards/nova/networks/ports/tabs.py b/horizon/dashboards/nova/networks/ports/tabs.py
new file mode 100644
index 00000000..f4ebf2f4
--- /dev/null
+++ b/horizon/dashboards/nova/networks/ports/tabs.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tabs
+
+import logging
+LOG = logging.getLogger(__name__)
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+ template_name = "nova/networks/ports/_detail_overview.html"
+
+ def get_context_data(self, request):
+ port_id = self.tab_group.kwargs['port_id']
+ try:
+ port = api.quantum.port_get(self.request, port_id)
+ except:
+ redirect = reverse('horizon:nova:networks:index')
+ msg = _('Unable to retrieve port details.')
+ exceptions.handle(request, msg, redirect=redirect)
+ return {'port': port}
+
+
+class PortDetailTabs(tabs.TabGroup):
+ slug = "port_details"
+ tabs = (OverviewTab,)
diff --git a/horizon/dashboards/nova/networks/ports/urls.py b/horizon/dashboards/nova/networks/ports/urls.py
new file mode 100644
index 00000000..67fab547
--- /dev/null
+++ b/horizon/dashboards/nova/networks/ports/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import DetailView
+
+PORTS = r'^(?P<port_id>[^/]+)/%s$'
+
+urlpatterns = patterns('horizon.dashboards.nova.networks.ports.views',
+ url(PORTS % 'detail', DetailView.as_view(), name='detail'))
diff --git a/horizon/dashboards/nova/networks/ports/views.py b/horizon/dashboards/nova/networks/ports/views.py
new file mode 100644
index 00000000..17fc4354
--- /dev/null
+++ b/horizon/dashboards/nova/networks/ports/views.py
@@ -0,0 +1,28 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from horizon import tabs
+from .tabs import PortDetailTabs
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = PortDetailTabs
+ template_name = 'nova/networks/ports/detail.html'
diff --git a/horizon/dashboards/nova/networks/subnets/__init__.py b/horizon/dashboards/nova/networks/subnets/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/__init__.py
diff --git a/horizon/dashboards/nova/networks/subnets/forms.py b/horizon/dashboards/nova/networks/subnets/forms.py
new file mode 100644
index 00000000..111db366
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/forms.py
@@ -0,0 +1,138 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+import netaddr
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import forms
+from horizon import messages
+from horizon import exceptions
+from horizon.utils import fields
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateSubnet(forms.SelfHandlingForm):
+ network_name = forms.CharField(label=_("Network Name"),
+ required=False,
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ network_id = forms.CharField(label=_("Network ID"),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ name = forms.CharField(max_length=255,
+ label=_("Name"),
+ required=False)
+ cidr = fields.IPField(label=_("Network Address"),
+ required=True,
+ initial="",
+ help_text=_("Network address in CIDR format "
+ "(e.g. 192.168.0.0/24)"),
+ version=fields.IPv4 | fields.IPv6,
+ mask=True)
+ ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')],
+ label=_("IP Version"))
+ gateway_ip = fields.IPField(label=_("Gateway IP"),
+ required=False,
+ initial="",
+ help_text=_("IP address of Gateway "
+ "(e.g. 192.168.0.1)"),
+ version=fields.IPv4 | fields.IPv6,
+ mask=False)
+ failure_url = 'horizon:nova:networks:detail'
+
+ def clean(self):
+ cleaned_data = super(CreateSubnet, self).clean()
+ cidr = cleaned_data.get('cidr')
+ ip_version = int(cleaned_data.get('ip_version'))
+ gateway_ip = cleaned_data.get('gateway_ip')
+ if cidr:
+ if netaddr.IPNetwork(cidr).version is not ip_version:
+ msg = _('Network Address and IP version are inconsistent.')
+ raise forms.ValidationError(msg)
+ if gateway_ip:
+ if netaddr.IPAddress(gateway_ip).version is not ip_version:
+ msg = _('Gateway IP and IP version are inconsistent.')
+ raise forms.ValidationError(msg)
+ return cleaned_data
+
+ def handle(self, request, data):
+ try:
+ LOG.debug('params = %s' % data)
+ data['ip_version'] = int(data['ip_version'])
+ if not data['gateway_ip']:
+ del data['gateway_ip']
+ subnet = api.quantum.subnet_create(request, **data)
+ msg = _('Subnet %s was successfully created.') % data['cidr']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return subnet
+ except Exception:
+ msg = _('Failed to create subnet %s') % data['cidr']
+ LOG.info(msg)
+ redirect = reverse(self.failure_url, args=[data['network_id']])
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class UpdateSubnet(forms.SelfHandlingForm):
+ network_id = forms.CharField(widget=forms.HiddenInput())
+ subnet_id = forms.CharField(widget=forms.HiddenInput())
+ cidr = forms.CharField(widget=forms.HiddenInput())
+ ip_version = forms.CharField(widget=forms.HiddenInput())
+ name = forms.CharField(max_length=255,
+ label=_("Name"),
+ required=False)
+ gateway_ip = fields.IPField(label=_("Gateway IP"),
+ required=True,
+ initial="",
+ help_text=_("IP address of Gateway "
+ "(e.g. 192.168.0.1)"),
+ version=fields.IPv4 | fields.IPv6,
+ mask=False)
+ failure_url = 'horizon:nova:networks:detail'
+
+ def clean(self):
+ cleaned_data = super(UpdateSubnet, self).clean()
+ ip_version = int(cleaned_data.get('ip_version'))
+ gateway_ip = cleaned_data.get('gateway_ip')
+ if gateway_ip:
+ if netaddr.IPAddress(gateway_ip).version is not ip_version:
+ msg = _('Gateway IP and IP version are inconsistent.')
+ raise forms.ValidationError(msg)
+ return cleaned_data
+
+ def handle(self, request, data):
+ try:
+ LOG.debug('params = %s' % data)
+ params = {'name': data['name']}
+ params['gateway_ip'] = data['gateway_ip']
+ subnet = api.quantum.subnet_modify(request, data['subnet_id'],
+ name=data['name'],
+ gateway_ip=data['gateway_ip'])
+ msg = _('Subnet %s was successfully updated.') % data['cidr']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return subnet
+ except Exception:
+ msg = _('Failed to update subnet %s') % data['cidr']
+ LOG.info(msg)
+ redirect = reverse(self.failure_url, args=[data['network_id']])
+ exceptions.handle(request, msg, redirect=redirect)
diff --git a/horizon/dashboards/nova/networks/subnets/tables.py b/horizon/dashboards/nova/networks/subnets/tables.py
new file mode 100644
index 00000000..a32e5eef
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/tables.py
@@ -0,0 +1,79 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteSubnet(tables.DeleteAction):
+ data_type_singular = _("Subnet")
+ data_type_plural = _("Subnets")
+
+ def delete(self, request, obj_id):
+ try:
+ api.quantum.subnet_delete(request, obj_id)
+ except:
+ msg = _('Failed to delete subnet %s') % obj_id
+ LOG.info(msg)
+ network_id = self.table.kwargs['network_id']
+ redirect = reverse('horizon:nova:networks:detail',
+ args=[network_id])
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class CreateSubnet(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Subnet")
+ url = "horizon:nova:networks:addsubnet"
+ classes = ("ajax-modal", "btn-create")
+
+ def get_link_url(self, datum=None):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id,))
+
+
+class UpdateSubnet(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit Subnet")
+ url = "horizon:nova:networks:editsubnet"
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, subnet):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id, subnet.id))
+
+
+class SubnetsTable(tables.DataTable):
+ name = tables.Column("name", verbose_name=_("Name"),
+ link='horizon:nova:networks:subnets:detail')
+ cidr = tables.Column("cidr", verbose_name=_("Network Address"))
+ ip_version = tables.Column("ipver_str", verbose_name=_("IP Version"))
+ gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP"))
+
+ class Meta:
+ name = "subnets"
+ verbose_name = _("Subnets")
+ table_actions = (CreateSubnet, DeleteSubnet)
+ row_actions = (UpdateSubnet, DeleteSubnet)
diff --git a/horizon/dashboards/nova/networks/subnets/tabs.py b/horizon/dashboards/nova/networks/subnets/tabs.py
new file mode 100644
index 00000000..c832fba6
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/tabs.py
@@ -0,0 +1,48 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tabs
+
+
+LOG = logging.getLogger(__name__)
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+ template_name = "nova/networks/subnets/_detail_overview.html"
+
+ def get_context_data(self, request):
+ subnet_id = self.tab_group.kwargs['subnet_id']
+ try:
+ subnet = api.quantum.subnet_get(self.request, subnet_id)
+ except:
+ redirect = reverse('horizon:nova:networks:index')
+ msg = _('Unable to retrieve subnet details.')
+ exceptions.handle(request, msg, redirect=redirect)
+ return {'subnet': subnet}
+
+
+class SubnetDetailTabs(tabs.TabGroup):
+ slug = "subnet_details"
+ tabs = (OverviewTab,)
diff --git a/horizon/dashboards/nova/networks/subnets/urls.py b/horizon/dashboards/nova/networks/subnets/urls.py
new file mode 100644
index 00000000..9e4523f2
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from .views import DetailView
+
+SUBNETS = r'^(?P<subnet_id>[^/]+)/%s$'
+
+urlpatterns = patterns('horizon.dashboards.nova.networks.subnets.views',
+ url(SUBNETS % 'detail', DetailView.as_view(), name='detail'))
diff --git a/horizon/dashboards/nova/networks/subnets/views.py b/horizon/dashboards/nova/networks/subnets/views.py
new file mode 100644
index 00000000..d773beca
--- /dev/null
+++ b/horizon/dashboards/nova/networks/subnets/views.py
@@ -0,0 +1,109 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# 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.
+
+"""
+Views for managing Quantum Subnets.
+"""
+import logging
+
+from django.core.urlresolvers import reverse_lazy, reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import forms
+from horizon import exceptions
+from horizon import api
+from horizon import tabs
+from .forms import CreateSubnet, UpdateSubnet
+from .tabs import SubnetDetailTabs
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateSubnet
+ template_name = 'nova/networks/subnets/create.html'
+ success_url = 'horizon:nova:networks:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['network_id'],))
+
+ def get_object(self):
+ if not hasattr(self, "_object"):
+ try:
+ network_id = self.kwargs["network_id"]
+ self._object = api.quantum.network_get(self.request,
+ network_id)
+ except:
+ redirect = reverse('horizon:nova:networks:index')
+ msg = _("Unable to retrieve network.")
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(CreateView, self).get_context_data(**kwargs)
+ context['network'] = self.get_object()
+ return context
+
+ def get_initial(self):
+ network = self.get_object()
+ return {"network_id": self.kwargs['network_id'],
+ "network_name": network.name}
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdateSubnet
+ template_name = 'nova/networks/subnets/update.html'
+ context_object_name = 'subnet'
+ success_url = reverse_lazy('horizon:nova:networks:detail')
+
+ def get_success_url(self):
+ return reverse('horizon:nova:networks:detail',
+ args=(self.kwargs['network_id'],))
+
+ def _get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ subnet_id = self.kwargs['subnet_id']
+ try:
+ self._object = api.quantum.subnet_get(self.request, subnet_id)
+ except:
+ redirect = reverse("horizon:nova:networks:index")
+ msg = _('Unable to retrieve subnet details')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ subnet = self._get_object()
+ context['subnet_id'] = subnet.id
+ context['network_id'] = subnet.network_id
+ context['cidr'] = subnet.cidr
+ context['ip_version'] = subnet.ipver_str
+ return context
+
+ def get_initial(self):
+ subnet = self._get_object()
+ return {'network_id': self.kwargs['network_id'],
+ 'subnet_id': subnet['id'],
+ 'cidr': subnet['cidr'],
+ 'ip_version': subnet['ip_version'],
+ 'name': subnet['name'],
+ 'gateway_ip': subnet['gateway_ip']}
+
+
+class DetailView(tabs.TabView):
+ tab_group_class = SubnetDetailTabs
+ template_name = 'nova/networks/subnets/detail.html'
diff --git a/horizon/dashboards/nova/networks/tables.py b/horizon/dashboards/nova/networks/tables.py
new file mode 100644
index 00000000..d94ca076
--- /dev/null
+++ b/horizon/dashboards/nova/networks/tables.py
@@ -0,0 +1,94 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import logging
+
+from django import template
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteNetwork(tables.DeleteAction):
+ data_type_singular = _("Network")
+ data_type_plural = _("Networks")
+
+ def delete(self, request, network_id):
+ try:
+ # Retrieve existing subnets belonging to the network.
+ subnets = api.quantum.subnet_list(request, network_id=network_id)
+ LOG.debug('Network %s has subnets: %s' %
+ (network_id, [s.id for s in subnets]))
+ for s in subnets:
+ api.quantum.subnet_delete(request, s.id)
+ LOG.debug('Deleted subnet %s' % s.id)
+
+ api.quantum.network_delete(request, network_id)
+ LOG.debug('Deleted network %s successfully' % network_id)
+ except:
+ msg = _('Failed to delete network %s') % network_id
+ LOG.info(msg)
+ redirect = reverse("horizon:nova:networks:index")
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class CreateNetwork(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Network")
+ url = "horizon:nova:networks:create"
+ classes = ("ajax-modal", "btn-create")
+
+
+class EditNetwork(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit Network")
+ url = "horizon:nova:networks:update"
+ classes = ("ajax-modal", "btn-edit")
+
+
+class CreateSubnet(tables.LinkAction):
+ name = "subnet"
+ verbose_name = _("Add Subnet")
+ url = "horizon:nova:networks:addsubnet"
+ classes = ("ajax-modal", "btn-create")
+
+
+def get_subnets(network):
+ template_name = 'nova/networks/_network_ips.html'
+ context = {"subnets": network.subnets}
+ return template.loader.render_to_string(template_name, context)
+
+
+class NetworksTable(tables.DataTable):
+ name = tables.Column("name",
+ verbose_name=_("Name"),
+ link='horizon:nova:networks:detail')
+ subnets = tables.Column(get_subnets,
+ verbose_name=_("Subnets Associated"),)
+ status = tables.Column("status", verbose_name=_("Status"))
+ admin_state = tables.Column("admin_state",
+ verbose_name=_("Admin State"))
+
+ class Meta:
+ name = "networks"
+ verbose_name = _("Networks")
+ table_actions = (CreateNetwork, DeleteNetwork)
+ row_actions = (EditNetwork, CreateSubnet, DeleteNetwork)
diff --git a/horizon/dashboards/nova/networks/templates/networks/_create.html b/horizon/dashboards/nova/networks/templates/networks/_create.html
new file mode 100644
index 00000000..664a6b99
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/_create.html
@@ -0,0 +1,24 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_network_form{% endblock %}
+{% block form_action %}{% url horizon:nova:networks:create %}{% endblock %}
+
+{% block modal-header %}{% trans "Create Network" %}{% 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 "Select a name for your network."%}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Network" %}" />
+ <a href="{% url horizon:nova:networks:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html
new file mode 100644
index 00000000..a1d3d7ff
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/_detail_overview.html
@@ -0,0 +1,18 @@
+{% load i18n sizeformat %}
+
+<h3>{% trans "Network Overview" %}</h3>
+
+<div class="info detail">
+ <dl>
+ <dt>{% trans "Name" %}</dt>
+ <dd>{{ network.name|default:"None" }}</dd>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ network.id|default:"None" }}</dd>
+ <dt>{% trans "Project ID" %}</dt>
+ <dd>{{ network.tenant_id|default:"-" }}</dd>
+ <dt>{% trans "Status" %}</dt>
+ <dd>{{ network.status|default:"Unknown" }}</dd>
+ <dt>{% trans "Admin State" %}</dt>
+ <dd>{{ network.admin_state|default:"Unknown" }}</dd>
+ </dl>
+</div>
diff --git a/horizon/dashboards/nova/networks/templates/networks/_network_ips.html b/horizon/dashboards/nova/networks/templates/networks/_network_ips.html
new file mode 100644
index 00000000..a80e7479
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/_network_ips.html
@@ -0,0 +1,10 @@
+{% for subnet in subnets %}
+<ul>
+ <li>
+ {% if subnet.name|length > 0 %}
+ <b>{{ subnet.name }}</b>
+ {% endif %}
+ {{ subnet.cidr }}
+ </li>
+</ul>
+{% endfor %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/_update.html b/horizon/dashboards/nova/networks/templates/networks/_update.html
new file mode 100644
index 00000000..50413cfe
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/_update.html
@@ -0,0 +1,24 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}update_network_form{% endblock %}
+{% block form_action %}{% url horizon:nova:networks:update network_id %}{% endblock %}
+
+{% block modal-header %}{% trans "Edit Network" %}{% 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 may update the editable properties of your network here." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
+ <a href="{% url horizon:nova:networks:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/create.html b/horizon/dashboards/nova/networks/templates/networks/create.html
new file mode 100644
index 00000000..5a9d7da3
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Network" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Network") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "horizon/common/_workflow.html" %}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/detail.html b/horizon/dashboards/nova/networks/templates/networks/detail.html
new file mode 100644
index 00000000..3a8ac2d8
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/detail.html
@@ -0,0 +1,18 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Network Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Network Detail: ")|add:network.name %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "nova/networks/_detail_overview.html" %}
+ <hr>
+ <div id="subnets">
+ {{ subnets_table.render }}
+ </div>
+ <div id="ports">
+ {{ ports_table.render }}
+ </div>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/index.html b/horizon/dashboards/nova/networks/templates/networks/index.html
new file mode 100644
index 00000000..d458220a
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/index.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Networks" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Networks") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html
new file mode 100644
index 00000000..401a5db6
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/ports/_detail_overview.html
@@ -0,0 +1,41 @@
+{% load i18n sizeformat %}
+
+<h3>{% trans "Port Overview" %}</h3>
+
+<div class="info row-fluid detail">
+ <h4>{% trans "Port" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Name" %}</dt>
+ <dd>{{ port.name|default:"None" }}</dd>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ port.id|default:"None" }}</dd>
+ <dt>{% trans "Network ID" %}</dt>
+ <dd>{{ port.network_id|default:"None" }}</dd>
+ <dt>{% trans "Project ID" %}</dt>
+ <dd>{{ port.tenant_id|default:"-" }}</dd>
+ <dt>{% trans "Fixed IP" %}</dt>
+ <dd>
+ {% if port.fixed_ips.items|length > 1 %}
+ {% for ip in port.fixed_ips %}
+ <b>{% trans "IP address:" %}</b> {{ ip.ip_address }},
+ <b>{% trans "Subnet ID" %}</b> {{ ip.subnet_id }}<br>
+ {% endfor %}
+ {% else %}
+ "None"
+ {% endif %}
+ </dd>
+ <dt>{% trans "Mac Address" %}</dt>
+ <dd>{{ port.mac_address|default:"None" }}</dd>
+ <dt>{% trans "Status" %}</dt>
+ <dd>{{ port.status|default:"None" }}</dd>
+ <dt>{% trans "Admin State" %}</dt>
+ <dd>{{ port.admin_state|default:"None" }}</dd>
+ <dt>{% trans "Device ID" %}</dt>
+ {% if port.device_id|length > 1 %}
+ <dd>{{ port.device_id }}</dd>
+ {% else %}
+ <dd>No attached device</dd>
+ {% endif %}
+ </dl>
+</div>
diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html b/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html
new file mode 100644
index 00000000..bfd5ea9f
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/ports/_port_ips.html
@@ -0,0 +1,7 @@
+{% for ip in ips %}
+<ul>
+ <li>
+ {{ ip.ip_address }}
+ </li>
+</ul>
+{% endfor %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/ports/detail.html b/horizon/dashboards/nova/networks/templates/networks/ports/detail.html
new file mode 100644
index 00000000..634c6d67
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/ports/detail.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Port Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Port Detail") %}
+{% endblock page_header %}
+
+{% block main %}
+<div id="row-fluid">
+ <div class="span12">
+ {{ tab_group.render }}
+ </div>
+</div>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html
new file mode 100644
index 00000000..942ece06
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_create.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_subnet_form{% endblock %}
+{% block form_action %}{% url horizon:nova:networks:addsubnet network.id %}
+{% endblock %}
+
+{% block modal-header %}{% trans "Create Subnet" %}{% 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 subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Subnet" %}" />
+ <a href="{% url horizon:nova:networks:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html
new file mode 100644
index 00000000..4c09fde0
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_detail_overview.html
@@ -0,0 +1,29 @@
+{% load i18n sizeformat %}
+
+<h3>{% trans "Subnet Overview" %}</h3>
+
+<div class="info row-fluid detail">
+ <h4>{% trans "Subnet" %}</h4>
+ <hr class="header_rule">
+ <dl>
+ <dt>{% trans "Name" %}</dt>
+ <dd>{{ subnet.name|default:"None" }}</dd>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ subnet.id|default:"None" }}</dd>
+ <dt>{% trans "Network ID" %}</dt>
+ <dd>{{ subnet.network_id|default:"None" }}</dd>
+ <dt>{% trans "CIDR" %}</dt>
+ <dd>{{ subnet.cidr|default:"None" }}</dd>
+ <dt>{% trans "IP version" %}</dt>
+ <dd>{{ subnet.ipver_str|default:"-" }}</dd>
+ <dt>{% trans "Gateway IP" %}</dt>
+ <dd>{{ subnet.gateway_ip|default:"-" }}</dd>
+ <dt>{% trans "IP allocation pool" %}</dt>
+ <dd>
+ {% for pool in subnet.allocation_pools %}
+ {% trans "Start" %} {{ pool.start }}
+ {% trans " - End" %} {{ pool.end }}<br>
+ {% endfor %}
+ </dd>
+ </dl>
+</div>
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html b/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html
new file mode 100644
index 00000000..4093b06c
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/_update.html
@@ -0,0 +1,33 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}update_subnet_form{% endblock %}
+{% block form_action %}{% url horizon:nova:networks:editsubnet network_id subnet_id %}{% endblock %}
+
+{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <dl>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ subnet_id }}</dd>
+ <dt>{% trans "Network Address" %}</dt>
+ <dd>{{ cidr }}</dd>
+ <dt>{% trans "IP version" %}</dt>
+ <dd>{{ ip_version }}</dd>
+ </dl>
+ <hr>
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description:" %}</h3>
+ <p>{% trans "You may update the editable properties of your subnet here." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
+ <a href="{% url horizon:nova:networks:detail network_id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/create.html b/horizon/dashboards/nova/networks/templates/networks/subnets/create.html
new file mode 100644
index 00000000..01e052c2
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Subnet" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Subnet") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "nova/networks/subnets/_create.html" %}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html b/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html
new file mode 100644
index 00000000..c4e35bd0
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/detail.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Subnet Detail"%}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Subnet Detail") %}
+{% endblock page_header %}
+
+{% block main %}
+<div id="row-fluid">
+ <div class="span12">
+ {{ tab_group.render }}
+ </div>
+</div>
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/index.html b/horizon/dashboards/nova/networks/templates/networks/subnets/index.html
new file mode 100644
index 00000000..833399a2
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/index.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Network" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Network") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/subnets/update.html b/horizon/dashboards/nova/networks/templates/networks/subnets/update.html
new file mode 100644
index 00000000..d5b81372
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/subnets/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Subnet" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Subnet") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'nova/networks/subnets/_update.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/templates/networks/update.html b/horizon/dashboards/nova/networks/templates/networks/update.html
new file mode 100644
index 00000000..599de61a
--- /dev/null
+++ b/horizon/dashboards/nova/networks/templates/networks/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Network" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Network") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'nova/networks/_update.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/nova/networks/tests.py b/horizon/dashboards/nova/networks/tests.py
new file mode 100644
index 00000000..da15e9b2
--- /dev/null
+++ b/horizon/dashboards/nova/networks/tests.py
@@ -0,0 +1,753 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# 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 import http
+from django.core.urlresolvers import reverse
+from django.utils.http import urlencode
+from django.utils.html import escape
+from django.utils.datastructures import SortedDict
+from mox import IsA, IgnoreArg
+from copy import deepcopy
+
+from horizon import api
+from horizon import test
+
+from .workflows import CreateNetwork
+
+
+INDEX_URL = reverse('horizon:nova:networks:index')
+
+
+class NetworkTests(test.TestCase):
+ @test.create_stubs({api.quantum: ('network_list',)})
+ def test_index(self):
+ api.quantum.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id) \
+ .AndReturn(self.networks.list())
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+
+ self.assertTemplateUsed(res, 'nova/networks/index.html')
+ networks = res.context['networks_table'].data
+ self.assertItemsEqual(networks, self.networks.list())
+
+ @test.create_stubs({api.quantum: ('network_list',)})
+ def test_index_network_list_exception(self):
+ api.quantum.network_list(IsA(http.HttpRequest),
+ tenant_id=self.tenant.id) \
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+
+ self.assertTemplateUsed(res, 'nova/networks/index.html')
+ self.assertEqual(len(res.context['networks_table'].data), 0)
+ self.assertMessageCount(res, error=1)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertItemsEqual(subnets, [self.subnets.first()])
+ self.assertItemsEqual(ports, [self.ports.first()])
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_network_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova:networks:detail', args=[network_id])
+ res = self.client.get(url)
+
+ redir_url = INDEX_URL
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_subnet_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id).\
+ AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndRaise(self.exceptions.quantum)
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertEqual(len(subnets), 0)
+ self.assertItemsEqual(ports, [self.ports.first()])
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_port_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id).\
+ AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertItemsEqual(subnets, [self.subnets.first()])
+ self.assertEqual(len(ports), 0)
+
+ def test_network_create_get(self):
+ # no api methods are called.
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.get(url)
+
+ workflow = res.context['workflow']
+ self.assertTemplateUsed(res, 'nova/networks/create.html')
+ self.assertEqual(workflow.name, CreateNetwork.name)
+ expected_objs = ['<CreateNetworkInfo: createnetworkinfoaction>',
+ '<CreateSubnetInfo: createsubnetinfoaction>']
+ self.assertQuerysetEqual(workflow.steps, expected_objs)
+
+ @test.create_stubs({api.quantum: ('network_create',)})
+ def test_network_create_post(self):
+ network = self.networks.first()
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': False,
+ 'subnet_name': '',
+ 'cidr': '',
+ 'ip_version': 4,
+ 'gateway_ip': ''}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_create',
+ 'subnet_create',)})
+ def test_network_create_post_with_subnet(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\
+ .AndReturn(network)
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_create',)})
+ def test_network_create_post_network_exception(self):
+ network = self.networks.first()
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': False,
+ 'subnet_name': '',
+ 'cidr': '',
+ 'ip_version': 4,
+ 'gateway_ip': ''}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_create',)})
+ def test_network_create_post_with_subnet_network_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_create',
+ 'subnet_create',)})
+ def test_network_create_post_with_subnet_subnet_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name)\
+ .AndReturn(network)
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ def test_network_create_post_with_subnet_nocidr(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ self.mox.ReplayAll()
+
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': '',
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertContains(res, escape('Specify "Network Address" or '
+ 'clear "Create Subnet" checkbox.'))
+
+ def test_network_create_post_with_subnet_cidr_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60'
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ expected_msg = 'Network Address and IP version are inconsistent.'
+ self.assertContains(res, expected_msg)
+
+ def test_network_create_post_with_subnet_gw_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF'
+ form_data = {'net_name': network.name,
+ 'with_subnet': True,
+ 'subnet_name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': gateway_ip}
+ url = reverse('horizon:nova:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertContains(res, 'Gateway IP and IP version are inconsistent.')
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_network_update_get(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova:networks:update', args=[network.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'nova/networks/update.html')
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_network_update_get_exception(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova:networks:update', args=[network.id])
+ res = self.client.get(url)
+
+ redir_url = INDEX_URL
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_modify',
+ 'network_get',)})
+ def test_network_update_post(self):
+ network = self.networks.first()
+ api.quantum.network_modify(IsA(http.HttpRequest), network.id,
+ name=network.name)\
+ .AndReturn(network)
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ formData = {'network_id': network.id,
+ 'name': network.name,
+ 'tenant_id': network.tenant_id}
+ url = reverse('horizon:nova:networks:update', args=[network.id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_modify',
+ 'network_get',)})
+ def test_network_update_post_exception(self):
+ network = self.networks.first()
+ api.quantum.network_modify(IsA(http.HttpRequest), network.id,
+ name=network.name)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': network.id,
+ 'name': network.name,
+ 'tenant_id': network.tenant_id}
+ url = reverse('horizon:nova:networks:update', args=[network.id])
+ res = self.client.post(url, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_list',
+ 'subnet_list',
+ 'network_delete')})
+ def test_delete_network_no_subnet(self):
+ network = self.networks.first()
+ api.quantum.network_list(IsA(http.HttpRequest),
+ tenant_id=network.tenant_id)\
+ .AndReturn([network])
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
+ .AndReturn([])
+ api.quantum.network_delete(IsA(http.HttpRequest), network.id)
+
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'networks__delete__%s' % network.id}
+ res = self.client.post(INDEX_URL, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_list',
+ 'subnet_list',
+ 'network_delete',
+ 'subnet_delete')})
+ def test_delete_network_with_subnet(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_list(IsA(http.HttpRequest),
+ tenant_id=network.tenant_id)\
+ .AndReturn([network])
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
+ .AndReturn([subnet])
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
+ api.quantum.network_delete(IsA(http.HttpRequest), network.id)
+
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'networks__delete__%s' % network.id}
+ res = self.client.post(INDEX_URL, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_list',
+ 'subnet_list',
+ 'network_delete',
+ 'subnet_delete')})
+ def test_delete_network_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_list(IsA(http.HttpRequest),
+ tenant_id=network.tenant_id)\
+ .AndReturn([network])
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
+ .AndReturn([subnet])
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
+ api.quantum.network_delete(IsA(http.HttpRequest), network.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'networks__delete__%s' % network.id}
+ res = self.client.post(INDEX_URL, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('subnet_get',)})
+ def test_subnet_detail(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(self.subnets.first())
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:subnets:detail',
+ args=[subnet.id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/subnets/detail.html')
+ self.assertEqual(res.context['subnet'].id, subnet.id)
+
+ @test.create_stubs({api.quantum: ('subnet_get',)})
+ def test_subnet_detail_exception(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:subnets:detail',
+ args=[subnet.id]))
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_get(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[network.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'nova/networks/subnets/create.html')
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ network_name=network.name,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ redir_url = reverse('horizon:nova:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post_network_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post_subnet_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ network_name=network.name,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ redir_url = reverse('horizon:nova:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_post_cidr_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60'
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ expected_msg = 'Network Address and IP version are inconsistent.'
+ self.assertContains(res, expected_msg)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_post_gw_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF'
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': gateway_ip}
+ url = reverse('horizon:nova:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertContains(res, 'Gateway IP and IP version are inconsistent.')
+
+ @test.create_stubs({api.quantum: ('subnet_modify',
+ 'subnet_get',)})
+ def test_subnet_update_post(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(subnet)
+ api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id,
+ name=subnet.name,
+ gateway_ip=subnet.gateway_ip)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ formData = {'network_id': subnet.network_id,
+ 'subnet_id': subnet.id,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:nova:networks:editsubnet',
+ args=[subnet.network_id, subnet.id])
+ res = self.client.post(url, formData)
+
+ redir_url = reverse('horizon:nova:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('subnet_modify',
+ 'subnet_get',)})
+ def test_subnet_update_post_gw_inconsistent(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF'
+ formData = {'network_id': subnet.network_id,
+ 'subnet_id': subnet.id,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': gateway_ip}
+ url = reverse('horizon:nova:networks:editsubnet',
+ args=[subnet.network_id, subnet.id])
+ res = self.client.post(url, formData)
+
+ self.assertContains(res, 'Gateway IP and IP version are inconsistent.')
+
+ @test.create_stubs({api.quantum: ('subnet_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_subnet_delete(self):
+ subnet = self.subnets.first()
+ network_id = subnet.network_id
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'subnets__delete__%s' % subnet.id}
+ url = reverse('horizon:nova:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
+
+ @test.create_stubs({api.quantum: ('subnet_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_subnet_delete_exception(self):
+ subnet = self.subnets.first()
+ network_id = subnet.network_id
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'subnets__delete__%s' % subnet.id}
+ url = reverse('horizon:nova:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
+
+ @test.create_stubs({api.quantum: ('port_get',)})
+ def test_port_detail(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndReturn(self.ports.first())
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:ports:detail',
+ args=[port.id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/ports/detail.html')
+ self.assertEqual(res.context['port'].id, port.id)
+
+ @test.create_stubs({api.quantum: ('port_get',)})
+ def test_port_detail_exception(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:nova:networks:ports:detail',
+ args=[port.id]))
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
diff --git a/horizon/dashboards/nova/networks/urls.py b/horizon/dashboards/nova/networks/urls.py
new file mode 100644
index 00000000..f5fb60e9
--- /dev/null
+++ b/horizon/dashboards/nova/networks/urls.py
@@ -0,0 +1,37 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url, include
+
+from .views import IndexView, CreateView, DetailView, UpdateView
+from .subnets.views import CreateView as AddSubnetView
+from .subnets.views import UpdateView as EditSubnetView
+from .subnets import urls as subnet_urls
+from .ports import urls as port_urls
+
+NETWORKS = r'^(?P<network_id>[^/]+)/%s$'
+
+urlpatterns = patterns('',
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'^create$', CreateView.as_view(), name='create'),
+ url(NETWORKS % 'detail', DetailView.as_view(), name='detail'),
+ url(NETWORKS % 'update', UpdateView.as_view(), name='update'),
+ url(NETWORKS % 'subnets/create', AddSubnetView.as_view(),
+ name='addsubnet'),
+ url(r'^(?P<network_id>[^/]+)/subnets/(?P<subnet_id>[^/]+)/update$',
+ EditSubnetView.as_view(), name='editsubnet'),
+ url(r'^subnets/', include(subnet_urls, namespace='subnets')),
+ url(r'^ports/', include(port_urls, namespace='ports')))
diff --git a/horizon/dashboards/nova/networks/views.py b/horizon/dashboards/nova/networks/views.py
new file mode 100644
index 00000000..a6b02a94
--- /dev/null
+++ b/horizon/dashboards/nova/networks/views.py
@@ -0,0 +1,146 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# 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.
+
+"""
+Views for managing Quantum Networks.
+"""
+import logging
+
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+from horizon import workflows
+
+from .tables import NetworksTable
+from .subnets.tables import SubnetsTable
+from .ports.tables import PortsTable
+from .forms import UpdateNetwork
+from .workflows import CreateNetwork
+
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tables.DataTableView):
+ table_class = NetworksTable
+ template_name = 'nova/networks/index.html'
+
+ def get_data(self):
+ try:
+ # If a user has admin role, network list returned by Quantum API
+ # contains networks that does not belong to that tenant.
+ # So we need to specify tenant_id when calling network_list().
+ tenant_id = self.request.user.tenant_id
+ networks = api.quantum.network_list(self.request,
+ tenant_id=tenant_id)
+ except:
+ networks = []
+ msg = _('Network list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ for n in networks:
+ n.set_id_as_name_if_empty()
+ return networks
+
+
+class CreateView(workflows.WorkflowView):
+ workflow_class = CreateNetwork
+ template_name = 'nova/networks/create.html'
+
+ def get_initial(self):
+ pass
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdateNetwork
+ template_name = 'nova/networks/update.html'
+ context_object_name = 'network'
+ success_url = reverse_lazy("horizon:nova:networks:index")
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ context["network_id"] = self.kwargs['network_id']
+ return context
+
+ def _get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ network_id = self.kwargs['network_id']
+ try:
+ self._object = api.quantum.network_get(self.request,
+ network_id)
+ except:
+ redirect = self.success_url
+ msg = _('Unable to retrieve network details.')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_initial(self):
+ network = self._get_object()
+ return {'network_id': network['id'],
+ 'tenant_id': network['tenant_id'],
+ 'name': network['name']}
+
+
+class DetailView(tables.MultiTableView):
+ table_classes = (SubnetsTable, PortsTable)
+ template_name = 'nova/networks/detail.html'
+ failure_url = reverse_lazy('horizon:nova:networks:index')
+
+ def get_subnets_data(self):
+ try:
+ network_id = self.kwargs['network_id']
+ subnets = api.quantum.subnet_list(self.request,
+ network_id=network_id)
+ except:
+ subnets = []
+ msg = _('Subnet list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ for s in subnets:
+ s.set_id_as_name_if_empty()
+ return subnets
+
+ def get_ports_data(self):
+ try:
+ network_id = self.kwargs['network_id']
+ ports = api.quantum.port_list(self.request, network_id=network_id)
+ except:
+ ports = []
+ msg = _('Port list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ for p in ports:
+ p.set_id_as_name_if_empty()
+ return ports
+
+ def _get_data(self):
+ if not hasattr(self, "_network"):
+ try:
+ network_id = self.kwargs['network_id']
+ network = api.quantum.network_get(self.request, network_id)
+ network.set_id_as_name_if_empty(length=0)
+ except:
+ msg = _('Unable to retrieve details for network "%s".') \
+ % (network_id)
+ exceptions.handle(self.request, msg, redirect=self.failure_url)
+ self._network = network
+ return self._network
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["network"] = self._get_data()
+ return context
diff --git a/horizon/dashboards/nova/networks/workflows.py b/horizon/dashboards/nova/networks/workflows.py
new file mode 100644
index 00000000..1837694b
--- /dev/null
+++ b/horizon/dashboards/nova/networks/workflows.py
@@ -0,0 +1,162 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import logging
+import netaddr
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import workflows
+from horizon.utils import fields
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateNetworkInfoAction(workflows.Action):
+ net_name = forms.CharField(max_length=255,
+ label=_("Network Name (optional)"),
+ required=False)
+
+ class Meta:
+ name = ("Network")
+ help_text = _("From here you can create a new network.\n"
+ "In addition a subnet associated with the network "
+ "can be created in the next panel.")
+
+
+class CreateNetworkInfo(workflows.Step):
+ action_class = CreateNetworkInfoAction
+ contributes = ("net_name",)
+
+
+class CreateSubnetInfoAction(workflows.Action):
+ with_subnet = forms.BooleanField(label=_("Create Subnet"),
+ initial=True, required=False)
+ subnet_name = forms.CharField(max_length=255,
+ label=_("Subnet Name (optional)"),
+ required=False)
+ cidr = fields.IPField(label=_("Network Address"),
+ required=False,
+ initial="",
+ help_text=_("Network address in CIDR format "
+ "(e.g. 192.168.0.0/24)"),
+ version=fields.IPv4 | fields.IPv6,
+ mask=True)
+ ip_version = forms.ChoiceField(choices=[(4, 'IPv4'), (6, 'IPv6')],
+ label=_("IP Version"))
+ gateway_ip = fields.IPField(label=_("Gateway IP (optional)"),
+ required=False,
+ initial="",
+ help_text=_("IP address of Gateway "
+ "(e.g. 192.168.0.1)"),
+ version=fields.IPv4 | fields.IPv6,
+ mask=False)
+
+ class Meta:
+ name = ("Subnet")
+ help_text = _("You can create a subnet associated with the new "
+ "network. \"Network Address\" must be specified. "
+ "\n\n"
+ "If you are creating a network WITHOUT a subnet, "
+ "clear \"Create Subnet\" checkbox.")
+
+ def clean(self):
+ cleaned_data = super(CreateSubnetInfoAction, self).clean()
+ with_subnet = cleaned_data.get('with_subnet')
+ cidr = cleaned_data.get('cidr')
+ ip_version = int(cleaned_data.get('ip_version'))
+ gateway_ip = cleaned_data.get('gateway_ip')
+ if with_subnet and not cidr:
+ msg = _('Specify "Network Address" or '
+ 'clear "Create Subnet" checkbox.')
+ raise forms.ValidationError(msg)
+ if cidr:
+ if netaddr.IPNetwork(cidr).version is not ip_version:
+ msg = _('Network Address and IP version are inconsistent.')
+ raise forms.ValidationError(msg)
+ if gateway_ip:
+ if netaddr.IPAddress(gateway_ip).version is not ip_version:
+ msg = _('Gateway IP and IP version are inconsistent.')
+ raise forms.ValidationError(msg)
+ return cleaned_data
+
+
+class CreateSubnetInfo(workflows.Step):
+ action_class = CreateSubnetInfoAction
+ contributes = ("with_subnet", "subnet_name", "cidr",
+ "ip_version", "gateway_ip")
+
+
+class CreateNetwork(workflows.Workflow):
+ slug = "create_network"
+ name = _("Create Network")
+ finalize_button_name = _("Create")
+ success_message = _('Created new network "%s".')
+ failure_message = _('Unable to create network "%s".')
+ success_url = "horizon:nova:networks:index"
+ default_steps = (CreateNetworkInfo,
+ CreateSubnetInfo)
+
+ def format_status_message(self, message):
+ name = self.context.get('net_name') or self.context.get('net_id', '')
+ return message % name
+
+ def handle(self, request, data):
+ # create the network
+ try:
+ network = api.quantum.network_create(request,
+ name=data['net_name'])
+ network.set_id_as_name_if_empty()
+ self.context['net_id'] = network.id
+ msg = _('Network %s was successfully created.') % network.name
+ LOG.debug(msg)
+ except:
+ msg = _('Failed to create network %s') % data['net_name']
+ LOG.info(msg)
+ redirect = reverse('horizon:nova:networks:index')
+ exceptions.handle(request, msg, redirect=redirect)
+ return False
+
+ # If we do not need to create a subnet, return here.
+ if not data['with_subnet']:
+ return True
+
+ # create the subnet
+ try:
+ params = {'network_id': network.id,
+ 'name': data['subnet_name'],
+ 'cidr': data['cidr'],
+ 'ip_version': int(data['ip_version'])}
+ if data['gateway_ip']:
+ params['gateway_ip'] = data['gateway_ip']
+ api.quantum.subnet_create(request, **params)
+ msg = _('Subnet %s was successfully created.') % data['cidr']
+ LOG.debug(msg)
+ except Exception:
+ msg = _('Failed to create subnet %s for network %s') % \
+ (data['cidr'], network.id)
+ LOG.info(msg)
+ redirect = reverse('horizon:nova:networks:index')
+ exceptions.handle(request, msg, redirect=redirect)
+ return False
+
+ return True
diff --git a/horizon/dashboards/syspanel/dashboard.py b/horizon/dashboards/syspanel/dashboard.py
index bc2f10c9..a25c9fca 100644
--- a/horizon/dashboards/syspanel/dashboard.py
+++ b/horizon/dashboards/syspanel/dashboard.py
@@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup):
slug = "syspanel"
name = _("System Panel")
panels = ('overview', 'instances', 'volumes', 'services', 'flavors',
- 'images', 'projects', 'users', 'quotas',)
+ 'images', 'projects', 'users', 'quotas', 'networks',)
class Syspanel(horizon.Dashboard):
diff --git a/horizon/dashboards/syspanel/instances/tests.py b/horizon/dashboards/syspanel/instances/tests.py
index f0323707..e5db7598 100644
--- a/horizon/dashboards/syspanel/instances/tests.py
+++ b/horizon/dashboards/syspanel/instances/tests.py
@@ -143,6 +143,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'security_group_list', 'volume_list',
'volume_snapshot_list',
'tenant_quota_usages', 'server_create'),
+ api.quantum: ('network_list',),
api.glance: ('image_list_detailed',)})
def test_launch_post(self):
flavor = self.flavors.first()
@@ -155,6 +156,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
device_name = u'vda'
volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice}
+ nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
@@ -171,6 +173,8 @@ class InstanceViewTest(test.BaseAdminViewTests):
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
api.nova.server_create(IsA(http.HttpRequest),
server.name,
image.id,
@@ -179,6 +183,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
customization_script,
[sec_group.name],
block_device_mapping,
+ nics=nics,
instance_count=IsA(int))
self.mox.ReplayAll()
@@ -194,6 +199,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'volume_type': 'volume_id',
'volume_id': volume_choice,
'device_name': device_name,
+ 'network': self.networks.first().id,
'count': 1}
url = reverse('horizon:syspanel:instances:launch')
res = self.client.post(url, form_data)
diff --git a/horizon/dashboards/syspanel/networks/__init__.py b/horizon/dashboards/syspanel/networks/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/__init__.py
diff --git a/horizon/dashboards/syspanel/networks/forms.py b/horizon/dashboards/syspanel/networks/forms.py
new file mode 100644
index 00000000..88bff09c
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/forms.py
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+from horizon.dashboards.nova.networks import forms as user_forms
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateNetwork(forms.SelfHandlingForm):
+ name = forms.CharField(max_length=255,
+ label=_("Name"),
+ required=False)
+ tenant_id = forms.ChoiceField(label=_("Project"))
+
+ @classmethod
+ def _instantiate(cls, request, *args, **kwargs):
+ return cls(request, *args, **kwargs)
+
+ def __init__(self, request, *args, **kwargs):
+ super(CreateNetwork, self).__init__(request, *args, **kwargs)
+ tenant_choices = [('', _("Select a project"))]
+ for tenant in api.keystone.tenant_list(request, admin=True):
+ if tenant.enabled:
+ tenant_choices.append((tenant.id, tenant.name))
+ self.fields['tenant_id'].choices = tenant_choices
+
+ def handle(self, request, data):
+ try:
+ network = api.quantum.network_create(request,
+ name=data['name'],
+ tenant_id=data['tenant_id'])
+ msg = _('Network %s was successfully created.') % data['name']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return network
+ except:
+ redirect = reverse('horizon:syspanel:networks:index')
+ msg = _('Failed to create network %s') % data['name']
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class UpdateNetwork(user_forms.UpdateNetwork):
+ failure_url = 'horizon:syspanel:networks:index'
diff --git a/horizon/dashboards/syspanel/networks/panel.py b/horizon/dashboards/syspanel/networks/panel.py
new file mode 100644
index 00000000..638c2bed
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/panel.py
@@ -0,0 +1,28 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.utils.translation import ugettext_lazy as _
+
+import horizon
+from horizon.dashboards.syspanel import dashboard
+
+
+class Networks(horizon.Panel):
+ name = _("Networks")
+ slug = 'networks'
+ permissions = ('openstack.services.network',)
+
+dashboard.Syspanel.register(Networks)
diff --git a/horizon/dashboards/syspanel/networks/ports/__init__.py b/horizon/dashboards/syspanel/networks/ports/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/__init__.py
diff --git a/horizon/dashboards/syspanel/networks/ports/forms.py b/horizon/dashboards/syspanel/networks/ports/forms.py
new file mode 100644
index 00000000..2e86ed13
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/forms.py
@@ -0,0 +1,92 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import messages
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreatePort(forms.SelfHandlingForm):
+ network_name = forms.CharField(label=_("Network Name"),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ network_id = forms.CharField(label=_("Network ID"),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ name = forms.CharField(max_length=255,
+ label=_("Name"),
+ required=False)
+ device_id = forms.CharField(max_length=100, label=_("Device ID"),
+ help_text='Device ID attached to the port',
+ required=False)
+
+ def handle(self, request, data):
+ try:
+ # We must specify tenant_id of the network which a subnet is
+ # created for if admin user does not belong to the tenant.
+ network = api.quantum.network_get(request, data['network_id'])
+ data['tenant_id'] = network.tenant_id
+
+ port = api.quantum.port_create(request, **data)
+ msg = _('Port %s was successfully created.') % port['id']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return port
+ except:
+ msg = _('Failed to create a port for network %s') \
+ % data['network_id']
+ LOG.info(msg)
+ redirect = reverse('horizon:syspanel:networks:detail',
+ args=(data['network_id'],))
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class UpdatePort(forms.SelfHandlingForm):
+ network_id = forms.CharField(widget=forms.HiddenInput())
+ tenant_id = forms.CharField(widget=forms.HiddenInput())
+ port_id = forms.CharField(widget=forms.HiddenInput())
+ name = forms.CharField(max_length=255,
+ label=_("Name"),
+ required=False)
+ device_id = forms.CharField(max_length=100, label=_("Device ID"),
+ help_text='Device ID attached to the port',
+ required=False)
+
+ def handle(self, request, data):
+ try:
+ LOG.debug('params = %s' % data)
+ port = api.quantum.port_modify(request, data['port_id'],
+ name=data['name'],
+ device_id=data['device_id'])
+ msg = _('Port %s was successfully updated.') % data['port_id']
+ LOG.debug(msg)
+ messages.success(request, msg)
+ return port
+ except Exception:
+ msg = _('Failed to update port %s') % data['port_id']
+ LOG.info(msg)
+ redirect = reverse('horizon:syspanel:networks:detail',
+ args=[data['network_id']])
+ exceptions.handle(request, msg, redirect=redirect)
diff --git a/horizon/dashboards/syspanel/networks/ports/tables.py b/horizon/dashboards/syspanel/networks/ports/tables.py
new file mode 100644
index 00000000..824d6703
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/tables.py
@@ -0,0 +1,85 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tables
+
+from horizon.dashboards.nova.networks.ports.tables import (get_fixed_ips,
+ get_attached)
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeletePort(tables.DeleteAction):
+ data_type_singular = _("Port")
+ data_type_plural = _("Ports")
+
+ def delete(self, request, obj_id):
+ try:
+ api.quantum.port_delete(request, obj_id)
+ except:
+ msg = _('Failed to delete subnet %s') % obj_id
+ LOG.info(msg)
+ network_id = self.table.kwargs['network_id']
+ redirect = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class CreatePort(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Port")
+ url = "horizon:syspanel:networks:addport"
+ classes = ("ajax-modal", "btn-create")
+
+ def get_link_url(self, datum=None):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id,))
+
+
+class UpdatePort(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit Port")
+ url = "horizon:syspanel:networks:editport"
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, port):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id, port.id))
+
+
+class PortsTable(tables.DataTable):
+ name = tables.Column("name",
+ verbose_name=_("Name"),
+ link="horizon:syspanel:networks:ports:detail")
+ fixed_ips = tables.Column(get_fixed_ips, verbose_name=_("Fixed IPs"))
+ device_id = tables.Column(get_attached, verbose_name=_("Device Attached"))
+ status = tables.Column("status", verbose_name=_("Status"))
+ admin_state = tables.Column("admin_state",
+ verbose_name=_("Admin State"))
+
+ class Meta:
+ name = "ports"
+ verbose_name = _("Ports")
+ table_actions = (CreatePort, DeletePort)
+ row_actions = (UpdatePort, DeletePort,)
diff --git a/horizon/dashboards/syspanel/networks/ports/tabs.py b/horizon/dashboards/syspanel/networks/ports/tabs.py
new file mode 100644
index 00000000..a1f723e0
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/tabs.py
@@ -0,0 +1,46 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tabs
+
+import logging
+LOG = logging.getLogger(__name__)
+
+
+class OverviewTab(tabs.Tab):
+ name = _("Overview")
+ slug = "overview"
+ template_name = "nova/networks/ports/_detail_overview.html"
+
+ def get_context_data(self, request):
+ port_id = self.tab_group.kwargs['port_id']
+ try:
+ port = api.quantum.port_get(self.request, port_id)
+ except:
+ redirect = reverse('horizon:syspanel:networks:index')
+ msg = _('Unable to retrieve port details.')
+ exceptions.handle(request, msg, redirect=redirect)
+ return {'port': port}
+
+
+class PortDetailTabs(tabs.TabGroup):
+ slug = "port_details"
+ tabs = (OverviewTab,)
diff --git a/horizon/dashboards/syspanel/networks/ports/urls.py b/horizon/dashboards/syspanel/networks/ports/urls.py
new file mode 100644
index 00000000..5bac3a03
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from horizon.dashboards.nova.networks.ports.views import DetailView
+
+PORTS = r'^(?P<port_id>[^/]+)/%s$'
+
+urlpatterns = patterns('horizon.dashboards.syspanel.networks.ports.views',
+ url(PORTS % 'detail', DetailView.as_view(), name='detail'))
diff --git a/horizon/dashboards/syspanel/networks/ports/views.py b/horizon/dashboards/syspanel/networks/ports/views.py
new file mode 100644
index 00000000..9ac57de6
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/ports/views.py
@@ -0,0 +1,98 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from .forms import CreatePort, UpdatePort
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreatePort
+ template_name = 'syspanel/networks/ports/create.html'
+ success_url = 'horizon:syspanel:networks:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['network_id'],))
+
+ def get_object(self):
+ if not hasattr(self, "_object"):
+ try:
+ network_id = self.kwargs["network_id"]
+ self._object = api.quantum.network_get(self.request,
+ network_id)
+ except:
+ redirect = reverse("horizon:syspanel:networks:detail",
+ args=(self.kwargs['network_id'],))
+ msg = _("Unable to retrieve network.")
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(CreateView, self).get_context_data(**kwargs)
+ context['network'] = self.get_object()
+ return context
+
+ def get_initial(self):
+ network = self.get_object()
+ return {"network_id": self.kwargs['network_id'],
+ "network_name": network.name}
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdatePort
+ template_name = 'syspanel/networks/ports/update.html'
+ context_object_name = 'port'
+ success_url = 'horizon:syspanel:networks:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['network_id'],))
+
+ def _get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ port_id = self.kwargs['port_id']
+ try:
+ self._object = api.quantum.port_get(self.request, port_id)
+ except:
+ redirect = reverse("horizon:syspanel:networks:detail",
+ args=(self.kwargs['network_id'],))
+ msg = _('Unable to retrieve port details')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ port = self._get_object()
+ context['port_id'] = port['id']
+ context['network_id'] = port['network_id']
+ return context
+
+ def get_initial(self):
+ port = self._get_object()
+ return {'port_id': port['id'],
+ 'network_id': port['network_id'],
+ 'tenant_id': port['tenant_id'],
+ 'name': port['name'],
+ 'device_id': port['device_id']}
diff --git a/horizon/dashboards/syspanel/networks/subnets/__init__.py b/horizon/dashboards/syspanel/networks/subnets/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/subnets/__init__.py
diff --git a/horizon/dashboards/syspanel/networks/subnets/forms.py b/horizon/dashboards/syspanel/networks/subnets/forms.py
new file mode 100644
index 00000000..9727831e
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/subnets/forms.py
@@ -0,0 +1,52 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import forms
+from horizon import exceptions
+
+from horizon.dashboards.nova.networks.subnets import forms as user_forms
+
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateSubnet(user_forms.CreateSubnet):
+ failure_url = 'horizon:syspanel:networks:detail'
+
+ def handle(self, request, data):
+ try:
+ # We must specify tenant_id of the network which a subnet is
+ # created for if admin user does not belong to the tenant.
+ network = api.quantum.network_get(request, data['network_id'])
+ data['tenant_id'] = network.tenant_id
+ except:
+ msg = _('Failed to retrieve network %s for a subnet') \
+ % data['network_id']
+ LOG.info(msg)
+ redirect = reverse(self.failure_url, args=[data['network_id']])
+ exceptions.handle(request, msg, redirect=redirect)
+ return super(CreateSubnet, self).handle(request, data)
+
+
+class UpdateSubnet(user_forms.UpdateSubnet):
+ tenant_id = forms.CharField(widget=forms.HiddenInput())
+ failure_url = 'horizon:syspanel:networks:detail'
diff --git a/horizon/dashboards/syspanel/networks/subnets/tables.py b/horizon/dashboards/syspanel/networks/subnets/tables.py
new file mode 100644
index 00000000..20c74014
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/subnets/tables.py
@@ -0,0 +1,82 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tables
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteSubnet(tables.DeleteAction):
+ data_type_singular = _("Subnet")
+ data_type_plural = _("Subnets")
+
+ def delete(self, request, obj_id):
+ try:
+ api.quantum.subnet_delete(request, obj_id)
+ except:
+ msg = _('Failed to delete subnet %s') % obj_id
+ LOG.info(msg)
+ network_id = self.table.kwargs['network_id']
+ redirect = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class CreateSubnet(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Subnet")
+ url = "horizon:syspanel:networks:addsubnet"
+ classes = ("ajax-modal", "btn-create")
+
+ def get_link_url(self, datum=None):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id,))
+
+
+class UpdateSubnet(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit Subnet")
+ url = "horizon:syspanel:networks:editsubnet"
+ classes = ("ajax-modal", "btn-edit")
+
+ def get_link_url(self, subnet):
+ network_id = self.table.kwargs['network_id']
+ return reverse(self.url, args=(network_id, subnet.id))
+
+
+class SubnetsTable(tables.DataTable):
+ name = tables.Column("name", verbose_name=_("Name"),
+ link='horizon:syspanel:networks:subnets:detail')
+ cidr = tables.Column("cidr", verbose_name=_("CIDR"))
+ ip_version = tables.Column("ipver_str", verbose_name=_("IP Version"))
+ gateway_ip = tables.Column("gateway_ip", verbose_name=_("Gateway IP"))
+
+ def get_object_display(self, subnet):
+ return subnet.id
+
+ class Meta:
+ name = "subnets"
+ verbose_name = _("Subnets")
+ table_actions = (CreateSubnet, DeleteSubnet)
+ row_actions = (UpdateSubnet, DeleteSubnet,)
diff --git a/horizon/dashboards/syspanel/networks/subnets/urls.py b/horizon/dashboards/syspanel/networks/subnets/urls.py
new file mode 100644
index 00000000..7017e8f1
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/subnets/urls.py
@@ -0,0 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url
+
+from horizon.dashboards.nova.networks.subnets.views import DetailView
+
+SUBNETS = r'^(?P<subnet_id>[^/]+)/%s$'
+
+urlpatterns = patterns('horizon.dashboards.syspanel.networks.subnets.views',
+ url(SUBNETS % 'detail', DetailView.as_view(), name='detail'))
diff --git a/horizon/dashboards/syspanel/networks/subnets/views.py b/horizon/dashboards/syspanel/networks/subnets/views.py
new file mode 100644
index 00000000..0c5a0c1e
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/subnets/views.py
@@ -0,0 +1,101 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from .forms import CreateSubnet, UpdateSubnet
+
+LOG = logging.getLogger(__name__)
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateSubnet
+ template_name = 'syspanel/networks/subnets/create.html'
+ success_url = 'horizon:syspanel:networks:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['network_id'],))
+
+ def get_object(self):
+ if not hasattr(self, "_object"):
+ try:
+ network_id = self.kwargs["network_id"]
+ self._object = api.quantum.network_get(self.request,
+ network_id)
+ except:
+ redirect = reverse('horizon:nova:networks:index')
+ msg = _("Unable to retrieve network.")
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(CreateView, self).get_context_data(**kwargs)
+ context['network'] = self.get_object()
+ return context
+
+ def get_initial(self):
+ network = self.get_object()
+ return {"network_id": self.kwargs['network_id'],
+ "network_name": network.name}
+
+
+class UpdateView(forms.ModalFormView):
+ form_class = UpdateSubnet
+ template_name = 'syspanel/networks/subnets/update.html'
+ context_object_name = 'subnet'
+ success_url = 'horizon:syspanel:networks:detail'
+
+ def get_success_url(self):
+ return reverse(self.success_url,
+ args=(self.kwargs['network_id'],))
+
+ def _get_object(self, *args, **kwargs):
+ if not hasattr(self, "_object"):
+ subnet_id = self.kwargs['subnet_id']
+ try:
+ self._object = api.quantum.subnet_get(self.request, subnet_id)
+ except:
+ redirect = reverse("horizon:syspanel:networks:detail",
+ args=(self.kwargs['network_id'],))
+ msg = _('Unable to retrieve subnet details')
+ exceptions.handle(self.request, msg, redirect=redirect)
+ return self._object
+
+ def get_context_data(self, **kwargs):
+ context = super(UpdateView, self).get_context_data(**kwargs)
+ subnet = self._get_object()
+ context['subnet_id'] = subnet['id']
+ context['network_id'] = subnet['network_id']
+ context['cidr'] = subnet['cidr']
+ context['ip_version'] = {4: 'IPv4', 6: 'IPv6'}[subnet['ip_version']]
+ return context
+
+ def get_initial(self):
+ subnet = self._get_object()
+ return {'network_id': self.kwargs['network_id'],
+ 'subnet_id': subnet['id'],
+ 'tenant_id': subnet['tenant_id'],
+ 'cidr': subnet['cidr'],
+ 'ip_version': subnet['ip_version'],
+ 'name': subnet['name'],
+ 'gateway_ip': subnet['gateway_ip']}
diff --git a/horizon/dashboards/syspanel/networks/tables.py b/horizon/dashboards/syspanel/networks/tables.py
new file mode 100644
index 00000000..77ba968a
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/tables.py
@@ -0,0 +1,79 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext_lazy as _
+
+from horizon import api
+from horizon import exceptions
+from horizon import tables
+
+from horizon.dashboards.nova.networks.tables import get_subnets
+
+
+LOG = logging.getLogger(__name__)
+
+
+class DeleteNetwork(tables.DeleteAction):
+ data_type_singular = _("Network")
+ data_type_plural = _("Networks")
+
+ def delete(self, request, obj_id):
+ try:
+ api.quantum.network_delete(request, obj_id)
+ except:
+ msg = _('Failed to delete network %s') % obj_id
+ LOG.info(msg)
+ redirect = reverse('horizon:syspanel:networks:index')
+ exceptions.handle(request, msg, redirect=redirect)
+
+
+class CreateNetwork(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Network")
+ url = "horizon:syspanel:networks:create"
+ classes = ("ajax-modal", "btn-create")
+
+
+class EditNetwork(tables.LinkAction):
+ name = "update"
+ verbose_name = _("Edit Network")
+ url = "horizon:syspanel:networks:update"
+ classes = ("ajax-modal", "btn-edit")
+
+
+#def _get_subnets(network):
+# cidrs = [subnet.get('cidr') for subnet in network.subnets]
+# return ','.join(cidrs)
+
+
+class NetworksTable(tables.DataTable):
+ tenant = tables.Column("tenant_name", verbose_name=_("Project"))
+ name = tables.Column("name", verbose_name=_("Network Name"),
+ link='horizon:syspanel:networks:detail')
+ subnets = tables.Column(get_subnets,
+ verbose_name=_("Subnets Associated"),)
+ status = tables.Column("status", verbose_name=_("Status"))
+ admin_state = tables.Column("admin_state",
+ verbose_name=_("Admin State"))
+
+ class Meta:
+ name = "networks"
+ verbose_name = _("Networks")
+ table_actions = (CreateNetwork, DeleteNetwork)
+ row_actions = (EditNetwork, DeleteNetwork)
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/_create.html
new file mode 100644
index 00000000..7b35a321
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/_create.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_network_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:create %}{% endblock %}
+
+{% block modal_id %}create_network_modal{% endblock %}
+{% block modal-header %}{% trans "Create Network" %}{% 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 "Select a name for your network."%}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Network" %}" />
+ <a href="{% url horizon:syspanel:networks:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/_update.html
new file mode 100644
index 00000000..63d5c2fb
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/_update.html
@@ -0,0 +1,24 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}update_network_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:update network_id %}{% endblock %}
+
+{% block modal-header %}{% trans "Edit Network" %}{% 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 may update the editable properties of your network here." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
+ <a href="{% url horizon:syspanel:networks:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/create.html b/horizon/dashboards/syspanel/networks/templates/networks/create.html
new file mode 100644
index 00000000..c39cc085
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Network" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Network") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "syspanel/networks/_create.html" %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/index.html b/horizon/dashboards/syspanel/networks/templates/networks/index.html
new file mode 100644
index 00000000..bcb1b74f
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/index.html
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Networks" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Networks") %}
+{% endblock page_header %}
+
+{% block main %}
+ <div id="networks">
+ {{ networks_table.render }}
+ </div>
+
+ <div id="subnets">
+ {{ subnets_table.render }}
+ </div>
+
+ <div id="ports">
+ {{ ports_table.render }}
+ </div>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html
new file mode 100644
index 00000000..ff654936
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/_create.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_port_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:addport network.id %}
+{% endblock %}
+
+{% block modal-header %}{% trans "Create Port" %}{% 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 port for the network. If you specify device ID to be attached, the device specified will be attached to the port created."%}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Port" %}" />
+ <a href="{% url horizon:syspanel:networks:detail network.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html
new file mode 100644
index 00000000..6481c2a6
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/_update.html
@@ -0,0 +1,29 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}update_port_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:editport network_id port_id %}{% endblock %}
+
+{% block modal-header %}{% trans "Edit Port" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <dl>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ port_id }}</dd>
+ </dl>
+ <hr>
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description:" %}</h3>
+ <p>{% trans "You may update the editable properties of your port here." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
+ <a href="{% url horizon:syspanel:networks:detail network_id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html
new file mode 100644
index 00000000..76f1019c
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Port" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Port") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "syspanel/networks/ports/_create.html" %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html b/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html
new file mode 100644
index 00000000..33b64d89
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/ports/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Port" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Port") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'syspanel/networks/ports/_update.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html
new file mode 100644
index 00000000..1e16b900
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_create.html
@@ -0,0 +1,25 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_subnet_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:addsubnet network.id %}
+{% endblock %}
+
+{% block modal-header %}{% trans "Create Subnet" %}{% 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 subnet for the network. Any network address can be specified unless the network address does not overlap other subnets in the network." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Subnet" %}" />
+ <a href="{% url horizon:syspanel:networks:detail network.id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html
new file mode 100644
index 00000000..e7a41af7
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/_update.html
@@ -0,0 +1,33 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}update_subnet_form{% endblock %}
+{% block form_action %}{% url horizon:syspanel:networks:editsubnet network_id subnet_id %}{% endblock %}
+
+{% block modal-header %}{% trans "Edit Subnet" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <dl>
+ <dt>{% trans "ID" %}</dt>
+ <dd>{{ subnet_id }}</dd>
+ <dt>{% trans "Network Address" %}</dt>
+ <dd>{{ cidr }}</dd>
+ <dt>{% trans "IP version" %}</dt>
+ <dd>{{ ip_version }}</dd>
+ </dl>
+ <hr>
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description:" %}</h3>
+ <p>{% trans "You may update the editable properties of your subnet here." %}</p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Save Changes" %}" />
+ <a href="{% url horizon:syspanel:networks:detail network_id %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html
new file mode 100644
index 00000000..59bd5305
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/create.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create Subnet" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create Subnet") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include "syspanel/networks/subnets/_create.html" %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html
new file mode 100644
index 00000000..9c25d565
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/index.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Network Detail" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Network Detail") %}
+{% endblock page_header %}
+
+{% block main %}
+ {{ table.render }}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html b/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html
new file mode 100644
index 00000000..92dbff0c
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/subnets/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Subnet" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Subnet") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'syspanel/networks/subnets/_update.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/templates/networks/update.html b/horizon/dashboards/syspanel/networks/templates/networks/update.html
new file mode 100644
index 00000000..9870a365
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/templates/networks/update.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Update Network" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Update Network") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'syspanel/networks/_update.html' %}
+{% endblock %}
diff --git a/horizon/dashboards/syspanel/networks/tests.py b/horizon/dashboards/syspanel/networks/tests.py
new file mode 100644
index 00000000..9bbc5fa3
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/tests.py
@@ -0,0 +1,801 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# 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 import http
+from django.core.urlresolvers import reverse
+from django.utils.html import escape
+from mox import IsA
+
+from horizon import api
+from horizon import test
+
+
+INDEX_URL = reverse('horizon:syspanel:networks:index')
+
+
+class NetworkTests(test.BaseAdminViewTests):
+ @test.create_stubs({api.quantum: ('network_list',),
+ api.keystone: ('tenant_list',)})
+ def test_index(self):
+ tenants = self.tenants.list()
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndReturn(self.networks.list())
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/index.html')
+ networks = res.context['networks_table'].data
+ self.assertItemsEqual(networks, self.networks.list())
+
+ @test.create_stubs({api.quantum: ('network_list',)})
+ def test_index_network_list_exception(self):
+ api.quantum.network_list(IsA(http.HttpRequest)) \
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(INDEX_URL)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/index.html')
+ self.assertEqual(len(res.context['networks_table'].data), 0)
+ self.assertMessageCount(res, error=1)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:syspanel:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertItemsEqual(subnets, [self.subnets.first()])
+ self.assertItemsEqual(ports, [self.ports.first()])
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_network_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:detail', args=[network_id])
+ res = self.client.get(url)
+
+ redir_url = INDEX_URL
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_subnet_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id).\
+ AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndRaise(self.exceptions.quantum)
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndReturn([self.ports.first()])
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:syspanel:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertEqual(len(subnets), 0)
+ self.assertItemsEqual(ports, [self.ports.first()])
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_list',
+ 'port_list',)})
+ def test_network_detail_port_exception(self):
+ network_id = self.networks.first().id
+ api.quantum.network_get(IsA(http.HttpRequest), network_id).\
+ AndReturn(self.networks.first())
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
+ AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:syspanel:networks:detail',
+ args=[network_id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/detail.html')
+ subnets = res.context['subnets_table'].data
+ ports = res.context['ports_table'].data
+ self.assertItemsEqual(subnets, [self.subnets.first()])
+ self.assertEqual(len(ports), 0)
+
+ @test.create_stubs({api.keystone: ('tenant_list',)})
+ def test_network_create_get(self):
+ tenants = self.tenants.list()
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:create')
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/create.html')
+
+ @test.create_stubs({api.quantum: ('network_create',),
+ api.keystone: ('tenant_list',)})
+ def test_network_create_post(self):
+ tenants = self.tenants.list()
+ tenant_id = self.tenants.first().id
+ network = self.networks.first()
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name,
+ tenant_id=tenant_id)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ form_data = {'tenant_id': tenant_id,
+ 'name': network.name}
+ url = reverse('horizon:syspanel:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_create',),
+ api.keystone: ('tenant_list',)})
+ def test_network_create_post_network_exception(self):
+ tenants = self.tenants.list()
+ tenant_id = self.tenants.first().id
+ network = self.networks.first()
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+ api.quantum.network_create(IsA(http.HttpRequest), name=network.name,
+ tenant_id=tenant_id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'tenant_id': tenant_id,
+ 'name': network.name}
+ url = reverse('horizon:syspanel:networks:create')
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_network_update_get(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:update', args=[network.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/update.html')
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_network_update_get_exception(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:update', args=[network.id])
+ res = self.client.get(url)
+
+ redir_url = INDEX_URL
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_modify',
+ 'network_get',)})
+ def test_network_update_post(self):
+ network = self.networks.first()
+ api.quantum.network_modify(IsA(http.HttpRequest), network.id,
+ name=network.name)\
+ .AndReturn(network)
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ formData = {'network_id': network.id,
+ 'name': network.name,
+ 'tenant_id': network.tenant_id}
+ url = reverse('horizon:syspanel:networks:update', args=[network.id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_modify',
+ 'network_get',)})
+ def test_network_update_post_exception(self):
+ network = self.networks.first()
+ api.quantum.network_modify(IsA(http.HttpRequest), network.id,
+ name=network.name)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.network_get(IsA(http.HttpRequest), network.id)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': network.id,
+ 'name': network.name,
+ 'tenant_id': network.tenant_id}
+ url = reverse('horizon:syspanel:networks:update', args=[network.id])
+ res = self.client.post(url, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_list',
+ 'network_delete'),
+ api.keystone: ('tenant_list',)})
+ def test_delete_network(self):
+ tenants = self.tenants.list()
+ network = self.networks.first()
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+ api.quantum.network_list(IsA(http.HttpRequest))\
+ .AndReturn([network])
+ api.quantum.network_delete(IsA(http.HttpRequest), network.id)
+
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'networks__delete__%s' % network.id}
+ res = self.client.post(INDEX_URL, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('network_list',
+ 'network_delete'),
+ api.keystone: ('tenant_list',)})
+ def test_delete_network_exception(self):
+ tenants = self.tenants.list()
+ network = self.networks.first()
+ api.keystone.tenant_list(IsA(http.HttpRequest), admin=True)\
+ .AndReturn(tenants)
+ api.quantum.network_list(IsA(http.HttpRequest))\
+ .AndReturn([network])
+ api.quantum.network_delete(IsA(http.HttpRequest), network.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ form_data = {'action': 'networks__delete__%s' % network.id}
+ res = self.client.post(INDEX_URL, form_data)
+
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @test.create_stubs({api.quantum: ('subnet_get',)})
+ def test_subnet_detail(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(self.subnets.first())
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:subnets:detail',
+ args=[subnet.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'nova/networks/subnets/detail.html')
+ self.assertEqual(res.context['subnet'].id, subnet.id)
+
+ @test.create_stubs({api.quantum: ('subnet_get',)})
+ def test_subnet_detail_exception(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:subnets:detail',
+ args=[subnet.id])
+ res = self.client.get(url)
+
+ # syspanel DetailView is shared with userpanel one, so
+ # redirection URL on error is userpanel index.
+ redir_url = reverse('horizon:nova:networks:index')
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_get(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[network.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/subnets/create.html')
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ network_name=network.name,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip,
+ tenant_id=subnet.tenant_id)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post_network_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ # syspanel DetailView is shared with userpanel one, so
+ # redirection URL on error is userpanel index.
+ redir_url = reverse('horizon:nova:networks:index')
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'subnet_create',)})
+ def test_subnet_create_post_subnet_exception(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.subnet_create(IsA(http.HttpRequest),
+ network_id=network.id,
+ network_name=network.name,
+ name=subnet.name,
+ cidr=subnet.cidr,
+ ip_version=subnet.ip_version,
+ gateway_ip=subnet.gateway_ip,
+ tenant_id=subnet.tenant_id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_post_cidr_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ cidr = '2001:0DB8:0:CD30:123:4567:89AB:CDEF/60'
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ expected_msg = 'Network Address and IP version are inconsistent.'
+ self.assertContains(res, expected_msg)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_subnet_create_post_gw_inconsistent(self):
+ network = self.networks.first()
+ subnet = self.subnets.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF'
+ form_data = {'network_id': subnet.network_id,
+ 'network_name': network.name,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': gateway_ip}
+ url = reverse('horizon:syspanel:networks:addsubnet',
+ args=[subnet.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertContains(res, 'Gateway IP and IP version are inconsistent.')
+
+ @test.create_stubs({api.quantum: ('subnet_modify',
+ 'subnet_get',)})
+ def test_subnet_update_post(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(subnet)
+ api.quantum.subnet_modify(IsA(http.HttpRequest), subnet.id,
+ name=subnet.name,
+ gateway_ip=subnet.gateway_ip)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ formData = {'network_id': subnet.network_id,
+ 'tenant_id': subnet.tenant_id,
+ 'subnet_id': subnet.id,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': subnet.gateway_ip}
+ url = reverse('horizon:syspanel:networks:editsubnet',
+ args=[subnet.network_id, subnet.id])
+ res = self.client.post(url, formData)
+
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[subnet.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('subnet_modify',
+ 'subnet_get',)})
+ def test_subnet_update_post_gw_inconsistent(self):
+ subnet = self.subnets.first()
+ api.quantum.subnet_get(IsA(http.HttpRequest), subnet.id)\
+ .AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ # dummy IPv6 address
+ gateway_ip = '2001:0DB8:0:CD30:123:4567:89AB:CDEF'
+ formData = {'network_id': subnet.network_id,
+ 'tenant_id': subnet.tenant_id,
+ 'subnet_id': subnet.id,
+ 'name': subnet.name,
+ 'cidr': subnet.cidr,
+ 'ip_version': subnet.ip_version,
+ 'gateway_ip': gateway_ip}
+ url = reverse('horizon:syspanel:networks:editsubnet',
+ args=[subnet.network_id, subnet.id])
+ res = self.client.post(url, formData)
+
+ self.assertContains(res, 'Gateway IP and IP version are inconsistent.')
+
+ @test.create_stubs({api.quantum: ('subnet_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_subnet_delete(self):
+ subnet = self.subnets.first()
+ network_id = subnet.network_id
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'subnets__delete__%s' % subnet.id}
+ url = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
+
+ @test.create_stubs({api.quantum: ('subnet_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_subnet_delete_exception(self):
+ subnet = self.subnets.first()
+ network_id = subnet.network_id
+ api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'subnets__delete__%s' % subnet.id}
+ url = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
+
+ @test.create_stubs({api.quantum: ('port_get',)})
+ def test_port_detail(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndReturn(self.ports.first())
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:syspanel:networks:ports:detail',
+ args=[port.id]))
+
+ self.assertTemplateUsed(res, 'nova/networks/ports/detail.html')
+ self.assertEqual(res.context['port'].id, port.id)
+
+ @test.create_stubs({api.quantum: ('port_get',)})
+ def test_port_detail_exception(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndRaise(self.exceptions.quantum)
+
+ self.mox.ReplayAll()
+
+ res = self.client.get(reverse('horizon:syspanel:networks:ports:detail',
+ args=[port.id]))
+
+ # syspanel DetailView is shared with userpanel one, so
+ # redirection URL on error is userpanel index.
+ redir_url = reverse('horizon:nova:networks:index')
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',)})
+ def test_port_create_get(self):
+ network = self.networks.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:addport',
+ args=[network.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/ports/create.html')
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'port_create')})
+ def test_port_create_post(self):
+ network = self.networks.first()
+ port = self.ports.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.port_create(IsA(http.HttpRequest),
+ tenant_id=network.tenant_id,
+ network_id=network.id,
+ network_name=network.name,
+ name=port.name,
+ device_id=port.device_id)\
+ .AndReturn(port)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': port.network_id,
+ 'network_name': network.name,
+ 'name': port.name,
+ 'device_id': port.device_id}
+ url = reverse('horizon:syspanel:networks:addport',
+ args=[port.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[port.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('network_get',
+ 'port_create')})
+ def test_port_create_post_exception(self):
+ network = self.networks.first()
+ port = self.ports.first()
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.network_get(IsA(http.HttpRequest),
+ network.id)\
+ .AndReturn(self.networks.first())
+ api.quantum.port_create(IsA(http.HttpRequest),
+ tenant_id=network.tenant_id,
+ network_id=network.id,
+ network_name=network.name,
+ name=port.name,
+ device_id=port.device_id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ form_data = {'network_id': port.network_id,
+ 'network_name': network.name,
+ 'name': port.name,
+ 'device_id': port.device_id}
+ url = reverse('horizon:syspanel:networks:addport',
+ args=[port.network_id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[port.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('port_get',)})
+ def test_port_update_get(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest),
+ port.id)\
+ .AndReturn(port)
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:syspanel:networks:editport',
+ args=[port.network_id, port.id])
+ res = self.client.get(url)
+
+ self.assertTemplateUsed(res, 'syspanel/networks/ports/update.html')
+
+ @test.create_stubs({api.quantum: ('port_get',
+ 'port_modify')})
+ def test_port_update_post(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndReturn(port)
+ api.quantum.port_modify(IsA(http.HttpRequest), port.id,
+ name=port.name, device_id=port.device_id)\
+ .AndReturn(port)
+ self.mox.ReplayAll()
+
+ formData = {'tenant_id': port.tenant_id,
+ 'network_id': port.network_id,
+ 'port_id': port.id,
+ 'name': port.name,
+ 'device_id': port.device_id}
+ url = reverse('horizon:syspanel:networks:editport',
+ args=[port.network_id, port.id])
+ res = self.client.post(url, formData)
+
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[port.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('port_get',
+ 'port_modify')})
+ def test_port_update_post_exception(self):
+ port = self.ports.first()
+ api.quantum.port_get(IsA(http.HttpRequest), port.id)\
+ .AndReturn(port)
+ api.quantum.port_modify(IsA(http.HttpRequest), port.id,
+ name=port.name, device_id=port.device_id)\
+ .AndRaise(self.exceptions.quantum)
+ self.mox.ReplayAll()
+
+ formData = {'tenant_id': port.tenant_id,
+ 'network_id': port.network_id,
+ 'port_id': port.id,
+ 'name': port.name,
+ 'device_id': port.device_id}
+ url = reverse('horizon:syspanel:networks:editport',
+ args=[port.network_id, port.id])
+ res = self.client.post(url, formData)
+
+ redir_url = reverse('horizon:syspanel:networks:detail',
+ args=[port.network_id])
+ self.assertRedirectsNoFollow(res, redir_url)
+
+ @test.create_stubs({api.quantum: ('port_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_port_delete(self):
+ port = self.ports.first()
+ network_id = port.network_id
+ api.quantum.port_delete(IsA(http.HttpRequest), port.id)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'ports__delete__%s' % port.id}
+ url = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
+
+ @test.create_stubs({api.quantum: ('port_delete',
+ 'subnet_list',
+ 'port_list',)})
+ def test_port_delete_exception(self):
+ port = self.ports.first()
+ network_id = port.network_id
+ api.quantum.port_delete(IsA(http.HttpRequest), port.id)\
+ .AndRaise(self.exceptions.quantum)
+ api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.subnets.first()])
+ api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
+ .AndReturn([self.ports.first()])
+ self.mox.ReplayAll()
+
+ formData = {'action': 'ports__delete__%s' % port.id}
+ url = reverse('horizon:syspanel:networks:detail',
+ args=[network_id])
+ res = self.client.post(url, formData)
+
+ self.assertRedirectsNoFollow(res, url)
diff --git a/horizon/dashboards/syspanel/networks/urls.py b/horizon/dashboards/syspanel/networks/urls.py
new file mode 100644
index 00000000..efc77ce7
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/urls.py
@@ -0,0 +1,45 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from django.conf.urls.defaults import patterns, url, include
+from .views import IndexView, CreateView, DetailView, UpdateView
+
+from .subnets.views import CreateView as AddSubnetView
+from .subnets.views import UpdateView as EditSubnetView
+from .ports.views import CreateView as AddPortView
+from .ports.views import UpdateView as EditPortView
+
+from .subnets import urls as subnet_urls
+from .ports import urls as port_urls
+
+NETWORKS = r'^(?P<network_id>[^/]+)/%s$'
+
+urlpatterns = patterns('',
+ url(r'^$', IndexView.as_view(), name='index'),
+ url(r'^create/$', CreateView.as_view(), name='create'),
+ url(NETWORKS % 'update', UpdateView.as_view(), name='update'),
+ # for detail view
+ url(NETWORKS % 'detail', DetailView.as_view(), name='detail'),
+ url(NETWORKS % 'subnets/create', AddSubnetView.as_view(),
+ name='addsubnet'),
+ url(NETWORKS % 'ports/create', AddPortView.as_view(), name='addport'),
+ url(r'^(?P<network_id>[^/]+)/subnets/(?P<subnet_id>[^/]+)/update$',
+ EditSubnetView.as_view(), name='editsubnet'),
+ url(r'^(?P<network_id>[^/]+)/ports/(?P<port_id>[^/]+)/update$',
+ EditPortView.as_view(), name='editport'),
+
+ url(r'^subnets/', include(subnet_urls, namespace='subnets')),
+ url(r'^ports/', include(port_urls, namespace='ports')))
diff --git a/horizon/dashboards/syspanel/networks/views.py b/horizon/dashboards/syspanel/networks/views.py
new file mode 100644
index 00000000..6d61d2e1
--- /dev/null
+++ b/horizon/dashboards/syspanel/networks/views.py
@@ -0,0 +1,133 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import logging
+
+from django.core.urlresolvers import reverse_lazy
+from django.utils.translation import ugettext_lazy as _
+from django.utils.datastructures import SortedDict
+
+from horizon import api
+from horizon import exceptions
+from horizon import forms
+from horizon import tables
+
+from .tables import NetworksTable
+from .subnets.tables import SubnetsTable
+from .ports.tables import PortsTable
+from .forms import CreateNetwork, UpdateNetwork
+
+from horizon.dashboards.nova.networks import views as user_views
+
+LOG = logging.getLogger(__name__)
+
+
+class IndexView(tables.DataTableView):
+ table_class = NetworksTable
+ template_name = 'syspanel/networks/index.html'
+
+ def _get_tenant_list(self):
+ if not hasattr(self, "_tenants"):
+ try:
+ tenants = api.keystone.tenant_list(self.request, admin=True)
+ except:
+ tenants = []
+ msg = _('Unable to retrieve instance tenant information.')
+ exceptions.handle(self.request, msg)
+
+ tenant_dict = SortedDict([(t.id, t) for t in tenants])
+ self._tenants = tenant_dict
+ return self._tenants
+
+ def get_data(self):
+ try:
+ networks = api.quantum.network_list(self.request)
+ except:
+ networks = []
+ msg = _('Network list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ if networks:
+ tenant_dict = self._get_tenant_list()
+ for n in networks:
+ # Set tenant name
+ tenant = tenant_dict.get(n.tenant_id, None)
+ n.tenant_name = getattr(tenant, 'name', None)
+ # If name is empty use UUID as name
+ n.set_id_as_name_if_empty()
+ return networks
+
+
+class CreateView(forms.ModalFormView):
+ form_class = CreateNetwork
+ template_name = 'syspanel/networks/create.html'
+ success_url = reverse_lazy('horizon:syspanel:networks:index')
+
+
+class DetailView(tables.MultiTableView):
+ table_classes = (SubnetsTable, PortsTable)
+ template_name = 'nova/networks/detail.html'
+ failure_url = reverse_lazy('horizon:syspanel:networks:index')
+
+ def get_subnets_data(self):
+ try:
+ network_id = self.kwargs['network_id']
+ subnets = api.quantum.subnet_list(self.request,
+ network_id=network_id)
+ except:
+ subnets = []
+ msg = _('Subnet list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ for s in subnets:
+ s.set_id_as_name_if_empty()
+ return subnets
+
+ def get_ports_data(self):
+ try:
+ network_id = self.kwargs['network_id']
+ ports = api.quantum.port_list(self.request, network_id=network_id)
+ except:
+ ports = []
+ msg = _('Port list can not be retrieved.')
+ exceptions.handle(self.request, msg)
+ for p in ports:
+ p.set_id_as_name_if_empty()
+ return ports
+
+ def _get_data(self):
+ if not hasattr(self, "_network"):
+ try:
+ network_id = self.kwargs['network_id']
+ network = api.quantum.network_get(self.request, network_id)
+ network.set_id_as_name_if_empty(length=0)
+ except:
+ redirect = self.failure_url
+ exceptions.handle(self.request,
+ _('Unable to retrieve details for '
+ 'network "%s".') % network_id,
+ redirect=redirect)
+ self._network = network
+ return self._network
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ context["network"] = self._get_data()
+ return context
+
+
+class UpdateView(user_views.UpdateView):
+ form_class = UpdateNetwork
+ template_name = 'syspanel/networks/update.html'
+ success_url = reverse_lazy('horizon:syspanel:networks:index')
diff --git a/horizon/tables/base.py b/horizon/tables/base.py
index 660868fb..49917465 100644
--- a/horizon/tables/base.py
+++ b/horizon/tables/base.py
@@ -734,7 +734,7 @@ class DataTableOptions(object):
self.table_actions_template = \
'horizon/common/_data_table_table_actions.html'
self.context_var_name = unicode(getattr(options,
- 'context_var_nam',
+ 'context_var_name',
'table'))
self.actions_column = getattr(options,
'actions_column',
diff --git a/horizon/templates/horizon/common/_data_table.html b/horizon/templates/horizon/common/_data_table.html
index 9713752f..34c4dd97 100644
--- a/horizon/templates/horizon/common/_data_table.html
+++ b/horizon/templates/horizon/common/_data_table.html
@@ -48,7 +48,7 @@
<a href="?{{ table.get_pagination_string }}">More&nbsp;&raquo;</a>
{% endif %}
</td>
- </td>
+ </tr>
</tfoot>
</table>
{% endwith %}
diff --git a/horizon/templates/horizon/common/_workflow_step.html b/horizon/templates/horizon/common/_workflow_step.html
index 0c998875..17b6deff 100644
--- a/horizon/templates/horizon/common/_workflow_step.html
+++ b/horizon/templates/horizon/common/_workflow_step.html
@@ -2,12 +2,12 @@
<table class="table-fixed">
<tbody>
<tr>
- <td class="help_text">
- {{ step.get_help_text }}
- </td>
<td class="actions">
{% include "horizon/common/_form_fields.html" %}
</td>
+ <td class="help_text">
+ {{ step.get_help_text }}
+ </td>
</tr>
</tbody>
</table>
diff --git a/horizon/test.py b/horizon/test.py
index f5ce9958..1b294eb9 100644
--- a/horizon/test.py
+++ b/horizon/test.py
@@ -35,6 +35,7 @@ from django.utils import unittest
import glanceclient
from keystoneclient.v2_0 import client as keystone_client
from novaclient.v1_1 import client as nova_client
+from quantumclient.v2_0 import client as quantum_client
from selenium.webdriver.firefox.webdriver import WebDriver
import httplib2
@@ -290,17 +291,20 @@ class APITestCase(TestCase):
self._original_glanceclient = api.glance.glanceclient
self._original_keystoneclient = api.keystone.keystoneclient
self._original_novaclient = api.nova.novaclient
+ self._original_quantumclient = api.quantum.quantumclient
# Replace the clients with our stubs.
api.glance.glanceclient = lambda request: self.stub_glanceclient()
api.keystone.keystoneclient = fake_keystoneclient
api.nova.novaclient = lambda request: self.stub_novaclient()
+ api.quantum.quantumclient = lambda request: self.stub_quantumclient()
def tearDown(self):
super(APITestCase, self).tearDown()
api.glance.glanceclient = self._original_glanceclient
api.nova.novaclient = self._original_novaclient
api.keystone.keystoneclient = self._original_keystoneclient
+ api.quantum.quantumclient = self._original_quantumclient
def stub_novaclient(self):
if not hasattr(self, "novaclient"):
@@ -320,6 +324,12 @@ class APITestCase(TestCase):
self.glanceclient = self.mox.CreateMock(glanceclient.Client)
return self.glanceclient
+ def stub_quantumclient(self):
+ if not hasattr(self, "quantumclient"):
+ self.mox.StubOutWithMock(quantum_client, 'Client')
+ self.quantumclient = self.mox.CreateMock(quantum_client.Client)
+ return self.quantumclient
+
def stub_swiftclient(self, expected_calls=1):
if not hasattr(self, "swiftclient"):
self.mox.StubOutWithMock(swift_client, 'Connection')
diff --git a/horizon/tests/api_tests/quantum_tests.py b/horizon/tests/api_tests/quantum_tests.py
new file mode 100644
index 00000000..09a51a78
--- /dev/null
+++ b/horizon/tests/api_tests/quantum_tests.py
@@ -0,0 +1,205 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 NEC Corporation
+#
+# 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 horizon import api
+from horizon import test
+
+
+class QuantumApiTests(test.APITestCase):
+ def test_network_list(self):
+ networks = {'networks': self.api_networks.list()}
+ subnets = {'subnets': self.api_subnets.list()}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.list_networks().AndReturn(networks)
+ quantumclient.list_subnets().AndReturn(subnets)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.network_list(self.request)
+ for n in ret_val:
+ self.assertIsInstance(n, api.quantum.Network)
+
+ def test_network_get(self):
+ network = {'network': self.api_networks.first()}
+ subnet = {'subnet': self.api_subnets.first()}
+ network_id = self.api_networks.first()['id']
+ subnet_id = self.api_networks.first()['subnets'][0]
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.show_network(network_id).AndReturn(network)
+ quantumclient.show_subnet(subnet_id).AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.network_get(self.request, network_id)
+ self.assertIsInstance(ret_val, api.quantum.Network)
+
+ def test_network_create(self):
+ network = {'network': self.api_networks.first()}
+
+ quantumclient = self.stub_quantumclient()
+ form_data = {'network': {'name': 'net1'}}
+ quantumclient.create_network(body=form_data).AndReturn(network)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.network_create(self.request, name='net1')
+ self.assertIsInstance(ret_val, api.quantum.Network)
+
+ def test_network_modify(self):
+ network = {'network': self.api_networks.first()}
+ network_id = self.api_networks.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ form_data = {'network': {'name': 'net1'}}
+ quantumclient.update_network(network_id, body=form_data)\
+ .AndReturn(network)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.network_modify(self.request, network_id,
+ name='net1')
+ self.assertIsInstance(ret_val, api.quantum.Network)
+
+ def test_network_delete(self):
+ network_id = self.api_networks.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.delete_network(network_id)
+ self.mox.ReplayAll()
+
+ api.quantum.network_delete(self.request, network_id)
+
+ def test_subnet_list(self):
+ subnets = {'subnets': self.api_subnets.list()}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.list_subnets().AndReturn(subnets)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.subnet_list(self.request)
+ for n in ret_val:
+ self.assertIsInstance(n, api.quantum.Subnet)
+
+ def test_subnet_get(self):
+ subnet = {'subnet': self.api_subnets.first()}
+ subnet_id = self.api_subnets.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.show_subnet(subnet_id).AndReturn(subnet)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.subnet_get(self.request, subnet_id)
+ self.assertIsInstance(ret_val, api.quantum.Subnet)
+
+ def test_subnet_create(self):
+ subnet_data = self.api_subnets.first()
+ params = {'network_id': subnet_data['network_id'],
+ 'tenant_id': subnet_data['tenant_id'],
+ 'name': subnet_data['name'],
+ 'cidr': subnet_data['cidr'],
+ 'ip_version': subnet_data['ip_version'],
+ 'gateway_ip': subnet_data['gateway_ip']}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.create_subnet(body={'subnet': params})\
+ .AndReturn({'subnet': subnet_data})
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.subnet_create(self.request, **params)
+ self.assertIsInstance(ret_val, api.quantum.Subnet)
+
+ def test_subnet_modify(self):
+ subnet_data = self.api_subnets.first()
+ subnet_id = subnet_data['id']
+ params = {'name': subnet_data['name'],
+ 'gateway_ip': subnet_data['gateway_ip']}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.update_subnet(subnet_id, body={'subnet': params})\
+ .AndReturn({'subnet': subnet_data})
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.subnet_modify(self.request, subnet_id, **params)
+ self.assertIsInstance(ret_val, api.quantum.Subnet)
+
+ def test_subnet_delete(self):
+ subnet_id = self.api_subnets.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.delete_subnet(subnet_id)
+ self.mox.ReplayAll()
+
+ api.quantum.subnet_delete(self.request, subnet_id)
+
+ def test_port_list(self):
+ ports = {'ports': self.api_ports.list()}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.list_ports().AndReturn(ports)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.port_list(self.request)
+ for p in ret_val:
+ self.assertIsInstance(p, api.quantum.Port)
+
+ def test_port_get(self):
+ port = {'port': self.api_ports.first()}
+ port_id = self.api_ports.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.show_port(port_id).AndReturn(port)
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.port_get(self.request, port_id)
+ self.assertIsInstance(ret_val, api.quantum.Port)
+
+ def test_port_create(self):
+ port_data = self.api_ports.first()
+ params = {'network_id': port_data['network_id'],
+ 'tenant_id': port_data['tenant_id'],
+ 'name': port_data['name'],
+ 'device_id': port_data['device_id']}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.create_port(body={'port': params})\
+ .AndReturn({'port': port_data})
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.port_create(self.request, **params)
+ self.assertIsInstance(ret_val, api.quantum.Port)
+ self.assertEqual(ret_val.id, api.quantum.Port(port_data).id)
+
+ def test_port_modify(self):
+ port_data = self.api_ports.first()
+ port_id = port_data['id']
+ params = {'name': port_data['name'],
+ 'device_id': port_data['device_id']}
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.update_port(port_id, body={'port': params})\
+ .AndReturn({'port': port_data})
+ self.mox.ReplayAll()
+
+ ret_val = api.quantum.port_modify(self.request, port_id, **params)
+ self.assertIsInstance(ret_val, api.quantum.Port)
+ self.assertEqual(ret_val.id, api.quantum.Port(port_data).id)
+
+ def test_port_delete(self):
+ port_id = self.api_ports.first()['id']
+
+ quantumclient = self.stub_quantumclient()
+ quantumclient.delete_port(port_id)
+ self.mox.ReplayAll()
+
+ api.quantum.port_delete(self.request, port_id)
diff --git a/horizon/tests/test_data/exceptions.py b/horizon/tests/test_data/exceptions.py
index a33978fa..ab55941b 100644
--- a/horizon/tests/test_data/exceptions.py
+++ b/horizon/tests/test_data/exceptions.py
@@ -15,6 +15,7 @@
import glanceclient.exc as glance_exceptions
from keystoneclient import exceptions as keystone_exceptions
from novaclient import exceptions as nova_exceptions
+from quantumclient.common import exceptions as quantum_exceptions
from .utils import TestDataContainer
@@ -53,3 +54,6 @@ def data(TEST):
glance_exception = glance_exceptions.ClientException
TEST.exceptions.glance = create_stubbed_exception(glance_exception)
+
+ quantum_exception = quantum_exceptions.QuantumClientException
+ TEST.exceptions.quantum = create_stubbed_exception(quantum_exception)
diff --git a/horizon/tests/test_data/quantum_data.py b/horizon/tests/test_data/quantum_data.py
new file mode 100644
index 00000000..b229e274
--- /dev/null
+++ b/horizon/tests/test_data/quantum_data.py
@@ -0,0 +1,109 @@
+# 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.
+
+import copy
+
+from horizon.api.quantum import Network, Subnet, Port
+
+from .utils import TestDataContainer
+
+
+def data(TEST):
+ # data returned by horizon.api.quantum wrapper
+ TEST.networks = TestDataContainer()
+ TEST.subnets = TestDataContainer()
+ TEST.ports = TestDataContainer()
+
+ # data return by quantumclient
+ TEST.api_networks = TestDataContainer()
+ TEST.api_subnets = TestDataContainer()
+ TEST.api_ports = TestDataContainer()
+
+ # 1st network
+ network_dict = {'admin_state_up': True,
+ 'id': '82288d84-e0a5-42ac-95be-e6af08727e42',
+ 'name': 'net1',
+ 'status': 'ACTIVE',
+ 'subnets': ['e8abc972-eb0c-41f1-9edd-4bc6e3bcd8c9'],
+ 'tenant_id': '1'}
+ subnet_dict = {'allocation_pools': [{'end': '10.0.0.254',
+ 'start': '10.0.0.2'}],
+ 'cidr': '10.0.0.0/24',
+ 'enable_dhcp': True,
+ 'gateway_ip': '10.0.0.1',
+ 'id': network_dict['subnets'][0],
+ 'ip_version': 4,
+ 'name': 'mysubnet1',
+ 'network_id': network_dict['id'],
+ 'tenant_id': network_dict['tenant_id']}
+ port_dict = {'admin_state_up': True,
+ 'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890',
+ 'fixed_ips': [{'ip_address': '10.0.0.3',
+ 'subnet_id': subnet_dict['id']}],
+ 'id': '3ec7f3db-cb2f-4a34-ab6b-69a64d3f008c',
+ 'mac_address': 'fa:16:3e:9c:d5:7e',
+ 'name': '',
+ 'network_id': network_dict['id'],
+ 'status': 'ACTIVE',
+ 'tenant_id': network_dict['tenant_id']}
+
+ TEST.api_networks.add(network_dict)
+ TEST.api_subnets.add(subnet_dict)
+ TEST.api_ports.add(port_dict)
+
+ network = copy.deepcopy(network_dict)
+ subnet = Subnet(subnet_dict)
+ network['subnets'] = [subnet]
+ TEST.networks.add(Network(network))
+ TEST.subnets.add(subnet)
+ TEST.ports.add(Port(port_dict))
+
+ # 2nd network
+ network_dict = {'admin_state_up': True,
+ 'id': '72c3ab6c-c80f-4341-9dc5-210fa31ac6c2',
+ 'name': 'net2',
+ 'status': 'ACTIVE',
+ 'subnets': ['3f7c5d79-ee55-47b0-9213-8e669fb03009'],
+ 'tenant_id': '2'}
+ subnet_dict = {'allocation_pools': [{'end': '172.16.88.254',
+ 'start': '172.16.88.2'}],
+ 'cidr': '172.16.88.0/24',
+ 'enable_dhcp': True,
+ 'gateway_ip': '172.16.88.1',
+ 'id': '3f7c5d79-ee55-47b0-9213-8e669fb03009',
+ 'ip_version': 4,
+ 'name': 'aaaa',
+ 'network_id': network_dict['id'],
+ 'tenant_id': network_dict['tenant_id']}
+ port_dict = {'admin_state_up': True,
+ 'device_id': '40e536b1-b9fd-4eb7-82d6-84db5d65a2ac',
+ 'fixed_ips': [{'ip_address': '172.16.88.3',
+ 'subnet_id': subnet_dict['id']}],
+ 'id': '7e6ce62c-7ea2-44f8-b6b4-769af90a8406',
+ 'mac_address': 'fa:16:3e:56:e6:2f',
+ 'name': '',
+ 'network_id': network_dict['id'],
+ 'status': 'ACTIVE',
+ 'tenant_id': network_dict['tenant_id']}
+
+ TEST.api_networks.add(network_dict)
+ TEST.api_subnets.add(subnet_dict)
+ TEST.api_ports.add(port_dict)
+
+ network = copy.deepcopy(network_dict)
+ subnet = Subnet(subnet_dict)
+ network['subnets'] = [subnet]
+ TEST.networks.add(Network(network))
+ TEST.subnets.add(subnet)
+ TEST.ports.add(Port(port_dict))
diff --git a/horizon/tests/test_data/utils.py b/horizon/tests/test_data/utils.py
index b8a3f786..28f6147f 100644
--- a/horizon/tests/test_data/utils.py
+++ b/horizon/tests/test_data/utils.py
@@ -18,6 +18,7 @@ def load_test_data(load_onto=None):
from . import glance_data
from . import keystone_data
from . import nova_data
+ from . import quantum_data
from . import swift_data
# The order of these loaders matters, some depend on others.
@@ -25,6 +26,7 @@ def load_test_data(load_onto=None):
keystone_data.data,
glance_data.data,
nova_data.data,
+ quantum_data.data,
swift_data.data)
if load_onto:
for data_func in loaders:
diff --git a/openstack_dashboard/exceptions.py b/openstack_dashboard/exceptions.py
index 04b8ef29..518c330d 100644
--- a/openstack_dashboard/exceptions.py
+++ b/openstack_dashboard/exceptions.py
@@ -22,6 +22,7 @@ from cloudfiles import errors as swiftclient
from glanceclient.common import exceptions as glanceclient
from keystoneclient import exceptions as keystoneclient
from novaclient import exceptions as novaclient
+from quantumclient.common import exceptions as quantumclient
UNAUTHORIZED = (keystoneclient.Unauthorized,
@@ -29,12 +30,16 @@ UNAUTHORIZED = (keystoneclient.Unauthorized,
novaclient.Unauthorized,
novaclient.Forbidden,
glanceclient.Unauthorized,
+ quantumclient.Unauthorized,
+ quantumclient.Forbidden,
swiftclient.AuthenticationFailed,
swiftclient.AuthenticationError)
NOT_FOUND = (keystoneclient.NotFound,
novaclient.NotFound,
glanceclient.NotFound,
+ quantumclient.NetworkNotFoundClient,
+ quantumclient.PortNotFoundClient,
swiftclient.NoSuchContainer,
swiftclient.NoSuchObject)
@@ -44,4 +49,12 @@ RECOVERABLE = (keystoneclient.ClientException,
keystoneclient.AuthorizationFailure,
novaclient.ClientException,
glanceclient.ClientException,
+ # NOTE(amotoki): Quantum exceptions other than the first one
+ # are recoverable in many cases (e.g., NetworkInUse is not
+ # raised once VMs which use the network are terminated).
+ quantumclient.QuantumClientException,
+ quantumclient.NetworkInUseClient,
+ quantumclient.PortInUseClient,
+ quantumclient.AlreadyAttachedClient,
+ quantumclient.StateInvalidClient,
swiftclient.Error)
diff --git a/openstack_dashboard/static/dashboard/less/horizon.less b/openstack_dashboard/static/dashboard/less/horizon.less
index 72d543d4..aca092a8 100644
--- a/openstack_dashboard/static/dashboard/less/horizon.less
+++ b/openstack_dashboard/static/dashboard/less/horizon.less
@@ -701,14 +701,14 @@ form.horizontal fieldset {
.workflow td.actions {
vertical-align: top;
width: 308px;
- padding-left: 10px;
+ padding-right: 10px;
}
.workflow td.help_text {
vertical-align: top;
width: 340px;
- padding-right: 10px;
- border-right: 1px solid #DDD;
+ padding-left: 10px;
+ border-left: 1px solid #DDD;
}
.workflow fieldset > table {
diff --git a/tools/pip-requires b/tools/pip-requires
index cb565cc1..8c6a82fd 100644
--- a/tools/pip-requires
+++ b/tools/pip-requires
@@ -6,6 +6,7 @@ python-cloudfiles
python-glanceclient<2
python-keystoneclient
python-novaclient
+python-quantumclient
pytz
# Horizon Utility Requirements