Add Quantum public network support.

Implements blueprint quantum-public-network.

Change-Id: I975532468b95c9198091ebf85c475d341d7e861a
This commit is contained in:
Akihiro MOTOKI 2012-08-22 16:07:00 +09:00
parent b177299953
commit 7d2e14cf84
14 changed files with 211 additions and 63 deletions

View File

@ -50,7 +50,8 @@ class QuantumAPIDictWrapper(APIDictWrapper):
class Network(QuantumAPIDictWrapper):
"""Wrapper for quantum Networks"""
_attrs = ['name', 'id', 'subnets', 'tenant_id', 'status', 'admin_state_up']
_attrs = ['name', 'id', 'subnets', 'tenant_id', 'status',
'admin_state_up', 'shared']
def __init__(self, apiresource):
apiresource['admin_state'] = \
@ -110,6 +111,27 @@ def network_list(request, **params):
return [Network(n) for n in networks]
def network_list_for_tenant(request, tenant_id, **params):
"""Return a network list available for the tenant.
The list contains networks owned by the tenant and public networks.
If requested_networks specified, it searches requested_networks only.
"""
LOG.debug("network_list_for_tenant(): tenant_id=%s, params=%s"
% (tenant_id, params))
# If a user has admin role, network list returned by Quantum API
# contains networks that do not belong to that tenant.
# So we need to specify tenant_id when calling network_list().
networks = network_list(request, tenant_id=tenant_id,
shared=False, **params)
# In the current Quantum API, there is no way to retrieve
# both owner networks and public networks in a single API call.
networks += network_list(request, shared=True, **params)
return 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,

View File

@ -608,8 +608,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(quota_usages)
api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -678,8 +682,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
@ -747,8 +755,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
api.nova.keypair_list(IsA(http.HttpRequest)) \
@ -805,8 +817,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(self.quota_usages.first())
api.nova.flavor_list(IsA(http.HttpRequest)) \
@ -858,8 +874,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
api.nova.server_create(IsA(http.HttpRequest),
server.name,
@ -926,8 +946,12 @@ class InstanceTests(test.TestCase):
'status': 'active'}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
tenant_id=self.tenant.id,
shared=False) \
.AndReturn(self.networks.list()[:1])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True) \
.AndReturn(self.networks.list()[1:])
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])

View File

@ -421,12 +421,8 @@ class SetNetworkAction(workflows.Action):
def populate_network_choices(self, request, context):
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(request,
tenant_id=tenant_id)
networks = api.quantum.network_list_for_tenant(request, tenant_id)
for n in networks:
n.set_id_as_name_if_empty()
network_list = [(network.id, network.name) for network in networks]

View File

