diff --git a/doc/source/man/nova-manage.rst b/doc/source/man/nova-manage.rst index 25889ceaac8e..4d8049d65883 100644 --- a/doc/source/man/nova-manage.rst +++ b/doc/source/man/nova-manage.rst @@ -160,12 +160,14 @@ Nova Cells v2 uuid are shown. Use the --verbose option to see transport url and database connection details. -``nova-manage cell_v2 delete_cell --cell_uuid `` +``nova-manage cell_v2 delete_cell [--force] --cell_uuid `` - Delete an empty cell by the given uuid. Returns 0 if the empty cell is - found and deleted successfully, 1 if a cell with that uuid could not be - found, 2 if host mappings were found for the cell (cell not empty), and - 3 if there are instances mapped to the cell (cell not empty). + Delete a cell by the given uuid. Returns 0 if the empty cell is + found and deleted successfully or the cell that has hosts is found and + the cell and the hosts are deleted successfully with ``--force`` option, + 1 if a cell with that uuid could not be found, 2 if host mappings were + found for the cell (cell not empty) without ``--force`` option, and 3 + if there are instances mapped to the cell (cell not empty). ``nova-manage cell_v2 update_cell --cell_uuid [--name ] [--transport-url ] [--database_connection ]`` @@ -182,6 +184,14 @@ Nova Cells v2 a running system will NOT result in all nodes immediately using the new values. Use caution when changing these values. +``nova-manage cell_v2 delete_host --cell_uuid --host `` + + Delete a host by the given host name and the given cell uuid. Returns 0 + if the empty host is found and deleted successfully, 1 if a cell with + that uuid could not be found, 2 if a host with that name could not be + found, 3 if a host with that name is not in a cell with that uuid, 4 if + a host with that name has instances (host not empty). + Nova Logs ~~~~~~~~~ diff --git a/nova/cmd/manage.py b/nova/cmd/manage.py index 941bd65daa74..09875a54b544 100644 --- a/nova/cmd/manage.py +++ b/nova/cmd/manage.py @@ -1481,15 +1481,26 @@ class CellV2Commands(object): print(t) return 0 + @args('--force', action='store_true', default=False, + help=_('Delete hosts that belong to the cell as well.')) @args('--cell_uuid', metavar='', dest='cell_uuid', required=True, help=_('The uuid of the cell to delete.')) - def delete_cell(self, cell_uuid): + def delete_cell(self, cell_uuid, force=False): """Delete an empty cell by the given uuid. - If the cell is not found by uuid or it is not empty (it has host or - instance mappings) this command will return a non-zero exit code. + This command will return a non-zero exit code in the following cases. - Returns 0 if the empty cell is found and deleted successfully. + * The cell is not found by uuid. + * It has hosts and force is False. + * It has instance mappings. + + If force is True and the cell has host, hosts are deleted as well. + + Returns 0 in the following cases. + + * The empty cell is found and deleted successfully. + * The cell has hosts and force is True and the cell and the hosts are + deleted successfully. """ ctxt = context.get_admin_context() # Find the CellMapping given the uuid. @@ -1502,7 +1513,7 @@ class CellV2Commands(object): # Check to see if there are any HostMappings for this cell. host_mappings = objects.HostMappingList.get_by_cell_id( ctxt, cell_mapping.id) - if host_mappings: + if host_mappings and not force: print(_('There are existing hosts mapped to cell with uuid %s.') % cell_uuid) return 2 @@ -1515,6 +1526,10 @@ class CellV2Commands(object): 'uuid %s.') % cell_uuid) return 3 + # Delete hosts mapped to the cell. + for host_mapping in host_mappings: + host_mapping.destroy() + # There are no hosts or instances mapped to the cell so delete it. cell_mapping.destroy() return 0 @@ -1567,6 +1582,52 @@ class CellV2Commands(object): return 0 + @args('--cell_uuid', metavar='', dest='cell_uuid', + required=True, help=_('The uuid of the cell.')) + @args('--host', metavar='', dest='host', + required=True, help=_('The host to delete.')) + def delete_host(self, cell_uuid, host): + """Delete a host in a cell (host mappings) by the given host name + + This command will return a non-zero exit code in the following cases. + + * The cell is not found by uuid. + * The host is not found by host name. + * The host is not in the cell. + * The host has instances. + + Returns 0 if the host is deleted successfully. + """ + ctxt = context.get_admin_context() + # Find the CellMapping given the uuid. + try: + cell_mapping = objects.CellMapping.get_by_uuid(ctxt, cell_uuid) + except exception.CellMappingNotFound: + print(_('Cell with uuid %s was not found.') % cell_uuid) + return 1 + + try: + host_mapping = objects.HostMapping.get_by_host(ctxt, host) + except exception.HostMappingNotFound: + print(_('The host %s was not found.') % host) + return 2 + + if host_mapping.cell_mapping.uuid != cell_mapping.uuid: + print(_('The host %(host)s was not found ' + 'in the cell %(cell_uuid)s.') % {'host': host, + 'cell_uuid': cell_uuid}) + return 3 + + with context.target_cell(ctxt, cell_mapping) as cctxt: + instances = objects.InstanceList.get_by_host(cctxt, host) + + if instances: + print(_('There are instances on the host %s.') % host) + return 4 + + host_mapping.destroy() + return 0 + CATEGORIES = { 'account': AccountCommands, diff --git a/nova/tests/unit/test_nova_manage.py b/nova/tests/unit/test_nova_manage.py index ccb0da8108da..4986c78fee26 100644 --- a/nova/tests/unit/test_nova_manage.py +++ b/nova/tests/unit/test_nova_manage.py @@ -1579,7 +1579,7 @@ class CellV2CommandsTestCase(test.NoDBTestCase): output = self.output.getvalue().strip() self.assertIn('There are existing instances mapped to cell', output) - def test_delete_cell_success(self): + def test_delete_cell_success_without_host_mappings(self): """Tests trying to delete an empty cell.""" cell_uuid = uuidutils.generate_uuid() ctxt = context.get_admin_context() @@ -1592,6 +1592,28 @@ class CellV2CommandsTestCase(test.NoDBTestCase): output = self.output.getvalue().strip() self.assertEqual('', output) + @mock.patch.object(objects.HostMapping, 'destroy') + @mock.patch.object(objects.CellMapping, 'destroy') + def test_delete_cell_success_with_host_mappings(self, mock_cell_destroy, + mock_hm_destroy): + """Tests trying to delete a cell with host.""" + ctxt = context.get_admin_context() + # create the cell mapping + cm = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell1, + database_connection='fake:///db', transport_url='fake:///mq') + cm.create() + # create a host mapping in this cell + hm = objects.HostMapping( + context=ctxt, host='fake-host', cell_mapping=cm) + hm.create() + self.assertEqual(0, self.commands.delete_cell(uuidsentinel.cell1, + force=True)) + output = self.output.getvalue().strip() + self.assertEqual('', output) + mock_hm_destroy.assert_called_once_with() + mock_cell_destroy.assert_called_once_with() + def test_update_cell_not_found(self): self.assertEqual(1, self.commands.update_cell( uuidsentinel.cell1, 'foo', 'fake://new', 'fake:///new')) @@ -1640,6 +1662,97 @@ class CellV2CommandsTestCase(test.NoDBTestCase): output = self.output.getvalue().strip() self.assertEqual('', output) + def test_delete_host_cell_not_found(self): + """Tests trying to delete a host but a specified cell is not found.""" + self.assertEqual(1, self.commands.delete_host(uuidsentinel.cell1, + 'fake-host')) + output = self.output.getvalue().strip() + self.assertEqual( + 'Cell with uuid %s was not found.' % uuidsentinel.cell1, output) + + def test_delete_host_host_not_found(self): + """Tests trying to delete a host but the host is not found.""" + ctxt = context.get_admin_context() + # create the cell mapping + cm = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell1, + database_connection='fake:///db', transport_url='fake:///mq') + cm.create() + self.assertEqual(2, self.commands.delete_host(uuidsentinel.cell1, + 'fake-host')) + output = self.output.getvalue().strip() + self.assertEqual('The host fake-host was not found.', output) + + def test_delete_host_host_not_in_cell(self): + """Tests trying to delete a host + but the host does not belongs to a specified cell. + """ + ctxt = context.get_admin_context() + # create the cell mapping + cm1 = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell1, + database_connection='fake:///db', transport_url='fake:///mq') + cm1.create() + cm2 = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell2, + database_connection='fake:///db', transport_url='fake:///mq') + cm2.create() + # create a host mapping in another cell + hm = objects.HostMapping( + context=ctxt, host='fake-host', cell_mapping=cm2) + hm.create() + self.assertEqual(3, self.commands.delete_host(uuidsentinel.cell1, + 'fake-host')) + output = self.output.getvalue().strip() + self.assertEqual(('The host fake-host was not found in the cell %s.' % + uuidsentinel.cell1), output) + + @mock.patch.object(objects.InstanceList, 'get_by_host') + def test_delete_host_instances_exist(self, mock_get_by_host): + """Tests trying to delete a host but the host has instances.""" + ctxt = context.get_admin_context() + # create the cell mapping + cm1 = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell1, + database_connection='fake:///db', transport_url='fake:///mq') + cm1.create() + # create a host mapping in the cell + hm = objects.HostMapping( + context=ctxt, host='fake-host', cell_mapping=cm1) + hm.create() + mock_get_by_host.return_value = [objects.Instance( + ctxt, uuid=uuidsentinel.instance)] + self.assertEqual(4, self.commands.delete_host(uuidsentinel.cell1, + 'fake-host')) + output = self.output.getvalue().strip() + self.assertEqual('There are instances on the host fake-host.', output) + mock_get_by_host.assert_called_once_with( + test.MatchType(context.RequestContext), 'fake-host') + + @mock.patch.object(objects.InstanceList, 'get_by_host', + return_value=[]) + @mock.patch.object(objects.HostMapping, 'destroy') + def test_delete_host_success(self, mock_destroy, mock_get_by_host): + """Tests trying to delete a host that has not instances.""" + ctxt = context.get_admin_context() + # create the cell mapping + cm1 = objects.CellMapping( + context=ctxt, uuid=uuidsentinel.cell1, + database_connection='fake:///db', transport_url='fake:///mq') + cm1.create() + # create a host mapping in the cell + hm = objects.HostMapping( + context=ctxt, host='fake-host', cell_mapping=cm1) + hm.create() + + self.assertEqual(0, self.commands.delete_host(uuidsentinel.cell1, + 'fake-host')) + output = self.output.getvalue().strip() + self.assertEqual('', output) + mock_get_by_host.assert_called_once_with( + test.MatchType(context.RequestContext), 'fake-host') + mock_destroy.assert_called_once_with() + class TestNovaManageMain(test.NoDBTestCase): """Tests the nova-manage:main() setup code.""" diff --git a/releasenotes/notes/bug-1721179-87bc7b64215944c0.yaml b/releasenotes/notes/bug-1721179-87bc7b64215944c0.yaml new file mode 100644 index 000000000000..06e0368b3782 --- /dev/null +++ b/releasenotes/notes/bug-1721179-87bc7b64215944c0.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The ``delete_host`` command has been added in ``nova-manage cell_v2`` + to delete a host from a cell (host mappings). + The ``force`` option has been added in ``nova-manage cell_v2 delete_cell``. + If the ``force`` option is specified, a cell can be deleted + even if the cell has hosts.