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
This commit is contained in:
Akihiro MOTOKI 2012-07-23 14:26:30 +09:00
parent b0066309fd
commit 05cf900492
89 changed files with 4849 additions and 11 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
*.pyc *.pyc
*.swp *.swp
*.sqlite3
.environment_version .environment_version
.selenium_log .selenium_log
.coverage* .coverage*

View File

@ -36,3 +36,4 @@ from horizon.api.glance import *
from horizon.api.keystone import * from horizon.api.keystone import *
from horizon.api.nova import * from horizon.api.nova import *
from horizon.api.swift import * from horizon.api.swift import *
from horizon.api.quantum import *

View File

@ -292,11 +292,13 @@ def keypair_list(request):
def server_create(request, name, image, flavor, key_name, user_data, 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( return Server(novaclient(request).servers.create(
name, image, flavor, userdata=user_data, name, image, flavor, userdata=user_data,
security_groups=security_groups, security_groups=security_groups,
key_name=key_name, block_device_mapping=block_device_mapping, key_name=key_name, block_device_mapping=block_device_mapping,
nics=nics,
min_count=instance_count), request) min_count=instance_count), request)

239
horizon/api/quantum.py Normal file
View File

@ -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)

View File

@ -26,7 +26,8 @@ class BasePanels(horizon.PanelGroup):
'instances', 'instances',
'volumes', 'volumes',
'images_and_snapshots', 'images_and_snapshots',
'access_and_security') 'access_and_security',
'networks')
class ObjectStorePanels(horizon.PanelGroup): class ObjectStorePanels(horizon.PanelGroup):

View File