@ -16,7 +16,7 @@
import logging
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import api
@ -27,7 +27,19 @@ from horizon import tables
LOG = logging.getLogger(__name__)
class DeleteSubnet(tables.DeleteAction):
class CheckNetworkEditable(object):
"""Mixin class to determine the specified network is editable."""
def allowed(self, request, datum=None):
# Only administrator is allowed to create and manage subnets
# on shared networks.
network = self.table._get_network()
if network.shared:
return False
return True
class DeleteSubnet(CheckNetworkEditable, tables.DeleteAction):
data_type_singular = _("Subnet")
data_type_plural = _("Subnets")
@ -43,7 +55,7 @@ class DeleteSubnet(tables.DeleteAction):
exceptions.handle(request, msg, redirect=redirect)
class CreateSubnet(tables.LinkAction):
class CreateSubnet(CheckNetworkEditable, tables.LinkAction):
name = "create"
verbose_name = _("Create Subnet")
url = "horizon:nova:networks:addsubnet"
@ -54,7 +66,7 @@ class CreateSubnet(tables.LinkAction):
return reverse(self.url, args=(network_id,))
class UpdateSubnet(tables.LinkAction):
class UpdateSubnet(CheckNetworkEditable, tables.LinkAction):
name = "update"
verbose_name = _("Edit Subnet")
url = "horizon:nova:networks:editsubnet"
@ -71,6 +83,20 @@ class SubnetsTable(tables.DataTable):
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"))
failure_url = reverse_lazy('horizon:nova:networks:index')
def _get_network(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
class Meta:
name = "subnets"

View File

@ -17,6 +17,7 @@ import logging
from django import template
from django.core.urlresolvers import reverse
from django.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _
from horizon import api
@ -27,7 +28,17 @@ from horizon import tables
LOG = logging.getLogger(__name__)
class DeleteNetwork(tables.DeleteAction):
class CheckNetworkEditable(object):
"""Mixin class to determine the specified network is editable."""
def allowed(self, request, datum=None):
# Only administrator is allowed to create and manage shared networks.
if datum and datum.shared:
return False
return True
class DeleteNetwork(CheckNetworkEditable, tables.DeleteAction):
data_type_singular = _("Network")
data_type_plural = _("Networks")
@ -57,14 +68,14 @@ class CreateNetwork(tables.LinkAction):
classes = ("ajax-modal", "btn-create")
class EditNetwork(tables.LinkAction):
class EditNetwork(CheckNetworkEditable, tables.LinkAction):
name = "update"
verbose_name = _("Edit Network")
url = "horizon:nova:networks:update"
classes = ("ajax-modal", "btn-edit")
class CreateSubnet(tables.LinkAction):
class CreateSubnet(CheckNetworkEditable, tables.LinkAction):
name = "subnet"
verbose_name = _("Add Subnet")
url = "horizon:nova:networks:addsubnet"
@ -83,6 +94,8 @@ class NetworksTable(tables.DataTable):
link='horizon:nova:networks:detail')
subnets = tables.Column(get_subnets,
verbose_name=_("Subnets Associated"),)
shared = tables.Column("shared", verbose_name=_("Shared"),
filters=(filters.yesno, filters.capfirst))
status = tables.Column("status", verbose_name=_("Status"))
admin_state = tables.Column("admin_state",
verbose_name=_("Admin State"))

View File

@ -14,5 +14,7 @@
<dd>{{ network.status|default:"Unknown" }}</dd>
<dt>{% trans "Admin State" %}</dt>
<dd>{{ network.admin_state|default:"Unknown" }}</dd>
<dt>{% trans "Shared" %}</dt>
<dd>{{ network.shared|yesno|capfirst }}</dd>
</dl>
</div>

View File

