Merge "TrunkPort, Horizon workflow: launch instance"

This commit is contained in:
Jenkins 2017-09-07 09:00:39 +00:00 committed by Gerrit Code Review
commit b624d2f0aa
9 changed files with 334 additions and 33 deletions
openstack_dashboard
api
dashboards/project
instances
static/dashboard/project/workflow/launch-instance
test

@ -148,6 +148,34 @@ class Port(NeutronAPIDictWrapper):
super(Port, self).__init__(apidict)
class PortTrunkParent(Port):
"""Neutron ports that are trunk parents.
There's no need to add extra attributes for a trunk parent, because it
already has 'trunk_details'. See also class PortTrunkSubport.
"""
class PortTrunkSubport(Port):
"""Neutron ports that are trunk subports.
The Neutron API expresses port subtyping information in a surprisingly
complex way. When you see a port with attribute 'trunk_details' you know
it's a trunk parent. But when you see a port without the 'trunk_details'
attribute you can't tell if it's a trunk subport or a regular one without
looking beyond the port's attributes. You must go and check if trunks
(and/or trunk_details of trunk parent ports) refer to this port.
Since this behavior is awfully complex we hide this from the rest of
horizon by introducing types PortTrunkParent and PortTrunkSubport.
"""
def __init__(self, apidict, trunk_subport_info):
for field in ['trunk_id', 'segmentation_type', 'segmentation_id']:
apidict[field] = trunk_subport_info[field]
super(PortTrunkSubport, self).__init__(apidict)
class PortAllowedAddressPair(NeutronAPIDictWrapper):
"""Wrapper for neutron port allowed address pairs."""
@ -1066,6 +1094,45 @@ def port_list(request, **params):
return [Port(p) for p in ports]
@profiler.trace
@memoized
def port_list_with_trunk_types(request, **params):
"""List neutron Ports for this tenant with possible TrunkPort indicated
:param request: request context
NOTE Performing two API calls is not atomic, but this is not worse
than the original idea when we call port_list repeatedly for
each network to perform identification run-time. We should
handle the inconsistencies caused by non-atomic API requests
gracefully.
"""
LOG.debug("port_list_with_trunk_types(): params=%s", params)
ports = neutronclient(request).list_ports(**params)['ports']
trunk_filters = {}
if 'tenant_id' in params:
trunk_filters['tenant_id'] = params['tenant_id']
trunks = neutronclient(request).list_trunks(**trunk_filters)['trunks']
parent_ports = set([t['port_id'] for t in trunks])
# Create a dict map for child ports (port ID to trunk info)
child_ports = dict([(s['port_id'],
{'trunk_id': t['id'],
'segmentation_type': s['segmentation_type'],
'segmentation_id': s['segmentation_id']})
for t in trunks
for s in t['sub_ports']])
def _get_port_info(port):
if port['id'] in parent_ports:
return PortTrunkParent(port)
elif port['id'] in child_ports:
return PortTrunkSubport(port, child_ports[port['id']])
else:
return Port(port)
return [_get_port_info(p) for p in ports]
@profiler.trace
def port_get(request, port_id, **params):
LOG.debug("port_get(): portid=%(port_id)s, params=%(params)s",

@ -133,8 +133,9 @@ class Ports(generic.View):
"""
# see
# https://github.com/openstack/neutron/blob/master/neutron/api/v2/attributes.py
result = api.neutron.port_list(request, **request.GET.dict())
return{'items': [n.to_dict() for n in result]}
result = api.neutron.port_list_with_trunk_types(request,
**request.GET.dict())
return {'items': [n.to_dict() for n in result]}
@urls.register

@ -91,8 +91,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
shared=True) \
.AndReturn(self.networks.list()[1:])
for net in self.networks.list():
api.neutron.port_list(IsA(http.HttpRequest),
network_id=net.id) \
api.neutron.port_list_with_trunk_types(IsA(http.HttpRequest),
network_id=net.id,
tenant_id=self.tenant.id) \
.AndReturn(self.ports.list())
def _mock_nova_lists(self):
@ -1596,7 +1597,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.glance: ('image_list_detailed',),
quotas: ('tenant_limit_usages',)})
@ -1642,8 +1643,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
shared=True) \
.AndReturn(self.networks.list()[1:])
for net in self.networks.list():
api.neutron.port_list(IsA(http.HttpRequest),
network_id=net.id) \
api.neutron.port_list_with_trunk_types(IsA(http.HttpRequest),
network_id=net.id,
tenant_id=self.tenant.id) \
.AndReturn(self.ports.list())
api.nova.extension_supported('DiskConfig',
@ -1846,7 +1848,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.glance: ('image_list_detailed',),
quotas: ('tenant_limit_usages',)})
@ -1888,8 +1890,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
shared=True) \
.AndReturn(self.networks.list()[1:])
for net in self.networks.list():
api.neutron.port_list(IsA(http.HttpRequest),
network_id=net.id) \
api.neutron.port_list_with_trunk_types(IsA(http.HttpRequest),
network_id=net.id,
tenant_id=self.tenant.id) \
.AndReturn(self.ports.list())
api.nova.extension_supported('DiskConfig',
IsA(http.HttpRequest)) \
@ -1936,7 +1939,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_create',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2050,7 +2053,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_create',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2174,7 +2177,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_create',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('server_create',
'extension_supported',
@ -2277,7 +2280,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2356,7 +2359,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_create',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2484,7 +2487,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_create',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2555,7 +2558,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
cinder: ('volume_list',
'volume_snapshot_list',),
@ -2614,7 +2617,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
api.neutron: ('network_list',
'port_create',
'port_delete',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2724,7 +2727,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2805,7 +2808,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -2913,7 +2916,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -3022,7 +3025,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -3081,8 +3084,9 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
shared=True) \
.AndReturn(self.networks.list()[1:])
for net in self.networks.list():
api.neutron.port_list(IsA(http.HttpRequest),
network_id=net.id) \
api.neutron.port_list_with_trunk_types(IsA(http.HttpRequest),
network_id=net.id,
tenant_id=self.tenant.id) \
.AndReturn(self.ports.list())
api.nova.extension_supported(
'DiskConfig', IsA(http.HttpRequest)).AndReturn(True)
@ -3168,7 +3172,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -3383,7 +3387,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.nova: ('extension_supported',
'is_feature_available',
@ -3536,7 +3540,7 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
cinder: ('volume_snapshot_list',
'volume_list',),
api.neutron: ('network_list',
'port_list',
'port_list_with_trunk_types',
'security_group_list',),
api.glance: ('image_list_detailed',),
quotas: ('tenant_limit_usages',)})

@ -190,9 +190,11 @@ def port_field_data(request):
for network in network_list:
ports.extend(
[(port.id, add_more_info_port_name(port))
for port in api.neutron.port_list(request,
network_id=network.id)
if port.device_owner == ''])
for port in api.neutron.port_list_with_trunk_types(
request, network_id=network.id,
tenant_id=request.user.tenant_id)
if (not port.device_owner and
not isinstance(port, api.neutron.PortTrunkSubport))])
ports.sort(key=lambda obj: obj[1])
return ports

@ -514,7 +514,9 @@
if (port.device_owner === "" && port.admin_state === "UP") {
port.subnet_names = getPortSubnets(port, network.subnets);
port.network_name = network.name;
ports.push(port);
if (!port.hasOwnProperty("trunk_id")) {
ports.push(port);
}
}
});
push.apply(model.ports, ports);

@ -582,6 +582,28 @@
expect(model.newInstanceSpec.networks).toEqual(networks);
});
it('getPorts at launch should not return child port', function () {
var ports = [ { id: 'parent',
trunk_details: { trunk_id: 'trunk1',
sub_ports: [ { port_id: 'child' } ] } },
{ id: 'child' },
{ id : 'plain' } ];
var networks = [ { id: 'net-1', subnets: [ { id: 'subnet1' } ] } ];
spyOn(neutronApi, 'getNetworks').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve({ data: { items: networks } });
return deferred.promise;
});
spyOn(neutronApi, 'getPorts').and.callFake(function () {
var deferred = $q.defer();
deferred.resolve({ data: { items: ports } });
return deferred.promise;
});
neutronEnabled = true;
model.initialize(true);
scope.$apply();
});
it('should have the proper entries in allowedBootSources', function() {
model.initialize(true);
scope.$apply();

@ -155,10 +155,10 @@ class NeutronPortsTestCase(test.TestCase):
params = django_request.QueryDict('network_id=%s' %
self._networks[0].id)
request = self.mock_rest_request(GET=params)
client.port_list.return_value = [self._ports[0]]
client.port_list_with_trunk_types.return_value = [self._ports[0]]
response = neutron.Ports().get(request)
self.assertStatusCode(response, 200)
client.port_list.assert_called_once_with(
client.port_list_with_trunk_types.assert_called_once_with(
request, network_id=TEST.api_networks.first().get("id"))

@ -389,6 +389,39 @@ class NeutronApiTests(test.APITestCase):
for p in ret_val:
self.assertIsInstance(p, api.neutron.Port)
def test_port_list_with_trunk_types(self):
ports = self.api_tp_ports.list()
trunks = self.api_tp_trunks.list()
neutronclient = self.stub_neutronclient()
neutronclient.list_ports().AndReturn({'ports': ports})
neutronclient.list_trunks().AndReturn({'trunks': trunks})
self.mox.ReplayAll()
expected_parent_port_ids = set()
expected_subport_ids = set()
for trunk in trunks:
expected_parent_port_ids.add(trunk['port_id'])
expected_subport_ids |= set([p['port_id'] for p
in trunk['sub_ports']])
expected_normal_port_ids = ({p['id'] for p in ports}
- expected_parent_port_ids
- expected_subport_ids)
ret_val = api.neutron.port_list_with_trunk_types(self.request)
self.assertEqual(len(ports), len(ret_val))
parent_port_ids = {p.id for p in ret_val
if isinstance(p, api.neutron.PortTrunkParent)}
subport_ids = {p.id for p in ret_val
if isinstance(p, api.neutron.PortTrunkSubport)}
normal_port_ids = ({p.id for p in ret_val}
- parent_port_ids - subport_ids)
self.assertEqual(expected_parent_port_ids, parent_port_ids)
self.assertEqual(expected_subport_ids, subport_ids)
self.assertEqual(expected_normal_port_ids, normal_port_ids)
def test_port_get(self):
port = {'port': self.api_ports.first()}
port_id = self.api_ports.first()['id']

@ -45,6 +45,7 @@ def data(TEST):
TEST.neutron_quota_usages = utils.TestDataContainer()
TEST.ip_availability = utils.TestDataContainer()
TEST.qos_policies = utils.TestDataContainer()
TEST.tp_ports = utils.TestDataContainer()
# Data return by neutronclient.
TEST.api_agents = utils.TestDataContainer()
@ -65,6 +66,8 @@ def data(TEST):
TEST.api_extensions = utils.TestDataContainer()
TEST.api_ip_availability = utils.TestDataContainer()
TEST.api_qos_policies = utils.TestDataContainer()
TEST.api_tp_trunks = utils.TestDataContainer()
TEST.api_tp_ports = utils.TestDataContainer()
# 1st network.
network_dict = {'admin_state_up': True,
@ -709,3 +712,170 @@ def data(TEST):
'tenant_id': '1'}
TEST.api_qos_policies.add(policy_dict1)
TEST.qos_policies.add(neutron.QoSPolicy(policy_dict1))
# TRUNKPORT
#
# The test setup was created by the following command sequence:
# openstack network create tst
# openstack subnet create tstsub --network tst\
# --subnet-range 10.10.16.128/26
# openstack network create tstalt
# openstack subnet create tstaltsub --network tstalt\
# --subnet-range 10.10.17.128/26
# openstack port create --network tst plain
# openstack port create --network tst parent
# openstack port create --network tst child1
# openstack port create --network tstalt child2
# openstack network trunk create --parent-port parent trunk
# openstack network trunk set\
# --subport port=child1,segmentation-type=vlan,segmentation-id=100 trunk
# openstack network trunk set\
# --subport port=child2,segmentation-type=vlan,segmentation-id=200 trunk
# ids/uuids are captured from a live setup.
# This collection holds the test setup.
tdata = {'tenant_id': '19c9123a944644cb9e923497a018d0b7',
'trunk_id': '920625a3-13de-46b4-b6c9-8b35f29b3cfe',
'security_group': '3fd8c007-9093-4aa3-b475-a0c178d4e1e4',
'tag_1': 100,
'tag_2': 200,
'net': {'tst_id': '5a340332-cc92-42aa-8980-15f47c0d0f3d',
'tstalt_id': '0fb41ffd-3933-4da4-8a83-025d328aedf3'},
'subnet': {'tst_id': '0b883baf-5a21-4605-ab56-229a24ec585b',
'tstalt_id': '0e184cf2-97dc-4738-b4b3-1871faf5d685'},
'child1': {'id': '9c151ffb-d7a6-4f15-8eae-d0950999fdfe',
'ip': '10.10.16.140',
'mac': 'fa:16:3e:22:63:6f',
'device_id': '279989f7-54bb-41d9-ba42-0d61f12fda61'},
'child2': {'id': 'cedb145f-c163-4630-98a3-e1990744bdef',
'ip': '10.10.17.137',
'mac': 'fa:16:3e:0d:ca:eb',
'device_id': '9872faaa-b2b2-eeee-9911-21332eedaa77'},
'parent': {'id': '5b27429d-048b-40fa-88f9-8e2c4ff7d28b',
'ip': '10.10.16.141',
'mac': 'fa:16:3e:ab:a8:22',
'device_id': 'af75c8e5-a1cc-4567-8d04-44fcd6922890'},
'plain': {'id': 'bc04da56-d7fc-461e-b95d-a2c66e77ad9a',
'ip': '10.10.16.135',
'mac': 'fa:16:3e:9c:d5:7f',
'device_id': '7180cede-bcd8-4334-b19f-f7ef2f331f53'}}
# network tst
# trunk
tp_trunk_dict = {
'status': 'UP',
'sub_ports': [{'segmentation_type': 'vlan',
'segmentation_id': tdata['tag_1'],
'port_id': tdata['child1']['id']},
{'segmentation_type': u'vlan',
'segmentation_id': tdata['tag_2'],
'port_id': tdata['child2']['id']}],
'name': 'trunk',
'admin_state_up': True,
'tenant_id': tdata['tenant_id'],
'project_id': tdata['tenant_id'],
'port_id': tdata['parent']['id'],
'id': tdata['trunk_id']
}
TEST.api_tp_trunks.add(tp_trunk_dict)
# port parent
parent_port_dict = {
'admin_state_up': True,
'device_id': tdata['parent']['device_id'],
'device_owner': 'compute:nova',
'fixed_ips': [{'ip_address': tdata['parent']['ip'],
'subnet_id': tdata['subnet']['tst_id']}],
'id': tdata['parent']['id'],
'mac_address': tdata['parent']['mac'],
'name': 'parent',
'network_id': tdata['net']['tst_id'],
'status': 'ACTIVE',
'tenant_id': tdata['tenant_id'],
'binding:vnic_type': 'normal',
'binding:host_id': 'host',
'security_groups': [tdata['security_group']],
'trunk_details': {
'sub_ports': [{'segmentation_type': 'vlan',
'mac_address': tdata['child1']['mac'],
'segmentation_id': tdata['tag_1'],
'port_id': tdata['child1']['id']},
{'segmentation_type': 'vlan',
'mac_address': tdata['child2']['mac'],
'segmentation_id': tdata['tag_2'],
'port_id': tdata['child2']['id']}],
'trunk_id': tdata['trunk_id']}
}
TEST.api_tp_ports.add(parent_port_dict)
TEST.tp_ports.add(neutron.PortTrunkParent(parent_port_dict))
# port child1
child1_port_dict = {
'admin_state_up': True,
'device_id': tdata['child1']['device_id'],
'device_owner': 'compute:nova',
'fixed_ips': [{'ip_address': tdata['child1']['ip'],
'subnet_id': tdata['subnet']['tst_id']}],
'id': tdata['child1']['id'],
'mac_address': tdata['child1']['mac'],
'name': 'child1',
'network_id': tdata['net']['tst_id'],
'status': 'ACTIVE',
'tenant_id': tdata['tenant_id'],
'binding:vnic_type': 'normal',
'binding:host_id': 'host',
'security_groups': [tdata['security_group']]
}
TEST.api_tp_ports.add(child1_port_dict)
TEST.tp_ports.add(neutron.PortTrunkSubport(
child1_port_dict,
{'trunk_id': tdata['trunk_id'],
'segmentation_type': 'vlan',
'segmentation_id': tdata['tag_1']}))
# port plain
port_dict = {
'admin_state_up': True,
'device_id': tdata['plain']['device_id'],
'device_owner': 'compute:nova',
'fixed_ips': [{'ip_address': tdata['plain']['ip'],
'subnet_id': tdata['subnet']['tst_id']}],
'id': tdata['plain']['id'],
'mac_address': tdata['plain']['mac'],
'name': 'plain',
'network_id': tdata['net']['tst_id'],
'status': 'ACTIVE',
'tenant_id': tdata['tenant_id'],
'binding:vnic_type': 'normal',
'binding:host_id': 'host',
'security_groups': [tdata['security_group']]
}
TEST.api_tp_ports.add(port_dict)
TEST.tp_ports.add(neutron.Port(port_dict))
# network tstalt
# port child2
child2_port_dict = {
'admin_state_up': True,
'device_id': tdata['child2']['device_id'],
'device_owner': 'compute:nova',
'fixed_ips': [{'ip_address': tdata['child2']['ip'],
'subnet_id': tdata['subnet']['tstalt_id']}],
'id': tdata['child2']['id'],
'mac_address': tdata['child2']['mac'],
'name': 'child2',
'network_id': tdata['net']['tstalt_id'],
'status': 'ACTIVE',
'tenant_id': tdata['tenant_id'],
'binding:vnic_type': 'normal',
'binding:host_id': 'host',
'security_groups': [tdata['security_group']]
}
TEST.api_tp_ports.add(child2_port_dict)
TEST.tp_ports.add(neutron.PortTrunkSubport(
child2_port_dict,
{'trunk_id': tdata['trunk_id'],
'segmentation_type': 'vlan',
'segmentation_id': tdata['tag_2']}))