diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py
index ab4593b85..6fef34a72 100644
--- a/ironic_python_agent/agent.py
+++ b/ironic_python_agent/agent.py
@@ -70,7 +70,6 @@ class IronicPythonAgentHeartbeater(threading.Thread):
         """
         super(IronicPythonAgentHeartbeater, self).__init__()
         self.agent = agent
-        self.hardware = hardware.get_manager()
         self.api = ironic_api_client.APIClient(agent.api_url,
                                                agent.driver_name)
         self.log = log.getLogger(__name__)
@@ -156,7 +155,6 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
         self.api = app.VersionSelectorApplication(self)
         self.heartbeater = IronicPythonAgentHeartbeater(self)
         self.heartbeat_timeout = None
-        self.hardware = hardware.get_manager()
         self.log = log.getLogger(__name__)
         self.started_at = None
         self.node = None
@@ -201,7 +199,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
         attempts = 0
         while (attempts < self.ip_lookup_attempts):
             for iface in ifaces:
-                found_ip = self.hardware.get_ipv4_addr(iface)
+                found_ip = hardware.dispatch_to_managers('get_ipv4_addr',
+                                                         iface)
                 if found_ip is not None:
                     self.advertise_address = (found_ip,
                                               self.advertise_address[1])
@@ -223,7 +222,7 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
                  be found.
         """
         iface_list = [iface.serialize()['name'] for iface in
-                self.hardware.list_network_interfaces()]
+                hardware.dispatch_to_managers('list_network_interfaces')]
         iface_list = [name for name in iface_list if 'lo' not in name]
 
         if len(iface_list) == 0:
@@ -278,7 +277,8 @@ class IronicPythonAgent(base.ExecuteCommandMixin):
         self.started_at = _time()
         if not self.standalone:
             content = self.api_client.lookup_node(
-                hardware_info=self.hardware.list_hardware_info(),
+                hardware_info=hardware.dispatch_to_managers(
+                                  'list_hardware_info'),
                 timeout=self.lookup_timeout,
                 starting_interval=self.lookup_interval)
 
diff --git a/ironic_python_agent/errors.py b/ironic_python_agent/errors.py
index 7219fbdac..a7abdb301 100644
--- a/ironic_python_agent/errors.py
+++ b/ironic_python_agent/errors.py
@@ -247,3 +247,43 @@ class UnknownNodeError(Exception):
         if message is not None:
             self.message = message
         super(UnknownNodeError, self).__init__(self.message)
+
+
+class HardwareManagerNotFound(Exception):
+    """Error raised when no valid HardwareManager can be found."""
+
+    message = 'No valid HardwareManager found.'
+
+    def __init__(self, message=None):
+        if message is not None:
+            self.message = message
+        super(HardwareManagerNotFound, self).__init__(self.message)
+
+
+class HardwareManagerMethodNotFound(RESTError):
+    """Error raised when all HardwareManagers fail to handle a method."""
+
+    msg = 'No HardwareManager found to handle method'
+    message = msg + '.'
+
+    def __init__(self, method=None):
+        if method is not None:
+            self.details = (self.msg + ': "{0}".').format(method)
+        else:
+            self.details = self.message
+        super(HardwareManagerMethodNotFound, self).__init__(self.details)
+
+
+class IncompatibleHardwareMethodError(RESTError):
+    """Error raised when HardwareManager method is incompatible with node
+    hardware.
+    """
+
+    message = 'HardwareManager method is not compatible with hardware.'
+
+    def __init__(self, details=None):
+        if details is not None:
+            self.details = details
+        else:
+            self.details = self.message
+        super(IncompatibleHardwareMethodError, self).__init__(self.details)
diff --git a/ironic_python_agent/extensions/decom.py b/ironic_python_agent/extensions/decom.py
index def97ba4a..cc73387da 100644
--- a/ironic_python_agent/extensions/decom.py
+++ b/ironic_python_agent/extensions/decom.py
@@ -19,4 +19,4 @@ from ironic_python_agent import hardware
 class DecomExtension(base.BaseAgentExtension):
     @base.async_command('erase_hardware')
     def erase_hardware(self):