@ -589,6 +589,7 @@ class InstanceTests(test.TestCase):
'security_group_list', 'security_group_list',
'volume_snapshot_list', 'volume_snapshot_list',
'volume_list',), 'volume_list',),
api.quantum: ('network_list',),
api.glance: ('image_list_detailed',)}) api.glance: ('image_list_detailed',)})
def test_launch_instance_get(self): def test_launch_instance_get(self):
quota_usages = self.quota_usages.first() quota_usages = self.quota_usages.first()
@ -604,6 +605,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(quota_usages) .AndReturn(quota_usages)
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -631,10 +634,12 @@ class InstanceTests(test.TestCase):
self.assertQuerysetEqual(workflow.steps, self.assertQuerysetEqual(workflow.steps,
['<SetInstanceDetails: setinstancedetailsaction>', ['<SetInstanceDetails: setinstancedetailsaction>',
'<SetAccessControls: setaccesscontrolsaction>', '<SetAccessControls: setaccesscontrolsaction>',
'<SetNetwork: setnetworkaction>',
'<VolumeOptions: volumeoptionsaction>', '<VolumeOptions: volumeoptionsaction>',
'<PostCreationStep: customizeaction>']) '<PostCreationStep: customizeaction>'])
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.quantum: ('network_list',),
api.nova: ('flavor_list', api.nova: ('flavor_list',
'keypair_list', 'keypair_list',
'security_group_list', 'security_group_list',
@ -653,6 +658,7 @@ class InstanceTests(test.TestCase):
device_name = u'vda' device_name = u'vda'
volume_choice = "%s:vol" % volume.id volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice} 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)) \ api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list()) .AndReturn(self.flavors.list())
@ -666,6 +672,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \ api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list()) .AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
@ -677,6 +685,7 @@ class InstanceTests(test.TestCase):
customization_script, customization_script,
[sec_group.name], [sec_group.name],
block_device_mapping, block_device_mapping,
nics=nics,
instance_count=IsA(int)) instance_count=IsA(int))
self.mox.ReplayAll() self.mox.ReplayAll()
@ -693,6 +702,7 @@ class InstanceTests(test.TestCase):
'volume_type': 'volume_id', 'volume_type': 'volume_id',
'volume_id': volume_choice, 'volume_id': volume_choice,
'device_name': device_name, 'device_name': device_name,
'network': self.networks.first().id,
'count': 1} 'count': 1}
url = reverse('horizon:nova:instances:launch') url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data) res = self.client.post(url, form_data)
@ -702,6 +712,7 @@ class InstanceTests(test.TestCase):
reverse('horizon:nova:instances:index')) reverse('horizon:nova:instances:index'))
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.quantum: ('network_list',),
api.nova: ('flavor_list', api.nova: ('flavor_list',
'keypair_list', 'keypair_list',
'security_group_list', 'security_group_list',
@ -727,6 +738,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list()) .AndReturn(self.flavors.list())
api.nova.keypair_list(IsA(http.HttpRequest)) \ api.nova.keypair_list(IsA(http.HttpRequest)) \
@ -762,6 +775,7 @@ class InstanceTests(test.TestCase):
'nova/instances/launch.html') 'nova/instances/launch.html')
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.quantum: ('network_list',),
api.nova: ('tenant_quota_usages', api.nova: ('tenant_quota_usages',
'flavor_list', 'flavor_list',
'keypair_list', 'keypair_list',
@ -779,6 +793,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \ api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(self.quota_usages.first()) .AndReturn(self.quota_usages.first())
api.nova.flavor_list(IsA(http.HttpRequest)) \ api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -799,6 +815,7 @@ class InstanceTests(test.TestCase):
'nova/instances/launch.html') 'nova/instances/launch.html')
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.quantum: ('network_list',),
api.nova: ('flavor_list', api.nova: ('flavor_list',
'keypair_list', 'keypair_list',
'security_group_list', 'security_group_list',
@ -812,6 +829,7 @@ class InstanceTests(test.TestCase):
server = self.servers.first() server = self.servers.first()
sec_group = self.security_groups.first() sec_group = self.security_groups.first()
customization_script = 'userData' customization_script = 'userData'
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \ api.nova.volume_snapshot_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list()) .AndReturn(self.volumes.list())
@ -825,6 +843,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list()) api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
api.nova.server_create(IsA(http.HttpRequest), api.nova.server_create(IsA(http.HttpRequest),
server.name, server.name,
@ -834,6 +854,7 @@ class InstanceTests(test.TestCase):
customization_script, customization_script,
[sec_group.name], [sec_group.name],
None, None,
nics=nics,
instance_count=IsA(int)) \ instance_count=IsA(int)) \
.AndRaise(self.exceptions.keystone) .AndRaise(self.exceptions.keystone)
@ -849,6 +870,7 @@ class InstanceTests(test.TestCase):
'user_id': self.user.id, 'user_id': self.user.id,
'groups': sec_group.name, 'groups': sec_group.name,
'volume_type': '', 'volume_type': '',
'network': self.networks.first().id,
'count': 1} 'count': 1}
url = reverse('horizon:nova:instances:launch') url = reverse('horizon:nova:instances:launch')
res = self.client.post(url, form_data) res = self.client.post(url, form_data)
@ -856,6 +878,7 @@ class InstanceTests(test.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.quantum: ('network_list',),
api.nova: ('flavor_list', api.nova: ('flavor_list',
'keypair_list', 'keypair_list',
'security_group_list', 'security_group_list',
@ -885,6 +908,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest), api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \ filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False]) .AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \ api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list()) .AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])

View File

