api: Reject duplicate port IDs in server create

Specifying a duplicate port ID is currently "allowed" but results in an
integrity error when nova attempts to create a duplicate
'VirtualInterface' entry. Start rejecting these requests by checking for
duplicate IDs and rejecting offending requests. This is arguably an API
change because there isn't a HTTP 5xx error (server create is an async
operation), however, users shouldn't have to opt in to non-broken
behavior and the underlying instance was never actually created
previously, meaning automation that relied on this "feature" was always
going to fail in a later step. We're also silently failing to do what
the user asked (per flow chart at [1]).

[1] https://docs.openstack.org/nova/latest/contributor/microversions.html#when-do-i-need-a-new-microversion

Change-Id: Ie90fb83662dd06e7188f042fc6340596f93c5ef9
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
Closes-Bug: #1821088
This commit is contained in:
Stephen Finucane 2022-01-31 12:56:57 +00:00
parent 452913a284
commit 9fe4654273
3 changed files with 56 additions and 16 deletions

View File

@ -409,6 +409,7 @@ class ServersController(wsgi.Controller):
networks = [] networks = []
network_uuids = [] network_uuids = []
port_uuids = []
for network in requested_networks: for network in requested_networks:
request = objects.NetworkRequest() request = objects.NetworkRequest()
try: try:
@ -417,18 +418,31 @@ class ServersController(wsgi.Controller):
# it will use one of the available IP address from the network # it will use one of the available IP address from the network
request.address = network.get('fixed_ip', None) request.address = network.get('fixed_ip', None)
request.port_id = network.get('port', None) request.port_id = network.get('port', None)
request.tag = network.get('tag', None) request.tag = network.get('tag', None)
if request.port_id: if request.port_id:
request.network_id = None if request.port_id in port_uuids:
if request.address is not None: msg = _(
msg = _("Specified Fixed IP '%(addr)s' cannot be used " "Port ID '%(port)s' was specified twice: you "
"with port '%(port)s': the two cannot be " "cannot attach a port multiple times."
"specified together.") % { ) % {
"addr": request.address, "port": request.port_id,
"port": request.port_id} }
raise exc.HTTPBadRequest(explanation=msg) raise exc.HTTPBadRequest(explanation=msg)
if request.address is not None:
msg = _(
"Specified Fixed IP '%(addr)s' cannot be used "
"with port '%(port)s': the two cannot be "
"specified together."
) % {
"addr": request.address,
"port": request.port_id,
}
raise exc.HTTPBadRequest(explanation=msg)
request.network_id = None
port_uuids.append(request.port_id)
else: else:
request.network_id = network['uuid'] request.network_id = network['uuid']
self._validate_network_id( self._validate_network_id(

View File

@ -389,17 +389,36 @@ class ServersControllerTest(_ServersControllerTest):
(network, None, None, None, None, None)], (network, None, None, None, None, None)],
res.as_tuples()) res.as_tuples())
def test_requested_networks_enabled_conflict_on_fixed_ip(self): def test_requested_networks_duplicate_ports(self):
network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' """The same port can't be specified twice."""
port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
addr = '10.0.0.1' requested_networks = [{'port': port}, {'port': port}]
requested_networks = [{'uuid': network, exc = self.assertRaises(
'fixed_ip': addr,
'port': port}]
self.assertRaises(
webob.exc.HTTPBadRequest, webob.exc.HTTPBadRequest,
self.controller._get_requested_networks, self.controller._get_requested_networks,
requested_networks) requested_networks,
)
self.assertIn(
"Port ID 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee' was specified "
"twice",
str(exc),
)
def test_requested_networks_conflict_on_fixed_ip(self):
"""A fixed IP can't be specified at the same as a port ID."""
port = 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'
addr = '10.0.0.1'
requested_networks = [{'fixed_ip': addr, 'port': port}]
exc = self.assertRaises(
webob.exc.HTTPBadRequest,
self.controller._get_requested_networks,
requested_networks,
)
self.assertIn(
"Specified Fixed IP '10.0.0.1' cannot be used with port "
"'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'",
str(exc),
)
def test_requested_networks_api_enabled_with_v2_subclass(self): def test_requested_networks_api_enabled_with_v2_subclass(self):
network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' network = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'

View File

@ -0,0 +1,7 @@
---
fixes:
- |
The ``POST /servers`` (create server) API will now reject attempts to
create a server with the same port specified multiple times. This was
previously accepted by the API but the instance would fail to spawn and
would instead transition to the error state.