-        hardware.get_manager().erase_devices()
+        hardware.dispatch_to_managers('erase_devices')
diff --git a/ironic_python_agent/extensions/standby.py b/ironic_python_agent/extensions/standby.py
index d2de593c3..63b831972 100644
--- a/ironic_python_agent/extensions/standby.py
+++ b/ironic_python_agent/extensions/standby.py
@@ -193,7 +193,7 @@ class StandbyExtension(base.BaseAgentExtension):
 
     @base.async_command('cache_image', _validate_image_info)
     def cache_image(self, image_info=None, force=False):
-        device = hardware.get_manager().get_os_install_device()
+        device = hardware.dispatch_to_managers('get_os_install_device')
 
         if self.cached_image_id != image_info['id'] or force:
             _download_image(image_info)
@@ -204,7 +204,7 @@ class StandbyExtension(base.BaseAgentExtension):
     def prepare_image(self,
                       image_info=None,
                       configdrive=None):
-        device = hardware.get_manager().get_os_install_device()
+        device = hardware.dispatch_to_managers('get_os_install_device')
 
         # don't write image again if already cached
         if self.cached_image_id != image_info['id']:
diff --git a/ironic_python_agent/hardware.py b/ironic_python_agent/hardware.py
index 55cbb5723..d7e0fd510 100644
--- a/ironic_python_agent/hardware.py
+++ b/ironic_python_agent/hardware.py
@@ -27,15 +27,20 @@ from ironic_python_agent import errors
 from ironic_python_agent.openstack.common import log
 from ironic_python_agent import utils
 
-_global_manager = None
+_global_managers = None
+LOG = log.getLogger()
 
 
 class HardwareSupport(object):
-    """These are just guidelines to suggest values that might be returned by
-    calls to `evaluate_hardware_support`. No HardwareManager in mainline
-    ironic-python-agent will ever offer a value greater than `MAINLINE`.
-    Service Providers should feel free to return values greater than
-    SERVICE_PROVIDER to distinguish between additional levels of support.
+    """Example priorities for hardware managers.
+
+    Priorities for HardwareManagers are integers, where largest means most
+    specific and smallest means most generic. These values are guidelines
+    that suggest values that might be returned by calls to
+    `evaluate_hardware_support()`. No HardwareManager in mainline IPA will
+    ever return a value greater than MAINLINE. Third party hardware managers
+    should feel free to return values of SERVICE_PROVIDER or greater to
+    distinguish between additional levels of hardware support.
     """
     NONE = 0
     GENERIC = 1
@@ -91,27 +96,21 @@ class HardwareManager(object):
     def evaluate_hardware_support(self):
         pass
 
-    @abc.abstractmethod
     def list_network_interfaces(self):
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
-    @abc.abstractmethod
     def get_cpus(self):
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
-    @abc.abstractmethod
     def list_block_devices(self):
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
-    @abc.abstractmethod
     def get_memory(self):
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
-    @abc.abstractmethod
     def get_os_install_device(self):
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
-    @abc.abstractmethod
     def erase_block_device(self, block_device):
         """Attempt to erase a block device.
 
@@ -129,11 +128,12 @@ class HardwareManager(object):
         encouraged.
 
         :param block_device: a BlockDevice indicating a device to be erased.
-        :raises: BlockDeviceEraseError when an error occurs erasing a block
-                 device, or if the block device is not supported.
-
+        :raises IncompatibleHardwareMethodError: when there is no known way to
+                erase the block device
+        :raises BlockDeviceEraseError: when there is an error erasing the
+                block device
         """
-        pass
+        raise errors.IncompatibleHardwareMethodError
 
     def erase_devices(self):
         """Erase any device that holds user data.
@@ -270,10 +270,10 @@ class GenericHardwareManager(HardwareManager):
         if self._ata_erase(block_device):
             return
 