@ -18,6 +18,8 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import logging
from django.utils.text import normalize_newlines from django.utils.text import normalize_newlines
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -28,6 +30,9 @@ from horizon import workflows
from horizon.openstack.common import jsonutils from horizon.openstack.common import jsonutils
LOG = logging.getLogger(__name__)
class SelectProjectUserAction(workflows.Action): class SelectProjectUserAction(workflows.Action):
project_id = forms.ChoiceField(label=_("Project")) project_id = forms.ChoiceField(label=_("Project"))
user_id = forms.ChoiceField(label=_("User")) user_id = forms.ChoiceField(label=_("User"))
@ -400,6 +405,46 @@ class PostCreationStep(workflows.Step):
contributes = ("customization_script",) 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): class LaunchInstance(workflows.Workflow):
slug = "launch_instance" slug = "launch_instance"
name = _("Launch Instance") name = _("Launch Instance")
@ -410,6 +455,7 @@ class LaunchInstance(workflows.Workflow):
default_steps = (SelectProjectUser, default_steps = (SelectProjectUser,
SetInstanceDetails, SetInstanceDetails,
SetAccessControls, SetAccessControls,
SetNetwork,
VolumeOptions, VolumeOptions,
PostCreationStep) PostCreationStep)
@ -437,6 +483,13 @@ class LaunchInstance(workflows.Workflow):
else: else:
dev_mapping = None 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: try:
api.nova.server_create(request, api.nova.server_create(request,
context['name'], context['name'],
@ -446,6 +499,7 @@ class LaunchInstance(workflows.Workflow):
normalize_newlines(custom_script), normalize_newlines(custom_script),
context['security_group_ids'], context['security_group_ids'],
dev_mapping, dev_mapping,
nics=nics,
instance_count=int(context['count'])) instance_count=int(context['count']))
return True return True
except: except:

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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,)

View File

@ -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'))

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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,)

View File

@ -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'))

View File

@ -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'

View File

@ -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)

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -0,0 +1,7 @@
{% for ip in ips %}
<ul>
<li>
{{ ip.ip_address }}
</li>
</ul>
{% endfor %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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')))

View File

@ -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

View File

@ -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

View File

@ -23,7 +23,7 @@ class SystemPanels(horizon.PanelGroup):
slug = "syspanel" slug = "syspanel"
name = _("System Panel") name = _("System Panel")
panels = ('overview', 'instances', 'volumes', 'services', 'flavors', panels = ('overview', 'instances', 'volumes', 'services', 'flavors',
'images', 'projects', 'users', 'quotas',) 'images', 'projects', 'users', 'quotas', 'networks',)
class Syspanel(horizon.Dashboard): class Syspanel(horizon.Dashboard):

View File

@ -143,6 +143,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'security_group_list', 'volume_list', 'security_group_list', 'volume_list',
'volume_snapshot_list', 'volume_snapshot_list',
'tenant_quota_usages', 'server_create'), 'tenant_quota_usages', 'server_create'),
api.quantum: ('network_list',),
api.glance: ('image_list_detailed',)}) api.glance: ('image_list_detailed',)})
def test_launch_post(self): def test_launch_post(self):
flavor = self.flavors.first() flavor = self.flavors.first()
@ -155,6 +156,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
device_name = u'vda' device_name = u'vda'
volume_choice = "%s:vol" % volume.id volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice} 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)) \ api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list()) .AndReturn(self.flavors.list())
@ -171,6 +173,8 @@ class InstanceViewTest(test.BaseAdminViewTests):
api.nova.volume_list(IsA(http.HttpRequest)) \ api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list()) .AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([]) 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), api.nova.server_create(IsA(http.HttpRequest),
server.name, server.name,
image.id, image.id,
@ -179,6 +183,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
customization_script, customization_script,
[sec_group.name], [sec_group.name],
block_device_mapping, block_device_mapping,
nics=nics,
instance_count=IsA(int)) instance_count=IsA(int))
self.mox.ReplayAll() self.mox.ReplayAll()
@ -194,6 +199,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'volume_type': 'volume_id', 'volume_type': 'volume_id',
'volume_id': volume_choice, 'volume_id': volume_choice,
'device_name': device_name, 'device_name': device_name,
'network': self.networks.first().id,
'count': 1} 'count': 1}
url = reverse('horizon:syspanel:instances:launch') url = reverse('horizon:syspanel:instances:launch')
res = self.client.post(url, form_data) res = self.client.post(url, form_data)

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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,)

View File

@ -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,)

View File

@ -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'))

View File

@ -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']}

View File

@ -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'

View File

@ -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,)

View File

@ -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'))

View File

@ -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']}

View File

@ -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)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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')))

View File

@ -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')

View File

