diff --git a/nova/db/api.py b/nova/db/api.py index 11372bf2109a..844e31ea8fed 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -244,6 +244,21 @@ def compute_node_get_by_host_and_nodename(context, host, nodename): return IMPL.compute_node_get_by_host_and_nodename(context, host, nodename) +def compute_node_get_by_nodename(context, hypervisor_hostname): + """Get a compute node by hypervisor_hostname. + + :param context: The security context (admin) + :param hypervisor_hostname: Name of the node + + :returns: Dictionary-like object containing properties of the compute node, + including its statistics + + Raises ComputeHostNotFound if hypervisor_hostname with the given name + doesn't exist. + """ + return IMPL.compute_node_get_by_nodename(context, hypervisor_hostname) + + def compute_node_get_all(context): """Get all computeNodes. diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 55262a44c73d..5f009a73293d 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -653,6 +653,15 @@ def compute_node_get_by_host_and_nodename(context, host, nodename): return results[0] +@pick_context_manager_reader +def compute_node_get_by_nodename(context, hypervisor_hostname): + results = _compute_node_fetchall(context, + {"hypervisor_hostname": hypervisor_hostname}) + if not results: + raise exception.ComputeHostNotFound(host=hypervisor_hostname) + return results[0] + + @pick_context_manager_reader_allow_async def compute_node_get_all_by_host(context, host): results = _compute_node_fetchall(context, {"host": host}) diff --git a/nova/objects/compute_node.py b/nova/objects/compute_node.py index 3f9514ed1ed5..77e926ba2e74 100644 --- a/nova/objects/compute_node.py +++ b/nova/objects/compute_node.py @@ -53,7 +53,8 @@ class ComputeNode(base.NovaPersistentObject, base.NovaObject): # Version 1.16: Added disk_allocation_ratio # Version 1.17: Added mapped # Version 1.18: Added get_by_uuid(). - VERSION = '1.18' + # Version 1.19: Added get_by_nodename(). + VERSION = '1.19' fields = { 'id': fields.IntegerField(read_only=True), @@ -270,6 +271,17 @@ class ComputeNode(base.NovaPersistentObject, base.NovaObject): context, host, nodename) return cls._from_db_object(context, cls(), db_compute) + @base.remotable_classmethod + def get_by_nodename(cls, context, hypervisor_hostname): + '''Get by node name (i.e. hypervisor hostname). + + Raises ComputeHostNotFound if hypervisor_hostname with the given name + doesn't exist. + ''' + db_compute = db.compute_node_get_by_nodename( + context, hypervisor_hostname) + return cls._from_db_object(context, cls(), db_compute) + # TODO(pkholkin): Remove this method in the next major version bump @base.remotable_classmethod def get_first_node_by_host_for_old_compat(cls, context, host, diff --git a/nova/scheduler/host_manager.py b/nova/scheduler/host_manager.py index bb9624a2a3f4..381db37ddc25 100644 --- a/nova/scheduler/host_manager.py +++ b/nova/scheduler/host_manager.py @@ -641,6 +641,69 @@ class HostManager(object): for service in _services}) return compute_nodes, services + def _get_cell_by_host(self, ctxt, host): + '''Get CellMapping object of a cell the given host belongs to.''' + try: + host_mapping = objects.HostMapping.get_by_host(ctxt, host) + return host_mapping.cell_mapping + except exception.HostMappingNotFound: + LOG.warning('No host-to-cell mapping found for selected ' + 'host %(host)s.', {'host': host}) + return + + def get_compute_nodes_by_host_or_node(self, ctxt, host, node, cell=None): + '''Get compute nodes from given host or node''' + def return_empty_list_for_not_found(func): + def wrapper(*args, **kwargs): + try: + ret = func(*args, **kwargs) + except exception.NotFound: + ret = objects.ComputeNodeList() + return ret + return wrapper + + @return_empty_list_for_not_found + def _get_by_host_and_node(ctxt): + compute_node = objects.ComputeNode.get_by_host_and_nodename( + ctxt, host, node) + return objects.ComputeNodeList(objects=[compute_node]) + + @return_empty_list_for_not_found + def _get_by_host(ctxt): + return objects.ComputeNodeList.get_all_by_host(ctxt, host) + + @return_empty_list_for_not_found + def _get_by_node(ctxt): + compute_node = objects.ComputeNode.get_by_nodename(ctxt, node) + return objects.ComputeNodeList(objects=[compute_node]) + + if host and node: + target_fnc = _get_by_host_and_node + elif host: + target_fnc = _get_by_host + else: + target_fnc = _get_by_node + + if host and not cell: + # optimization not to issue queries to every cell DB + cell = self._get_cell_by_host(ctxt, host) + + cells = [cell] if cell else self.enabled_cells + + timeout = context_module.CELL_TIMEOUT + nodes_by_cell = context_module.scatter_gather_cells( + ctxt, cells, timeout, target_fnc) + try: + # Only one cell should have a value for the compute nodes + # so we get it here + nodes = next( + nodes for nodes in nodes_by_cell.values() if nodes) + except StopIteration: + # ...or we find no node if none of the cells has a value + nodes = objects.ComputeNodeList() + + return nodes + def refresh_cells_caches(self): # NOTE(tssurya): This function is called from the scheduler manager's # reset signal handler and also upon startup of the scheduler. diff --git a/nova/tests/unit/db/test_db_api.py b/nova/tests/unit/db/test_db_api.py index 654db0b737c2..c11d1f40266e 100644 --- a/nova/tests/unit/db/test_db_api.py +++ b/nova/tests/unit/db/test_db_api.py @@ -7219,6 +7219,28 @@ class ComputeNodeTestCase(test.TestCase, ModelsObjectComparatorMixin): db.compute_node_get_by_host_and_nodename, self.ctxt, 'host1', 'wrong') + def test_compute_node_get_by_nodename(self): + # Create another node on top of the same service + compute_node_same_host = self.compute_node_dict.copy() + compute_node_same_host['uuid'] = uuidutils.generate_uuid() + compute_node_same_host['stats'] = jsonutils.dumps(self.stats) + compute_node_same_host['hypervisor_hostname'] = 'node_2' + + node = db.compute_node_create(self.ctxt, compute_node_same_host) + + expected = node + result = db.compute_node_get_by_nodename( + self.ctxt, 'node_2') + + self._assertEqualObjects(expected, result, + ignored_keys=self._ignored_keys + + ['stats', 'service']) + + def test_compute_node_get_by_nodename_not_found(self): + self.assertRaises(exception.ComputeHostNotFound, + db.compute_node_get_by_nodename, + self.ctxt, 'wrong') + def test_compute_node_get(self): compute_node_id = self.item['id'] node = db.compute_node_get(self.ctxt, compute_node_id) diff --git a/nova/tests/unit/objects/test_compute_node.py b/nova/tests/unit/objects/test_compute_node.py index 5579e6f7367d..51423c540bd4 100644 --- a/nova/tests/unit/objects/test_compute_node.py +++ b/nova/tests/unit/objects/test_compute_node.py @@ -239,6 +239,16 @@ class _TestComputeNodeObject(object): subs=self.subs(), comparators=self.comparators()) + @mock.patch.object(db, 'compute_node_get_by_nodename') + def test_get_by_nodename(self, cn_get_by_n): + cn_get_by_n.return_value = fake_compute_node + + compute = compute_node.ComputeNode.get_by_nodename( + self.context, 'vm.danplanet.com') + self.compare_obj(compute, fake_compute_node, + subs=self.subs(), + comparators=self.comparators()) + @mock.patch('nova.db.api.compute_node_get_all_by_host') def test_get_first_node_by_host_for_old_compat( self, cn_get_all_by_host): diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index ba1a1e569160..fc749c5dcacb 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1043,7 +1043,7 @@ object_data = { 'BuildRequestList': '1.0-cd95608eccb89fbc702c8b52f38ec738', 'CellMapping': '1.1-5d652928000a5bc369d79d5bde7e497d', 'CellMappingList': '1.1-496ef79bb2ab41041fff8bcb57996352', - 'ComputeNode': '1.18-431fafd8ac4a5f3559bd9b1f1332cc22', + 'ComputeNode': '1.19-af6bd29a6c3b225da436a0d8487096f2', 'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192', 'ConsoleAuthToken': '1.0-a61bf7b54517c4013a12289c5a5268ea', 'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47', diff --git a/nova/tests/unit/scheduler/test_host_manager.py b/nova/tests/unit/scheduler/test_host_manager.py index a4fc0d840fbc..8f33feba19ce 100644 --- a/nova/tests/unit/scheduler/test_host_manager.py +++ b/nova/tests/unit/scheduler/test_host_manager.py @@ -1048,6 +1048,100 @@ class HostManagerTestCase(test.NoDBTestCase): mock.sentinel.c1n2]}, cns) self.assertEqual(['a', 'b'], sorted(srv.keys())) + @mock.patch('nova.objects.HostMapping.get_by_host') + @mock.patch('nova.objects.ComputeNode.get_by_nodename') + @mock.patch('nova.objects.ComputeNode.get_by_host_and_nodename') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_get_compute_nodes_by_host_or_node(self, + mock_get_all, mock_get_host_node, mock_get_node, mock_get_hm): + def _varify_result(expected, result): + self.assertEqual(len(expected), len(result)) + for expected_cn, result_cn in zip(expected, result): + self.assertEqual(expected_cn.host, result_cn.host) + self.assertEqual(expected_cn.node, result_cn.node) + + context = nova_context.RequestContext('fake', 'fake') + + cn1 = objects.ComputeNode(host='fake_multihost', node='fake_node1') + cn2 = objects.ComputeNode(host='fake_multihost', node='fake_node2') + cn3 = objects.ComputeNode(host='fake_host1', node='fake_node') + mock_get_all.return_value = objects.ComputeNodeList(objects=[cn1, cn2]) + mock_get_host_node.return_value = cn1 + mock_get_node.return_value = cn3 + + mock_get_hm.return_value = objects.HostMapping( + context=context, + host='fake_multihost', + cell_mapping=objects.CellMapping(uuid=uuids.cell1, + db_connection='none://1', + transport_url='none://')) + + # Case1: call it with host + host = 'fake_multihost' + node = None + + result = self.host_manager.get_compute_nodes_by_host_or_node( + context, host, node) + expected = objects.ComputeNodeList(objects=[cn1, cn2]) + + _varify_result(expected, result) + mock_get_all.assert_called_once_with(context, 'fake_multihost') + mock_get_host_node.assert_not_called() + mock_get_node.assert_not_called() + mock_get_hm.assert_called_once_with(context, 'fake_multihost') + + mock_get_all.reset_mock() + mock_get_hm.reset_mock() + + # Case2: call it with host and node + host = 'fake_multihost' + node = 'fake_node1' + + result = self.host_manager.get_compute_nodes_by_host_or_node( + context, host, node) + expected = objects.ComputeNodeList(objects=[cn1]) + + _varify_result(expected, result) + mock_get_all.assert_not_called() + mock_get_host_node.assert_called_once_with( + context, 'fake_multihost', 'fake_node1') + mock_get_node.assert_not_called() + mock_get_hm.assert_called_once_with(context, 'fake_multihost') + + mock_get_host_node.reset_mock() + mock_get_hm.reset_mock() + + # Case3: call it with node + host = None + node = 'fake_node' + + result = self.host_manager.get_compute_nodes_by_host_or_node( + context, host, node) + expected = objects.ComputeNodeList(objects=[cn3]) + + _varify_result(expected, result) + mock_get_all.assert_not_called() + mock_get_host_node.assert_not_called() + mock_get_node.assert_called_once_with(context, 'fake_node') + mock_get_hm.assert_not_called() + + @mock.patch('nova.objects.HostMapping.get_by_host') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_get_compute_nodes_by_host_or_node_empty_list( + self, mock_get_all, mock_get_hm): + mock_get_all.side_effect = exception.ComputeHostNotFound(host='fake') + mock_get_hm.side_effect = exception.HostMappingNotFound(name='fake') + + context = nova_context.RequestContext('fake', 'fake') + + host = 'fake' + node = None + + result = self.host_manager.get_compute_nodes_by_host_or_node( + context, host, node) + + self.assertEqual(0, len(result)) + class HostManagerChangedNodesTestCase(test.NoDBTestCase): """Test case for HostManager class."""