-        # NOTE(russell_h): Support for additional generic erase methods should
-        # be added above this raise, in order of precedence.
-        raise errors.BlockDeviceEraseError(('Unable to erase block device '
-            '{0}: device is unsupported.').format(block_device.name))
+        msg = ('Unable to erase block device {0}: device is unsupported.'
+              ).format(block_device.name)
+        LOG.error(msg)
+        raise errors.IncompatibleHardwareMethodError(msg)
 
     def _get_ata_security_lines(self, block_device):
         output = utils.execute('hdparm', '-I', block_device.name)[0]
@@ -332,11 +332,19 @@ def _compare_extensions(ext1, ext2):
     return mgr2.evaluate_hardware_support() - mgr1.evaluate_hardware_support()
 
 
-def get_manager():
-    global _global_manager
+def _get_managers():
+    """Get a list of hardware managers in priority order.
 
-    if not _global_manager:
-        LOG = log.getLogger()
+    Use stevedore to find all eligible hardware managers, sort them based on
+    self-reported (via evaluate_hardware_support()) priorities, and return them
+    in a list. The resulting list is cached in _global_managers.
+
+    :returns: Priority-sorted list of hardware managers
+    :raises HardwareManagerNotFound: if no valid hardware managers found
+    """
+    global _global_managers
+
+    if not _global_managers:
         extension_manager = stevedore.ExtensionManager(
             namespace='ironic_python_agent.hardware_managers',
             invoke_on_load=True)
@@ -344,22 +352,54 @@ def get_manager():
         # There will always be at least one extension available (the
         # GenericHardwareManager).
         if six.PY2:
-            preferred_extension = sorted(
-                    extension_manager,
-                    _compare_extensions)[0]
+            extensions = sorted(extension_manager, _compare_extensions)
         else:
-            preferred_extension = sorted(
-                    extension_manager,
-                    key=functools.cmp_to_key(_compare_extensions))[0]
+            extensions = sorted(extension_manager,
+                            key=functools.cmp_to_key(_compare_extensions))
 
-        preferred_manager = preferred_extension.obj
+        preferred_managers = []
 
-        if preferred_manager.evaluate_hardware_support() <= 0:
-            raise RuntimeError('No suitable HardwareManager could be found')
+        for extension in extensions:
+            if extension.obj.evaluate_hardware_support() > 0:
+                preferred_managers.append(extension.obj)
+                LOG.info('Hardware manager found: {0}'.format(
+                    extension.entry_point_target))
 
-        LOG.info('selected hardware manager {0}'.format(
-                 preferred_extension.entry_point_target))
+        if not preferred_managers:
+            raise errors.HardwareManagerNotFound
 
-        _global_manager = preferred_manager
+        _global_managers = preferred_managers
 
-    return _global_manager
+    return _global_managers
+
+
+def dispatch_to_managers(method, *args, **kwargs):
+    """Dispatch a method to best suited hardware manager.
+
+    Dispatches the given method in priority order as sorted by
+    `_get_managers`. If the method doesn't exist or raises
+    IncompatibleHardwareMethodError, it is attempted again with a more generic
+    hardware manager. This continues until a method executes that returns
+    any result without raising an IncompatibleHardwareMethodError.
+
+    :param method: hardware manager method to dispatch
+    :param *args: arguments to dispatched method
+    :param **kwargs: keyword arguments to dispatched method
+
+    :returns: result of successful dispatch of method
+    :raises HardwareManagerMethodNotFound: if all managers failed the method
+    :raises HardwareManagerNotFound: if no valid hardware managers found
+    """
+    managers = _get_managers()
+    for manager in managers:
+        if getattr(manager, method, None):
+            try:
+                return getattr(manager, method)(*args, **kwargs)
+            except(errors.IncompatibleHardwareMethodError):
+                LOG.debug('HardwareManager {0} does not support {1}'
+                        .format(manager, method))
+        else:
+            LOG.debug('HardwareManager {0} does not have method {1}'
+                      .format(manager, method))
+
+    raise errors.HardwareManagerMethodNotFound(method)
diff --git a/ironic_python_agent/tests/agent.py b/ironic_python_agent/tests/agent.py
index a3622bbb3..f11473e4b 100644
--- a/ironic_python_agent/tests/agent.py
+++ b/ironic_python_agent/tests/agent.py
@@ -201,7 +201,15 @@ class TestBaseAgent(test_base.BaseTestCase):
     @mock.patch('os.read')
     @mock.patch('select.poll')
     @mock.patch('time.sleep', return_value=None)