@ -734,7 +734,7 @@ class DataTableOptions(object):
self.table_actions_template = \ self.table_actions_template = \
'horizon/common/_data_table_table_actions.html' 'horizon/common/_data_table_table_actions.html'
self.context_var_name = unicode(getattr(options, self.context_var_name = unicode(getattr(options,
'context_var_nam', 'context_var_name',
'table')) 'table'))
self.actions_column = getattr(options, self.actions_column = getattr(options,
'actions_column', 'actions_column',

View File

@ -48,7 +48,7 @@
<a href="?{{ table.get_pagination_string }}">More&nbsp;&raquo;</a> <a href="?{{ table.get_pagination_string }}">More&nbsp;&raquo;</a>
{% endif %} {% endif %}
</td> </td>
</td> </tr>
</tfoot> </tfoot>
</table> </table>
{% endwith %} {% endwith %}

View File

@ -2,12 +2,12 @@
<table class="table-fixed"> <table class="table-fixed">
<tbody> <tbody>
<tr> <tr>
<td class="help_text">
{{ step.get_help_text }}
</td>
<td class="actions"> <td class="actions">
{% include "horizon/common/_form_fields.html" %} {% include "horizon/common/_form_fields.html" %}
</td> </td>
<td class="help_text">
{{ step.get_help_text }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -35,6 +35,7 @@ from django.utils import unittest
import glanceclient import glanceclient
from keystoneclient.v2_0 import client as keystone_client from keystoneclient.v2_0 import client as keystone_client
from novaclient.v1_1 import client as nova_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 from selenium.webdriver.firefox.webdriver import WebDriver
import httplib2 import httplib2
@ -290,17 +291,20 @@ class APITestCase(TestCase):
self._original_glanceclient = api.glance.glanceclient self._original_glanceclient = api.glance.glanceclient
self._original_keystoneclient = api.keystone.keystoneclient self._original_keystoneclient = api.keystone.keystoneclient
self._original_novaclient = api.nova.novaclient self._original_novaclient = api.nova.novaclient
self._original_quantumclient = api.quantum.quantumclient
# Replace the clients with our stubs. # Replace the clients with our stubs.
api.glance.glanceclient = lambda request: self.stub_glanceclient() api.glance.glanceclient = lambda request: self.stub_glanceclient()
api.keystone.keystoneclient = fake_keystoneclient api.keystone.keystoneclient = fake_keystoneclient
api.nova.novaclient = lambda request: self.stub_novaclient() api.nova.novaclient = lambda request: self.stub_novaclient()
api.quantum.quantumclient = lambda request: self.stub_quantumclient()
def tearDown(self): def tearDown(self):
super(APITestCase, self).tearDown() super(APITestCase, self).tearDown()
api.glance.glanceclient = self._original_glanceclient api.glance.glanceclient = self._original_glanceclient
api.nova.novaclient = self._original_novaclient api.nova.novaclient = self._original_novaclient
api.keystone.keystoneclient = self._original_keystoneclient api.keystone.keystoneclient = self._original_keystoneclient
api.quantum.quantumclient = self._original_quantumclient
def stub_novaclient(self): def stub_novaclient(self):
if not hasattr(self, "novaclient"): if not hasattr(self, "novaclient"):
@ -320,6 +324,12 @@ class APITestCase(TestCase):
self.glanceclient = self.mox.CreateMock(glanceclient.Client) self.glanceclient = self.mox.CreateMock(glanceclient.Client)
return self.glanceclient 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): def stub_swiftclient(self, expected_calls=1):
if not hasattr(self, "swiftclient"): if not hasattr(self, "swiftclient"):
self.mox.StubOutWithMock(swift_client, 'Connection') self.mox.StubOutWithMock(swift_client, 'Connection')

View File

@ -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)

View File

@ -15,6 +15,7 @@
import glanceclient.exc as glance_exceptions import glanceclient.exc as glance_exceptions
from keystoneclient import exceptions as keystone_exceptions from keystoneclient import exceptions as keystone_exceptions
from novaclient import exceptions as nova_exceptions from novaclient import exceptions as nova_exceptions
from quantumclient.common import exceptions as quantum_exceptions
from .utils import TestDataContainer from .utils import TestDataContainer
@ -53,3 +54,6 @@ def data(TEST):
glance_exception = glance_exceptions.ClientException glance_exception = glance_exceptions.ClientException
TEST.exceptions.glance = create_stubbed_exception(glance_exception) TEST.exceptions.glance = create_stubbed_exception(glance_exception)
quantum_exception = quantum_exceptions.QuantumClientException
TEST.exceptions.quantum = create_stubbed_exception(quantum_exception)

View File

@ -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))

