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.
1402 lines
64 KiB
1402 lines
64 KiB
# Copyright 2010 OpenStack Foundation |
|
# Copyright 2011 Piston Cloud Computing, Inc |
|
# All Rights Reserved. |
|
# |
|
# 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. |
|
|
|
import copy |
|
|
|
from oslo_log import log as logging |
|
import oslo_messaging as messaging |
|
from oslo_utils import strutils |
|
from oslo_utils import timeutils |
|
from oslo_utils import uuidutils |
|
import six |
|
import webob |
|
from webob import exc |
|
|
|
from nova.api.openstack import api_version_request |
|
from nova.api.openstack import common |
|
from nova.api.openstack.compute import helpers |
|
from nova.api.openstack.compute.schemas import servers as schema_servers |
|
from nova.api.openstack.compute.views import servers as views_servers |
|
from nova.api.openstack import wsgi |
|
from nova.api import validation |
|
from nova import block_device |
|
from nova.compute import api as compute |
|
from nova.compute import flavors |
|
from nova.compute import utils as compute_utils |
|
import nova.conf |
|
from nova import context as nova_context |
|
from nova import exception |
|
from nova.i18n import _ |
|
from nova.image import glance |
|
from nova.network import neutron |
|
from nova import objects |
|
from nova.policies import servers as server_policies |
|
from nova import utils |
|
|
|
TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any') |
|
PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION = '2.69' |
|
PAGING_SORTING_PARAMS = ('sort_key', 'sort_dir', 'limit', 'marker') |
|
|
|
CONF = nova.conf.CONF |
|
|
|
LOG = logging.getLogger(__name__) |
|
|
|
INVALID_FLAVOR_IMAGE_EXCEPTIONS = ( |
|
exception.BadRequirementEmulatorThreadsPolicy, |
|
exception.CPUThreadPolicyConfigurationInvalid, |
|
exception.FlavorImageConflict, |
|
exception.ImageCPUPinningForbidden, |
|
exception.ImageCPUThreadPolicyForbidden, |
|
exception.ImageNUMATopologyAsymmetric, |
|
exception.ImageNUMATopologyCPUDuplicates, |
|
exception.ImageNUMATopologyCPUOutOfRange, |
|
exception.ImageNUMATopologyCPUsUnassigned, |
|
exception.ImageNUMATopologyForbidden, |
|
exception.ImageNUMATopologyIncomplete, |
|
exception.ImageNUMATopologyMemoryOutOfRange, |
|
exception.ImageNUMATopologyRebuildConflict, |
|
exception.ImagePMUConflict, |
|
exception.ImageSerialPortNumberExceedFlavorValue, |
|
exception.ImageSerialPortNumberInvalid, |
|
exception.ImageVCPULimitsRangeExceeded, |
|
exception.ImageVCPUTopologyRangeExceeded, |
|
exception.InvalidCPUAllocationPolicy, |
|
exception.InvalidCPUThreadAllocationPolicy, |
|
exception.InvalidEmulatorThreadsPolicy, |
|
exception.InvalidMachineType, |
|
exception.InvalidNUMANodesNumber, |
|
exception.InvalidRequest, |
|
exception.MemoryPageSizeForbidden, |
|
exception.MemoryPageSizeInvalid, |
|
exception.PciInvalidAlias, |
|
exception.PciRequestAliasNotDefined, |
|
exception.RealtimeConfigurationInvalid, |
|
exception.RealtimeMaskNotFoundOrInvalid, |
|
) |
|
|
|
MIN_COMPUTE_MOVE_BANDWIDTH = 39 |
|
|
|
|
|
class ServersController(wsgi.Controller): |
|
"""The Server API base controller class for the OpenStack API.""" |
|
|
|
_view_builder_class = views_servers.ViewBuilder |
|
|
|
@staticmethod |
|
def _add_location(robj): |
|
# Just in case... |
|
if 'server' not in robj.obj: |
|
return robj |
|
|
|
link = [l for l in robj.obj['server']['links'] if l['rel'] == 'self'] |
|
if link: |
|
robj['Location'] = link[0]['href'] |
|
|
|
# Convenience return |
|
return robj |
|
|
|
def __init__(self): |
|
super(ServersController, self).__init__() |
|
self.compute_api = compute.API() |
|
self.network_api = neutron.API() |
|
|
|
@wsgi.expected_errors((400, 403)) |
|
@validation.query_schema(schema_servers.query_params_v275, '2.75') |
|
@validation.query_schema(schema_servers.query_params_v273, '2.73', '2.74') |
|
@validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72') |
|
@validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65') |
|
@validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25') |
|
def index(self, req): |
|
"""Returns a list of server names and ids for a given user.""" |
|
context = req.environ['nova.context'] |
|
context.can(server_policies.SERVERS % 'index') |
|
try: |
|
servers = self._get_servers(req, is_detail=False) |
|
except exception.Invalid as err: |
|
raise exc.HTTPBadRequest(explanation=err.format_message()) |
|
return servers |
|
|
|
@wsgi.expected_errors((400, 403)) |
|
@validation.query_schema(schema_servers.query_params_v275, '2.75') |
|
@validation.query_schema(schema_servers.query_params_v273, '2.73', '2.74') |
|
@validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72') |
|
@validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65') |
|
@validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25') |
|
def detail(self, req): |
|
"""Returns a list of server details for a given user.""" |
|
context = req.environ['nova.context'] |
|
context.can(server_policies.SERVERS % 'detail') |
|
try: |
|
servers = self._get_servers(req, is_detail=True) |
|
except exception.Invalid as err: |
|
raise exc.HTTPBadRequest(explanation=err.format_message()) |
|
return servers |
|
|
|
@staticmethod |
|
def _is_cell_down_supported(req, search_opts): |
|
cell_down_support = api_version_request.is_supported( |
|
req, min_version=PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION) |
|
|
|
if cell_down_support: |
|
# NOTE(tssurya): Minimal constructs would be returned from the down |
|
# cells if cell_down_support is True, however if filtering, sorting |
|
# or paging is requested by the user, then cell_down_support should |
|
# be made False and the down cells should be skipped (depending on |
|
# CONF.api.list_records_by_skipping_down_cells) as there is no |
|
# way to return correct results for the down cells in those |
|
# situations due to missing keys/information. |
|
# NOTE(tssurya): Since there is a chance that |
|
# remove_invalid_options function could have removed the paging and |
|
# sorting parameters, we add the additional check for that from the |
|
# request. |
|
pag_sort = any( |
|
ps in req.GET.keys() for ps in PAGING_SORTING_PARAMS) |
|
# NOTE(tssurya): ``nova list --all_tenants`` is the only |
|
# allowed filter exception when handling down cells. |
|
filters = list(search_opts.keys()) not in ([u'all_tenants'], []) |
|
if pag_sort or filters: |
|
cell_down_support = False |
|
return cell_down_support |
|
|
|
def _get_servers(self, req, is_detail): |
|
"""Returns a list of servers, based on any search options specified.""" |
|
|
|
search_opts = {} |
|
search_opts.update(req.GET) |
|
|
|
context = req.environ['nova.context'] |
|
remove_invalid_options(context, search_opts, |
|
self._get_server_search_options(req)) |
|
|
|
cell_down_support = self._is_cell_down_supported(req, search_opts) |
|
|
|
for search_opt in search_opts: |
|
if (search_opt in |
|
schema_servers.JOINED_TABLE_QUERY_PARAMS_SERVERS.keys() or |
|
search_opt.startswith('_')): |
|
msg = _("Invalid filter field: %s.") % search_opt |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
# Verify search by 'status' contains a valid status. |
|
# Convert it to filter by vm_state or task_state for compute_api. |
|
# For non-admin user, vm_state and task_state are filtered through |
|
# remove_invalid_options function, based on value of status field. |
|
# Set value to vm_state and task_state to make search simple. |
|
search_opts.pop('status', None) |
|
if 'status' in req.GET.keys(): |
|
statuses = req.GET.getall('status') |
|
states = common.task_and_vm_state_from_status(statuses) |
|
vm_state, task_state = states |
|
if not vm_state and not task_state: |
|
if api_version_request.is_supported(req, min_version='2.38'): |
|
msg = _('Invalid status value') |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
return {'servers': []} |
|
search_opts['vm_state'] = vm_state |
|
# When we search by vm state, task state will return 'default'. |
|
# So we don't need task_state search_opt. |
|
if 'default' not in task_state: |
|
search_opts['task_state'] = task_state |
|
|
|
if 'changes-since' in search_opts: |
|
try: |
|
search_opts['changes-since'] = timeutils.parse_isotime( |
|
search_opts['changes-since']) |
|
except ValueError: |
|
# NOTE: This error handling is for V2.0 API to pass the |
|
# experimental jobs at the gate. V2.1 API covers this case |
|
# with JSON-Schema and it is a hard burden to apply it to |
|
# v2.0 API at this time. |
|
msg = _("Invalid filter field: changes-since.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
if 'changes-before' in search_opts: |
|
try: |
|
search_opts['changes-before'] = timeutils.parse_isotime( |
|
search_opts['changes-before']) |
|
changes_since = search_opts.get('changes-since') |
|
if changes_since and search_opts['changes-before'] < \ |
|
search_opts['changes-since']: |
|
msg = _('The value of changes-since must be' |
|
' less than or equal to changes-before.') |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except ValueError: |
|
msg = _("Invalid filter field: changes-before.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
# By default, compute's get_all() will return deleted instances. |
|
# If an admin hasn't specified a 'deleted' search option, we need |
|
# to filter out deleted instances by setting the filter ourselves. |
|
# ... Unless 'changes-since' or 'changes-before' is specified, |
|
# because those will return recently deleted instances according to |
|
# the API spec. |
|
|
|
if 'deleted' not in search_opts: |
|
if 'changes-since' not in search_opts and \ |
|
'changes-before' not in search_opts: |
|
# No 'changes-since' or 'changes-before', so we only |
|
# want non-deleted servers |
|
search_opts['deleted'] = False |
|
else: |
|
# Convert deleted filter value to a valid boolean. |
|
# Return non-deleted servers if an invalid value |
|
# is passed with deleted filter. |
|
search_opts['deleted'] = strutils.bool_from_string( |
|
search_opts['deleted'], default=False) |
|
|
|
if search_opts.get("vm_state") == ['deleted']: |
|
if context.is_admin: |
|
search_opts['deleted'] = True |
|
else: |
|
msg = _("Only administrators may list deleted instances") |
|
raise exc.HTTPForbidden(explanation=msg) |
|
|
|
if api_version_request.is_supported(req, min_version='2.26'): |
|
for tag_filter in TAG_SEARCH_FILTERS: |
|
if tag_filter in search_opts: |
|
search_opts[tag_filter] = search_opts[ |
|
tag_filter].split(',') |
|
|
|
all_tenants = common.is_all_tenants(search_opts) |
|
# use the boolean from here on out so remove the entry from search_opts |
|
# if it's present. |
|
# NOTE(tssurya): In case we support handling down cells |
|
# we need to know further down the stack whether the 'all_tenants' |
|
# filter was passed with the true value or not, so we pass the flag |
|
# further down the stack. |
|
search_opts.pop('all_tenants', None) |
|
|
|
if 'locked' in search_opts: |
|
search_opts['locked'] = common.is_locked(search_opts) |
|
|
|
elevated = None |
|
if all_tenants: |
|
if is_detail: |
|
context.can(server_policies.SERVERS % 'detail:get_all_tenants') |
|
else: |
|
context.can(server_policies.SERVERS % 'index:get_all_tenants') |
|
elevated = context.elevated() |
|
else: |
|
# As explained in lp:#1185290, if `all_tenants` is not passed |
|
# we must ignore the `tenant_id` search option. |
|
search_opts.pop('tenant_id', None) |
|
if context.project_id: |
|
search_opts['project_id'] = context.project_id |
|
else: |
|
search_opts['user_id'] = context.user_id |
|
|
|
limit, marker = common.get_limit_and_marker(req) |
|
sort_keys, sort_dirs = common.get_sort_params(req.params) |
|
blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY |
|
if api_version_request.is_supported(req, min_version='2.73'): |
|
blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY_V273 |
|
sort_keys, sort_dirs = remove_invalid_sort_keys( |
|
context, sort_keys, sort_dirs, blacklist, ('host', 'node')) |
|
|
|
expected_attrs = [] |
|
if is_detail: |
|
if api_version_request.is_supported(req, '2.16'): |
|
expected_attrs.append('services') |
|
if api_version_request.is_supported(req, '2.26'): |
|
expected_attrs.append("tags") |
|
if api_version_request.is_supported(req, '2.63'): |
|
expected_attrs.append("trusted_certs") |
|
if api_version_request.is_supported(req, '2.73'): |
|
expected_attrs.append("system_metadata") |
|
|
|
# merge our expected attrs with what the view builder needs for |
|
# showing details |
|
expected_attrs = self._view_builder.get_show_expected_attrs( |
|
expected_attrs) |
|
|
|
try: |
|
instance_list = self.compute_api.get_all(elevated or context, |
|
search_opts=search_opts, limit=limit, marker=marker, |
|
expected_attrs=expected_attrs, sort_keys=sort_keys, |
|
sort_dirs=sort_dirs, cell_down_support=cell_down_support, |
|
all_tenants=all_tenants) |
|
except exception.MarkerNotFound: |
|
msg = _('marker [%s] not found') % marker |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.FlavorNotFound: |
|
LOG.debug("Flavor '%s' could not be found ", |
|
search_opts['flavor']) |
|
instance_list = objects.InstanceList() |
|
|
|
if is_detail: |
|
instance_list._context = context |
|
instance_list.fill_faults() |
|
response = self._view_builder.detail( |
|
req, instance_list, cell_down_support=cell_down_support) |
|
else: |
|
response = self._view_builder.index( |
|
req, instance_list, cell_down_support=cell_down_support) |
|
return response |
|
|
|
def _get_server(self, context, req, instance_uuid, is_detail=False, |
|
cell_down_support=False, columns_to_join=None): |
|
"""Utility function for looking up an instance by uuid. |
|
|
|
:param context: request context for auth |
|
:param req: HTTP request. |
|
:param instance_uuid: UUID of the server instance to get |
|
:param is_detail: True if you plan on showing the details of the |
|
instance in the response, False otherwise. |
|
:param cell_down_support: True if the API (and caller) support |
|
returning a minimal instance |
|
construct if the relevant cell is |
|
down. |
|
:param columns_to_join: optional list of extra fields to join on the |
|
Instance object |
|
""" |
|
expected_attrs = ['flavor', 'numa_topology'] |
|
if is_detail: |
|
if api_version_request.is_supported(req, '2.26'): |
|
expected_attrs.append("tags") |
|
if api_version_request.is_supported(req, '2.63'): |
|
expected_attrs.append("trusted_certs") |
|
expected_attrs = self._view_builder.get_show_expected_attrs( |
|
expected_attrs) |
|
if columns_to_join: |
|
expected_attrs.extend(columns_to_join) |
|
instance = common.get_instance(self.compute_api, context, |
|
instance_uuid, |
|
expected_attrs=expected_attrs, |
|
cell_down_support=cell_down_support) |
|
return instance |
|
|
|
@staticmethod |
|
def _validate_network_id(net_id, network_uuids): |
|
"""Validates that a requested network id. |
|
|
|
This method checks that the network id is in the proper UUID format. |
|
|
|
:param net_id: The network id to validate. |
|
:param network_uuids: A running list of requested network IDs that have |
|
passed validation already. |
|
:raises: webob.exc.HTTPBadRequest if validation fails |
|
""" |
|
if not uuidutils.is_uuid_like(net_id): |
|
msg = _("Bad networks format: network uuid is " |
|
"not in proper format (%s)") % net_id |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
def _get_requested_networks(self, requested_networks): |
|
"""Create a list of requested networks from the networks attribute.""" |
|
|
|
# Starting in the 2.37 microversion, requested_networks is either a |
|
# list or a string enum with value 'auto' or 'none'. The auto/none |
|
# values are verified via jsonschema so we don't check them again here. |
|
if isinstance(requested_networks, six.string_types): |
|
return objects.NetworkRequestList( |
|
objects=[objects.NetworkRequest( |
|
network_id=requested_networks)]) |
|
|
|
networks = [] |
|
network_uuids = [] |
|
for network in requested_networks: |
|
request = objects.NetworkRequest() |
|
try: |
|
# fixed IP address is optional |
|
# if the fixed IP address is not provided then |
|
# it will use one of the available IP address from the network |
|
request.address = network.get('fixed_ip', None) |
|
request.port_id = network.get('port', None) |
|
|
|
request.tag = network.get('tag', None) |
|
|
|
if request.port_id: |
|
request.network_id = None |
|
if request.address is not None: |
|
msg = _("Specified Fixed IP '%(addr)s' cannot be used " |
|
"with port '%(port)s': the two cannot be " |
|
"specified together.") % { |
|
"addr": request.address, |
|
"port": request.port_id} |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
else: |
|
request.network_id = network['uuid'] |
|
self._validate_network_id( |
|
request.network_id, network_uuids) |
|
network_uuids.append(request.network_id) |
|
|
|
networks.append(request) |
|
except KeyError as key: |
|
expl = _('Bad network format: missing %s') % key |
|
raise exc.HTTPBadRequest(explanation=expl) |
|
except TypeError: |
|
expl = _('Bad networks format') |
|
raise exc.HTTPBadRequest(explanation=expl) |
|
|
|
return objects.NetworkRequestList(objects=networks) |
|
|
|
@wsgi.expected_errors(404) |
|
def show(self, req, id): |
|
"""Returns server details by server id.""" |
|
context = req.environ['nova.context'] |
|
cell_down_support = api_version_request.is_supported( |
|
req, min_version=PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION) |
|
show_server_groups = api_version_request.is_supported( |
|
req, min_version='2.71') |
|
|
|
instance = self._get_server( |
|
context, req, id, is_detail=True, |
|
cell_down_support=cell_down_support) |
|
context.can(server_policies.SERVERS % 'show', |
|
target={'project_id': instance.project_id}) |
|
|
|
return self._view_builder.show( |
|
req, instance, cell_down_support=cell_down_support, |
|
show_server_groups=show_server_groups) |
|
|
|
@staticmethod |
|
def _process_bdms_for_create( |
|
context, target, server_dict, create_kwargs): |
|
"""Processes block_device_mapping(_v2) req parameters for server create |
|
|
|
:param context: The nova auth request context |
|
:param target: The target dict for ``context.can`` policy checks |
|
:param server_dict: The POST /servers request body "server" entry |
|
:param create_kwargs: dict that gets populated by this method and |
|
passed to nova.comptue.api.API.create() |
|
:raises: webob.exc.HTTPBadRequest if the request parameters are invalid |
|
:raises: nova.exception.Forbidden if a policy check fails |
|
""" |
|
block_device_mapping_legacy = server_dict.get('block_device_mapping', |
|
[]) |
|
block_device_mapping_v2 = server_dict.get('block_device_mapping_v2', |
|
[]) |
|
|
|
if block_device_mapping_legacy and block_device_mapping_v2: |
|
expl = _('Using different block_device_mapping syntaxes ' |
|
'is not allowed in the same request.') |
|
raise exc.HTTPBadRequest(explanation=expl) |
|
|
|
if block_device_mapping_legacy: |
|
for bdm in block_device_mapping_legacy: |
|
if 'delete_on_termination' in bdm: |
|
bdm['delete_on_termination'] = strutils.bool_from_string( |
|
bdm['delete_on_termination']) |
|
create_kwargs[ |
|
'block_device_mapping'] = block_device_mapping_legacy |
|
# Sets the legacy_bdm flag if we got a legacy block device mapping. |
|
create_kwargs['legacy_bdm'] = True |
|
elif block_device_mapping_v2: |
|
# Have to check whether --image is given, see bug 1433609 |
|
image_href = server_dict.get('imageRef') |
|
image_uuid_specified = image_href is not None |
|
try: |
|
block_device_mapping = [ |
|
block_device.BlockDeviceDict.from_api(bdm_dict, |
|
image_uuid_specified) |
|
for bdm_dict in block_device_mapping_v2] |
|
except exception.InvalidBDMFormat as e: |
|
raise exc.HTTPBadRequest(explanation=e.format_message()) |
|
create_kwargs['block_device_mapping'] = block_device_mapping |
|
# Unset the legacy_bdm flag if we got a block device mapping. |
|
create_kwargs['legacy_bdm'] = False |
|
|
|
block_device_mapping = create_kwargs.get("block_device_mapping") |
|
if block_device_mapping: |
|
context.can(server_policies.SERVERS % 'create:attach_volume', |
|
target) |
|
|
|
def _process_networks_for_create( |
|
self, context, target, server_dict, create_kwargs): |
|
"""Processes networks request parameter for server create |
|
|
|
:param context: The nova auth request context |
|
:param target: The target dict for ``context.can`` policy checks |
|
:param server_dict: The POST /servers request body "server" entry |
|
:param create_kwargs: dict that gets populated by this method and |
|
passed to nova.comptue.api.API.create() |
|
:raises: webob.exc.HTTPBadRequest if the request parameters are invalid |
|
:raises: nova.exception.Forbidden if a policy check fails |
|
""" |
|
requested_networks = server_dict.get('networks', None) |
|
|
|
if requested_networks is not None: |
|
requested_networks = self._get_requested_networks( |
|
requested_networks) |
|
|
|
# Skip policy check for 'create:attach_network' if there is no |
|
# network allocation request. |
|
if requested_networks and len(requested_networks) and \ |
|
not requested_networks.no_allocate: |
|
context.can(server_policies.SERVERS % 'create:attach_network', |
|
target) |
|
|
|
create_kwargs['requested_networks'] = requested_networks |
|
|
|
@staticmethod |
|
def _process_hosts_for_create( |
|
context, target, server_dict, create_kwargs, host, node): |
|
"""Processes hosts request parameter for server create |
|
|
|
:param context: The nova auth request context |
|
:param target: The target dict for ``context.can`` policy checks |
|
:param server_dict: The POST /servers request body "server" entry |
|
:param create_kwargs: dict that gets populated by this method and |
|
passed to nova.comptue.api.API.create() |
|
:param host: Forced host of availability_zone |
|
:param node: Forced node of availability_zone |
|
:raise: webob.exc.HTTPBadRequest if the request parameters are invalid |
|
:raise: nova.exception.Forbidden if a policy check fails |
|
""" |
|
requested_host = server_dict.get('host') |
|
requested_hypervisor_hostname = server_dict.get('hypervisor_hostname') |
|
if requested_host or requested_hypervisor_hostname: |
|
# If the policy check fails, this will raise Forbidden exception. |
|
context.can(server_policies.REQUESTED_DESTINATION, target=target) |
|
if host or node: |
|
msg = _("One mechanism with host and/or " |
|
"hypervisor_hostname and another mechanism " |
|
"with zone:host:node are mutually exclusive.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
create_kwargs['requested_host'] = requested_host |
|
create_kwargs['requested_hypervisor_hostname'] = ( |
|
requested_hypervisor_hostname) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 403, 409)) |
|
@validation.schema(schema_servers.base_create_v20, '2.0', '2.0') |
|
@validation.schema(schema_servers.base_create, '2.1', '2.18') |
|
@validation.schema(schema_servers.base_create_v219, '2.19', '2.31') |
|
@validation.schema(schema_servers.base_create_v232, '2.32', '2.32') |
|
@validation.schema(schema_servers.base_create_v233, '2.33', '2.36') |
|
@validation.schema(schema_servers.base_create_v237, '2.37', '2.41') |
|
@validation.schema(schema_servers.base_create_v242, '2.42', '2.51') |
|
@validation.schema(schema_servers.base_create_v252, '2.52', '2.56') |
|
@validation.schema(schema_servers.base_create_v257, '2.57', '2.62') |
|
@validation.schema(schema_servers.base_create_v263, '2.63', '2.66') |
|
@validation.schema(schema_servers.base_create_v267, '2.67', '2.73') |
|
@validation.schema(schema_servers.base_create_v274, '2.74') |
|
def create(self, req, body): |
|
"""Creates a new server for a given user.""" |
|
context = req.environ['nova.context'] |
|
server_dict = body['server'] |
|
password = self._get_server_admin_password(server_dict) |
|
name = common.normalize_name(server_dict['name']) |
|
description = name |
|
if api_version_request.is_supported(req, min_version='2.19'): |
|
description = server_dict.get('description') |
|
|
|
# Arguments to be passed to instance create function |
|
create_kwargs = {} |
|
|
|
create_kwargs['user_data'] = server_dict.get('user_data') |
|
# NOTE(alex_xu): The v2.1 API compat mode, we strip the spaces for |
|
# keypair create. But we didn't strip spaces at here for |
|
# backward-compatible some users already created keypair and name with |
|
# leading/trailing spaces by legacy v2 API. |
|
create_kwargs['key_name'] = server_dict.get('key_name') |
|
create_kwargs['config_drive'] = server_dict.get('config_drive') |
|
security_groups = server_dict.get('security_groups') |
|
if security_groups is not None: |
|
create_kwargs['security_groups'] = [ |
|
sg['name'] for sg in security_groups if sg.get('name')] |
|
create_kwargs['security_groups'] = list( |
|
set(create_kwargs['security_groups'])) |
|
|
|
scheduler_hints = {} |
|
if 'os:scheduler_hints' in body: |
|
scheduler_hints = body['os:scheduler_hints'] |
|
elif 'OS-SCH-HNT:scheduler_hints' in body: |
|
scheduler_hints = body['OS-SCH-HNT:scheduler_hints'] |
|
create_kwargs['scheduler_hints'] = scheduler_hints |
|
|
|
# min_count and max_count are optional. If they exist, they may come |
|
# in as strings. Verify that they are valid integers and > 0. |
|
# Also, we want to default 'min_count' to 1, and default |
|
# 'max_count' to be 'min_count'. |
|
min_count = int(server_dict.get('min_count', 1)) |
|
max_count = int(server_dict.get('max_count', min_count)) |
|
if min_count > max_count: |
|
msg = _('min_count must be <= max_count') |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
create_kwargs['min_count'] = min_count |
|
create_kwargs['max_count'] = max_count |
|
|
|
availability_zone = server_dict.pop("availability_zone", None) |
|
|
|
if api_version_request.is_supported(req, min_version='2.52'): |
|
create_kwargs['tags'] = server_dict.get('tags') |
|
|
|
helpers.translate_attributes(helpers.CREATE, |
|
server_dict, create_kwargs) |
|
|
|
target = { |
|
'project_id': context.project_id, |
|
'user_id': context.user_id, |
|
'availability_zone': availability_zone} |
|
context.can(server_policies.SERVERS % 'create', target) |
|
|
|
# Skip policy check for 'create:trusted_certs' if no trusted |
|
# certificate IDs were provided. |
|
trusted_certs = server_dict.get('trusted_image_certificates', None) |
|
if trusted_certs: |
|
create_kwargs['trusted_certs'] = trusted_certs |
|
context.can(server_policies.SERVERS % 'create:trusted_certs', |
|
target=target) |
|
|
|
parse_az = self.compute_api.parse_availability_zone |
|
try: |
|
availability_zone, host, node = parse_az(context, |
|
availability_zone) |
|
except exception.InvalidInput as err: |
|
raise exc.HTTPBadRequest(explanation=six.text_type(err)) |
|
if host or node: |
|
context.can(server_policies.SERVERS % 'create:forced_host', |
|
target=target) |
|
|
|
if api_version_request.is_supported(req, min_version='2.74'): |
|
self._process_hosts_for_create(context, target, server_dict, |
|
create_kwargs, host, node) |
|
|
|
self._process_bdms_for_create( |
|
context, target, server_dict, create_kwargs) |
|
|
|
image_uuid = self._image_from_req_data(server_dict, create_kwargs) |
|
|
|
self._process_networks_for_create( |
|
context, target, server_dict, create_kwargs) |
|
|
|
flavor_id = self._flavor_id_from_req_data(body) |
|
try: |
|
inst_type = flavors.get_flavor_by_flavor_id( |
|
flavor_id, ctxt=context, read_deleted="no") |
|
|
|
supports_multiattach = common.supports_multiattach_volume(req) |
|
supports_port_resource_request = \ |
|
common.supports_port_resource_request(req) |
|
(instances, resv_id) = self.compute_api.create(context, |
|
inst_type, |
|
image_uuid, |
|
display_name=name, |
|
display_description=description, |
|
availability_zone=availability_zone, |
|
forced_host=host, forced_node=node, |
|
metadata=server_dict.get('metadata', {}), |
|
admin_password=password, |
|
check_server_group_quota=True, |
|
supports_multiattach=supports_multiattach, |
|
supports_port_resource_request=supports_port_resource_request, |
|
**create_kwargs) |
|
except (exception.QuotaError, |
|
exception.PortLimitExceeded) as error: |
|
raise exc.HTTPForbidden( |
|
explanation=error.format_message()) |
|
except exception.ImageNotFound: |
|
msg = _("Can not find requested image") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.KeypairNotFound: |
|
msg = _("Invalid key_name provided.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.ConfigDriveInvalidValue: |
|
msg = _("Invalid config_drive provided.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except (exception.BootFromVolumeRequiredForZeroDiskFlavor, |
|
exception.ExternalNetworkAttachForbidden) as error: |
|
raise exc.HTTPForbidden(explanation=error.format_message()) |
|
except messaging.RemoteError as err: |
|
msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type, |
|
'err_msg': err.value} |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except UnicodeDecodeError as error: |
|
msg = "UnicodeError: %s" % error |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except (exception.ImageNotActive, |
|
exception.ImageBadRequest, |
|
exception.ImageNotAuthorized, |
|
exception.ImageUnacceptable, |
|
exception.FixedIpNotFoundForAddress, |
|
exception.FlavorNotFound, |
|
exception.FlavorDiskTooSmall, |
|
exception.FlavorMemoryTooSmall, |
|
exception.InvalidMetadata, |
|
exception.InvalidVolume, |
|
exception.MismatchVolumeAZException, |
|
exception.MultiplePortsNotApplicable, |
|
exception.InvalidFixedIpAndMaxCountRequest, |
|
exception.InstanceUserDataMalformed, |
|
exception.PortNotFound, |
|
exception.FixedIpAlreadyInUse, |
|
exception.SecurityGroupNotFound, |
|
exception.PortRequiresFixedIP, |
|
exception.NetworkRequiresSubnet, |
|
exception.NetworkNotFound, |
|
exception.InvalidBDM, |
|
exception.InvalidBDMSnapshot, |
|
exception.InvalidBDMVolume, |
|
exception.InvalidBDMImage, |
|
exception.InvalidBDMBootSequence, |
|
exception.InvalidBDMLocalsLimit, |
|
exception.InvalidBDMVolumeNotBootable, |
|
exception.InvalidBDMEphemeralSize, |
|
exception.InvalidBDMFormat, |
|
exception.InvalidBDMSwapSize, |
|
exception.InvalidBDMDiskBus, |
|
exception.VolumeTypeNotFound, |
|
exception.AutoDiskConfigDisabledByImage, |
|
exception.InstanceGroupNotFound, |
|
exception.SnapshotNotFound, |
|
exception.UnableToAutoAllocateNetwork, |
|
exception.MultiattachNotSupportedOldMicroversion, |
|
exception.CertificateValidationFailed, |
|
exception.CreateWithPortResourceRequestOldVersion, |
|
exception.DeviceProfileError, |
|
exception.ComputeHostNotFound) as error: |
|
raise exc.HTTPBadRequest(explanation=error.format_message()) |
|
except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error: |
|
raise exc.HTTPBadRequest(explanation=error.format_message()) |
|
except (exception.PortInUse, |
|
exception.InstanceExists, |
|
exception.NetworkAmbiguous, |
|
exception.NoUniqueMatch) as error: |
|
raise exc.HTTPConflict(explanation=error.format_message()) |
|
|
|
# If the caller wanted a reservation_id, return it |
|
if server_dict.get('return_reservation_id', False): |
|
return wsgi.ResponseObject({'reservation_id': resv_id}) |
|
|
|
server = self._view_builder.create(req, instances[0]) |
|
|
|
if CONF.api.enable_instance_password: |
|
server['server']['adminPass'] = password |
|
|
|
robj = wsgi.ResponseObject(server) |
|
|
|
return self._add_location(robj) |
|
|
|
def _delete(self, context, req, instance_uuid): |
|
instance = self._get_server(context, req, instance_uuid) |
|
context.can(server_policies.SERVERS % 'delete', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
if CONF.reclaim_instance_interval: |
|
try: |
|
self.compute_api.soft_delete(context, instance) |
|
except exception.InstanceInvalidState: |
|
# Note(yufang521247): instance which has never been active |
|
# is not allowed to be soft_deleted. Thus we have to call |
|
# delete() to clean up the instance. |
|
self.compute_api.delete(context, instance) |
|
else: |
|
self.compute_api.delete(context, instance) |
|
|
|
@wsgi.expected_errors(404) |
|
@validation.schema(schema_servers.base_update_v20, '2.0', '2.0') |
|
@validation.schema(schema_servers.base_update, '2.1', '2.18') |
|
@validation.schema(schema_servers.base_update_v219, '2.19') |
|
def update(self, req, id, body): |
|
"""Update server then pass on to version-specific controller.""" |
|
|
|
ctxt = req.environ['nova.context'] |
|
update_dict = {} |
|
instance = self._get_server(ctxt, req, id, is_detail=True) |
|
ctxt.can(server_policies.SERVERS % 'update', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
show_server_groups = api_version_request.is_supported( |
|
req, min_version='2.71') |
|
|
|
server = body['server'] |
|
|
|
if 'name' in server: |
|
update_dict['display_name'] = common.normalize_name( |
|
server['name']) |
|
|
|
if 'description' in server: |
|
# This is allowed to be None (remove description) |
|
update_dict['display_description'] = server['description'] |
|
|
|
helpers.translate_attributes(helpers.UPDATE, server, update_dict) |
|
|
|
try: |
|
instance = self.compute_api.update_instance(ctxt, instance, |
|
update_dict) |
|
|
|
# NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild |
|
# API response will show all attributes like GET /servers API. |
|
show_all_attributes = api_version_request.is_supported( |
|
req, min_version='2.75') |
|
extend_address = show_all_attributes |
|
show_AZ = show_all_attributes |
|
show_config_drive = show_all_attributes |
|
show_keypair = show_all_attributes |
|
show_srv_usg = show_all_attributes |
|
show_sec_grp = show_all_attributes |
|
show_extended_status = show_all_attributes |
|
show_extended_volumes = show_all_attributes |
|
# NOTE(gmann): Below attributes need to be added in response |
|
# if respective policy allows.So setting these as None |
|
# to perform the policy check in view builder. |
|
show_extended_attr = None if show_all_attributes else False |
|
show_host_status = None if show_all_attributes else False |
|
|
|
return self._view_builder.show( |
|
req, instance, |
|
extend_address=extend_address, |
|
show_AZ=show_AZ, |
|
show_config_drive=show_config_drive, |
|
show_extended_attr=show_extended_attr, |
|
show_host_status=show_host_status, |
|
show_keypair=show_keypair, |
|
show_srv_usg=show_srv_usg, |
|
show_sec_grp=show_sec_grp, |
|
show_extended_status=show_extended_status, |
|
show_extended_volumes=show_extended_volumes, |
|
show_server_groups=show_server_groups) |
|
except exception.InstanceNotFound: |
|
msg = _("Instance could not be found") |
|
raise exc.HTTPNotFound(explanation=msg) |
|
|
|
# NOTE(gmann): Returns 204 for backwards compatibility but should be 202 |
|
# for representing async API as this API just accepts the request and |
|
# request hypervisor driver to complete the same in async mode. |
|
@wsgi.response(204) |
|
@wsgi.expected_errors((400, 404, 409)) |
|
@wsgi.action('confirmResize') |
|
def _action_confirm_resize(self, req, id, body): |
|
context = req.environ['nova.context'] |
|
instance = self._get_server(context, req, id) |
|
context.can(server_policies.SERVERS % 'confirm_resize', |
|
target={'project_id': instance.project_id}) |
|
try: |
|
self.compute_api.confirm_resize(context, instance) |
|
except exception.MigrationNotFound: |
|
msg = _("Instance has not been resized.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except ( |
|
exception.InstanceIsLocked, |
|
exception.ServiceUnavailable, |
|
) as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'confirmResize', id) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 404, 409)) |
|
@wsgi.action('revertResize') |
|
def _action_revert_resize(self, req, id, body): |
|
context = req.environ['nova.context'] |
|
instance = self._get_server(context, req, id) |
|
context.can(server_policies.SERVERS % 'revert_resize', |
|
target={'project_id': instance.project_id}) |
|
try: |
|
self.compute_api.revert_resize(context, instance) |
|
except exception.MigrationNotFound: |
|
msg = _("Instance has not been resized.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.FlavorNotFound: |
|
msg = _("Flavor used by the instance could not be found.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.InstanceIsLocked as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'revertResize', id) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((404, 409)) |
|
@wsgi.action('reboot') |
|
@validation.schema(schema_servers.reboot) |
|
def _action_reboot(self, req, id, body): |
|
|
|
reboot_type = body['reboot']['type'].upper() |
|
context = req.environ['nova.context'] |
|
instance = self._get_server(context, req, id) |
|
context.can(server_policies.SERVERS % 'reboot', |
|
target={'project_id': instance.project_id}) |
|
|
|
try: |
|
self.compute_api.reboot(context, instance, reboot_type) |
|
except exception.InstanceIsLocked as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'reboot', id) |
|
|
|
def _resize(self, req, instance_id, flavor_id, auto_disk_config=None): |
|
"""Begin the resize process with given instance/flavor.""" |
|
context = req.environ["nova.context"] |
|
instance = self._get_server(context, req, instance_id, |
|
columns_to_join=['services']) |
|
context.can(server_policies.SERVERS % 'resize', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
|
|
if common.instance_has_port_with_resource_request( |
|
instance_id, self.network_api): |
|
# TODO(gibi): Remove when nova only supports compute newer than |
|
# Train |
|
source_service = objects.Service.get_by_host_and_binary( |
|
context, instance.host, 'nova-compute') |
|
if source_service.version < MIN_COMPUTE_MOVE_BANDWIDTH: |
|
msg = _("The resize action on a server with ports having " |
|
"resource requests, like a port with a QoS " |
|
"minimum bandwidth policy, is not yet supported.") |
|
raise exc.HTTPConflict(explanation=msg) |
|
|
|
try: |
|
self.compute_api.resize(context, instance, flavor_id, |
|
auto_disk_config=auto_disk_config) |
|
except (exception.QuotaError, |
|
exception.ForbiddenWithAccelerators) as error: |
|
raise exc.HTTPForbidden( |
|
explanation=error.format_message()) |
|
except (exception.InstanceIsLocked, |
|
exception.InstanceNotReady, |
|
exception.ServiceUnavailable) as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'resize', instance_id) |
|
except exception.ImageNotAuthorized: |
|
msg = _("You are not authorized to access the image " |
|
"the instance was started with.") |
|
raise exc.HTTPUnauthorized(explanation=msg) |
|
except exception.ImageNotFound: |
|
msg = _("Image that the instance was started " |
|
"with could not be found.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except (exception.AutoDiskConfigDisabledByImage, |
|
exception.CannotResizeDisk, |
|
exception.CannotResizeToSameFlavor, |
|
exception.FlavorNotFound) as e: |
|
raise exc.HTTPBadRequest(explanation=e.format_message()) |
|
except INVALID_FLAVOR_IMAGE_EXCEPTIONS as e: |
|
raise exc.HTTPBadRequest(explanation=e.format_message()) |
|
except exception.Invalid: |
|
msg = _("Invalid instance image.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
@wsgi.response(204) |
|
@wsgi.expected_errors((404, 409)) |
|
def delete(self, req, id): |
|
"""Destroys a server.""" |
|
try: |
|
self._delete(req.environ['nova.context'], req, id) |
|
except exception.InstanceNotFound: |
|
msg = _("Instance could not be found") |
|
raise exc.HTTPNotFound(explanation=msg) |
|
except (exception.InstanceIsLocked, |
|
exception.AllocationDeleteFailed) as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'delete', id) |
|
|
|
def _image_from_req_data(self, server_dict, create_kwargs): |
|
"""Get image data from the request or raise appropriate |
|
exceptions. |
|
|
|
The field imageRef is mandatory when no block devices have been |
|
defined and must be a proper uuid when present. |
|
""" |
|
image_href = server_dict.get('imageRef') |
|
|
|
if not image_href and create_kwargs.get('block_device_mapping'): |
|
return '' |
|
elif image_href: |
|
return image_href |
|
else: |
|
msg = _("Missing imageRef attribute") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
|
|
def _flavor_id_from_req_data(self, data): |
|
flavor_ref = data['server']['flavorRef'] |
|
return common.get_id_from_href(flavor_ref) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 401, 403, 404, 409)) |
|
@wsgi.action('resize') |
|
@validation.schema(schema_servers.resize) |
|
def _action_resize(self, req, id, body): |
|
"""Resizes a given instance to the flavor size requested.""" |
|
resize_dict = body['resize'] |
|
flavor_ref = str(resize_dict["flavorRef"]) |
|
|
|
kwargs = {} |
|
helpers.translate_attributes(helpers.RESIZE, resize_dict, kwargs) |
|
|
|
self._resize(req, id, flavor_ref, **kwargs) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 403, 404, 409)) |
|
@wsgi.action('rebuild') |
|
@validation.schema(schema_servers.base_rebuild_v20, '2.0', '2.0') |
|
@validation.schema(schema_servers.base_rebuild, '2.1', '2.18') |
|
@validation.schema(schema_servers.base_rebuild_v219, '2.19', '2.53') |
|
@validation.schema(schema_servers.base_rebuild_v254, '2.54', '2.56') |
|
@validation.schema(schema_servers.base_rebuild_v257, '2.57', '2.62') |
|
@validation.schema(schema_servers.base_rebuild_v263, '2.63') |
|
def _action_rebuild(self, req, id, body): |
|
"""Rebuild an instance with the given attributes.""" |
|
rebuild_dict = body['rebuild'] |
|
|
|
image_href = rebuild_dict["imageRef"] |
|
|
|
password = self._get_server_admin_password(rebuild_dict) |
|
|
|
context = req.environ['nova.context'] |
|
instance = self._get_server(context, req, id) |
|
target = {'user_id': instance.user_id, |
|
'project_id': instance.project_id} |
|
context.can(server_policies.SERVERS % 'rebuild', target=target) |
|
attr_map = { |
|
'name': 'display_name', |
|
'description': 'display_description', |
|
'metadata': 'metadata', |
|
} |
|
|
|
kwargs = {} |
|
|
|
helpers.translate_attributes(helpers.REBUILD, rebuild_dict, kwargs) |
|
|
|
if (api_version_request.is_supported(req, min_version='2.54') and |
|
'key_name' in rebuild_dict): |
|
kwargs['key_name'] = rebuild_dict.get('key_name') |
|
|
|
# If user_data is not specified, we don't include it in kwargs because |
|
# we don't want to overwrite the existing user_data. |
|
include_user_data = api_version_request.is_supported( |
|
req, min_version='2.57') |
|
if include_user_data and 'user_data' in rebuild_dict: |
|
kwargs['user_data'] = rebuild_dict['user_data'] |
|
|
|
# Skip policy check for 'rebuild:trusted_certs' if no trusted |
|
# certificate IDs were provided. |
|
if ((api_version_request.is_supported(req, min_version='2.63')) and |
|
# Note that this is different from server create since with |
|
# rebuild a user can unset/reset the trusted certs by |
|
# specifying trusted_image_certificates=None, similar to |
|
# key_name. |
|
('trusted_image_certificates' in rebuild_dict)): |
|
kwargs['trusted_certs'] = rebuild_dict.get( |
|
'trusted_image_certificates') |
|
context.can(server_policies.SERVERS % 'rebuild:trusted_certs', |
|
target=target) |
|
|
|
for request_attribute, instance_attribute in attr_map.items(): |
|
try: |
|
if request_attribute == 'name': |
|
kwargs[instance_attribute] = common.normalize_name( |
|
rebuild_dict[request_attribute]) |
|
else: |
|
kwargs[instance_attribute] = rebuild_dict[ |
|
request_attribute] |
|
except (KeyError, TypeError): |
|
pass |
|
|
|
try: |
|
self.compute_api.rebuild(context, |
|
instance, |
|
image_href, |
|
password, |
|
**kwargs) |
|
except exception.InstanceIsLocked as e: |
|
raise exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'rebuild', id) |
|
except exception.InstanceNotFound: |
|
msg = _("Instance could not be found") |
|
raise exc.HTTPNotFound(explanation=msg) |
|
except exception.ImageNotFound: |
|
msg = _("Cannot find image for rebuild") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except exception.KeypairNotFound: |
|
msg = _("Invalid key_name provided.") |
|
raise exc.HTTPBadRequest(explanation=msg) |
|
except (exception.QuotaError, |
|
exception.ForbiddenWithAccelerators) as error: |
|
raise exc.HTTPForbidden(explanation=error.format_message()) |
|
except (exception.AutoDiskConfigDisabledByImage, |
|
exception.CertificateValidationFailed, |
|
exception.FlavorDiskTooSmall, |
|
exception.FlavorMemoryTooSmall, |
|
exception.ImageNotActive, |
|
exception.ImageUnacceptable, |
|
exception.InvalidMetadata, |
|
exception.InvalidArchitectureName, |
|
exception.InvalidVolume, |
|
) as error: |
|
raise exc.HTTPBadRequest(explanation=error.format_message()) |
|
except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error: |
|
raise exc.HTTPBadRequest(explanation=error.format_message()) |
|
|
|
instance = self._get_server(context, req, id, is_detail=True) |
|
|
|
# NOTE(liuyulong): set the new key_name for the API response. |
|
# from microversion 2.54 onwards. |
|
show_keypair = api_version_request.is_supported( |
|
req, min_version='2.54') |
|
show_server_groups = api_version_request.is_supported( |
|
req, min_version='2.71') |
|
|
|
# NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild |
|
# API response will show all attributes like GET /servers API. |
|
show_all_attributes = api_version_request.is_supported( |
|
req, min_version='2.75') |
|
extend_address = show_all_attributes |
|
show_AZ = show_all_attributes |
|
show_config_drive = show_all_attributes |
|
show_srv_usg = show_all_attributes |
|
show_sec_grp = show_all_attributes |
|
show_extended_status = show_all_attributes |
|
show_extended_volumes = show_all_attributes |
|
# NOTE(gmann): Below attributes need to be added in response |
|
# if respective policy allows.So setting these as None |
|
# to perform the policy check in view builder. |
|
show_extended_attr = None if show_all_attributes else False |
|
show_host_status = None if show_all_attributes else False |
|
|
|
view = self._view_builder.show( |
|
req, instance, |
|
extend_address=extend_address, |
|
show_AZ=show_AZ, |
|
show_config_drive=show_config_drive, |
|
show_extended_attr=show_extended_attr, |
|
show_host_status=show_host_status, |
|
show_keypair=show_keypair, |
|
show_srv_usg=show_srv_usg, |
|
show_sec_grp=show_sec_grp, |
|
show_extended_status=show_extended_status, |
|
show_extended_volumes=show_extended_volumes, |
|
show_server_groups=show_server_groups, |
|
# NOTE(gmann): user_data has been added in response (by code at |
|
# the end of this API method) since microversion 2.57 so tell |
|
# view builder not to include it. |
|
show_user_data=False) |
|
|
|
# Add on the admin_password attribute since the view doesn't do it |
|
# unless instance passwords are disabled |
|
if CONF.api.enable_instance_password: |
|
view['server']['adminPass'] = password |
|
|
|
if include_user_data: |
|
view['server']['user_data'] = instance.user_data |
|
|
|
robj = wsgi.ResponseObject(view) |
|
return self._add_location(robj) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 403, 404, 409)) |
|
@wsgi.action('createImage') |
|
@validation.schema(schema_servers.create_image, '2.0', '2.0') |
|
@validation.schema(schema_servers.create_image, '2.1') |
|
def _action_create_image(self, req, id, body): |
|
"""Snapshot a server instance.""" |
|
context = req.environ['nova.context'] |
|
instance = self._get_server(context, req, id) |
|
target = {'project_id': instance.project_id} |
|
context.can(server_policies.SERVERS % 'create_image', |
|
target=target) |
|
|
|
entity = body["createImage"] |
|
image_name = common.normalize_name(entity["name"]) |
|
metadata = entity.get('metadata', {}) |
|
|
|
# Starting from microversion 2.39 we don't check quotas on createImage |
|
if api_version_request.is_supported( |
|
req, max_version= |
|
api_version_request.MAX_IMAGE_META_PROXY_API_VERSION): |
|
common.check_img_metadata_properties_quota(context, metadata) |
|
|
|
bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( |
|
context, instance.uuid) |
|
|
|
try: |
|
if compute_utils.is_volume_backed_instance(context, instance, |
|
bdms): |
|
context.can(server_policies.SERVERS % |
|
'create_image:allow_volume_backed', target=target) |
|
image = self.compute_api.snapshot_volume_backed( |
|
context, |
|
instance, |
|
image_name, |
|
extra_properties= |
|
metadata) |
|
else: |
|
image = self.compute_api.snapshot(context, |
|
instance, |
|
image_name, |
|
extra_properties=metadata) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'createImage', id) |
|
except exception.Invalid as err: |
|
raise exc.HTTPBadRequest(explanation=err.format_message()) |
|
except exception.OverQuota as e: |
|
raise exc.HTTPForbidden(explanation=e.format_message()) |
|
|
|
# Starting with microversion 2.45 we return a response body containing |
|
# the snapshot image id without the Location header. |
|
if api_version_request.is_supported(req, '2.45'): |
|
return {'image_id': image['id']} |
|
|
|
# build location of newly-created image entity |
|
image_id = str(image['id']) |
|
image_ref = glance.API().generate_image_url(image_id, context) |
|
|
|
resp = webob.Response(status_int=202) |
|
resp.headers['Location'] = image_ref |
|
return resp |
|
|
|
def _get_server_admin_password(self, server): |
|
"""Determine the admin password for a server on creation.""" |
|
if 'adminPass' in server: |
|
password = server['adminPass'] |
|
else: |
|
password = utils.generate_password() |
|
return password |
|
|
|
def _get_server_search_options(self, req): |
|
"""Return server search options allowed by non-admin.""" |
|
# NOTE(mriedem): all_tenants is admin-only by default but because of |
|
# tight-coupling between this method, the remove_invalid_options method |
|
# and how _get_servers uses them, we include all_tenants here but it |
|
# will be removed later for non-admins. Fixing this would be nice but |
|
# probably not trivial. |
|
opt_list = ('reservation_id', 'name', 'status', 'image', 'flavor', |
|
'ip', 'changes-since', 'all_tenants') |
|
if api_version_request.is_supported(req, min_version='2.5'): |
|
opt_list += ('ip6',) |
|
if api_version_request.is_supported(req, min_version='2.26'): |
|
opt_list += TAG_SEARCH_FILTERS |
|
if api_version_request.is_supported(req, min_version='2.66'): |
|
opt_list += ('changes-before',) |
|
if api_version_request.is_supported(req, min_version='2.73'): |
|
opt_list += ('locked',) |
|
if api_version_request.is_supported(req, min_version='2.83'): |
|
opt_list += ('availability_zone', 'config_drive', 'key_name', |
|
'created_at', 'launched_at', 'terminated_at', |
|
'power_state', 'task_state', 'vm_state', 'progress', |
|
'user_id',) |
|
return opt_list |
|
|
|
def _get_instance(self, context, instance_uuid): |
|
try: |
|
attrs = ['system_metadata', 'metadata'] |
|
mapping = objects.InstanceMapping.get_by_instance_uuid( |
|
context, instance_uuid) |
|
nova_context.set_target_cell(context, mapping.cell_mapping) |
|
return objects.Instance.get_by_uuid( |
|
context, instance_uuid, expected_attrs=attrs) |
|
except (exception.InstanceNotFound, |
|
exception.InstanceMappingNotFound) as e: |
|
raise webob.exc.HTTPNotFound(explanation=e.format_message()) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((404, 409)) |
|
@wsgi.action('os-start') |
|
def _start_server(self, req, id, body): |
|
"""Start an instance.""" |
|
context = req.environ['nova.context'] |
|
instance = self._get_instance(context, id) |
|
context.can(server_policies.SERVERS % 'start', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
try: |
|
self.compute_api.start(context, instance) |
|
except (exception.InstanceNotReady, exception.InstanceIsLocked) as e: |
|
raise webob.exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'start', id) |
|
|
|
@wsgi.response(202) |
|
@wsgi.expected_errors((404, 409)) |
|
@wsgi.action('os-stop') |
|
def _stop_server(self, req, id, body): |
|
"""Stop an instance.""" |
|
context = req.environ['nova.context'] |
|
instance = self._get_instance(context, id) |
|
context.can(server_policies.SERVERS % 'stop', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
try: |
|
self.compute_api.stop(context, instance) |
|
except (exception.InstanceNotReady, exception.InstanceIsLocked) as e: |
|
raise webob.exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'stop', id) |
|
|
|
@wsgi.Controller.api_version("2.17") |
|
@wsgi.response(202) |
|
@wsgi.expected_errors((400, 404, 409)) |
|
@wsgi.action('trigger_crash_dump') |
|
@validation.schema(schema_servers.trigger_crash_dump) |
|
def _action_trigger_crash_dump(self, req, id, body): |
|
"""Trigger crash dump in an instance""" |
|
context = req.environ['nova.context'] |
|
instance = self._get_instance(context, id) |
|
context.can(server_policies.SERVERS % 'trigger_crash_dump', |
|
target={'user_id': instance.user_id, |
|
'project_id': instance.project_id}) |
|
try: |
|
self.compute_api.trigger_crash_dump(context, instance) |
|
except (exception.InstanceNotReady, exception.InstanceIsLocked) as e: |
|
raise webob.exc.HTTPConflict(explanation=e.format_message()) |
|
except exception.InstanceInvalidState as state_error: |
|
common.raise_http_conflict_for_instance_invalid_state(state_error, |
|
'trigger_crash_dump', id) |
|
|
|
|
|
def remove_invalid_options(context, search_options, allowed_search_options): |
|
"""Remove search options that are not permitted unless policy allows.""" |
|
|
|
if context.can(server_policies.SERVERS % 'allow_all_filters', |
|
fatal=False): |
|
# Only remove parameters for sorting and pagination |
|
for key in PAGING_SORTING_PARAMS: |
|
search_options.pop(key, None) |
|
return |
|
# Otherwise, strip out all unknown options |
|
unknown_options = [opt for opt in search_options |
|
if opt not in allowed_search_options] |
|
if unknown_options: |
|
LOG.debug("Removing options '%s' from query", |
|
", ".join(unknown_options)) |
|
for opt in unknown_options: |
|
search_options.pop(opt, None) |
|
|
|
|
|
def remove_invalid_sort_keys(context, sort_keys, sort_dirs, |
|
blacklist, admin_only_fields): |
|
key_list = copy.deepcopy(sort_keys) |
|
for key in key_list: |
|
# NOTE(Kevin Zheng): We are intend to remove the sort_key |
|
# in the blacklist and its' corresponding sort_dir, since |
|
# the sort_key and sort_dir are not strict to be provide |
|
# in pairs in the current implement, sort_dirs could be |
|
# less than sort_keys, in order to avoid IndexError, we |
|
# only pop sort_dir when number of sort_dirs is no less |
|
# than the sort_key index. |
|
if key in blacklist: |
|
if len(sort_dirs) > sort_keys.index(key): |
|
sort_dirs.pop(sort_keys.index(key)) |
|
sort_keys.pop(sort_keys.index(key)) |
|
elif key in admin_only_fields and not context.is_admin: |
|
msg = _("Only administrators can sort servers " |
|
"by %s") % key |
|
raise exc.HTTPForbidden(explanation=msg) |
|
|
|
return sort_keys, sort_dirs
|
|
|