-    def test_ipv4_lookup(self, mock_time_sleep, mock_poll, mock_read):
+    @mock.patch.object(hardware.GenericHardwareManager,
+                       'list_network_interfaces')
+    @mock.patch.object(hardware.GenericHardwareManager, 'get_ipv4_addr')
+    def test_ipv4_lookup(self,
+                         mock_get_ipv4,
+                         mock_list_net,
+                         mock_time_sleep,
+                         mock_poll,
+                         mock_read):
         homeless_agent = agent.IronicPythonAgent('https://fake_api.example.'
                                                  'org:8081/',
                                                  (None, 9990),
@@ -214,10 +222,6 @@ class TestBaseAgent(test_base.BaseTestCase):
                                                  'agent_ipmitool',
                                                  False)
 
-        homeless_agent.hardware = mock.Mock()
-        mock_list_net = homeless_agent.hardware.list_network_interfaces
-        mock_get_ipv4 = homeless_agent.hardware.get_ipv4_addr
-
         mock_poll.return_value.poll.return_value = True
         mock_read.return_value = 'a'
 
diff --git a/ironic_python_agent/tests/extensions/decom.py b/ironic_python_agent/tests/extensions/decom.py
index 2101d237e..cfa01dd4c 100644
--- a/ironic_python_agent/tests/extensions/decom.py
+++ b/ironic_python_agent/tests/extensions/decom.py
@@ -23,9 +23,9 @@ class TestDecomExtension(test_base.BaseTestCase):
         super(TestDecomExtension, self).setUp()
         self.agent_extension = decom.DecomExtension()
 
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
-    def test_erase_hardware(self, mocked_get_manager):
-        hardware_manager = mocked_get_manager.return_value
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
+    def test_erase_devices(self, mocked_dispatch):
         result = self.agent_extension.erase_hardware()
         result.join()
-        hardware_manager.erase_devices.assert_called_once_with()
+        mocked_dispatch.assert_called_once_with('erase_devices')
diff --git a/ironic_python_agent/tests/extensions/standby.py b/ironic_python_agent/tests/extensions/standby.py
index 01cfa3141..b49dacf02 100644
--- a/ironic_python_agent/tests/extensions/standby.py
+++ b/ironic_python_agent/tests/extensions/standby.py
@@ -265,66 +265,70 @@ class TestStandbyExtension(test_base.BaseTestCase):
         self.assertFalse(verified)
         self.assertEqual(md5_mock.call_count, 1)
 
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._write_image',
                 autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._download_image',
                 autospec=True)
-    def test_cache_image(self, download_mock, write_mock, hardware_mock):
+    def test_cache_image(self, download_mock, write_mock, dispatch_mock):
         image_info = self._build_fake_image_info()
         download_mock.return_value = None
         write_mock.return_value = None
-        manager_mock = hardware_mock.return_value
-        manager_mock.get_os_install_device.return_value = 'manager'
+        dispatch_mock.return_value = 'manager'
         async_result = self.agent_extension.cache_image(image_info=image_info)
         async_result.join()
         download_mock.assert_called_once_with(image_info)
         write_mock.assert_called_once_with(image_info, 'manager')
+        dispatch_mock.assert_called_once_with('get_os_install_device')
         self.assertEqual(self.agent_extension.cached_image_id,
                          image_info['id'])
         self.assertEqual('SUCCEEDED', async_result.command_status)
         self.assertEqual(None, async_result.command_result)
 
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._write_image',
                 autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._download_image',
                 autospec=True)
-    def test_cache_image_force(self, download_mock, write_mock, hardware_mock):
+    def test_cache_image_force(self, download_mock, write_mock,
+            dispatch_mock):
         image_info = self._build_fake_image_info()
         self.agent_extension.cached_image_id = image_info['id']
         download_mock.return_value = None
         write_mock.return_value = None
