diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 78ab16d72b..694a1f728f 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -670,7 +670,7 @@ class AddNetwork(command.Command): class AddServerSecurityGroup(command.Command): - _description = _("Add security group to server") + _description = _("Add security group(s) to server") def get_parser(self, prog_name): parser = super().get_parser(prog_name) @@ -680,9 +680,13 @@ class AddServerSecurityGroup(command.Command): help=_('Server (name or ID)'), ) parser.add_argument( - 'group', - metavar='', - help=_('Security group to add (name or ID)'), + 'security_groups', + metavar='', + nargs='+', + help=_( + 'Security group(s) to add to the server (name or ID) ' + '(repeat option to add multiple groups)' + ), ) return parser @@ -694,14 +698,43 @@ class AddServerSecurityGroup(command.Command): ) if self.app.client_manager.is_network_endpoint_enabled(): # the server handles both names and IDs for neutron SGs, so just - # pass things through - security_group = parsed_args.group + # pass things through if using neutron + security_groups = parsed_args.security_groups else: - # however, if using nova-network then it needs a name, not an ID - security_group = compute_v2.find_security_group( - compute_client, parsed_args.group - )['name'] - compute_client.add_security_group_to_server(server, security_group) + # however, if using nova-network then it needs names, not IDs + security_groups = [] + for security_group in parsed_args.security_groups: + security_groups.append( + compute_v2.find_security_group( + compute_client, security_group + )['name'] + ) + + errors = 0 + for security_group in security_groups: + try: + compute_client.add_security_group_to_server( + server, security_group + ) + except sdk_exceptions.HttpException as e: + errors += 1 + LOG.error( + _( + "Failed to add security group with name or ID " + "'%(security_group)s' to server '%(server)s': %(e)s" + ), + { + 'security_group': security_group, + 'server': server.id, + 'e': e, + }, + ) + + if errors > 0: + msg = _( + "%(errors)d of %(total)d security groups were not added." + ) % {'errors': errors, 'total': len(security_groups)} + raise exceptions.CommandError(msg) class AddServerVolume(command.ShowOne): @@ -1328,6 +1361,7 @@ class CreateServer(command.ShowOne): metavar='', action='append', default=[], + dest='security_groups', help=_( 'Security group to assign to this server (name or ID) ' '(repeat option to set multiple groups)' @@ -1948,21 +1982,22 @@ class CreateServer(command.ShowOne): # 'auto' to maintain legacy behavior if a nic wasn't specified. networks = 'auto' - # Check security group exist and convert ID to name + # Check security group(s) exist and convert ID to name security_groups = [] if self.app.client_manager.is_network_endpoint_enabled(): network_client = self.app.client_manager.network - for each_sg in parsed_args.security_group: + for security_group in parsed_args.security_groups: sg = network_client.find_security_group( - each_sg, ignore_missing=False + security_group, ignore_missing=False ) # Use security group ID to avoid multiple security group have # same name in neutron networking backend security_groups.append({'name': sg.id}) - else: - # Handle nova-network case - for each_sg in parsed_args.security_group: - sg = compute_v2.find_security_group(compute_client, each_sg) + else: # nova-network + for security_group in parsed_args.security_groups: + sg = compute_v2.find_security_group( + compute_client, security_group + ) security_groups.append({'name': sg['name']}) hints = {} @@ -4016,9 +4051,13 @@ class RemoveServerSecurityGroup(command.Command): help=_('Server (name or ID)'), ) parser.add_argument( - 'group', - metavar='', - help=_('Security group to remove (name or ID)'), + 'security_groups', + metavar='', + nargs='+', + help=_( + 'Security group(s) to remove from server (name or ID) ' + '(repeat option to remove multiple groups)' + ), ) return parser @@ -4031,15 +4070,42 @@ class RemoveServerSecurityGroup(command.Command): if self.app.client_manager.is_network_endpoint_enabled(): # the server handles both names and IDs for neutron SGs, so just # pass things through - security_group = parsed_args.group + security_groups = parsed_args.security_groups else: - # however, if using nova-network then it needs a name, not an ID - security_group = compute_v2.find_security_group( - compute_client, parsed_args.group - )['name'] - compute_client.remove_security_group_from_server( - server, security_group - ) + # however, if using nova-network then it needs names, not IDs + security_groups = [] + for security_group in parsed_args.security_groups: + security_groups.append( + compute_v2.find_security_group( + compute_client, security_group + )['name'] + ) + + errors = 0 + for security_group in security_groups: + try: + compute_client.remove_security_group_from_server( + server, security_group + ) + except sdk_exceptions.HttpException as e: + errors += 1 + LOG.error( + _( + "Failed to remove security group with name or ID " + "'%(security_group)s' from server '%(server)s': %(e)s" + ), + { + 'security_group': security_group, + 'server': server.id, + 'e': e, + }, + ) + + if errors > 0: + msg = _( + "%(errors)d of %(total)d security groups were not removed." + ) % {'errors': errors, 'total': len(security_groups)} + raise exceptions.CommandError(msg) class RemoveServerVolume(command.Command): diff --git a/openstackclient/tests/functional/common/test_help.py b/openstackclient/tests/functional/common/test_help.py index f1aea8ee98..8f31c25ebe 100644 --- a/openstackclient/tests/functional/common/test_help.py +++ b/openstackclient/tests/functional/common/test_help.py @@ -21,7 +21,7 @@ class HelpTests(base.TestCase): """Functional tests for openstackclient help output.""" SERVER_COMMANDS = [ - ('server add security group', 'Add security group to server'), + ('server add security group', 'Add security group(s) to server'), ('server add volume', 'Add volume to server'), ('server backup create', 'Create a server backup image'), ('server create', 'Create a new server'), diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index c96e0fada1..8d62aacd33 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -1166,7 +1166,7 @@ class TestServerAddSecurityGroup(compute_fakes.TestComputev2): arglist = [self.server.id, 'fake_sg'] verifylist = [ ('server', self.server.id), - ('group', 'fake_sg'), + ('security_groups', ['fake_sg']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1197,7 +1197,7 @@ class TestServerAddSecurityGroup(compute_fakes.TestComputev2): arglist = [self.server.id, 'fake_sg'] verifylist = [ ('server', self.server.id), - ('group', 'fake_sg'), + ('security_groups', ['fake_sg']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1429,7 +1429,7 @@ class TestServerCreate(TestServer): ('flavor', self.flavor.id), ('key_name', 'keyname'), ('properties', {'Beta': 'b'}), - ('security_group', [security_group.id]), + ('security_groups', [security_group.id]), ('hints', {'a': ['b', 'c']}), ('server_group', server_group.id), ('config_drive', True), @@ -1499,7 +1499,7 @@ class TestServerCreate(TestServer): ('image', self.image.id), ('flavor', self.flavor.id), ('key_name', 'keyname'), - ('security_group', ['not_exist_sg']), + ('security_groups', ['not_exist_sg']), ('server_name', self.server.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1525,7 +1525,7 @@ class TestServerCreate(TestServer): verifylist = [ ('image', self.image.id), ('flavor', self.flavor.id), - ('security_group', [sg_name]), + ('security_groups', [sg_name]), ('server_name', self.server.name), ] @@ -7416,7 +7416,7 @@ class TestServerRemoveSecurityGroup(TestServer): arglist = [self.server.id, 'fake_sg'] verifylist = [ ('server', self.server.id), - ('group', 'fake_sg'), + ('security_groups', ['fake_sg']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -7447,7 +7447,7 @@ class TestServerRemoveSecurityGroup(TestServer): arglist = [self.server.id, 'fake_sg'] verifylist = [ ('server', self.server.id), - ('group', 'fake_sg'), + ('security_groups', ['fake_sg']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/releasenotes/notes/add-remove-multiple-security-groups-2c0b2d599124c9c9.yaml b/releasenotes/notes/add-remove-multiple-security-groups-2c0b2d599124c9c9.yaml new file mode 100644 index 0000000000..1938f7121c --- /dev/null +++ b/releasenotes/notes/add-remove-multiple-security-groups-2c0b2d599124c9c9.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``server add security group`` and ``server remove security group`` + commands now accept multiple security groups.