summaryrefslogtreecommitdiff
path: root/openstackclient/compute
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient/compute')
-rw-r--r--openstackclient/compute/v2/server.py174
-rw-r--r--openstackclient/compute/v2/service.py75
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)