View File

@ -18,6 +18,7 @@ def load_test_data(load_onto=None):
from . import glance_data from . import glance_data
from . import keystone_data from . import keystone_data
from . import nova_data from . import nova_data
from . import quantum_data
from . import swift_data from . import swift_data
# The order of these loaders matters, some depend on others. # The order of these loaders matters, some depend on others.
@ -25,6 +26,7 @@ def load_test_data(load_onto=None):
keystone_data.data, keystone_data.data,
glance_data.data, glance_data.data,
nova_data.data, nova_data.data,
quantum_data.data,
swift_data.data) swift_data.data)
if load_onto: if load_onto:
for data_func in loaders: for data_func in loaders:

View File

@ -22,6 +22,7 @@ from cloudfiles import errors as swiftclient
from glanceclient.common import exceptions as glanceclient from glanceclient.common import exceptions as glanceclient
from keystoneclient import exceptions as keystoneclient from keystoneclient import exceptions as keystoneclient
from novaclient import exceptions as novaclient from novaclient import exceptions as novaclient
from quantumclient.common import exceptions as quantumclient
UNAUTHORIZED = (keystoneclient.Unauthorized, UNAUTHORIZED = (keystoneclient.Unauthorized,
@ -29,12 +30,16 @@ UNAUTHORIZED = (keystoneclient.Unauthorized,
novaclient.Unauthorized, novaclient.Unauthorized,
novaclient.Forbidden, novaclient.Forbidden,
glanceclient.Unauthorized, glanceclient.Unauthorized,
quantumclient.Unauthorized,
quantumclient.Forbidden,
swiftclient.AuthenticationFailed, swiftclient.AuthenticationFailed,
swiftclient.AuthenticationError) swiftclient.AuthenticationError)
NOT_FOUND = (keystoneclient.NotFound, NOT_FOUND = (keystoneclient.NotFound,
novaclient.NotFound, novaclient.NotFound,
glanceclient.NotFound, glanceclient.NotFound,
quantumclient.NetworkNotFoundClient,
quantumclient.PortNotFoundClient,
swiftclient.NoSuchContainer, swiftclient.NoSuchContainer,
swiftclient.NoSuchObject) swiftclient.NoSuchObject)
@ -44,4 +49,12 @@ RECOVERABLE = (keystoneclient.ClientException,
keystoneclient.AuthorizationFailure, keystoneclient.AuthorizationFailure,
novaclient.ClientException, novaclient.ClientException,
glanceclient.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) swiftclient.Error)

View File

@ -701,14 +701,14 @@ form.horizontal fieldset {
.workflow td.actions { .workflow td.actions {
vertical-align: top; vertical-align: top;
width: 308px; width: 308px;
padding-left: 10px; padding-right: 10px;
} }
.workflow td.help_text { .workflow td.help_text {
vertical-align: top; vertical-align: top;
width: 340px; width: 340px;
padding-right: 10px; padding-left: 10px;
border-right: 1px solid #DDD; border-left: 1px solid #DDD;
} }
.workflow fieldset > table { .workflow fieldset > table {

View File

@ -6,6 +6,7 @@ python-cloudfiles
python-glanceclient<2 python-glanceclient<2
python-keystoneclient python-keystoneclient
python-novaclient python-novaclient
python-quantumclient
pytz pytz
# Horizon Utility Requirements # Horizon Utility Requirements