-        manager_mock = hardware_mock.return_value
-        manager_mock.get_os_install_device.return_value = 'manager'
+        dispatch_mock.return_value = 'manager'
         async_result = self.agent_extension.cache_image(
             image_info=image_info, force=True
         )
         async_result.join()
         download_mock.assert_called_once_with(image_info)
         write_mock.assert_called_once_with(image_info, 'manager')
+        dispatch_mock.assert_called_once_with('get_os_install_device')
         self.assertEqual(self.agent_extension.cached_image_id,
                          image_info['id'])
         self.assertEqual('SUCCEEDED', async_result.command_status)
         self.assertEqual(None, async_result.command_result)
 
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._write_image',
                 autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._download_image',
                 autospec=True)
     def test_cache_image_cached(self, download_mock, write_mock,
-                                hardware_mock):
+                                dispatch_mock):
         image_info = self._build_fake_image_info()
         self.agent_extension.cached_image_id = image_info['id']
         download_mock.return_value = None
         write_mock.return_value = None
-        manager_mock = hardware_mock.return_value
-        manager_mock.get_os_install_device.return_value = 'manager'
+        dispatch_mock.return_value = 'manager'
         async_result = self.agent_extension.cache_image(image_info=image_info)
         async_result.join()
         self.assertFalse(download_mock.called)
         self.assertFalse(write_mock.called)
+        dispatch_mock.assert_called_once_with('get_os_install_device')
         self.assertEqual(self.agent_extension.cached_image_id,
                          image_info['id'])
         self.assertEqual('SUCCEEDED', async_result.command_status)
@@ -333,7 +337,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
     @mock.patch(('ironic_python_agent.extensions.standby.'
                  '_write_configdrive_to_partition'),
                 autospec=True)
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._write_image',
                 autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._download_image',
@@ -344,14 +349,13 @@ class TestStandbyExtension(test_base.BaseTestCase):
                            location_mock,
                            download_mock,
                            write_mock,
-                           hardware_mock,
+                           dispatch_mock,
                            configdrive_copy_mock):
         image_info = self._build_fake_image_info()
         location_mock.return_value = '/tmp/configdrive'
         download_mock.return_value = None
         write_mock.return_value = None
-        manager_mock = hardware_mock.return_value
-        manager_mock.get_os_install_device.return_value = 'manager'
+        dispatch_mock.return_value = 'manager'
         configdrive_copy_mock.return_value = None
 
         async_result = self.agent_extension.prepare_image(
@@ -362,6 +366,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
 
         download_mock.assert_called_once_with(image_info)
         write_mock.assert_called_once_with(image_info, 'manager')
+        dispatch_mock.assert_called_once_with('get_os_install_device')
         configdrive_copy_mock.assert_called_once_with('configdrive_data',
                                                       'manager')
 
@@ -389,7 +394,8 @@ class TestStandbyExtension(test_base.BaseTestCase):
     @mock.patch(('ironic_python_agent.extensions.standby.'
                  '_write_configdrive_to_partition'),
                 autospec=True)
-    @mock.patch('ironic_python_agent.hardware.get_manager', autospec=True)
+    @mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
+                autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._write_image',
                 autospec=True)
     @mock.patch('ironic_python_agent.extensions.standby._download_image',
@@ -397,13 +403,12 @@ class TestStandbyExtension(test_base.BaseTestCase):
     def test_prepare_image_no_configdrive(self,
                                           download_mock,
                                           write_mock,
-                                          hardware_mock,
+                                          dispatch_mock,
                                           configdrive_copy_mock):
         image_info = self._build_fake_image_info()
         download_mock.return_value = None
         write_mock.return_value = None
-        manager_mock = hardware_mock.return_value
-        manager_mock.get_os_install_device.return_value = 'manager'
+        dispatch_mock.return_value = 'manager'
         configdrive_copy_mock.return_value = None
 
         async_result = self.agent_extension.prepare_image(
@@ -414,6 +419,7 @@ class TestStandbyExtension(test_base.BaseTestCase):
 
         download_mock.assert_called_once_with(image_info)
         write_mock.assert_called_once_with(image_info, 'manager')
+        dispatch_mock.assert_called_once_with('get_os_install_device')
 
         self.assertEqual(configdrive_copy_mock.call_count, 0)
         self.assertEqual('SUCCEEDED', async_result.command_status)
diff --git a/ironic_python_agent/tests/hardware.py b/ironic_python_agent/tests/hardware.py
index d6c5f1430..fa107d10b 100644
--- a/ironic_python_agent/tests/hardware.py
+++ b/ironic_python_agent/tests/hardware.py
@@ -157,17 +157,6 @@ class TestHardwareManagerLoading(test_base.BaseTestCase):
             ext1, ext2, ext3
         ])
 