@ -34,9 +34,13 @@ 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())
api.quantum.network_list(
IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).AndReturn(self.networks.list())
api.quantum.network_list(
IsA(http.HttpRequest),
shared=True).AndReturn([])
self.mox.ReplayAll()
@ -48,10 +52,10 @@ class NetworkTests(test.TestCase):
@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)
api.quantum.network_list(
IsA(http.HttpRequest),
tenant_id=self.tenant.id,
shared=False).AndRaise(self.exceptions.quantum)
self.mox.ReplayAll()
res = self.client.get(INDEX_URL)
@ -71,6 +75,8 @@ class NetworkTests(test.TestCase):
.AndReturn([self.subnets.first()])
api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
.AndReturn([self.ports.first()])
api.quantum.network_get(IsA(http.HttpRequest), network_id)\
.AndReturn(self.networks.first())
self.mox.ReplayAll()
@ -90,11 +96,6 @@ class NetworkTests(test.TestCase):
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])
@ -114,6 +115,9 @@ class NetworkTests(test.TestCase):
AndRaise(self.exceptions.quantum)
api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
AndReturn([self.ports.first()])
# Called from SubnetTable
api.quantum.network_get(IsA(http.HttpRequest), network_id).\
AndReturn(self.networks.first())
self.mox.ReplayAll()
@ -137,6 +141,9 @@ class NetworkTests(test.TestCase):
AndReturn([self.subnets.first()])
api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id).\
AndRaise(self.exceptions.quantum)
# Called from SubnetTable
api.quantum.network_get(IsA(http.HttpRequest), network_id).\
AndReturn(self.networks.first())
self.mox.ReplayAll()
@ -402,8 +409,12 @@ class NetworkTests(test.TestCase):
def test_delete_network_no_subnet(self):
network = self.networks.first()
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=network.tenant_id)\
tenant_id=network.tenant_id,
shared=False)\
.AndReturn([network])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True)\
.AndReturn([])
api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
.AndReturn([])
api.quantum.network_delete(IsA(http.HttpRequest), network.id)
@ -423,8 +434,11 @@ class NetworkTests(test.TestCase):
network = self.networks.first()
subnet = self.subnets.first()
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=network.tenant_id)\
tenant_id=network.tenant_id,
shared=False)\
.AndReturn([network])
api.quantum.network_list(IsA(http.HttpRequest), shared=True)\
.AndReturn([])
api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
.AndReturn([subnet])
api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
@ -445,8 +459,12 @@ class NetworkTests(test.TestCase):
network = self.networks.first()
subnet = self.subnets.first()
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=network.tenant_id)\
tenant_id=network.tenant_id,
shared=False)\
.AndReturn([network])
api.quantum.network_list(IsA(http.HttpRequest),
shared=True)\
.AndReturn([])
api.quantum.subnet_list(IsA(http.HttpRequest), network_id=network.id)\
.AndReturn([subnet])
api.quantum.subnet_delete(IsA(http.HttpRequest), subnet.id)
@ -686,6 +704,7 @@ class NetworkTests(test.TestCase):
@test.create_stubs({api.quantum: ('subnet_delete',
'subnet_list',
'network_get',
'port_list',)})
def test_subnet_delete(self):
subnet = self.subnets.first()
@ -693,8 +712,13 @@ class NetworkTests(test.TestCase):
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.network_get(IsA(http.HttpRequest), network_id)\
.AndReturn(self.networks.first())
api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
.AndReturn([self.ports.first()])
# Called from SubnetTable
api.quantum.network_get(IsA(http.HttpRequest), network_id)\
.AndReturn(self.networks.first())
self.mox.ReplayAll()
formData = {'action': 'subnets__delete__%s' % subnet.id}
@ -706,16 +730,22 @@ class NetworkTests(test.TestCase):
@test.create_stubs({api.quantum: ('subnet_delete',
'subnet_list',
'network_get',
'port_list',)})
def test_subnet_delete_exception(self):
def test_subnet_delete_excceeption(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.network_get(IsA(http.HttpRequest), network_id)\
.AndReturn(self.networks.first())
api.quantum.port_list(IsA(http.HttpRequest), network_id=network_id)\
.AndReturn([self.ports.first()])
# Called from SubnetTable
api.quantum.network_get(IsA(http.HttpRequest), network_id)\
.AndReturn(self.networks.first())
self.mox.ReplayAll()
formData = {'action': 'subnets__delete__%s' % subnet.id}

View File

@ -44,12 +44,9 @@ class IndexView(tables.DataTableView):
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)
networks = api.quantum.network_list_for_tenant(self.request,
tenant_id)
except:
networks = []
msg = _('Network list can not be retrieved.')
@ -104,9 +101,9 @@ class DetailView(tables.MultiTableView):
def get_subnets_data(self):
try:
network_id = self.kwargs['network_id']
network = self._get_data()
subnets = api.quantum.subnet_list(self.request,
network_id=network_id)
network_id=network.id)
except:
subnets = []
msg = _('Subnet list can not be retrieved.')

View File

@ -24,8 +24,6 @@ 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__)
@ -35,6 +33,8 @@ class CreateNetwork(forms.SelfHandlingForm):
label=_("Name"),
required=False)
tenant_id = forms.ChoiceField(label=_("Project"))
shared = forms.BooleanField(label=_("Shared"),
initial=False, required=False)
@classmethod
def _instantiate(cls, request, *args, **kwargs):
@ -52,7 +52,8 @@ class CreateNetwork(forms.SelfHandlingForm):
try:
network = api.quantum.network_create(request,
name=data['name'],
tenant_id=data['tenant_id'])
tenant_id=data['tenant_id'],
shared=data['shared'])
msg = _('Network %s was successfully created.') % data['name']
LOG.debug(msg)
messages.success(request, msg)
@ -63,5 +64,26 @@ class CreateNetwork(forms.SelfHandlingForm):
exceptions.handle(request, msg, redirect=redirect)
class UpdateNetwork(user_forms.UpdateNetwork):
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'}))
shared = forms.BooleanField(label=_("Shared"), required=False)
failure_url = 'horizon:syspanel:networks:index'
def handle(self, request, data):
try:
network = api.quantum.network_modify(request, data['network_id'],
name=data['name'],
shared=data['shared'])
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

