diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index d72278b3d1b4..54afea53fd2a 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -1478,6 +1478,39 @@ class CellV2Commands(object): mapping.cell_mapping.uuid)) return 0 + @args('--cell_uuid', metavar='', dest='cell_uuid', + help='If provided only this cell will be searched for new hosts to ' + 'map.') + def discover_hosts(self, cell_uuid=None): + """Searches cells, or a single cell, and maps found hosts. + + When a new host is added to a deployment it will add a service entry + to the db it's configured to use. This command will check the db for + each cell, or a single one if passed in, and map any hosts which are + not currently mapped. If a host is already mapped nothing will be done. + """ + ctxt = context.RequestContext() + + if cell_uuid: + cell_mappings = [objects.CellMapping.get_by_uuid(ctxt, cell_uuid)] + else: + cell_mappings = objects.CellMappingList.get_all(context) + + for cell_mapping in cell_mappings: + # TODO(alaski): Factor this into helper method on CellMapping + if cell_mapping.uuid == cell_mapping.CELL0_UUID: + continue + with context.target_cell(ctxt, cell_mapping): + compute_nodes = objects.ComputeNodeList.get_all(ctxt) + for compute in compute_nodes: + try: + objects.HostMapping.get_by_host(ctxt, compute.host) + except exception.HostMappingNotFound: + host_mapping = objects.HostMapping( + ctxt, host=compute.host, + cell_mapping=cell_mapping) + host_mapping.create() + CATEGORIES = { 'account': AccountCommands, diff --git a/nova/tests/unit/test_nova_manage.py b/nova/tests/unit/test_nova_manage.py index 81eba368d722..7bb1ac102bb8 100644 --- a/nova/tests/unit/test_nova_manage.py +++ b/nova/tests/unit/test_nova_manage.py @@ -1210,3 +1210,129 @@ class CellV2CommandsTestCase(test.TestCase): # and reasonably verify that path self.assertEqual(1, self.commands.verify_instance(uuidsentinel.foo, quiet=True)) + + def _return_compute_nodes(self, ctxt, num=1): + nodes = [] + for i in range(num): + nodes.append(objects.ComputeNode(ctxt, + uuid=uuidutils.generate_uuid(), + host='fake', + vcpus=1, + memory_mb=1, + local_gb=1, + vcpus_used=0, + memory_mb_used=0, + local_gb_used=0, + hypervisor_type='', + hypervisor_version=1, + cpu_info='')) + return nodes + + @mock.patch.object(context, 'target_cell') + @mock.patch.object(objects, 'HostMapping') + @mock.patch.object(objects.ComputeNodeList, 'get_all') + @mock.patch.object(objects.CellMappingList, 'get_all') + @mock.patch.object(objects.CellMapping, 'get_by_uuid') + def test_discover_hosts_single_cell(self, mock_cell_mapping_get_by_uuid, + mock_cell_mapping_get_all, + mock_compute_get_all, + mock_host_mapping, mock_target_cell): + host_mock = mock.MagicMock() + mock_host_mapping.return_value = host_mock + exc = exception.HostMappingNotFound(name='fake') + mock_host_mapping.get_by_host.side_effect = exc + + ctxt = context.RequestContext() + + compute_nodes = self._return_compute_nodes(ctxt) + mock_compute_get_all.return_value = objects.ComputeNodeList( + objects=compute_nodes) + + cell_mapping = objects.CellMapping(uuid=uuidutils.generate_uuid()) + mock_cell_mapping_get_by_uuid.return_value = cell_mapping + + self.commands.discover_hosts(cell_uuid=cell_mapping.uuid) + + mock_target_cell.assert_called_once_with( + test.MatchType(context.RequestContext), cell_mapping) + host_mock.create.assert_called_once() + mock_host_mapping.assert_called_once_with( + test.MatchType(context.RequestContext), host='fake', + cell_mapping=cell_mapping) + mock_cell_mapping_get_all.assert_not_called() + + @mock.patch.object(context, 'target_cell') + @mock.patch.object(objects, 'HostMapping') + @mock.patch.object(objects.ComputeNodeList, 'get_all') + @mock.patch.object(objects.CellMappingList, 'get_all') + @mock.patch.object(objects.CellMapping, 'get_by_uuid') + def test_discover_hosts_single_cell_no_new_hosts( + self, mock_cell_mapping_get_by_uuid, mock_cell_mapping_get_all, + mock_compute_get_all, mock_host_mapping, mock_target_cell): + + host_mock = mock.MagicMock() + mock_host_mapping.return_value = host_mock + + ctxt = context.RequestContext() + + compute_nodes = self._return_compute_nodes(ctxt) + mock_compute_get_all.return_value = objects.ComputeNodeList( + objects=compute_nodes) + + cell_mapping = objects.CellMapping(uuid=uuidutils.generate_uuid()) + mock_cell_mapping_get_by_uuid.return_value = cell_mapping + + self.commands.discover_hosts(cell_uuid=cell_mapping.uuid) + + mock_target_cell.assert_called_once_with( + test.MatchType(context.RequestContext), cell_mapping) + mock_host_mapping.assert_not_called() + mock_cell_mapping_get_all.assert_not_called() + + @mock.patch.object(context, 'target_cell') + @mock.patch.object(objects, 'HostMapping') + @mock.patch.object(objects.ComputeNodeList, 'get_all') + @mock.patch.object(objects.CellMappingList, 'get_all') + @mock.patch.object(objects.CellMapping, 'get_by_uuid') + def test_discover_hosts_multiple_cells(self, mock_cell_mapping_get_by_uuid, + mock_cell_mapping_get_all, + mock_compute_get_all, + mock_host_mapping, + mock_target_cell): + host_mock = mock.MagicMock() + mock_host_mapping.return_value = host_mock + exc = exception.HostMappingNotFound(name='fake') + mock_host_mapping.get_by_host.side_effect = exc + + ctxt = context.RequestContext() + + compute_nodes = self._return_compute_nodes(ctxt, num=2) + mock_compute_get_all.side_effect = ( + objects.ComputeNodeList(objects=compute_nodes[1:]), + objects.ComputeNodeList(objects=compute_nodes[:1])) + + cell_mapping1 = objects.CellMapping(uuid=uuidutils.generate_uuid()) + cell_mapping2 = objects.CellMapping(uuid=uuidutils.generate_uuid()) + mock_cell_mapping_get_all.return_value = objects.CellMappingList( + objects=[cell_mapping1, cell_mapping2]) + + self.commands.discover_hosts() + + self.assertEqual(2, mock_target_cell.call_count) + target_calls = [mock.call(test.MatchType(context.RequestContext), + cell_mapping1), + mock.call(test.MatchType(context.RequestContext), + cell_mapping2)] + self.assertEqual(target_calls, mock_target_cell.call_args_list) + + self.assertEqual(2, host_mock.create.call_count) + self.assertEqual(2, mock_host_mapping.call_count) + host_mapping_calls = [mock.call(test.MatchType(context.RequestContext), + host=compute_nodes[0].host, + cell_mapping=cell_mapping1), + mock.call(test.MatchType(context.RequestContext), + host=compute_nodes[1].host, + cell_mapping=cell_mapping2)] + self.assertEqual(host_mapping_calls, mock_host_mapping.call_args_list) + + mock_cell_mapping_get_by_uuid.assert_not_called() diff --git a/releasenotes/notes/cells-discover-hosts-06a3079ba687e092.yaml b/releasenotes/notes/cells-discover-hosts-06a3079ba687e092.yaml new file mode 100644 index 000000000000..1b602d7c4f09 --- /dev/null +++ b/releasenotes/notes/cells-discover-hosts-06a3079ba687e092.yaml @@ -0,0 +1,25 @@ +--- +features: + - A new nova-manage command has been added to discover any new hosts that are + added to a cell. If a deployment has migrated to cellsv2 using either the + simple_cell_setup or the map_cell0/map_cell_and_hosts/map_instances combo + then anytime a new host is added to a cell this new + "nova-manage cell_v2 discover_hosts" needs to be run before instances can + be booted on that host. If multiple hosts are added at one time the command + only needs to be run one time to discover all of them. + + Please note that adding a host to a cell and not running this command could + lead to build failures/reschedules if that host is selected by the + scheduler. The discover_hosts command is necessary to route requests to the + host but is not necessary in order for the scheduler to be aware of the + host. In order to avoid that it is advised that new compute hosts are + disabled until the discover command has been run. +issues: + - If a deployer has updated their deployment to using cellsv2 using either + the simple_cell_setup or the map_cell0/map_cell_and_hosts/map_instances + combo and they add a new host into the cell it may cause build failures + or reschedules until they run the "nova-manage cell_v2 discover_hosts" + command. This is because the scheduler will quickly become aware of the + host but nova-api will not know how to route the request to that host until + it has been "discovered". In order to avoid that it is advised that + new computes are disabled until the discover command has been run.