diff options
Diffstat (limited to 'openstackclient/compute')
| -rw-r--r-- | openstackclient/compute/v2/server.py | 174 | ||||
| -rw-r--r-- | openstackclient/compute/v2/service.py | 75 |
2 files changed, 196 insertions, 53 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 6734832b..86af9ac5 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -22,7 +22,9 @@ import logging import os import sys +from novaclient import api_versions from novaclient.v2 import servers +from openstack import exceptions as sdk_exceptions from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions @@ -145,12 +147,18 @@ def _prep_server_detail(compute_client, image_client, server): # Convert the flavor blob to a name flavor_info = info.get('flavor', {}) - flavor_id = flavor_info.get('id', '') - try: - flavor = utils.find_resource(compute_client.flavors, flavor_id) - info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) - except Exception: - info['flavor'] = flavor_id + # Microversion 2.47 puts the embedded flavor into the server response + # body but omits the id, so if not present we just expose the flavor + # dict in the server output. + if 'id' in flavor_info: + flavor_id = flavor_info.get('id', '') + try: + flavor = utils.find_resource(compute_client.flavors, flavor_id) + info['flavor'] = "%s (%s)" % (flavor.name, flavor_id) + except Exception: + info['flavor'] = flavor_id + else: + info['flavor'] = utils.format_dict(flavor_info) if 'os-extended-volumes:volumes_attached' in info: info.update( @@ -247,13 +255,16 @@ class AddFloatingIP(network_common.NetworkAndComputeCommand): parser.add_argument( "ip_address", metavar="<ip-address>", - help=_("Floating IP address to assign to server (IP only)"), + help=_("Floating IP address to assign to the first available " + "server port (IP only)"), ) parser.add_argument( "--fixed-ip-address", metavar="<ip-address>", help=_( - "Fixed IP address to associate with this floating IP address" + "Fixed IP address to associate with this floating IP address. " + "The first server port containing the fixed IP address will " + "be used" ), ) return parser @@ -270,12 +281,45 @@ class AddFloatingIP(network_common.NetworkAndComputeCommand): compute_client.servers, parsed_args.server, ) - port = list(client.ports(device_id=server.id))[0] - attrs['port_id'] = port.id + ports = list(client.ports(device_id=server.id)) + # If the fixed IP address was specified, we need to find the + # corresponding port. if parsed_args.fixed_ip_address: - attrs['fixed_ip_address'] = parsed_args.fixed_ip_address - - client.update_ip(obj, **attrs) + fip_address = parsed_args.fixed_ip_address + attrs['fixed_ip_address'] = fip_address + for port in ports: + for ip in port.fixed_ips: + if ip['ip_address'] == fip_address: + attrs['port_id'] = port.id + break + else: + continue + break + if 'port_id' not in attrs: + msg = _('No port found for fixed IP address %s') + raise exceptions.CommandError(msg % fip_address) + client.update_ip(obj, **attrs) + else: + # It's possible that one or more ports are not connected to a + # router and thus could fail association with a floating IP. + # Try each port until one succeeds. If none succeed, re-raise the + # last exception. + error = None + for port in ports: + attrs['port_id'] = port.id + try: + client.update_ip(obj, **attrs) + except sdk_exceptions.NotFoundException as exp: + # 404 ExternalGatewayForFloatingIPNotFound from neutron + LOG.info('Skipped port %s because it is not attached to ' + 'an external gateway', port.id) + error = exp + continue + else: + error = None + break + if error: + raise error def take_action_compute(self, client, parsed_args): client.api.floating_ip_add( @@ -754,9 +798,14 @@ class CreateServer(command.ShowOne): raise exceptions.CommandError(msg) nics = nics[0] else: - # Default to empty list if nothing was specified, let nova side to - # decide the default behavior. - nics = [] + # Compute API version >= 2.37 requires a value, so default to + # 'auto' to maintain legacy behavior if a nic wasn't specified. + if compute_client.api_version >= api_versions.APIVersion('2.37'): + nics = 'auto' + else: + # Default to empty list if nothing was specified, let nova + # side to decide the default behavior. + nics = [] # Check security group exist and convert ID to name security_group_names = [] @@ -984,11 +1033,22 @@ class ListServer(command.Lister): default=False, help=_('List additional fields in output'), ) - parser.add_argument( + name_lookup_group = parser.add_mutually_exclusive_group() + name_lookup_group.add_argument( '-n', '--no-name-lookup', action='store_true', default=False, - help=_('Skip flavor and image name lookup.'), + help=_('Skip flavor and image name lookup.' + 'Mutually exclusive with "--name-lookup-one-by-one"' + ' option.'), + ) + name_lookup_group.add_argument( + '--name-lookup-one-by-one', + action='store_true', + default=False, + help=_('When looking up flavor and image names, look them up' + 'one by one as needed instead of all together (default). ' + 'Mutually exclusive with "--no-name-lookup|-n" option.'), ) parser.add_argument( '--marker', @@ -1163,28 +1223,46 @@ class ListServer(command.Lister): limit=parsed_args.limit) images = {} - # Create a dict that maps image_id to image object. - # Needed so that we can display the "Image Name" column. - # "Image Name" is not crucial, so we swallow any exceptions. - if not parsed_args.no_name_lookup: - try: - images_list = self.app.client_manager.image.images.list() - for i in images_list: - images[i.id] = i - except Exception: - pass - flavors = {} - # Create a dict that maps flavor_id to flavor object. - # Needed so that we can display the "Flavor Name" column. - # "Flavor Name" is not crucial, so we swallow any exceptions. if not parsed_args.no_name_lookup: - try: - flavors_list = compute_client.flavors.list() - for i in flavors_list: - flavors[i.id] = i - except Exception: - pass + # Create a dict that maps image_id to image object. + # Needed so that we can display the "Image Name" column. + # "Image Name" is not crucial, so we swallow any exceptions. + # The 'image' attribute can be an empty string if the server was + # booted from a volume. + if parsed_args.name_lookup_one_by_one or image_id: + for i_id in set(filter(lambda x: x is not None, + (s.image.get('id') for s in data + if s.image))): + try: + images[i_id] = image_client.images.get(i_id) + except Exception: + pass + else: + try: + images_list = image_client.images.list() + for i in images_list: + images[i.id] = i + except Exception: + pass + + # Create a dict that maps flavor_id to flavor object. + # Needed so that we can display the "Flavor Name" column. + # "Flavor Name" is not crucial, so we swallow any exceptions. + if parsed_args.name_lookup_one_by_one or flavor_id: + for f_id in set(filter(lambda x: x is not None, + (s.flavor.get('id') for s in data))): + try: + flavors[f_id] = compute_client.flavors.get(f_id) + except Exception: + pass + else: + try: + flavors_list = compute_client.flavors.list(is_public=None) + for i in flavors_list: + flavors[i.id] = i + except Exception: + pass # Populate image_name, image_id, flavor_name and flavor_id attributes # of server objects so that we can display those columns. @@ -1203,6 +1281,10 @@ class ListServer(command.Lister): s.flavor_name = flavor.name s.flavor_id = s.flavor['id'] else: + # TODO(mriedem): Fix this for microversion >= 2.47 where the + # flavor is embedded in the server response without the id. + # We likely need to drop the Flavor ID column in that case if + # --long is specified. s.flavor_name = '' s.flavor_id = '' @@ -1315,11 +1397,13 @@ class MigrateServer(command.Command): parsed_args.server, ) if parsed_args.live: - server.live_migrate( - host=parsed_args.live, - block_migration=parsed_args.block_migration, - disk_over_commit=parsed_args.disk_overcommit, - ) + kwargs = { + 'host': parsed_args.live, + 'block_migration': parsed_args.block_migration + } + if compute_client.api_version < api_versions.APIVersion('2.25'): + kwargs['disk_over_commit'] = parsed_args.disk_overcommit + server.live_migrate(**kwargs) else: if parsed_args.block_migration or parsed_args.disk_overcommit: raise exceptions.CommandError("--live must be specified if " @@ -1916,7 +2000,9 @@ class ShelveServer(command.Command): class ShowServer(command.ShowOne): - _description = _("Show server details") + _description = _( + "Show server details. Specify ``--os-compute-api-version 2.47`` " + "or higher to see the embedded flavor information for the server.") def get_parser(self, prog_name): parser = super(ShowServer, self).get_parser(prog_name) diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 7331d29d..5e255723 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -17,6 +17,7 @@ import logging +from novaclient import api_versions from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils @@ -36,7 +37,10 @@ class DeleteService(command.Command): "service", metavar="<service>", nargs='+', - help=_("Compute service(s) to delete (ID only)") + help=_("Compute service(s) to delete (ID only). If using " + "``--os-compute-api-version`` 2.53 or greater, the ID is " + "a UUID which can be retrieved by listing compute services " + "using the same 2.53+ microversion.") ) return parser @@ -59,7 +63,11 @@ class DeleteService(command.Command): class ListService(command.Lister): - _description = _("List compute services") + _description = _("List compute services. Using " + "``--os-compute-api-version`` 2.53 or greater will " + "return the ID as a UUID value which can be used to " + "uniquely identify the service in a multi-cell " + "deployment.") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) @@ -158,6 +166,33 @@ class SetService(command.Command): ) return parser + @staticmethod + def _find_service_by_host_and_binary(cs, host, binary): + """Utility method to find a compute service by host and binary + + :param host: the name of the compute service host + :param binary: the compute service binary, e.g. nova-compute + :returns: novaclient.v2.services.Service dict-like object + :raises: CommandError if no or multiple results were found + """ + services = cs.list(host=host, binary=binary) + # Did we find anything? + if not len(services): + msg = _('Compute service for host "%(host)s" and binary ' + '"%(binary)s" not found.') % { + 'host': host, 'binary': binary} + raise exceptions.CommandError(msg) + # Did we find more than one result? This should not happen but let's + # be safe. + if len(services) > 1: + # TODO(mriedem): If we have an --id option for 2.53+ then we can + # say to use that option to uniquely identify the service. + msg = _('Multiple compute services found for host "%(host)s" and ' + 'binary "%(binary)s". Unable to proceed.') % { + 'host': host, 'binary': binary} + raise exceptions.CommandError(msg) + return services[0] + def take_action(self, parsed_args): compute_client = self.app.client_manager.compute cs = compute_client.services @@ -168,6 +203,20 @@ class SetService(command.Command): "--disable specified.") raise exceptions.CommandError(msg) + # Starting with microversion 2.53, there is a single + # PUT /os-services/{service_id} API for updating nova-compute + # services. If 2.53+ is used we need to find the nova-compute + # service using the --host and --service (binary) values. + requires_service_id = ( + compute_client.api_version >= api_versions.APIVersion('2.53')) + service_id = None + if requires_service_id: + # TODO(mriedem): Add an --id option so users can pass the service + # id (as a uuid) directly rather than make us look it up using + # host/binary. + service_id = SetService._find_service_by_host_and_binary( + cs, parsed_args.host, parsed_args.service).id + result = 0 enabled = None try: @@ -178,14 +227,21 @@ class SetService(command.Command): if enabled is not None: if enabled: - cs.enable(parsed_args.host, parsed_args.service) + args = (service_id,) if requires_service_id else ( + parsed_args.host, parsed_args.service) + cs.enable(*args) else: if parsed_args.disable_reason: - cs.disable_log_reason(parsed_args.host, - parsed_args.service, - parsed_args.disable_reason) + args = (service_id, parsed_args.disable_reason) if \ + requires_service_id else ( + parsed_args.host, + parsed_args.service, + parsed_args.disable_reason) + cs.disable_log_reason(*args) else: - cs.disable(parsed_args.host, parsed_args.service) + args = (service_id,) if requires_service_id else ( + parsed_args.host, parsed_args.service) + cs.disable(*args) except Exception: status = "enabled" if enabled else "disabled" LOG.error("Failed to set service status to %s", status) @@ -198,8 +254,9 @@ class SetService(command.Command): if parsed_args.up: force_down = False if force_down is not None: - cs.force_down(parsed_args.host, parsed_args.service, - force_down=force_down) + args = (service_id, force_down) if requires_service_id else ( + parsed_args.host, parsed_args.service, force_down) + cs.force_down(*args) except Exception: state = "down" if force_down else "up" LOG.error("Failed to set service state to %s", state) |