-    @mock.patch('stevedore.ExtensionManager')
-    def test_hardware_manager_loading(self, mocked_extension_mgr_constructor):
-        hardware._global_manager = None
-        mocked_extension_mgr_constructor.return_value = self.fake_ext_mgr
-
-        preferred_hw_manager = hardware.get_manager()
-        mocked_extension_mgr_constructor.assert_called_once_with(
-            namespace='ironic_python_agent.hardware_managers',
-            invoke_on_load=True)
-        self.assertEqual(self.correct_hw_manager, preferred_hw_manager)
-
 
 class TestGenericHardwareManager(test_base.BaseTestCase):
     def setUp(self):
@@ -355,7 +344,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
         ])
 
     @mock.patch.object(utils, 'execute')
-    def test_erase_block_device_ata_nosecurtiy(self, mocked_execute):
+    def test_erase_block_device_ata_nosecurity(self, mocked_execute):
         hdparm_output = HDPARM_INFO_TEMPLATE.split('\nSecurity:')[0]
 
         mocked_execute.side_effect = [
@@ -364,7 +353,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
 
         block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
                                             True)
-        self.assertRaises(errors.BlockDeviceEraseError,
+        self.assertRaises(errors.IncompatibleHardwareMethodError,
                           self.hardware.erase_block_device,
                           block_device)
 
@@ -382,7 +371,7 @@ class TestGenericHardwareManager(test_base.BaseTestCase):
 
         block_device = hardware.BlockDevice('/dev/sda', 'big', 1073741824,
                                             True)
-        self.assertRaises(errors.BlockDeviceEraseError,
+        self.assertRaises(errors.IncompatibleHardwareMethodError,
                           self.hardware.erase_block_device,
                           block_device)
 
diff --git a/ironic_python_agent/tests/multi_hardware.py b/ironic_python_agent/tests/multi_hardware.py
new file mode 100644
index 000000000..e08900cbf
--- /dev/null
+++ b/ironic_python_agent/tests/multi_hardware.py
@@ -0,0 +1,154 @@
+# Copyright 2013 Rackspace, Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+from oslotest import base as test_base
+from stevedore import extension
+
+from ironic_python_agent import errors
+from ironic_python_agent import hardware
+
+
+def counted(fn):
+    def wrapper(*args, **kwargs):
+        wrapper.called += 1
+        return fn(*args, **kwargs)
+    wrapper.called = 0
+    wrapper.__name__ = fn.__name__
+    return wrapper
+
+
+class FakeGenericHardwareManager(hardware.HardwareManager):
+    @counted
+    def generic_only(self):
+        return True
+
+    @counted
+    def specific_only(self):
+        raise Exception("Test fail: This method should not be called")
+
+    @counted
+    def mainline_fail(self):
+        return True
+
+    @counted
+    def both_succeed(self):
+        return True
+
+    @counted
+    def unexpected_fail(self):
+        raise Exception("Test fail: This method should not be called")
+
+    @counted
+    def evaluate_hardware_support(self):
+        return hardware.HardwareSupport.GENERIC
+
+
+class FakeMainlineHardwareManager(hardware.HardwareManager):
+    @counted
+    def specific_only(self):
+        return True
+
+    @counted
+    def mainline_fail(self):
+        raise errors.IncompatibleHardwareMethodError
+
+    @counted
+    def both_succeed(self):
+        return True
+
+    @counted
+    def unexpected_fail(self):
+        raise RuntimeError('A problem was encountered')
+
+    @counted
+    def evaluate_hardware_support(self):
+        return hardware.HardwareSupport.MAINLINE
+
+
+class TestMultipleHardwareManagerLoading(test_base.BaseTestCase):
+    def setUp(self):
+        super(TestMultipleHardwareManagerLoading, self).setUp()
+        fake_ep = mock.Mock()
+        fake_ep.module_name = 'fake'
+        fake_ep.attrs = ['fake attrs']
+        ext1 = extension.Extension('fake_generic', fake_ep, None,
+                FakeGenericHardwareManager())
+        ext2 = extension.Extension('fake_mainline', fake_ep, None,
+                FakeMainlineHardwareManager())
+        self.fake_ext_mgr = extension.ExtensionManager.make_test_instance([
+                ext1, ext2
+        ])
+
+        self.extension_mgr_patcher = mock.patch('stevedore.ExtensionManager')
+        self.mocked_extension_mgr = self.extension_mgr_patcher.start()
+        self.mocked_extension_mgr.return_value = self.fake_ext_mgr
+        hardware._global_managers = None
+
+    def tearDown(self):
+        super(TestMultipleHardwareManagerLoading, self).tearDown()
+        self.extension_mgr_patcher.stop()
+
+    def test_mainline_method_only(self):
+        hardware.dispatch_to_managers('specific_only')
+
+        self.assertEqual(1, FakeMainlineHardwareManager.specific_only.called)
+
+    def test_generic_method_only(self):
+        hardware.dispatch_to_managers('generic_only')
+
+        self.assertEqual(1, FakeGenericHardwareManager.generic_only.called)
+
+    def test_both_succeed(self):
+        """In the case where both managers will work; only the most specific
+        manager should have it's function called.
+        """
+        hardware.dispatch_to_managers('both_succeed')
+
+        self.assertEqual(1, FakeMainlineHardwareManager.both_succeed.called)
+        self.assertEqual(0, FakeGenericHardwareManager.both_succeed.called)
+
+    def test_mainline_fails(self):
+        """Ensure that if the mainline manager is unable to run the method
+        that we properly fall back to generic.
+        """
+        hardware.dispatch_to_managers('mainline_fail')
+
+        self.assertEqual(1, FakeMainlineHardwareManager.mainline_fail.called)
+        self.assertEqual(1, FakeGenericHardwareManager.mainline_fail.called)
+
+    def test_manager_method_not_found(self):
+        self.assertRaises(errors.HardwareManagerMethodNotFound,
+                          hardware.dispatch_to_managers,
+                          'fake_method')
+
+    def test_method_fails(self):
+        self.assertRaises(RuntimeError,
+                          hardware.dispatch_to_managers,
+                          'unexpected_fail')
+
+
+class TestNoHardwareManagerLoading(test_base.BaseTestCase):
+    def setUp(self):
+        super(TestNoHardwareManagerLoading, self).setUp()
+        self.empty_ext_mgr = extension.ExtensionManager.make_test_instance([])
+
+    @mock.patch('stevedore.ExtensionManager')
+    def test_no_managers_found(self, mocked_extension_mgr_constructor):
+        mocked_extension_mgr_constructor.return_value = self.empty_ext_mgr
+        hardware._global_managers = None
+
+        self.assertRaises(errors.HardwareManagerNotFound,
+                          hardware.dispatch_to_managers,
+                          'some_method')
diff --git a/ironic_python_agent/tests/netutils.py b/ironic_python_agent/tests/netutils.py
index 5a6c5d57f..9a45b48b8 100644
--- a/ironic_python_agent/tests/netutils.py
+++ b/ironic_python_agent/tests/netutils.py
@@ -17,7 +17,6 @@ import binascii
 import mock
 from oslotest import base as test_base
 
-from ironic_python_agent import hardware
 from ironic_python_agent import netutils
 
 # hexlify-ed output from LLDP packet
@@ -33,7 +32,6 @@ FAKE_LLDP_PACKET = binascii.unhexlify(
 class TestNetutils(test_base.BaseTestCase):
     def setUp(self):
         super(TestNetutils, self).setUp()
-        self.hardware = hardware.GenericHardwareManager()
 
     @mock.patch('fcntl.ioctl')
     @mock.patch('select.select')