API: lookup() ignore malformed MAC addresses

This patch is adding code to ignore malformed MAC addresses which are
called as part of the lookup() mechanism. Prior to this patch, if a MAC
address wasn't in the expected format (six octets) the deployment of the
node would fail because the ramdisk wouldn't be able to lookup which
node it was in the Ironic database. One way to trigger this problem was
to deploy a node with an Infiniband Card which the MAC address (or GID)
contains 20 octets, that would result in a deployment failure even when
that NIC wasn't used by Ironic at all (not enrolled as a port).

The ListOfMacAdresses type was also deleted as part of this patch
because it wasn't used anywhere else.

Change-Id: I614fe63236985438d2f354d17a15d17649e72912
Closes-Bug: #1633585
This commit is contained in:
Lucas Alvares Gomes 2016-11-01 11:30:26 +00:00
parent f750bcd521
commit e0fd53d183
5 changed files with 54 additions and 45 deletions

View File

@ -13,6 +13,7 @@
# under the License. # under the License.
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log
import pecan import pecan
from pecan import rest from pecan import rest
from six.moves import http_client from six.moves import http_client
@ -24,12 +25,15 @@ from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils as api_utils from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import expose from ironic.api import expose
from ironic.common import exception from ironic.common import exception
from ironic.common.i18n import _LW
from ironic.common import policy from ironic.common import policy
from ironic.common import states from ironic.common import states
from ironic.common import utils
from ironic import objects from ironic import objects
CONF = cfg.CONF CONF = cfg.CONF
LOG = log.getLogger(__name__)
_LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info', _LOOKUP_RETURN_FIELDS = ('uuid', 'properties', 'instance_info',
'driver_internal_info') 'driver_internal_info')
@ -78,7 +82,7 @@ class LookupResult(base.APIBase):
class LookupController(rest.RestController): class LookupController(rest.RestController):
"""Controller handling node lookup for a deploy ramdisk.""" """Controller handling node lookup for a deploy ramdisk."""
@expose.expose(LookupResult, types.list_of_macaddress, types.uuid) @expose.expose(LookupResult, types.listtype, types.uuid)
def get_all(self, addresses=None, node_uuid=None): def get_all(self, addresses=None, node_uuid=None):
"""Look up a node by its MAC addresses and optionally UUID. """Look up a node by its MAC addresses and optionally UUID.
@ -97,7 +101,29 @@ class LookupController(rest.RestController):
cdict = pecan.request.context.to_dict() cdict = pecan.request.context.to_dict()
policy.authorize('baremetal:driver:ipa_lookup', cdict, cdict) policy.authorize('baremetal:driver:ipa_lookup', cdict, cdict)
if not addresses and not node_uuid: # Validate the list of MAC addresses
if addresses is None:
addresses = []
valid_addresses = []
invalid_addresses = []
for addr in addresses:
try:
mac = utils.validate_and_normalize_mac(addr)
valid_addresses.append(mac)
except exception.InvalidMAC:
invalid_addresses.append(addr)
if invalid_addresses:
node_log = ('' if not node_uuid
else _LW('(Node UUID: %s)') % node_uuid)
LOG.warning(_LW('The following MAC addresses "%(addrs)s" are '
'invalid and will be ignored by the lookup '
'request %(node)s'),
{'addrs': ', '.join(invalid_addresses),
'node': node_log})
if not valid_addresses and not node_uuid:
raise exception.IncompleteLookup() raise exception.IncompleteLookup()
try: try:
@ -106,7 +132,7 @@ class LookupController(rest.RestController):
pecan.request.context, node_uuid) pecan.request.context, node_uuid)
else: else:
node = objects.Node.get_by_port_addresses( node = objects.Node.get_by_port_addresses(
pecan.request.context, addresses) pecan.request.context, valid_addresses)
except exception.NotFound: except exception.NotFound:
# NOTE(dtantsur): we are reraising the same exception to make sure # NOTE(dtantsur): we are reraising the same exception to make sure
# we don't disclose the difference between nodes that are not found # we don't disclose the difference between nodes that are not found

