compute: Add 'server * --all-projects' option

Add an '--all-projects' option to a number of commands:

- server delete
- server start
- server stop

This is in addition to 'server list', which already supports this
option.

This option allows users to request the corresponding action on one or
more servers using the server names when that server exists in another
project. This is admin-only by default.

As part of this work, we also introduce a 'boolenv' helper function that
allows us to parse the environment variable as a boolean using
'bool_from_string' helper provided by oslo.utils. This could probably be
clever and it has the unfortunate side effect of modifying the help
text in environments where this is configured, but it's good enough for
now.  It also appears to add a new dependency, in the form of
oslo.utils, but that dependency was already required by osc-lib and
probably more.

Change-Id: I4811f8f66dcb14ed99cc1cfb80b00e2d77afe45f
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
Stephen Finucane 2021-01-15 17:06:20 +00:00
parent bfa032cb18
commit 1a6df700be
4 changed files with 136 additions and 8 deletions

View File

@ -31,6 +31,7 @@ from osc_lib.cli import parseractions
from osc_lib.command import command from osc_lib.command import command
from osc_lib import exceptions from osc_lib import exceptions
from osc_lib import utils from osc_lib import utils
from oslo_utils import strutils
from openstackclient.i18n import _ from openstackclient.i18n import _
from openstackclient.identity import common as identity_common from openstackclient.identity import common as identity_common
@ -193,6 +194,24 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
return info return info
def boolenv(*vars, default=False):
"""Search for the first defined of possibly many bool-like env vars.
Returns the first environment variable defined in vars, or returns the
default.
:param vars: Arbitrary strings to search for. Case sensitive.
:param default: The default to return if no value found.
:returns: A boolean corresponding to the value found, else the default if
no value found.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return strutils.bool_from_string(value)
return default
class AddFixedIP(command.Command): class AddFixedIP(command.Command):
_description = _("Add fixed IP address to server") _description = _("Add fixed IP address to server")
@ -1322,6 +1341,15 @@ class DeleteServer(command.Command):
action='store_true', action='store_true',
help=_('Force delete server(s)'), help=_('Force delete server(s)'),
) )
parser.add_argument(
'--all-projects',
action='store_true',
default=boolenv('ALL_PROJECTS'),
help=_(
'Delete server(s) in another project by name (admin only)'
'(can be specified using the ALL_PROJECTS envvar)'
),
)
parser.add_argument( parser.add_argument(
'--wait', '--wait',
action='store_true', action='store_true',
@ -1339,7 +1367,8 @@ class DeleteServer(command.Command):
compute_client = self.app.client_manager.compute compute_client = self.app.client_manager.compute
for server in parsed_args.server: for server in parsed_args.server:
server_obj = utils.find_resource( server_obj = utils.find_resource(
compute_client.servers, server) compute_client.servers, server,
all_tenants=parsed_args.all_projects)
if parsed_args.force: if parsed_args.force:
compute_client.servers.force_delete(server_obj.id) compute_client.servers.force_delete(server_obj.id)
@ -1347,11 +1376,13 @@ class DeleteServer(command.Command):
compute_client.servers.delete(server_obj.id) compute_client.servers.delete(server_obj.id)
if parsed_args.wait: if parsed_args.wait:
if not utils.wait_for_delete(compute_client.servers, if not utils.wait_for_delete(
server_obj.id, compute_client.servers,
callback=_show_progress): server_obj.id,
LOG.error(_('Error deleting server: %s'), callback=_show_progress,
server_obj.id) ):
msg = _('Error deleting server: %s')
LOG.error(msg, server_obj.id)
self.app.stdout.write(_('Error deleting server\n')) self.app.stdout.write(_('Error deleting server\n'))
raise SystemExit raise SystemExit
@ -1446,8 +1477,11 @@ class ListServer(command.Lister):
parser.add_argument( parser.add_argument(
'--all-projects', '--all-projects',
action='store_true', action='store_true',
default=bool(int(os.environ.get("ALL_PROJECTS", 0))), default=boolenv('ALL_PROJECTS'),
help=_('Include all projects (admin only)'), help=_(
'Include all projects (admin only) '
'(can be specified using the ALL_PROJECTS envvar)'
),
) )
parser.add_argument( parser.add_argument(
'--project', '--project',
@ -3939,6 +3973,15 @@ class StartServer(command.Command):
nargs="+", nargs="+",
help=_('Server(s) to start (name or ID)'), help=_('Server(s) to start (name or ID)'),
) )
parser.add_argument(
'--all-projects',
action='store_true',
default=boolenv('ALL_PROJECTS'),
help=_(
'Start server(s) in another project by name (admin only)'
'(can be specified using the ALL_PROJECTS envvar)'
),
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
@ -3947,6 +3990,7 @@ class StartServer(command.Command):
utils.find_resource( utils.find_resource(
compute_client.servers, compute_client.servers,
server, server,
all_tenants=parsed_args.all_projects,
).start() ).start()
@ -3961,6 +4005,15 @@ class StopServer(command.Command):
nargs="+", nargs="+",
help=_('Server(s) to stop (name or ID)'), help=_('Server(s) to stop (name or ID)'),
) )
parser.add_argument(
'--all-projects',
action='store_true',
default=boolenv('ALL_PROJECTS'),
help=_(
'Stop server(s) in another project by name (admin only)'
'(can be specified using the ALL_PROJECTS envvar)'
),
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
@ -3969,6 +4022,7 @@ class StopServer(command.Command):
utils.find_resource( utils.find_resource(
compute_client.servers, compute_client.servers,
server, server,
all_tenants=parsed_args.all_projects,
).stop() ).stop()

View File

@ -2913,6 +2913,28 @@ class TestServerDelete(TestServer):
self.servers_mock.delete.assert_has_calls(calls) self.servers_mock.delete.assert_has_calls(calls)
self.assertIsNone(result) self.assertIsNone(result)
@mock.patch.object(common_utils, 'find_resource')
def test_server_delete_with_all_projects(self, mock_find_resource):
servers = self.setup_servers_mock(count=1)
mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
servers, 0,
)
arglist = [
servers[0].id,
'--all-projects',
]
verifylist = [
('server', [servers[0].id]),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
mock_find_resource.assert_called_once_with(
mock.ANY, servers[0].id, all_tenants=True,
)
@mock.patch.object(common_utils, 'wait_for_delete', return_value=True) @mock.patch.object(common_utils, 'wait_for_delete', return_value=True)
def test_server_delete_wait_ok(self, mock_wait_for_delete): def test_server_delete_wait_ok(self, mock_wait_for_delete):
servers = self.setup_servers_mock(count=1) servers = self.setup_servers_mock(count=1)
@ -6781,6 +6803,28 @@ class TestServerStart(TestServer):
def test_server_start_multi_servers(self): def test_server_start_multi_servers(self):
self.run_method_with_servers('start', 3) self.run_method_with_servers('start', 3)
@mock.patch.object(common_utils, 'find_resource')
def test_server_start_with_all_projects(self, mock_find_resource):
servers = self.setup_servers_mock(count=1)
mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
servers, 0,
)
arglist = [
servers[0].id,
'--all-projects',
]
verifylist = [
('server', [servers[0].id]),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
mock_find_resource.assert_called_once_with(
mock.ANY, servers[0].id, all_tenants=True,
)
class TestServerStop(TestServer): class TestServerStop(TestServer):
@ -6801,6 +6845,28 @@ class TestServerStop(TestServer):
def test_server_stop_multi_servers(self): def test_server_stop_multi_servers(self):
self.run_method_with_servers('stop', 3) self.run_method_with_servers('stop', 3)
@mock.patch.object(common_utils, 'find_resource')
def test_server_start_with_all_projects(self, mock_find_resource):
servers = self.setup_servers_mock(count=1)
mock_find_resource.side_effect = compute_fakes.FakeServer.get_servers(
servers, 0,
)
arglist = [
servers[0].id,
'--all-projects',
]
verifylist = [
('server', [servers[0].id]),
]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
mock_find_resource.assert_called_once_with(
mock.ANY, servers[0].id, all_tenants=True,
)
class TestServerSuspend(TestServer): class TestServerSuspend(TestServer):

View File

@ -0,0 +1,7 @@
---
features:
- |
The ``server delete``, ``server start`` and ``server stop`` commands now
support the ``--all-projects`` option. This allows you to perform the
specified action on a server in another project using the server name.
This is an admin-only action by default.

View File

@ -8,6 +8,7 @@ iso8601>=0.1.11 # MIT
openstacksdk>=0.52.0 # Apache-2.0 openstacksdk>=0.52.0 # Apache-2.0
osc-lib>=2.3.0 # Apache-2.0 osc-lib>=2.3.0 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0
oslo.utils>=3.33.0 # Apache-2.0
python-keystoneclient>=3.22.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0
python-novaclient>=17.0.0 # Apache-2.0 python-novaclient>=17.0.0 # Apache-2.0
python-cinderclient>=3.3.0 # Apache-2.0 python-cinderclient>=3.3.0 # Apache-2.0