Specify tenant_id when retrieving network list from Quantum.

Fixes bug 1036153

If a user has admin role, network list returned by Quantum API contains
networks that does not belong to that tenant. As a result networks owned
by other tenants are listed in "Network" tab of "Launch Intance" menu.
Thus we need to specify tenant_id when calling network_list() to
retrieve only the networks available for the tenant.

In addition this commit added a validation logic to syspanel instance
launching workflow. It checks whether the owner tenant of the requested
network(s) matches the tenant specified in the launching panel.

Change-Id: Ieadf33f0a41247b126669f2f1f2c1d29be01e4e9
This commit is contained in:
Akihiro MOTOKI
2012-08-14 09:33:10 +09:00
parent 647e8a5d27
commit 55319097cc
4 changed files with 173 additions and 13 deletions

View File

@@ -605,7 +605,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(quota_usages)
@@ -672,7 +673,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())
@@ -738,7 +740,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
@@ -793,7 +796,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.tenant_quota_usages(IsA(http.HttpRequest)) \
.AndReturn(self.quota_usages.first())
@@ -843,7 +847,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.volume_list(IgnoreArg()).AndReturn(self.volumes.list())
api.nova.server_create(IsA(http.HttpRequest),
@@ -908,7 +913,8 @@ class InstanceTests(test.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id}) \
.AndReturn([[], False])
api.quantum.network_list(IsA(http.HttpRequest)) \
api.quantum.network_list(IsA(http.HttpRequest),
tenant_id=self.tenant.id) \
.AndReturn(self.networks.list())
api.nova.volume_list(IsA(http.HttpRequest)) \
.AndReturn(self.volumes.list())

View File

@@ -96,8 +96,8 @@ class VolumeOptionsAction(workflows.Action):
volume_opt = cleaned_data.get('volume_type', None)
if volume_opt and not cleaned_data[volume_opt]:
raise forms.ValidationError('Please choose a volume, or select '
'%s.' % self.VOLUME_CHOICES[0][1])
raise forms.ValidationError(_('Please choose a volume, or select '
'%s.') % self.VOLUME_CHOICES[0][1])
return cleaned_data
def _get_volume_display_name(self, volume):
@@ -419,7 +419,12 @@ class SetNetworkAction(workflows.Action):
def populate_network_choices(self, request, context):
try:
networks = api.quantum.network_list(request)
# 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)
for n in networks:
n.set_id_as_name_if_empty()
network_list = [(network.id, network.name) for network in networks]

View File

@@ -23,6 +23,8 @@ from novaclient import exceptions as novaclient_exceptions
from horizon import api
from horizon import test
import json
class InstanceViewTest(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('flavor_list', 'server_list',),
@@ -143,6 +145,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'security_group_list', 'volume_list',
'volume_snapshot_list',
'tenant_quota_usages', 'server_create'),
api.keystone: ('tenant_list',),
api.quantum: ('network_list',),
api.glance: ('image_list_detailed',)})
def test_launch_post(self):
@@ -156,7 +159,11 @@ class InstanceViewTest(test.BaseAdminViewTests):
device_name = u'vda'
volume_choice = "%s:vol" % volume.id
block_device_mapping = {device_name: u"%s::0" % volume_choice}
nics = [{"net-id": self.networks.first().id, "v4-fixed-ip": ''}]
network = self.networks.first()
nics = [{"net-id": network.id, "v4-fixed-ip": ''}]
selected_network = json.dumps({'id': network.id,
'name': network.name,
'tenant_id': network.tenant_id})
api.nova.flavor_list(IsA(http.HttpRequest)) \
.AndReturn(self.flavors.list())
@@ -175,6 +182,8 @@ class InstanceViewTest(test.BaseAdminViewTests):
api.nova.volume_snapshot_list(IsA(http.HttpRequest)).AndReturn([])
api.quantum.network_list(IsA(http.HttpRequest)) \
.AndReturn(self.networks.list())
api.keystone.tenant_list(IsA(http.HttpRequest), admin=True) \
.AndReturn(self.tenants.list())
api.nova.server_create(IsA(http.HttpRequest),
server.name,
image.id,
@@ -199,7 +208,7 @@ class InstanceViewTest(test.BaseAdminViewTests):
'volume_type': 'volume_id',
'volume_id': volume_choice,
'device_name': device_name,
'network': self.networks.first().id,
'network': [selected_network],
'count': 1}
url = reverse('horizon:syspanel:instances:launch')
res = self.client.post(url, form_data)