View File

@ -176,26 +176,6 @@ class ListType(wtypes.UserType):
return ListType.validate(value) return ListType.validate(value)
class ListOfMacAddressesType(ListType):
"""List of MAC addresses."""
@staticmethod
def validate(value):
"""Validate and convert the input to a ListOfMacAddressesType.
:param value: A comma separated string of MAC addresses.
:returns: A list of unique MACs, whose order is not guaranteed.
"""
items = ListType.validate(value)
return [MacAddressType.validate(item) for item in items]
@staticmethod
def frombasetype(value):
if value is None:
return None
return ListOfMacAddressesType.validate(value)
macaddress = MacAddressType() macaddress = MacAddressType()
uuid_or_name = UuidOrNameType() uuid_or_name = UuidOrNameType()
name = NameType() name = NameType()
@ -204,7 +184,6 @@ boolean = BooleanType()
listtype = ListType() listtype = ListType()
# Can't call it 'json' because that's the name of the stdlib module # Can't call it 'json' because that's the name of the stdlib module
jsontype = JsonType() jsontype = JsonType()
list_of_macaddress = ListOfMacAddressesType()
class JsonPatchType(wtypes.Base): class JsonPatchType(wtypes.Base):

View File

@ -100,6 +100,23 @@ class TestLookup(test_api_base.BaseApiTest):
set(data['node'])) set(data['node']))
self._check_config(data) self._check_config(data)
@mock.patch.object(ramdisk.LOG, 'warning', autospec=True)
def test_ignore_malformed_address(self, mock_log):
obj_utils.create_test_port(self.context,
node_id=self.node.id,
address=self.addresses[1])
addresses = ('not-a-valid-address,80:00:02:48:fe:80:00:00:00:00:00:00'
':f4:52:14:03:00:54:06:c2,' + ','.join(self.addresses))
data = self.get_json(
'/lookup?addresses=%s' % addresses,
headers={api_base.Version.string: str(api_v1.MAX_VER)})
self.assertEqual(self.node.uuid, data['node']['uuid'])
self.assertEqual(set(ramdisk._LOOKUP_RETURN_FIELDS) | {'links'},
set(data['node']))
self._check_config(data)
self.assertTrue(mock_log.called)
def test_found_by_uuid(self): def test_found_by_uuid(self):
data = self.get_json( data = self.get_json(
'/lookup?addresses=%s&node_uuid=%s' % '/lookup?addresses=%s&node_uuid=%s' %

View File

@ -41,27 +41,6 @@ class TestMacAddressType(base.TestCase):
types.MacAddressType.validate, 'invalid-mac') types.MacAddressType.validate, 'invalid-mac')
class TestListOfMacAddressesType(base.TestCase):
def test_valid_mac_addr(self):
test_mac = 'aa:bb:cc:11:22:33'
self.assertEqual([test_mac],
types.ListOfMacAddressesType.validate(test_mac))
def test_valid_list(self):
test_mac = 'aa:bb:cc:11:22:33,11:22:33:44:55:66'
self.assertEqual(
sorted(test_mac.split(',')),
sorted(types.ListOfMacAddressesType.validate(test_mac)))
def test_invalid_mac_addr(self):
self.assertRaises(exception.InvalidMAC,
types.ListOfMacAddressesType.validate, 'invalid-mac')
self.assertRaises(exception.InvalidMAC,
types.ListOfMacAddressesType.validate,
'aa:bb:cc:11:22:33,invalid-mac')
class TestUuidType(base.TestCase): class TestUuidType(base.TestCase):
def test_valid_uuid(self): def test_valid_uuid(self):

View File

@ -0,0 +1,8 @@
---
fixes:
- Fixes a problem where the deployment of a node would fail to continue
if a malformed MAC address was passed to the lookup mechanism in the
Ironic API. For example, if a node contains an Infiniband card, the
lookup used to fail because the agent ramdisk passes a MAC address
(or GID) with 20 octets (instead of the expected 6 octets) as part
of the lookup request. Invalid addresses are now ignored.