summaryrefslogtreecommitdiff
path: root/openstackclient/compute/v2/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient/compute/v2/server.py')
-rw-r--r--openstackclient/compute/v2/server.py257
1 files changed, 194 insertions, 63 deletions
diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py
index c80b5a3c..306345bd 100644
--- a/openstackclient/compute/v2/server.py
+++ b/openstackclient/compute/v2/server.py
@@ -21,7 +21,9 @@ import io
import logging
import os
+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
@@ -120,17 +122,21 @@ def _prefix_checked_value(prefix):
return func
-def _prep_server_detail(compute_client, image_client, server):
+def _prep_server_detail(compute_client, image_client, server, refresh=True):
"""Prepare the detailed server dict for printing
:param compute_client: a compute client instance
+ :param image_client: an image client instance
:param server: a Server resource
+ :param refresh: Flag indicating if ``server`` is already the latest version
+ or if it needs to be refreshed, for example when showing
+ the latest details of a server after creating it.
:rtype: a dict of server details
"""
- info = server._info.copy()
-
- server = utils.find_resource(compute_client.servers, info['id'])
- info.update(server._info)
+ info = server.to_dict()
+ if refresh:
+ server = utils.find_resource(compute_client.servers, info['id'])
+ info.update(server.to_dict())
# Convert the image blob to a name
image_info = info.get('image', {})
@@ -144,12 +150,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(
@@ -178,7 +190,7 @@ def _prep_server_detail(compute_client, image_client, server):
if 'tenant_id' in info:
info['project_id'] = info.pop('tenant_id')
- # Map power state num to meanful string
+ # Map power state num to meaningful string
if 'OS-EXT-STS:power_state' in info:
info['OS-EXT-STS:power_state'] = _format_servers_list_power_state(
info['OS-EXT-STS:power_state'])
@@ -240,13 +252,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
@@ -263,12 +278,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(
@@ -556,7 +604,7 @@ class CreateServer(command.ShowOne):
type=_prefix_checked_value('port-id='),
help=_("Create a NIC on the server and connect it to port. "
"Specify option multiple times to create multiple NICs. "
- "This is a wrapper for the '--nic port-id=<pord>' "
+ "This is a wrapper for the '--nic port-id=<port>' "
"parameter that provides simple syntax for the standard "
"use case of connecting a new server to a given port. For "
"more advanced use cases, refer to the '--nic' parameter."),
@@ -798,9 +846,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 = []
@@ -946,13 +999,9 @@ class DeleteServer(command.Command):
compute_client.servers, server)
compute_client.servers.delete(server_obj.id)
if parsed_args.wait:
- if utils.wait_for_delete(
- compute_client.servers,
- server_obj.id,
- callback=_show_progress,
- ):
- self.app.stdout.write('\n')
- else:
+ if not utils.wait_for_delete(compute_client.servers,
+ server_obj.id,
+ callback=_show_progress):
LOG.error(_('Error deleting server: %s'),
server_obj.id)
self.app.stdout.write(_('Error deleting server\n'))
@@ -1034,11 +1083,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',
@@ -1213,32 +1273,55 @@ 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
+ if data and not parsed_args.no_name_lookup:
+ # 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 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))):
+ 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.
for s in data:
+ if compute_client.api_version >= api_versions.APIVersion('2.69'):
+ # NOTE(tssurya): From 2.69, we will have the keys 'flavor'
+ # and 'image' missing in the server response during
+ # infrastructure failure situations.
+ # For those servers with partial constructs we just skip the
+ # processing of the image and flavor informations.
+ if not hasattr(s, 'image') or not hasattr(s, 'flavor'):
+ continue
if 'id' in s.image:
image = images.get(s.image['id'])
if image:
@@ -1253,6 +1336,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 = ''
@@ -1370,11 +1457,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 "
@@ -1501,10 +1590,33 @@ class RebuildServer(command.ShowOne):
help=_("Set the password on the rebuilt instance"),
)
parser.add_argument(
+ '--property',
+ metavar='<key=value>',
+ action=parseractions.KeyValueAction,
+ help=_('Set a property on the rebuilt instance '
+ '(repeat option to set multiple values)'),
+ )
+ parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for rebuild to complete'),
)
+ key_group = parser.add_mutually_exclusive_group()
+ key_group.add_argument(
+ '--key-name',
+ metavar='<key-name>',
+ help=_("Set the key name of key pair on the rebuilt instance."
+ " Cannot be specified with the '--key-unset' option."
+ " (Supported by API versions '2.54' - '2.latest')"),
+ )
+ key_group.add_argument(
+ '--key-unset',
+ action='store_true',
+ default=False,
+ help=_("Unset the key name of key pair on the rebuilt instance."
+ " Cannot be specified with the '--key-name' option."
+ " (Supported by API versions '2.54' - '2.latest')"),
+ )
return parser
def take_action(self, parsed_args):
@@ -1521,10 +1633,25 @@ class RebuildServer(command.ShowOne):
compute_client.servers, parsed_args.server)
# If parsed_args.image is not set, default to the currently used one.
- image_id = parsed_args.image or server._info.get('image', {}).get('id')
+ image_id = parsed_args.image or server.to_dict().get(
+ 'image', {}).get('id')
image = utils.find_resource(image_client.images, image_id)
- server = server.rebuild(image, parsed_args.password)
+ kwargs = {}
+ if parsed_args.property:
+ kwargs['meta'] = parsed_args.property
+
+ if parsed_args.key_name or parsed_args.key_unset:
+ if compute_client.api_version < api_versions.APIVersion('2.54'):
+ msg = _('--os-compute-api-version 2.54 or later is required')
+ raise exceptions.CommandError(msg)
+
+ if parsed_args.key_unset:
+ kwargs['key_name'] = None
+ if parsed_args.key_name:
+ kwargs['key_name'] = parsed_args.key_name
+
+ server = server.rebuild(image, parsed_args.password, **kwargs)
if parsed_args.wait:
if utils.wait_for_status(
compute_client.servers.get,
@@ -1538,7 +1665,8 @@ class RebuildServer(command.ShowOne):
self.app.stdout.write(_('Error rebuilding server\n'))
raise SystemExit
- details = _prep_server_detail(compute_client, image_client, server)
+ details = _prep_server_detail(compute_client, image_client, server,
+ refresh=False)
return zip(*sorted(six.iteritems(details)))
@@ -1988,7 +2116,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)
@@ -2019,7 +2149,8 @@ class ShowServer(command.ShowOne):
return ({}, {})
else:
data = _prep_server_detail(compute_client,
- self.app.client_manager.image, server)
+ self.app.client_manager.image, server,
+ refresh=False)
return zip(*sorted(six.iteritems(data)))