@ -17,6 +17,7 @@
import logging
from django.core.urlresolvers import reverse
from django.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _
from horizon import api
@ -68,6 +69,8 @@ class NetworksTable(tables.DataTable):
link='horizon:syspanel:networks:detail')
subnets = tables.Column(get_subnets,
verbose_name=_("Subnets Associated"),)
shared = tables.Column("shared", verbose_name=_("Shared"),
filters=(filters.yesno, filters.capfirst))
status = tables.Column("status", verbose_name=_("Status"))
admin_state = tables.Column("admin_state",
verbose_name=_("Admin State"))

View File

@ -167,12 +167,13 @@ class NetworkTests(test.BaseAdminViewTests):
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)\
tenant_id=tenant_id, shared=True)\
.AndReturn(network)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name}
'name': network.name,
'shared': True}
url = reverse('horizon:syspanel:networks:create')
res = self.client.post(url, form_data)
@ -188,12 +189,13 @@ class NetworkTests(test.BaseAdminViewTests):
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)\
tenant_id=tenant_id, shared=False)\
.AndRaise(self.exceptions.quantum)
self.mox.ReplayAll()
form_data = {'tenant_id': tenant_id,
'name': network.name}
'name': network.name,
'shared': False}
url = reverse('horizon:syspanel:networks:create')
res = self.client.post(url, form_data)
@ -232,7 +234,7 @@ class NetworkTests(test.BaseAdminViewTests):
def test_network_update_post(self):
network = self.networks.first()
api.quantum.network_modify(IsA(http.HttpRequest), network.id,
name=network.name)\
name=network.name, shared=True)\
.AndReturn(network)
api.quantum.network_get(IsA(http.HttpRequest), network.id)\
.AndReturn(network)
@ -240,7 +242,8 @@ class NetworkTests(test.BaseAdminViewTests):
formData = {'network_id': network.id,
'name': network.name,
'tenant_id': network.tenant_id}
'tenant_id': network.tenant_id,
'shared': True}
url = reverse('horizon:syspanel:networks:update', args=[network.id])
res = self.client.post(url, formData)
@ -251,7 +254,7 @@ class NetworkTests(test.BaseAdminViewTests):
def test_network_update_post_exception(self):
network = self.networks.first()
api.quantum.network_modify(IsA(http.HttpRequest), network.id,
name=network.name)\
name=network.name, shared=False)\
.AndRaise(self.exceptions.quantum)
api.quantum.network_get(IsA(http.HttpRequest), network.id)\
.AndReturn(network)
@ -259,7 +262,8 @@ class NetworkTests(test.BaseAdminViewTests):
form_data = {'network_id': network.id,
'name': network.name,
'tenant_id': network.tenant_id}
'tenant_id': network.tenant_id,
'shared': False}
url = reverse('horizon:syspanel:networks:update', args=[network.id])
res = self.client.post(url, form_data)

View File

@ -131,3 +131,10 @@ class UpdateView(user_views.UpdateView):
form_class = UpdateNetwork
template_name = 'syspanel/networks/update.html'
success_url = reverse_lazy('horizon:syspanel:networks:index')
def get_initial(self):
network = self._get_object()
return {'network_id': network['id'],
'tenant_id': network['tenant_id'],
'name': network['name'],
'shared': network['shared']}

View File

@ -36,7 +36,8 @@ def data(TEST):
'name': 'net1',
'status': 'ACTIVE',
'subnets': ['e8abc972-eb0c-41f1-9edd-4bc6e3bcd8c9'],
'tenant_id': '1'}
'tenant_id': '1',
'shared': False}
subnet_dict = {'allocation_pools': [{'end': '10.0.0.254',
'start': '10.0.0.2'}],
'cidr': '10.0.0.0/24',
@ -75,7 +76,8 @@ def data(TEST):
'name': 'net2',
'status': 'ACTIVE',
'subnets': ['3f7c5d79-ee55-47b0-9213-8e669fb03009'],
'tenant_id': '2'}
'tenant_id': '2',
'shared': True}
subnet_dict = {'allocation_pools': [{'end': '172.16.88.254',
'start': '172.16.88.2'}],
'cidr': '172.16.88.0/24',