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:
@@ -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())
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user