View File

@@ -14,8 +14,148 @@
# License for the specific language governing permissions and limitations
# under the License.
from horizon.dashboards.nova.instances.workflows import LaunchInstance
import json
import logging
from django.utils.text import normalize_newlines
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 messages
from horizon.dashboards.nova.instances import workflows as proj_workflows
class AdminLaunchInstance(LaunchInstance):
LOG = logging.getLogger(__name__)
class AdminSetNetworkAction(proj_workflows.SetNetworkAction):
class Meta:
name = _("Networking")
permissions = ('openstack.services.network',)
help_text = _("Select networks for your instance.")
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 populate_network_choices(self, request, context):
try:
networks = api.quantum.network_list(request)
tenant_dict = self._get_tenant_list()
network_list = []
for network in networks:
# To distinguish which network belonging to which tenant,
# add tenant name as a prefix.
network.set_id_as_name_if_empty()
tenant_name = tenant_dict[network.tenant_id].name
name = '%s:%s' % (tenant_name, network.name)
# Encode as JSON to send network info as dict.
key = json.dumps({'id': network.id,
'name': network.name,
'tenant_id': network.tenant_id})
network_list.append((key, name))
except:
network_list = []
exceptions.handle(request,
_('Unable to retrieve networks.'))
return network_list
class AdminSetNetwork(proj_workflows.SetNetwork):
action_class = AdminSetNetworkAction
contributes = ("networks",)
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.
# In syspanel each element of networks is JSON-encoded dict.
networks = [json.loads(n) for n in networks if n != '']
if networks:
context['networks'] = networks
return context
class AdminLaunchInstance(proj_workflows.LaunchInstance):
success_url = "horizon:syspanel:instances:index"
default_steps = (proj_workflows.SelectProjectUser,
proj_workflows.SetInstanceDetails,
proj_workflows.SetAccessControls,
AdminSetNetwork,
proj_workflows.VolumeOptions,
proj_workflows.PostCreationStep)
def validate(self, context):
project_id = context.get('project_id')
networks = context.get('networks', [])
for n in networks:
if n['tenant_id'] != project_id:
msg = _("Selected network '%s' is not owned "
"by the specified project") % n['name']
# NOTE(amotoki):
# I tryied to raise WorkflowValidationError according to
# the docstring of horizon.workflows.base.validate(),
# but exception seems not to be caught appropriately
# and exception message passsed was lost.
# Thus I use messages.error() and then return False.
#raise exceptions.WorkflowValidationError(msg)
messages.error(self.request, msg)
return False
return True
def handle(self, request, context):
custom_script = context.get('customization_script', '')
# Determine volume mapping options
if context.get('volume_type', None):
if(context['delete_on_terminate']):
del_on_terminate = 1
else:
del_on_terminate = 0
mapping_opts = ("%s::%s"
% (context['volume_id'], del_on_terminate))
dev_mapping = {context['device_name']: mapping_opts}
else:
dev_mapping = None
netids = context.get('networks', None)
if netids:
nics = [{"net-id": netid['id'], "v4-fixed-ip": ""}
for netid in netids]
else:
nics = None
try:
# NOTE(amotoki): (well known bug?)
# project_id specified in launching panel is not referred and
# an instance is launched in the current project.
# In addition, api.nova.server_create() does not provide
# a method to launch an instance by different project/user.
api.nova.server_create(request,
context['name'],
context['source_id'],
context['flavor'],
context['keypair_id'],
normalize_newlines(custom_script),
context['security_group_ids'],
dev_mapping,
nics=nics,
instance_count=int(context['count']))
return True
except:
exceptions.handle(request)
return False