Client for OpenStack services
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
python-openstackclient/openstackclient/compute/v2/server.py

4603 lines
161 KiB

# Copyright 2012-2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Compute v2 Server action implementations"""
import argparse
import getpass
import io
import json
import logging
import os
from cliff import columns as cliff_columns
import iso8601
from novaclient import api_versions
from novaclient.v2 import servers
from openstack import exceptions as sdk_exceptions
from openstack import utils as sdk_utils
from osc_lib.cli import format_columns
from osc_lib.cli import parseractions
from osc_lib.command import command
from osc_lib import exceptions
from osc_lib import utils
from oslo_utils import strutils
from openstackclient.i18n import _
from openstackclient.identity import common as identity_common
from openstackclient.network import common as network_common
LOG = logging.getLogger(__name__)
IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)'
class PowerStateColumn(cliff_columns.FormattableColumn):
"""Generate a formatted string of a server's power state."""
power_states = [
'NOSTATE', # 0x00
'Running', # 0x01
'', # 0x02
'Paused', # 0x03
'Shutdown', # 0x04
'', # 0x05
'Crashed', # 0x06
'Suspended' # 0x07
]
def human_readable(self):
try:
return self.power_states[self._value]
except Exception:
return 'N/A'
def _get_ip_address(addresses, address_type, ip_address_family):
# Old style addresses
if address_type in addresses:
for addy in addresses[address_type]:
if int(addy['version']) in ip_address_family:
return addy['addr']
# New style addresses
new_address_type = address_type
if address_type == 'public':
new_address_type = 'floating'
if address_type == 'private':
new_address_type = 'fixed'
for network in addresses:
for addy in addresses[network]:
# Case where it is list of strings
if isinstance(addy, str):
if new_address_type == 'fixed':
return addresses[network][0]
else:
return addresses[network][-1]
# Case where it is a dict
if 'OS-EXT-IPS:type' not in addy:
continue
if addy['OS-EXT-IPS:type'] == new_address_type:
if int(addy['version']) in ip_address_family:
return addy['addr']
msg = _("ERROR: No %(type)s IP version %(family)s address found")
raise exceptions.CommandError(
msg % {"type": address_type,
"family": ip_address_family}
)
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.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', {})
if image_info:
image_id = image_info.get('id', '')
try:
image = image_client.get_image(image_id)
info['image'] = "%s (%s)" % (image.name, image_id)
except Exception:
info['image'] = image_id
else:
# NOTE(melwitt): An server booted from a volume will have no image
# associated with it. We fill in the image with "N/A (booted from
# volume)" to help users who want to be able to grep for
# boot-from-volume servers when using the CLI.
info['image'] = IMAGE_STRING_FOR_BFV
# Convert the flavor blob to a name
flavor_info = info.get('flavor', {})
# 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'] = format_columns.DictColumn(flavor_info)
if 'os-extended-volumes:volumes_attached' in info:
info.update(
{
'volumes_attached': format_columns.ListDictColumn(
info.pop('os-extended-volumes:volumes_attached'))
}
)
if 'security_groups' in info:
info.update(
{
'security_groups': format_columns.ListDictColumn(
info.pop('security_groups'))
}
)
if 'tags' in info:
info.update({'tags': format_columns.ListColumn(info.pop('tags'))})
# NOTE(dtroyer): novaclient splits these into separate entries...
# Format addresses in a useful way
info['addresses'] = format_columns.DictListColumn(server.networks)
# Map 'metadata' field to 'properties'
info['properties'] = format_columns.DictColumn(info.pop('metadata'))
# Migrate tenant_id to project_id naming
if 'tenant_id' in info:
info['project_id'] = info.pop('tenant_id')
# Map power state num to meaningful string
if 'OS-EXT-STS:power_state' in info:
info['OS-EXT-STS:power_state'] = PowerStateColumn(
info['OS-EXT-STS:power_state'])
# Remove values that are long and not too useful
info.pop('links', None)
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.ShowOne):
_description = _("Add fixed IP address to server")
def get_parser(self, prog_name):
parser = super(AddFixedIP, self).get_parser(prog_name)
parser.add_argument(
"server",
metavar="<server>",
help=_("Server to receive the fixed IP address (name or ID)"),
)
parser.add_argument(
"network",
metavar="<network>",
help=_(
"Network to allocate the fixed IP address from (name or ID)"
),
)
parser.add_argument(
"--fixed-ip-address",
metavar="<ip-address>",
help=_("Requested fixed IP address"),
)
parser.add_argument(
'--tag',
metavar='<tag>',
help=_(
'Tag for the attached interface. '
'(supported by --os-compute-api-version 2.49 or above)'
)
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute
server = compute_client.find_server(
parsed_args.server,
ignore_missing=False
)
if parsed_args.tag:
if not sdk_utils.supports_microversion(compute_client, '2.49'):
msg = _(
'--os-compute-api-version 2.49 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
net_id = network_client.find_network(
parsed_args.network,
ignore_missing=False
).id
else:
net_id = parsed_args.network
if not sdk_utils.supports_microversion(compute_client, '2.44'):
compute_client.add_fixed_ip_to_server(
server.id,
net_id
)
return ((), ())
kwargs = {
'net_id': net_id,
'fixed_ip': parsed_args.fixed_ip_address,
}
if parsed_args.tag:
kwargs['tag'] = parsed_args.tag
interface = compute_client.create_server_interface(server.id, **kwargs)
columns = (
'port_id', 'server_id', 'net_id', 'mac_addr', 'port_state',
'fixed_ips',
)
column_headers = (
'Port ID', 'Server ID', 'Network ID', 'MAC Address', 'Port State',
'Fixed IPs',
)
if sdk_utils.supports_microversion(compute_client, '2.49'):
columns += ('tag',)
column_headers += ('Tag',)
return (
column_headers,
utils.get_item_properties(
interface,
columns,
formatters={
'fixed_ips': format_columns.ListDictColumn,
},
),
)
class AddFloatingIP(network_common.NetworkAndComputeCommand):
_description = _("Add floating IP address to server")
def update_parser_common(self, parser):
parser.add_argument(
"server",
metavar="<server>",
help=_("Server to receive the floating IP address (name or ID)"),
)
parser.add_argument(
"ip_address",
metavar="<ip-address>",
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. "
"The first server port containing the fixed IP address will "
"be used"
),
)
return parser
def take_action_network(self, client, parsed_args):
compute_client = self.app.client_manager.compute
attrs = {}
obj = client.find_ip(
parsed_args.ip_address,
ignore_missing=False,
)
server = utils.find_resource(
compute_client.servers,
parsed_args.server,
)
ports = list(client.ports(device_id=server.id))
if not ports:
msg = _('No attached ports found to associate floating IP with')
raise exceptions.CommandError(msg)
# If the fixed IP address was specified, we need to find the
# corresponding port.
if parsed_args.fixed_ip_address:
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(
parsed_args.server,
parsed_args.ip_address,
fixed_address=parsed_args.fixed_ip_address,
)
class AddPort(command.Command):
_description = _("Add port to server")
def get_parser(self, prog_name):
parser = super(AddPort, self).get_parser(prog_name)
parser.add_argument(
"server",
metavar="<server>",
help=_("Server to add the port to (name or ID)"),
)
parser.add_argument(
"port",
metavar="<port>",
help=_("Port to add to the server (name or ID)"),
)
parser.add_argument(
'--tag',
metavar='<tag>',
help=_(
"Tag for the attached interface. "
"(Supported by API versions '2.49' - '2.latest')"
)
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute
server = compute_client.find_server(
parsed_args.server, ignore_missing=False)
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
port_id = network_client.find_port(
parsed_args.port, ignore_missing=False).id
else:
port_id = parsed_args.port
kwargs = {
'port_id': port_id,
'fixed_ip': None,
}
if parsed_args.tag:
if not sdk_utils.supports_microversion(compute_client, '2.49'):
msg = _(
'--os-compute-api-version 2.49 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
kwargs['tag'] = parsed_args.tag
compute_client.create_server_interface(server, **kwargs)
class AddNetwork(command.Command):
_description = _("Add network to server")
def get_parser(self, prog_name):
parser = super(AddNetwork, self).get_parser(prog_name)
parser.add_argument(
"server",
metavar="<server>",
help=_("Server to add the network to (name or ID)"),
)
parser.add_argument(
"network",
metavar="<network>",
help=_("Network to add to the server (name or ID)"),
)
parser.add_argument(
'--tag',
metavar='<tag>',
help=_(
'Tag for the attached interface. '
'(supported by --os-compute-api-version 2.49 or above)'
),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute
server = compute_client.find_server(
parsed_args.server, ignore_missing=False)
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
net_id = network_client.find_network(
parsed_args.network, ignore_missing=False).id
else:
net_id = parsed_args.network
kwargs = {
'net_id': net_id,
'fixed_ip': None,
}
if parsed_args.tag:
if not sdk_utils.supports_microversion(compute_client, '2.49'):
msg = _(
'--os-compute-api-version 2.49 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
kwargs['tag'] = parsed_args.tag
compute_client.create_server_interface(server, **kwargs)
class AddServerSecurityGroup(command.Command):
_description = _("Add security group to server")
def get_parser(self, prog_name):
parser = super(AddServerSecurityGroup, self).get_parser(prog_name)
parser.add_argument(
'server',
metavar='<server>',
help=_('Server (name or ID)'),
)
parser.add_argument(
'group',
metavar='<group>',
help=_('Security group to add (name or ID)'),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
server = utils.find_resource(
compute_client.servers,
parsed_args.server,
)
security_group = compute_client.api.security_group_find(
parsed_args.group,
)
server.add_security_group(security_group['id'])
class AddServerVolume(command.ShowOne):
_description = _(
"Add volume to server. "
"Specify ``--os-compute-api-version 2.20`` or higher to add a volume "
"to a server with status ``SHELVED`` or ``SHELVED_OFFLOADED``.")
def get_parser(self, prog_name):
parser = super(AddServerVolume, self).get_parser(prog_name)
parser.add_argument(
'server',
metavar='<server>',
help=_('Server (name or ID)'),
)
parser.add_argument(
'volume',
metavar='<volume>',
help=_('Volume to add (name or ID)'),
)
parser.add_argument(
'--device',
metavar='<device>',
help=_('Server internal device name for volume'),
)
parser.add_argument(
'--tag',
metavar='<tag>',
help=_(
'Tag for the attached volume '
'(supported by --os-compute-api-version 2.49 or above)'
),
)
# TODO(stephenfin): These should be called 'delete-on-termination' and
# 'preserve-on-termination'
termination_group = parser.add_mutually_exclusive_group()
termination_group.add_argument(
'--enable-delete-on-termination',
action='store_true',
help=_(
'Delete the volume when the server is destroyed '
'(supported by --os-compute-api-version 2.79 or above)'
),
)
termination_group.add_argument(
'--disable-delete-on-termination',
action='store_true',
help=_(
'Do not delete the volume when the server is destroyed '
'(supported by --os-compute-api-version 2.79 or above)'
),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.sdk_connection.compute
volume_client = self.app.client_manager.sdk_connection.volume
server = compute_client.find_server(
parsed_args.server,
ignore_missing=False,
)
volume = volume_client.find_volume(
parsed_args.volume,
ignore_missing=False,
)
kwargs = {
"volumeId": volume.id,
"device": parsed_args.device
}
if parsed_args.tag:
if not sdk_utils.supports_microversion(compute_client, '2.49'):
msg = _(
'--os-compute-api-version 2.49 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
kwargs['tag'] = parsed_args.tag
if parsed_args.enable_delete_on_termination:
if not sdk_utils.supports_microversion(compute_client, '2.79'):
msg = _(
'--os-compute-api-version 2.79 or greater is required to '
'support the --enable-delete-on-termination option.'
)
raise exceptions.CommandError(msg)
kwargs['delete_on_termination'] = True
if parsed_args.disable_delete_on_termination:
if not sdk_utils.supports_microversion(compute_client, '2.79'):
msg = _(
'--os-compute-api-version 2.79 or greater is required to '
'support the --disable-delete-on-termination option.'
)
raise exceptions.CommandError(msg)
kwargs['delete_on_termination'] = False
volume_attachment = compute_client.create_volume_attachment(
server,
**kwargs,
)
columns = ('id', 'server id', 'volume id', 'device')
column_headers = ('ID', 'Server ID', 'Volume ID', 'Device')
if sdk_utils.supports_microversion(compute_client, '2.49'):
columns += ('tag',)
column_headers += ('Tag',)
if sdk_utils.supports_microversion(compute_client, '2.79'):
columns += ('delete_on_termination',)
column_headers += ('Delete On Termination',)
return (
column_headers,
utils.get_item_properties(volume_attachment, columns,)
)
# TODO(stephenfin): Replace with 'MultiKeyValueAction' when we no longer
# support '--nic=auto' and '--nic=none'
class NICAction(argparse.Action):
def __init__(
self,
option_strings,
dest,
nargs=None,
const=None,
default=None,
type=None,
choices=None,
required=False,
help=None,
metavar=None,
key=None,
):
self.key = key
super().__init__(
option_strings=option_strings, dest=dest, nargs=nargs, const=const,
default=default, type=type, choices=choices, required=required,
help=help, metavar=metavar,
)
def __call__(self, parser, namespace, values, option_string=None):
# Make sure we have an empty dict rather than None
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
# Handle the special auto/none cases
if values in ('auto', 'none'):
getattr(namespace, self.dest).append(values)
return
if self.key:
if ',' in values or '=' in values:
msg = _(
"Invalid argument %s; characters ',' and '=' are not "
"allowed"
)
raise argparse.ArgumentTypeError(msg % values)
values = '='.join([self.key, values])
# We don't include 'tag' here by default since that requires a
# particular microversion
info = {
'net-id': '',
'port-id': '',
'v4-fixed-ip': '',
'v6-fixed-ip': '',
}
for kv_str in values.split(','):
k, sep, v = kv_str.partition("=")
if k not in list(info) + ['tag'] or not v:
msg = _(
"Invalid argument %s; argument must be of form "
"'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
"v6-fixed-ip=ip-addr,tag=tag'"
)
raise argparse.ArgumentTypeError(msg % values)
info[k] = v
if info['net-id'] and info['port-id']:
msg = _(
'Invalid argument %s; either network or port should be '
'specified but not both'
)
raise argparse.ArgumentTypeError(msg % values)
getattr(namespace, self.dest).append(info)
class BDMLegacyAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
# Make sure we have an empty list rather than None
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
dev_name, sep, dev_map = values.partition('=')
dev_map = dev_map.split(':') if dev_map else dev_map
if not dev_name or not dev_map or len(dev_map) > 4:
msg = _(
"Invalid argument %s; argument must be of form "
"'dev-name=id[:type[:size[:delete-on-terminate]]]'"
)
raise argparse.ArgumentTypeError(msg % values)
mapping = {
'device_name': dev_name,
# store target; this may be a name and will need verification later
'uuid': dev_map[0],
'source_type': 'volume',
'destination_type': 'volume',
}
# decide source and destination type
if len(dev_map) > 1 and dev_map[1]:
if dev_map[1] not in ('volume', 'snapshot', 'image'):
msg = _(
"Invalid argument %s; 'type' must be one of: volume, "
"snapshot, image"
)
raise argparse.ArgumentTypeError(msg % values)
mapping['source_type'] = dev_map[1]
# 3. append size and delete_on_termination, if present
if len(dev_map) > 2 and dev_map[2]:
mapping['volume_size'] = dev_map[2]
if len(dev_map) > 3 and dev_map[3]:
mapping['delete_on_termination'] = dev_map[3]
getattr(namespace, self.dest).append(mapping)
class BDMAction(parseractions.MultiKeyValueAction):
def __init__(self, option_strings, dest, **kwargs):
required_keys = []
optional_keys = [
'uuid', 'source_type', 'destination_type',
'disk_bus', 'device_type', 'device_name', 'volume_size',
'guest_format', 'boot_index', 'delete_on_termination', 'tag',
'volume_type',
]
super().__init__(
option_strings, dest, required_keys=required_keys,
optional_keys=optional_keys, **kwargs,
)
# TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b
# or similar lands in a release
def validate_keys(self, keys):
"""Validate the provided keys.
:param keys: A list of keys to validate.
"""
valid_keys = self.required_keys | self.optional_keys
invalid_keys = [k for k in keys if k not in valid_keys]
if invalid_keys:
msg = _(
"Invalid keys %(invalid_keys)s specified.\n"
"Valid keys are: %(valid_keys)s"
)
raise argparse.ArgumentTypeError(msg % {
'invalid_keys': ', '.join(invalid_keys),
'valid_keys': ', '.join(valid_keys),
})
missing_keys = [k for k in self.required_keys if k not in keys]
if missing_keys:
msg = _(
"Missing required keys %(missing_keys)s.\n"
"Required keys are: %(required_keys)s"
)
raise argparse.ArgumentTypeError(msg % {
'missing_keys': ', '.join(missing_keys),
'required_keys': ', '.join(self.required_keys),
})
def __call__(self, parser, namespace, values, option_string=None):
if getattr(namespace, self.dest, None) is None:
setattr(namespace, self.dest, [])
if os.path.exists(values):
with open(values) as fh:
data = json.load(fh)
# Validate the keys - other validation is left to later
self.validate_keys(list(data))
getattr(namespace, self.dest, []).append(data)
else:
super().__call__(parser, namespace, values, option_string)
class CreateServer(command.ShowOne):
_description = _("Create a new server")
def get_parser(self, prog_name):
parser = super(CreateServer, self).get_parser(prog_name)
parser.add_argument(
'server_name',
metavar='<server-name>',
help=_('New server name'),
)
parser.add_argument(
'--flavor',
metavar='<flavor>',
required=True,
help=_('Create server with this flavor (name or ID)'),
)
disk_group = parser.add_mutually_exclusive_group(
required=True,
)
disk_group.add_argument(
'--image',
metavar='<image>',
help=_('Create server boot disk from this image (name or ID)'),
)
# TODO(stephenfin): Is this actually useful? Looks like a straight port
# from 'nova boot --image-with'. Perhaps we should deprecate this.
disk_group.add_argument(
'--image-property',
metavar='<key=value>',
action=parseractions.KeyValueAction,
dest='image_properties',
help=_(
"Create server using the image that matches the specified "
"property. Property must match exactly one property."
),
)
disk_group.add_argument(
'--volume',
metavar='<volume>',
help=_(
'Create server using this volume as the boot disk (name or ID)'
'\n'
'This option automatically creates a block device mapping '
'with a boot index of 0. On many hypervisors (libvirt/kvm '
'for example) this will be device vda. Do not create a '
'duplicate mapping using --block-device-mapping for this '
'volume.'
),
)
disk_group.add_argument(
'--snapshot',
metavar='<snapshot>',
help=_(
'Create server using this snapshot as the boot disk (name or '
'ID)\n'
'This option automatically creates a block device mapping '
'with a boot index of 0. On many hypervisors (libvirt/kvm '
'for example) this will be device vda. Do not create a '
'duplicate mapping using --block-device-mapping for this '
'volume.'
),
)
parser.add_argument(
'--boot-from-volume',
metavar='<volume-size>',
type=int,
help=_(
'When used in conjunction with the ``--image`` or '
'``--image-property`` option, this option automatically '
'creates a block device mapping with a boot index of 0 '
'and tells the compute service to create a volume of the '
'given size (in GB) from the specified image and use it '
'as the root disk of the server. The root volume will not '
'be deleted when the server is deleted. This option is '
'mutually exclusive with the ``--volume`` and ``--snapshot`` '
'options.'
)
)
# TODO(stephenfin): Remove this in the v7.0
parser.add_argument(
'--block-device-mapping',
metavar='<dev-name=mapping>',
action=BDMLegacyAction,
default=[],
# NOTE(RuiChen): Add '\n' to the end of line to improve formatting;
# see cliff's _SmartHelpFormatter for more details.
help=_(
'**Deprecated** Create a block device on the server.\n'
'Block device mapping in the format\n'
'<dev-name>=<id>:<type>:<size(GB)>:<delete-on-terminate>\n'
'<dev-name>: block device name, like: vdb, xvdc '
'(required)\n'
'<id>: Name or ID of the volume, volume snapshot or image '
'(required)\n'
'<type>: volume, snapshot or image; default: volume '
'(optional)\n'
'<size(GB)>: volume size if create from image or snapshot '
'(optional)\n'
'<delete-on-terminate>: true or false; default: false '
'(optional)\n'
'Replaced by --block-device'
),
)
parser.add_argument(
'--block-device',
metavar='',
action=BDMAction,
dest='block_devices',
default=[],
help=_(
'Create a block device on the server.\n'
'Either a path to a JSON file or a CSV-serialized string '
'describing the block device mapping.\n'
'The following keys are accepted for both:\n'
'uuid=<uuid>: UUID of the volume, snapshot or ID '
'(required if using source image, snapshot or volume),\n'
'source_type=<source_type>: source type '
'(one of: image, snapshot, volume, blank),\n'
'destination_type=<destination_type>: destination type '
'(one of: volume, local) (optional),\n'
'disk_bus=<disk_bus>: device bus '
'(one of: uml, lxc, virtio, ...) (optional),\n'
'device_type=<device_type>: device type '
'(one of: disk, cdrom, etc. (optional),\n'
'device_name=<device_name>: name of the device (optional),\n'
'volume_size=<volume_size>: size of the block device in MiB '
'(for swap) or GiB (for everything else) (optional),\n'
'guest_format=<guest_format>: format of device (optional),\n'
'boot_index=<boot_index>: index of disk used to order boot '
'disk '
'(required for volume-backed instances),\n'
'delete_on_termination=<true|false>: whether to delete the '
'volume upon deletion of server (optional),\n'
'tag=<tag>: device metadata tag (optional),\n'
'volume_type=<volume_type>: type of volume to create (name or '
'ID) when source if blank, image or snapshot and dest is '
'volume (optional)'
),
)
parser.add_argument(
'--swap',
metavar='<swap>',
type=int,
help=(
"Create and attach a local swap block device of <swap_size> "
"MiB."
),
)
parser.add_argument(
'--ephemeral',
metavar='<size=size[,format=format]>',
action=parseractions.MultiKeyValueAction,
dest='ephemerals',
default=[],
required_keys=['size'],
optional_keys=['format'],
help=(
"Create and attach a local ephemeral block device of <size> "
"GiB and format it to <format>."
),
)
parser.add_argument(
'--network',
metavar="<network>",
dest='nics',
default=[],
action=NICAction,
key='net-id',
# NOTE(RuiChen): Add '\n' to the end of line to improve formatting;
# see cliff's _SmartHelpFormatter for more details.
help=_(
"Create a NIC on the server and connect it to network. "
"Specify option multiple times to create multiple NICs. "
"This is a wrapper for the '--nic net-id=<network>' "
"parameter that provides simple syntax for the standard "
"use case of connecting a new server to a given network. "
"For more advanced use cases, refer to the '--nic' "
"parameter."
),
)
parser.add_argument(
'--port',
metavar="<port>",
dest='nics',
default=[],
action=NICAction,
key='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=<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."
),
)
parser.add_argument(
'--nic',
metavar="<net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr,"
"v6-fixed-ip=ip-addr,tag=tag,auto,none>",
action=NICAction,
dest='nics',
default=[],
help=_(
"Create a NIC on the server.\n"
"NIC in the format:\n"
"net-id=<net-uuid>: attach NIC to network with this UUID,\n"
"port-id=<port-uuid>: attach NIC to port with this UUID,\n"
"v4-fixed-ip=<ip-addr>: IPv4 fixed address for NIC (optional),"
"\n"
"v6-fixed-ip=<ip-addr>: IPv6 fixed address for NIC (optional),"
"\n"
"tag: interface metadata tag (optional) "
"(supported by --os-compute-api-version 2.43 or above),\n"
"none: (v2.37+) no network is attached,\n"
"auto: (v2.37+) the compute service will automatically "
"allocate a network.\n"
"\n"
"Specify option multiple times to create multiple NICs.\n"
"Specifying a --nic of auto or none cannot be used with any "
"other --nic value.\n"
"Either net-id or port-id must be provided, but not both."
),
)
parser.add_argument(
'--password',
metavar='<password>',
help=_(
'Set the password to this server. '
'This option requires cloud support.'
),
)
parser.add_argument(
'--security-group',
metavar='<security-group>',
action='append',
default=[],
help=_(
'Security group to assign to this server (name or ID) '
'(repeat option to set multiple groups)'
),
)
parser.add_argument(
'--key-name',
metavar='<key-name>',
help=_('Keypair to inject into this server'),
)
parser.add_argument(
'--property',
metavar='<key=value>',
action=parseractions.KeyValueAction,
dest='properties',
help=_(
'Set a property on this server '
'(repeat option to set multiple values)'
),
)
parser.add_argument(
'--file',
metavar='<dest-filename=source-filename>',
action='append',
default=[],
help=_(
'File(s) to inject into image before boot '
'(repeat option to set multiple files)'
'(supported by --os-compute-api-version 2.57 or below)'
),
)
parser.add_argument(
'--user-data',
metavar='<user-data>',
help=_('User data file to serve from the metadata server'),
)
parser.add_argument(
'--description',
metavar='<description>',
help=_(
'Set description for the server '
'(supported by --os-compute-api-version 2.19 or above)'
),
)
parser.add_argument(
'--availability-zone',
metavar='<zone-name>',
help=_(
'Select an availability zone for the server. '
'Host and node are optional parameters. '
'Availability zone in the format '
'<zone-name>:<host-name>:<node-name>, '
'<zone-name>::<node-name>, <zone-name>:<host-name> '
'or <zone-name>'
),
)
parser.add_argument(
'--host',
metavar='<host>',
help=_(
'Requested host to create servers. '
'(admin only) '
'(supported by --os-compute-api-version 2.74 or above)'
),
)
parser.add_argument(
'--hypervisor-hostname',
metavar='<hypervisor-hostname>',
help=_(
'Requested hypervisor hostname to create servers. '
'(admin only) '
'(supported by --os-compute-api-version 2.74 or above)'
),
)
parser.add_argument(
'--hint',
metavar='<key=value>',
action=parseractions.KeyValueAppendAction,
default={},
help=_('Hints for the scheduler'),
)
config_drive_group = parser.add_mutually_exclusive_group()
config_drive_group.add_argument(
'--use-config-drive',
action='store_true',
dest='config_drive',
help=_("Enable config drive."),
)
config_drive_group.add_argument(
'--no-config-drive',
action='store_false',
dest='config_drive',
help=_("Disable config drive."),
)
# TODO(stephenfin): Drop support in the next major version bump after
# Victoria
config_drive_group.add_argument(
'--config-drive',
metavar='<config-drive-volume>|True',
default=False,
help=_(
"**Deprecated** Use specified volume as the config drive, "
"or 'True' to use an ephemeral drive. Replaced by "
"'--use-config-drive'."
),
)
parser.add_argument(
'--min',
metavar='<count>',
type=int,
default=1,
help=_('Minimum number of servers to launch (default=1)'),
)
parser.add_argument(
'--max',
metavar='<count>',
type=int,
default=1,
help=_('Maximum number of servers to launch (default=1)'),
)
parser.add_argument(
'--tag',
metavar='<tag>',
action='append',
default=[],
dest='tags',
help=_(
'Tags for the server. '
'Specify multiple times to add multiple tags. '
'(supported by --os-compute-api-version 2.52 or above)'
),
)
parser.add_argument(
'--hostname',
metavar='<hostname>',
help=_(
'Hostname configured for the server in the metadata service. '
'If unset, a hostname will be automatically generated from '
'the server name. '
'A utility such as cloud-init is required to propagate the '
'hostname in the metadata service to the guest OS itself. '
'(supported by --os-compute-api-version 2.90 or above)'
),
)
parser.add_argument(
'--wait',
action='store_true',
help=_('Wait for build to complete'),
)
parser.add_argument(
'--trusted-image-cert',
metavar='<trusted-cert-id>',
action='append',
dest='trusted_image_certs',
help=_(
'Trusted image certificate IDs used to validate certificates '
'during the image signature verification process. '
'May be specified multiple times to pass multiple trusted '
'image certificate IDs. '
'(supported by --os-compute-api-version 2.63 or above)'
),
)
return parser
def take_action(self, parsed_args):
def _show_progress(progress):
if progress:
self.app.stdout.write('\rProgress: %s' % progress)
self.app.stdout.flush()
compute_client = self.app.client_manager.compute
volume_client = self.app.client_manager.volume
image_client = self.app.client_manager.image
# Lookup parsed_args.image
image = None
if parsed_args.image:
image = image_client.find_image(
parsed_args.image, ignore_missing=False)
if not image and parsed_args.image_properties:
def emit_duplicated_warning(img):
img_uuid_list = [str(image.id) for image in img]
LOG.warning(
'Multiple matching images: %(img_uuid_list)s\n'
'Using image: %(chosen_one)s',
{
'img_uuid_list': img_uuid_list,
'chosen_one': img_uuid_list[0],
})
def _match_image(image_api, wanted_properties):
image_list = image_api.images()
images_matched = []
for img in image_list:
img_dict = {}
# exclude any unhashable entries
img_dict_items = list(img.items())
if img.properties:
img_dict_items.extend(list(img.properties.items()))
for key, value in img_dict_items:
try:
set([key, value])
except TypeError:
if key != 'properties':
LOG.debug(
'Skipped the \'%s\' attribute. '
'That cannot be compared. '
'(image: %s, value: %s)',
key, img.id, value,
)
pass
else:
img_dict[key] = value
if all(
k in img_dict and img_dict[k] == v
for k, v in wanted_properties.items()
):
images_matched.append(img)
return images_matched
images = _match_image(image_client, parsed_args.image_properties)
if len(images) > 1:
emit_duplicated_warning(images, parsed_args.image_properties)
if images:
image = images[0]
else:
msg = _(
'No images match the property expected by '
'--image-property'
)
raise exceptions.CommandError(msg)
volume = None
if parsed_args.volume:
# --volume and --boot-from-volume are mutually exclusive.
if parsed_args.boot_from_volume:
msg = _('--volume is not allowed with --boot-from-volume')
raise exceptions.CommandError(msg)
volume = utils.find_resource(
volume_client.volumes,
parsed_args.volume,
).id
snapshot = None
if parsed_args.snapshot:
# --snapshot and --boot-from-volume are mutually exclusive.
if parsed_args.boot_from_volume:
msg = _('--snapshot is not allowed with --boot-from-volume')
raise exceptions.CommandError(msg)
snapshot = utils.find_resource(
volume_client.volume_snapshots,
parsed_args.snapshot,
).id
flavor = utils.find_resource(
compute_client.flavors, parsed_args.flavor)
if parsed_args.file:
if compute_client.api_version >= api_versions.APIVersion('2.57'):
msg = _(
'Personality files are deprecated and are not supported '
'for --os-compute-api-version greater than 2.56; use '
'user data instead'
)
raise exceptions.CommandError(msg)
files = {}
for f in parsed_args.file:
dst, src = f.split('=', 1)
try:
files[dst] = io.open(src, 'rb')
except IOError as e:
msg = _("Can't open '%(source)s': %(exception)s")
raise exceptions.CommandError(
msg % {'source': src, 'exception': e}
)
if parsed_args.min > parsed_args.max:
msg = _("min instances should be <= max instances")
raise exceptions.CommandError(msg)
if parsed_args.min < 1:
msg = _("min instances should be > 0")
raise exceptions.CommandError(msg)
if parsed_args.max < 1:
msg = _("max instances should be > 0")
raise exceptions.CommandError(msg)
userdata = None
if parsed_args.user_data:
try:
userdata = io.open(parsed_args.user_data)
except IOError as e:
msg = _("Can't open '%(data)s': %(exception)s")
raise exceptions.CommandError(
msg % {'data': parsed_args.user_data, 'exception': e}
)
if parsed_args.description:
if compute_client.api_version < api_versions.APIVersion("2.19"):
msg = _("Description is not supported for "
"--os-compute-api-version less than 2.19")
raise exceptions.CommandError(msg)
block_device_mapping_v2 = []
if volume:
block_device_mapping_v2 = [{
'uuid': volume,
'boot_index': '0',
'source_type': 'volume',
'destination_type': 'volume'
}]
elif snapshot:
block_device_mapping_v2 = [{
'uuid': snapshot,
'boot_index': '0',
'source_type': 'snapshot',
'destination_type': 'volume',
'delete_on_termination': False
}]
elif parsed_args.boot_from_volume:
# Tell nova to create a root volume from the image provided.
block_device_mapping_v2 = [{
'uuid': image.id,
'boot_index': '0',
'source_type': 'image',
'destination_type': 'volume',
'volume_size': parsed_args.boot_from_volume
}]
# If booting from volume we do not pass an image to compute.
image = None
if parsed_args.swap:
block_device_mapping_v2.append({
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'guest_format': 'swap',
'volume_size': parsed_args.swap,
'delete_on_termination': True,
})
for mapping in parsed_args.ephemerals:
block_device_mapping_dict = {
'boot_index': -1,
'source_type': 'blank',
'destination_type': 'local',
'delete_on_termination': True,
'volume_size': mapping['size'],
}
if 'format' in mapping:
block_device_mapping_dict['guest_format'] = mapping['format']
block_device_mapping_v2.append(block_device_mapping_dict)
# Handle block device by device name order, like: vdb -> vdc -> vdd
for mapping in parsed_args.block_device_mapping:
# The 'uuid' field isn't necessarily a UUID yet; let's validate it
# just in case
if mapping['source_type'] == 'volume':
volume_id = utils.find_resource(
volume_client.volumes, mapping['uuid'],
).id
mapping['uuid'] = volume_id
elif mapping['source_type'] == 'snapshot':
snapshot_id = utils.find_resource(
volume_client.volume_snapshots, mapping['uuid'],
).id
mapping['uuid'] = snapshot_id
elif mapping['source_type'] == 'image':
# NOTE(mriedem): In case --image is specified with the same
# image, that becomes the root disk for the server. If the
# block device is specified with a root device name, e.g.
# vda, then the compute API will likely fail complaining
# that there is a conflict. So if using the same image ID,
# which doesn't really make sense but it's allowed, the
# device name would need to be a non-root device, e.g. vdb.
# Otherwise if the block device image is different from the
# one specified by --image, then the compute service will
# create a volume from the image and attach it to the
# server as a non-root volume.
image_id = image_client.find_image(
mapping['uuid'], ignore_missing=False,
).id
mapping['uuid'] = image_id
block_device_mapping_v2.append(mapping)
for mapping in parsed_args.block_devices:
if 'boot_index' in mapping:
try:
mapping['boot_index'] = int(mapping['boot_index'])
except ValueError:
msg = _(
'The boot_index key of --block-device should be an '
'integer'
)
raise exceptions.CommandError(msg)
if 'tag' in mapping and (
compute_client.api_version < api_versions.APIVersion('2.42')
):
msg = _(
'--os-compute-api-version 2.42 or greater is '
'required to support the tag key of --block-device'
)
raise exceptions.CommandError(msg)
if 'volume_type' in mapping and (
compute_client.api_version < api_versions.APIVersion('2.67')
):
msg = _(
'--os-compute-api-version 2.67 or greater is '
'required to support the volume_type key of --block-device'
)
raise exceptions.CommandError(msg)
if 'source_type' in mapping:
if mapping['source_type'] not in (
'volume', 'image', 'snapshot', 'blank',
):
msg = _(
'The source_type key of --block-device should be one '
'of: volume, image, snapshot, blank'
)
raise exceptions.CommandError(msg)
else:
mapping['source_type'] = 'blank'
if 'destination_type' in mapping:
if mapping['destination_type'] not in ('local', 'volume'):
msg = _(
'The destination_type key of --block-device should be '
'one of: local, volume'
)
raise exceptions.CommandError(msg)
else:
if mapping['source_type'] in ('blank',):
mapping['destination_type'] = 'local'
else: # volume, image, snapshot
mapping['destination_type'] = 'volume'
if 'delete_on_termination' in mapping:
try:
value = strutils.bool_from_string(
mapping['delete_on_termination'], strict=True)
except ValueError:
msg = _(
'The delete_on_termination key of --block-device '
'should be a boolean-like value'
)
raise exceptions.CommandError(msg)
mapping['delete_on_termination'] = value
else:
if mapping['destination_type'] == 'local':
mapping['delete_on_termination'] = True
block_device_mapping_v2.append(mapping)
nics = parsed_args.nics
if 'auto' in nics or 'none' in nics:
if len(nics) > 1:
msg = _(
'Specifying a --nic of auto or none cannot '
'be used with any other --nic, --network '
'or --port value.'
)
raise exceptions.CommandError(msg)
nics = nics[0]
else:
for nic in nics:
if 'tag' in nic:
if (
compute_client.api_version <
api_versions.APIVersion('2.43')
):
msg = _(
'--os-compute-api-version 2.43 or greater is '
'required to support the --nic tag field'
)
raise exceptions.CommandError(msg)
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
if nic['net-id']:
net = network_client.find_network(
nic['net-id'], ignore_missing=False,
)
nic['net-id'] = net.id
if nic['port-id']:
port = network_client.find_port(
nic['port-id'], ignore_missing=False,
)
nic['port-id'] = port.id
else:
if nic['net-id']:
nic['net-id'] = compute_client.api.network_find(
nic['net-id'],
)['id']
if nic['port-id']:
msg = _(
"Can't create server with port specified "
"since network endpoint not enabled"
)
raise exceptions.CommandError(msg)
if not 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 and let nova
# decide the default behavior.
nics = []
# Check security group exist and convert ID to name
security_group_names = []
if self.app.client_manager.is_network_endpoint_enabled():
network_client = self.app.client_manager.network
for each_sg in parsed_args.security_group:
sg = network_client.find_security_group(each_sg,
ignore_missing=False)
# Use security group ID to avoid multiple security group have
# same name in neutron networking backend
security_group_names.append(sg.id)
else:
# Handle nova-network case
for each_sg in parsed_args.security_group:
sg = compute_client.api.security_group_find(each_sg)
security_group_names.append(sg['name'])
hints = {}
for key, values in parsed_args.hint.items():
# only items with multiple values will result in a list
if len(values) == 1:
hints[key] = values[0]
else:
hints[key] = values
if isinstance(parsed_args.config_drive, bool):
# NOTE(stephenfin): The API doesn't accept False as a value :'(
config_drive = parsed_args.config_drive or None
else:
# TODO(stephenfin): Remove when we drop support for
# '--config-drive'
if str(parsed_args.config_drive).lower() in ("true", "1"):
config_drive = True
elif str(parsed_args.config_drive).lower() in ("false", "0",
"", "none"):
config_drive = None
else:
config_drive = parsed_args.config_drive
boot_args = [parsed_args.server_name, image, flavor]
boot_kwargs = dict(
meta=parsed_args.properties,
files=files,
reservation_id=None,
min_count=parsed_args.min,
max_count=parsed_args.max,
security_groups=security_group_names,
userdata=userdata,
key_name=parsed_args.key_name,
availability_zone=parsed_args.availability_zone,
admin_pass=parsed_args.password,
block_device_mapping_v2=block_device_mapping_v2,
nics=nics,
scheduler_hints=hints,
config_drive=config_drive)
if parsed_args.description:
boot_kwargs['description'] = parsed_args.description
if parsed_args.tags:
if compute_client.api_version < api_versions.APIVersion('2.52'):
msg = _(
'--os-compute-api-version 2.52 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
boot_kwargs['tags'] = parsed_args.tags
if parsed_args.host:
if compute_client.api_version < api_versions.APIVersion("2.74"):
msg = _(
'--os-compute-api-version 2.74 or greater is required to '
'support the --host option'
)
raise exceptions.CommandError(msg)
boot_kwargs['host'] = parsed_args.host
if parsed_args.hypervisor_hostname:
if compute_client.api_version < api_versions.APIVersion("2.74"):
msg = _(
'--os-compute-api-version 2.74 or greater is required to '
'support the --hypervisor-hostname option'
)
raise exceptions.CommandError(msg)
boot_kwargs['hypervisor_hostname'] = (
parsed_args.hypervisor_hostname)
if parsed_args.hostname:
if compute_client.api_version < api_versions.APIVersion("2.90"):
msg = _(
'--os-compute-api-version 2.90 or greater is required to '
'support the --hostname option'
)
raise exceptions.CommandError(msg)
boot_kwargs['hostname'] = parsed_args.hostname
# TODO(stephenfin): Handle OS_TRUSTED_IMAGE_CERTIFICATE_IDS
if parsed_args.trusted_image_certs:
if not (image and not parsed_args.boot_from_volume):
msg = _(
'--trusted-image-cert option is only supported for '
'servers booted directly from images'
)
raise exceptions.CommandError(msg)
if compute_client.api_version < api_versions.APIVersion('2.63'):
msg = _(
'--os-compute-api-version 2.63 or greater is required to '
'support the --trusted-image-cert option'
)
raise exceptions.CommandError(msg)
certs = parsed_args.trusted_image_certs
boot_kwargs['trusted_image_certificates'] = certs
LOG.debug('boot_args: %s', boot_args)
LOG.debug('boot_kwargs: %s', boot_kwargs)
# Wrap the call to catch exceptions in order to close files
try:
server = compute_client.servers.create(*boot_args, **boot_kwargs)
finally:
# Clean up open files - make sure they are not strings
for f in files:
if hasattr(f, 'close'):
f.close()
if hasattr(userdata, 'close'):
userdata.close()
if parsed_args.wait:
if utils.wait_for_status(
compute_client.servers.get,
server.id,
callback=_show_progress,
):
self.app.stdout.write('\n')
else:
LOG.error('Error creating server: %s', parsed_args.server_name)
self.app.stdout.write(_('Error creating server\n'))
raise SystemExit
details = _prep_server_detail(compute_client, image_client, server)
return zip(*sorted(details.items()))
class CreateServerDump(command.Command):
"""Create a dump file in server(s)
Trigger crash dump in server(s) with features like kdump in Linux.
It will create a dump file in the server(s) dumping the server(s)'
memory, and also crash the server(s). OSC sees the dump file
(server dump) as a kind of resource.
This command requires ``--os-compute-api-version`` 2.17 or greater.
"""
def get_parser(self, prog_name):
parser = super(CreateServerDump, self).get_parser(prog_name)
parser.add_argument(
'server',
metavar='<server>',
nargs='+',
help=_('Server(s) to create dump file (name or ID)'),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
for server in parsed_args.server:
utils.find_resource(
compute_client.servers,
server,
).trigger_crash_dump()
class DeleteServer(command.Command):
_description = _("Delete server(s)")
def get_parser(self, prog_name):
parser = super(DeleteServer, self).get_parser(prog_name)
parser.add_argument(
'server',
metavar='<server>',
nargs="+",
help=_('Server(s) to delete (name or ID)'),
)
parser.add_argument(
'--force',
action='store_true',
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(
'--wait',
action='store_true',
help=_('Wait for delete to complete'),
)
return parser
def take_action(self, parsed_args):
def _show_progress(progress):
if progress:
self.app.stdout.write('\rProgress: %s' % progress)
self.app.stdout.flush()
compute_client = self.app.client_manager.compute
for server in parsed_args.server:
server_obj = utils.find_resource(
compute_client.servers, server,
all_tenants=parsed_args.all_projects)
if parsed_args.force:
compute_client.servers.force_delete(server_obj.id)
else:
compute_client.servers.delete(server_obj.id)
if parsed_args.wait:
if not utils.wait_for_delete(
compute_client.servers,
server_obj.id,
callback=_show_progress,
):
msg = _('Error deleting server: %s')
LOG.error(msg, server_obj.id)
self.app.stdout.write(_('Error deleting server\n'))
raise SystemExit
def percent_type(x):
x = int(x)
if not 0 < x <= 100:
raise argparse.ArgumentTypeError("Must be between 0 and 100")
return x
class ListServer(command.Lister):
_description = _("List servers")
def get_parser(self, prog_name):
parser = super(ListServer, self).get_parser(prog_name)
parser.add_argument(
'--reservation-id',
metavar='<reservation-id>',
help=_('Only return instances that match the reservation'),
)
parser.add_argument(
'--ip',
metavar='<ip-address-regex>',
help=_('Regular expression to match IP addresses'),
)
parser.add_argument(
'--ip6',
metavar='<ip-address-regex>',
help=_(
'Regular expression to match IPv6 addresses. Note '
'that this option only applies for non-admin users '
'when using ``--os-compute-api-version`` 2.5 or greater.'
),
)
parser.add_argument(
'--name',
metavar='<name-regex>',
help=_('Regular expression to match names'),
)
parser.add_argument(
'--instance-name',
metavar='<server-name>',
help=_('Regular expression to match instance name (admin only)'),
)
# taken from 'task_and_vm_state_from_status' function in nova
# the API sadly reports these in upper case and while it would be
# wonderful to plaster over this ugliness client-side, there are
# already users in the wild doing this in upper case that we need to
# support
parser.add_argument(
'--status',
metavar='<status>',
choices=(
'ACTIVE',
'BUILD',
'DELETED',
'ERROR',
'HARD_REBOOT',
'MIGRATING',
'PASSWORD',
'PAUSED',
'REBOOT',
'REBUILD',
'RESCUE',
'RESIZE',
'REVERT_RESIZE',
'SHELVED',
'SHELVED_OFFLOADED',
'SHUTOFF',
'SOFT_DELETED',
'SUSPENDED',
'VERIFY_RESIZE'
),
help=_('Search by server status'),
)
parser.add_argument(
'--flavor',
metavar='<flavor>',
help=_('Search by flavor (name or ID)'),
)
parser.add_argument(
'--image',
metavar='<image>',
help=_('Search by image (name or ID)'),
)
parser.add_argument(
'--host',
metavar='<hostname>',
help=_('Search by hostname'),
)
parser.add_argument(
'--all-projects',
action='store_true',
default=boolenv('ALL_PROJECTS'),
help=_(
'Include all projects (admin only) '
'(can be specified using the ALL_PROJECTS envvar)'
),
)
parser.add_argument(
'--project',
metavar='<project>',
help=_("Search by project (admin only) (name or ID)")
)
identity_common.add_project_domain_option_to_parser(parser)
parser.add_argument(
'--user',
metavar='<user>',
help=_(
'Search by user (name or ID) '
'(admin only before microversion 2.83)'
),
)
identity_common.add_user_domain_option_to_parser(parser)
parser.add_argument(
'--deleted',
action='store_true',
default=False,
help=_('Only display deleted servers (admin only)'),
)
parser.add_argument(
'--availability-zone',
default=None,
help=_(
'Search by availability zone '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--key-name',
help=_(
'Search by keypair name '
'(admin only before microversion 2.83)'
),
)
config_drive_group = parser.add_mutually_exclusive_group()
config_drive_group.add_argument(
'--config-drive',
action='store_true',
dest='has_config_drive',
default=None,
help=_(
'Only display servers with a config drive attached '
'(admin only before microversion 2.83)'
),
)
# NOTE(gibi): this won't actually do anything until bug 1871409 is
# fixed and the REST API is cleaned up regarding the values of
# config_drive
config_drive_group.add_argument(
'--no-config-drive',
action='store_false',
dest='has_config_drive',
help=_(
'Only display servers without a config drive attached '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--progress',
type=percent_type,
default=None,
help=_(
'Search by progress value (%%) '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--vm-state',
metavar='<state>',
# taken from 'InstanceState' object field in nova
choices=(
'active',
'building',
'deleted',
'error',
'paused',
'stopped',
'suspended',
'rescued',
'resized',
'shelved',
'shelved_offloaded',
'soft-delete',
),
help=_(
'Search by vm_state value '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--task-state',
metavar='<state>',
# taken from 'InstanceTaskState' object field in nova
choices=(
'block_device_mapping',
'deleting',
'image_backup',
'image_pending_upload',
'image_snapshot',
'image_snapshot_pending',
'image_uploading',
'migrating',
'networking',
'pausing',
'powering-off',
'powering-on',
'rebooting',
'reboot_pending',
'reboot_started',
'reboot_pending_hard',
'reboot_started_hard',
'rebooting_hard',
'rebuilding',
'rebuild_block_device_mapping',
'rebuild_spawning',
'rescuing',
'resize_confirming',
'resize_finish',
'resize_migrated',
'resize_migrating',
'resize_prep',
'resize_reverting',
'restoring',
'resuming',
'scheduling',
'shelving',
'shelving_image_pending_upload',
'shelving_image_uploading',
'shelving_offloading',
'soft-deleting',
'spawning',
'suspending',
'updating_password',
'unpausing',
'unrescuing',
'unshelving',
),
help=_(
'Search by task_state value '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--power-state',
metavar='<state>',
# taken from 'InstancePowerState' object field in nova
choices=(
'pending',
'running',
'paused',
'shutdown',
'crashed',
'suspended',
),
help=_(
'Search by power_state value '
'(admin only before microversion 2.83)'
),
)
parser.add_argument(
'--long',
action='store_true',
default=False,
help=_('List additional fields in output'),
)
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. '
'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',
metavar='<server>',
default=None,
help=_(
'The last server of the previous page. Display '
'list of servers after marker. Display all servers if not '
'specified. When used with ``--deleted``, the marker must '
'be an ID, otherwise a name or ID can be used.'
),
)
parser.add_argument(
'--limit',
metavar='<num-servers>',
type=int,
default=None,
help=_(
"Maximum number of servers to display. If limit equals -1, "
"all servers will be displayed. If limit is greater than "
"'osapi_max_limit' option of Nova API, "
"'osapi_max_limit' will be used instead."
),
)
parser.add_argument(
'--changes-before',
metavar='<changes-before>',
default=None,
help=_(
"List only servers changed before a certain point of time. "
"The provided time should be an ISO 8061 formatted time "
"(e.g., 2016-03-05T06:27:59Z). "
'(supported by --os-compute-api-version 2.66 or above)'
),
)
parser.add_argument(
'--changes-since',
metavar='<changes-since>',
default=None,
help=_(
"List only servers changed after a certain point of time. "
"The provided time should be an ISO 8061 formatted time "
"(e.g., 2016-03-04T06:27:59Z)."
),
)
lock_group = parser.add_mutually_exclusive_group()
lock_group.add_argument(
'--locked',
action='store_true',
default=False,
help=_(
'Only display locked servers '
'(supported by --os-compute-api-version 2.73 or above)'
),
)
lock_group.add_argument(
'--unlocked',
action='store_true',
default=False,
help=_(
'Only display unlocked servers '
'(supported by --os-compute-api-version 2.73 or above)'
),
)
parser.add_argument(
'--tags',
metavar='<tag>',
action='append',
default=[],
dest='tags',
help=_(
'Only list servers with the specified tag. '
'Specify multiple times to filter on multiple tags. '
'(supported by --os-compute-api-version 2.26 or above)'
),
)
parser.add_argument(
'--not-tags',
metavar='<tag>',
action='append',
default=[],
dest='not_tags',
help=_(
'Only list servers without the specified tag. '
'Specify multiple times to filter on multiple tags. '
'(supported by --os-compute-api-version 2.26 or above)'
),
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
identity_client = self.app.client_manager.identity
image_client = self.app.client_manager.image
project_id = None
if parsed_args.project:
project_id = identity_common.find_project(
identity_client,
parsed_args.project,
parsed_args.project_domain,
).id
parsed_args.all_projects = True
user_id = None
if parsed_args.user:
user_id = identity_common.find_user(
identity_client,
parsed_args.user,
parsed_args.user_domain,
).id
# Nova only supports list servers searching by flavor ID. So if a
# flavor name is given, map it to ID.
flavor_id = None
if parsed_args.flavor:
flavor_id = utils.find_resource(
compute_client.flavors,
parsed_args.flavor,
).id
# Nova only supports list servers searching by image ID. So if a
# image name is given, map it to ID.
image_id = None
if parsed_args.image:
image_id = image_client.find_image(
parsed_args.image,
ignore_missing=False,
).id
search_opts = {
'reservation_id': parsed_args.reservation_id,
'ip': parsed_args.ip,
'ip6': parsed_args.ip6,
'name': parsed_args.name,
'instance_name': parsed_args.instance_name,
'status': parsed_args.status,
'flavor': flavor_id,
'image': image_id,
'host': parsed_args.host,
'tenant_id': project_id,
'all_tenants': parsed_args.all_projects,
'user_id': user_id,
'deleted': parsed_args.deleted,
'changes-before': parsed_args.changes_before,
'changes-since': parsed_args.changes_since,
}
if parsed_args.availability_zone:
search_opts['availability_zone'] = parsed_args.availability_zone
if parsed_args.key_name:
search_opts['key_name'] = parsed_args.key_name
if parsed_args.has_config_drive is not None:
search_opts['config_drive'] = parsed_args.has_config_drive
if parsed_args.progress is not None:
search_opts['progress'] = str(parsed_args.progress)
if parsed_args.vm_state:
search_opts['vm_state'] = parsed_args.vm_state
if parsed_args.task_state:
search_opts['task_state'] = parsed_args.task_state
if parsed_args.power_state:
# taken from 'InstancePowerState' object field in nova
power_state = {
'pending': 0,
'running': 1,
'paused': 3,
'shutdown': 4,
'crashed': 6,
'suspended': 7,
}[parsed_args.power_state]
search_opts['power_state'] = power_state
if parsed_args.tags:
if compute_client.api_version < api_versions.APIVersion('2.26'):
msg = _(
'--os-compute-api-version 2.26 or greater is required to '
'support the --tag option'
)
raise exceptions.CommandError(msg)
search_opts['tags'] = ','.join(parsed_args.tags)
if parsed_args.not_tags:
if compute_client.api_version < api_versions.APIVersion('2.26'):
msg = _(
'--os-compute-api-version 2.26 or greater is required to '
'support the --not-tag option'
)
raise exceptions.CommandError(msg)
search_opts['not-tags'] = ','.join(parsed_args.not_tags)
if parsed_args.locked:
if compute_client.api_version < api_versions.APIVersion('2.73'):
msg = _(
'--os-compute-api-version 2.73 or greater is required to '
'support the --locked option'
)
raise exceptions.CommandError(msg)
search_opts['locked'] = True
elif parsed_args.unlocked:
if compute_client.api_version < api_versions.APIVersion('2.73'):
msg = _(
'--os-compute-api-version 2.73 or greater is required to '
'support the --unlocked option'
)
raise exceptions.CommandError(msg)
search_opts['locked'] = False
LOG.debug('search options: %s', search_opts)
if search_opts['changes-before']:
if compute_client.api_version < api_versions.APIVersion('2.66'):
msg = _('--os-compute-api-version 2.66 or later is required')
raise exceptions.CommandError(msg)
try:
iso8601.parse_date(search_opts['changes-before'])
except (TypeError, iso8601.ParseError):
raise exceptions.CommandError(
_('Invalid changes-before value: %s') %
search_opts['changes-before']
)
if search_opts['changes-since']:
try:
iso8601.parse_date(search_opts['changes-since'])
except (TypeError, iso8601.ParseError):
msg = _('Invalid changes-since value: %s')
raise exceptions.CommandError(
msg % search_opts['changes-since']
)
columns = (
'id',
'name',
'status',
)
column_headers = (
'ID',
'Name',
'Status',
)
if parsed_args.long:
columns += (
'OS-EXT-STS:task_state',
'OS-EXT-STS:power_state',
)
column_headers += (
'Task State',
'Power State',
)
columns += ('networks',)
column_headers += ('Networks',)
if parsed_args.long:
columns += (
'image_name',
'image_id',
)
column_headers += (
'Image Name',
'Image ID',
)
else:
if parsed_args.no_name_lookup:
columns += ('image_id',)
else:
columns += ('image_name',)
column_headers += ('Image',)
# microversion 2.47 puts the embedded flavor into the server response
# body but omits the id, so if not present we just expose the original
# flavor name in the output
if compute_client.api_version >= api_versions.APIVersion('2.47'):
columns += ('flavor_name',)
column_headers += ('Flavor',)
else:
if parsed_args.long:
columns += (
'flavor_name',
'flavor_id',
)
column_headers += (
'Flavor Name',
'Flavor ID',
)
else:
if parsed_args.no_name_lookup:
columns += ('flavor_id',)
else:
columns += ('flavor_name',)
column_headers += ('Flavor',)
if parsed_args.long:
columns += (
'OS-EXT-AZ:availability_zone',
'OS-EXT-SRV-ATTR:host',
'metadata',
)
column_headers += (
'Availability Zone',
'Host',
'Properties',
)
marker_id = None
# support for additional columns
if parsed_args.columns:
for c in parsed_args.columns:
if c in ('Project ID', 'project_id'):
columns += ('tenant_id',)
column_headers += ('Project ID',)
if c in ('User ID', 'user_id'):
columns += ('user_id',)
column_headers += ('User ID',)
if c in ('Created At', 'created_at'):
columns += ('created',)
column_headers += ('Created At',)
if c in ('Security Groups', 'security_groups'):
columns += ('security_groups_name',)
column_headers += ('Security Groups',)
# convert back to tuple
column_headers = tuple(column_headers)
columns = tuple(columns)
if parsed_args.marker:
# Check if both "--marker" and "--deleted" are used.
# In that scenario a lookup is not needed as the marker
# needs to be an ID, because find_resource does not
# handle deleted resources
if parsed_args.deleted:
marker_id = parsed_args.marker
else:
marker_id = utils.find_resource(
compute_client.servers,
parsed_args.marker,
).id
data = compute_client.servers.list(
search_opts=search_opts,
marker=marker_id,
limit=parsed_args.limit)
images = {}
flavors = {}
if data and not parsed_args.no_name_lookup:
# create a dict that maps image_id to image object, which is used
# to display the "Image Name" column. Note that 'image.id' can be
# empty for BFV instances and 'image' can be missing entirely if
# there are infra failures
if parsed_args.name_lookup_one_by_one or image_id:
for i_id in set(
s.image['id'] for s in data
if s.image and s.image.get('id')
):
# "Image Name" is not crucial, so we swallow any exceptions
try:
images[i_id] = image_client.get_image(i_id)
except Exception:
pass
else:
try:
images_list = image_client.images()
for i in images_list:
images[i.id] = i
except Exception:
pass
# create a dict that maps flavor_id to flavor object, which is used
# to display the "Flavor Name" column. Note that 'flavor.id' is not
# present on microversion 2.47 or later and 'flavor' won't be
# present if there are infra failures
if parsed_args.name_lookup_one_by_one or flavor_id:
for f_id in set(
s.flavor['id'] for s in data
if s.flavor and s.flavor.get('id')
):
# "Flavor Name" is not crucial, so we swallow any
# exceptions
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 information.
if not hasattr(s, 'image') or not hasattr(s, 'flavor'):
continue
if 'id' in s.image:
image = images.get(s.image['id'])
if image:
s.image_name = image.name
s.image_id = s.image['id']
else:
# NOTE(melwitt): An server booted from a volume will have no
# image associated with it. We fill in the Image Name and ID
# with "N/A (booted from volume)" to help users who want to be
# able to grep for boot-from-volume servers when using the CLI.
s.image_name = IMAGE_STRING_FOR_BFV
s.image_id = IMAGE_STRING_FOR_BFV
if compute_client.api_version < api_versions.APIVersion('2.47'):
flavor = flavors.get(s.flavor['id'])
if flavor:
s.flavor_name = flavor.name
s.flavor_id = s.flavor['id']
else:
s.flavor_name = s.flavor['original_name']
# Add a list with security group name as attribute
for s in data:
if hasattr(s, 'security_groups'):
s.security_groups_name = [x["name"] for x in s.security_groups]
else:
s.security_groups_name = []
# The host_status field contains the status of the compute host the
# server is on. It is only returned by the API when the nova-api
# policy allows. Users can look at the host_status field when, for
# example, their server has status ACTIVE but is unresponsive. The
# host_status field can indicate a possible problem on the host
# it's on, providing useful information to a user in this
# situation.
if (
compute_client.api_version >= api_versions.APIVersion('2.16') and
parsed_args.long
):
if any([hasattr(s, 'host_status') for s in data]):
columns += ('Host Status',)
column_headers += ('Host Status',)
table = (
column_headers,
(
utils.get_item_properties(
s, columns,
mixed_case_fields=(
'OS-EXT-STS:task_state',
'OS-EXT-STS:power_state',
'OS-EXT-AZ:availability_zone',
'OS-EXT-SRV-ATTR:host',
),
formatters={
'OS-EXT-STS:power_state': PowerStateColumn,
'networks': format_columns.DictListColumn,
'metadata': format_columns.DictColumn,
'security_groups_name': format_columns.ListColumn,
},
) for s in data
),
)
return table
class LockServer(command.Command):
_description = _("Lock server(s). A non-admin user will not be able to "
"execute actions")
def get_parser(self, prog_name):
parser = super(LockServer, self).get_parser(prog_name)
parser.add_argument(
'server',
metavar='<server>',
nargs='+',
help=_('Server(s) to lock (name or ID)'),
)
parser.add_argument(
'--reason',
metavar='<reason>',
default=None,
help=_("Reason for locking the server(s). Requires "
"``--os-compute-api-version`` 2.73 or greater.")
)
return parser
def take_action(self, parsed_args):
compute_client = self.app.client_manager.compute
support_reason = compute_client.api_version >= api_versions.APIVersion(
'2.73')
if not support_reason and parsed_args.reason:
msg = _('--os-compute-api-version 2.73 or greater is required to '
'use the --reason option.')
raise exceptions.CommandError(msg)
for server in parsed_args.server:
serv = utils.find_resource(compute_client.servers, server)
(serv.lock(reason=parsed_args.reason) if support_reason
else serv.lock())
# FIXME(dtroyer): Here is what I want, how with argparse/cliff?
# server migrate [--wait] \
# [--live <hostname>
# [--shared-migration | --block-migration]
# [--disk-overcommit | --no-disk-overcommit]]
# <server>
#
# live_parser = parser.add_argument_group(title='Live migration options')
# then adding the groups doesn't seem to work
class MigrateServer(command.Command):
_description = _("""Migrate server to different host.
A migrate operation is implemented as a resize operation using the same flavor
as the old server. This means that, like resize, migrate works by creating a
new server using the same flavor and copying the contents of the original disk
into a new one. As with resize, the migrate operation is a two-step process for