OpenStack Compute (Nova)
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.

1383 lines
63KB

  1. # Copyright 2010 OpenStack Foundation
  2. # Copyright 2011 Piston Cloud Computing, Inc
  3. # All Rights Reserved.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. # not use this file except in compliance with the License. You may obtain
  7. # a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. # License for the specific language governing permissions and limitations
  15. # under the License.
  16. import copy
  17. from oslo_log import log as logging
  18. import oslo_messaging as messaging
  19. from oslo_utils import strutils
  20. from oslo_utils import timeutils
  21. from oslo_utils import uuidutils
  22. import six
  23. import webob
  24. from webob import exc
  25. from nova.api.openstack import api_version_request
  26. from nova.api.openstack import common
  27. from nova.api.openstack.compute import helpers
  28. from nova.api.openstack.compute.schemas import servers as schema_servers
  29. from nova.api.openstack.compute.views import servers as views_servers
  30. from nova.api.openstack import wsgi
  31. from nova.api import validation
  32. from nova import block_device
  33. from nova.compute import api as compute
  34. from nova.compute import flavors
  35. from nova.compute import utils as compute_utils
  36. import nova.conf
  37. from nova import context as nova_context
  38. from nova import exception
  39. from nova.i18n import _
  40. from nova.image import api as image_api
  41. from nova import network as network_api
  42. from nova import objects
  43. from nova.policies import servers as server_policies
  44. from nova import utils
  45. TAG_SEARCH_FILTERS = ('tags', 'tags-any', 'not-tags', 'not-tags-any')
  46. PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION = '2.69'
  47. PAGING_SORTING_PARAMS = ('sort_key', 'sort_dir', 'limit', 'marker')
  48. CONF = nova.conf.CONF
  49. LOG = logging.getLogger(__name__)
  50. INVALID_FLAVOR_IMAGE_EXCEPTIONS = (
  51. exception.BadRequirementEmulatorThreadsPolicy,
  52. exception.CPUThreadPolicyConfigurationInvalid,
  53. exception.FlavorImageConflict,
  54. exception.ImageCPUPinningForbidden,
  55. exception.ImageCPUThreadPolicyForbidden,
  56. exception.ImageNUMATopologyAsymmetric,
  57. exception.ImageNUMATopologyCPUDuplicates,
  58. exception.ImageNUMATopologyCPUOutOfRange,
  59. exception.ImageNUMATopologyCPUsUnassigned,
  60. exception.ImageNUMATopologyForbidden,
  61. exception.ImageNUMATopologyIncomplete,
  62. exception.ImageNUMATopologyMemoryOutOfRange,
  63. exception.ImagePMUConflict,
  64. exception.ImageSerialPortNumberExceedFlavorValue,
  65. exception.ImageSerialPortNumberInvalid,
  66. exception.ImageVCPULimitsRangeExceeded,
  67. exception.ImageVCPUTopologyRangeExceeded,
  68. exception.InvalidCPUAllocationPolicy,
  69. exception.InvalidCPUThreadAllocationPolicy,
  70. exception.InvalidEmulatorThreadsPolicy,
  71. exception.InvalidMachineType,
  72. exception.InvalidNUMANodesNumber,
  73. exception.InvalidRequest,
  74. exception.MemoryPageSizeForbidden,
  75. exception.MemoryPageSizeInvalid,
  76. exception.PciInvalidAlias,
  77. exception.PciRequestAliasNotDefined,
  78. exception.RealtimeConfigurationInvalid,
  79. exception.RealtimeMaskNotFoundOrInvalid,
  80. )
  81. MIN_COMPUTE_MOVE_BANDWIDTH = 39
  82. class ServersController(wsgi.Controller):
  83. """The Server API base controller class for the OpenStack API."""
  84. _view_builder_class = views_servers.ViewBuilder
  85. @staticmethod
  86. def _add_location(robj):
  87. # Just in case...
  88. if 'server' not in robj.obj:
  89. return robj
  90. link = [l for l in robj.obj['server']['links'] if l['rel'] == 'self']
  91. if link:
  92. robj['Location'] = link[0]['href']
  93. # Convenience return
  94. return robj
  95. def __init__(self):
  96. super(ServersController, self).__init__()
  97. self.compute_api = compute.API()
  98. self.network_api = network_api.API()
  99. @wsgi.expected_errors((400, 403))
  100. @validation.query_schema(schema_servers.query_params_v275, '2.75')
  101. @validation.query_schema(schema_servers.query_params_v273, '2.73', '2.74')
  102. @validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72')
  103. @validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65')
  104. @validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25')
  105. def index(self, req):
  106. """Returns a list of server names and ids for a given user."""
  107. context = req.environ['nova.context']
  108. context.can(server_policies.SERVERS % 'index')
  109. try:
  110. servers = self._get_servers(req, is_detail=False)
  111. except exception.Invalid as err:
  112. raise exc.HTTPBadRequest(explanation=err.format_message())
  113. return servers
  114. @wsgi.expected_errors((400, 403))
  115. @validation.query_schema(schema_servers.query_params_v275, '2.75')
  116. @validation.query_schema(schema_servers.query_params_v273, '2.73', '2.74')
  117. @validation.query_schema(schema_servers.query_params_v266, '2.66', '2.72')
  118. @validation.query_schema(schema_servers.query_params_v226, '2.26', '2.65')
  119. @validation.query_schema(schema_servers.query_params_v21, '2.1', '2.25')
  120. def detail(self, req):
  121. """Returns a list of server details for a given user."""
  122. context = req.environ['nova.context']
  123. context.can(server_policies.SERVERS % 'detail')
  124. try:
  125. servers = self._get_servers(req, is_detail=True)
  126. except exception.Invalid as err:
  127. raise exc.HTTPBadRequest(explanation=err.format_message())
  128. return servers
  129. @staticmethod
  130. def _is_cell_down_supported(req, search_opts):
  131. cell_down_support = api_version_request.is_supported(
  132. req, min_version=PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION)
  133. if cell_down_support:
  134. # NOTE(tssurya): Minimal constructs would be returned from the down
  135. # cells if cell_down_support is True, however if filtering, sorting
  136. # or paging is requested by the user, then cell_down_support should
  137. # be made False and the down cells should be skipped (depending on
  138. # CONF.api.list_records_by_skipping_down_cells) as there is no
  139. # way to return correct results for the down cells in those
  140. # situations due to missing keys/information.
  141. # NOTE(tssurya): Since there is a chance that
  142. # remove_invalid_options function could have removed the paging and
  143. # sorting parameters, we add the additional check for that from the
  144. # request.
  145. pag_sort = any(
  146. ps in req.GET.keys() for ps in PAGING_SORTING_PARAMS)
  147. # NOTE(tssurya): ``nova list --all_tenants`` is the only
  148. # allowed filter exception when handling down cells.
  149. filters = list(search_opts.keys()) not in ([u'all_tenants'], [])
  150. if pag_sort or filters:
  151. cell_down_support = False
  152. return cell_down_support
  153. def _get_servers(self, req, is_detail):
  154. """Returns a list of servers, based on any search options specified."""
  155. search_opts = {}
  156. search_opts.update(req.GET)
  157. context = req.environ['nova.context']
  158. remove_invalid_options(context, search_opts,
  159. self._get_server_search_options(req))
  160. cell_down_support = self._is_cell_down_supported(req, search_opts)
  161. for search_opt in search_opts:
  162. if (search_opt in
  163. schema_servers.JOINED_TABLE_QUERY_PARAMS_SERVERS.keys() or
  164. search_opt.startswith('_')):
  165. msg = _("Invalid filter field: %s.") % search_opt
  166. raise exc.HTTPBadRequest(explanation=msg)
  167. # Verify search by 'status' contains a valid status.
  168. # Convert it to filter by vm_state or task_state for compute_api.
  169. # For non-admin user, vm_state and task_state are filtered through
  170. # remove_invalid_options function, based on value of status field.
  171. # Set value to vm_state and task_state to make search simple.
  172. search_opts.pop('status', None)
  173. if 'status' in req.GET.keys():
  174. statuses = req.GET.getall('status')
  175. states = common.task_and_vm_state_from_status(statuses)
  176. vm_state, task_state = states
  177. if not vm_state and not task_state:
  178. if api_version_request.is_supported(req, min_version='2.38'):
  179. msg = _('Invalid status value')
  180. raise exc.HTTPBadRequest(explanation=msg)
  181. return {'servers': []}
  182. search_opts['vm_state'] = vm_state
  183. # When we search by vm state, task state will return 'default'.
  184. # So we don't need task_state search_opt.
  185. if 'default' not in task_state:
  186. search_opts['task_state'] = task_state
  187. if 'changes-since' in search_opts:
  188. try:
  189. search_opts['changes-since'] = timeutils.parse_isotime(
  190. search_opts['changes-since'])
  191. except ValueError:
  192. # NOTE: This error handling is for V2.0 API to pass the
  193. # experimental jobs at the gate. V2.1 API covers this case
  194. # with JSON-Schema and it is a hard burden to apply it to
  195. # v2.0 API at this time.
  196. msg = _("Invalid filter field: changes-since.")
  197. raise exc.HTTPBadRequest(explanation=msg)
  198. if 'changes-before' in search_opts:
  199. try:
  200. search_opts['changes-before'] = timeutils.parse_isotime(
  201. search_opts['changes-before'])
  202. changes_since = search_opts.get('changes-since')
  203. if changes_since and search_opts['changes-before'] < \
  204. search_opts['changes-since']:
  205. msg = _('The value of changes-since must be'
  206. ' less than or equal to changes-before.')
  207. raise exc.HTTPBadRequest(explanation=msg)
  208. except ValueError:
  209. msg = _("Invalid filter field: changes-before.")
  210. raise exc.HTTPBadRequest(explanation=msg)
  211. # By default, compute's get_all() will return deleted instances.
  212. # If an admin hasn't specified a 'deleted' search option, we need
  213. # to filter out deleted instances by setting the filter ourselves.
  214. # ... Unless 'changes-since' or 'changes-before' is specified,
  215. # because those will return recently deleted instances according to
  216. # the API spec.
  217. if 'deleted' not in search_opts:
  218. if 'changes-since' not in search_opts and \
  219. 'changes-before' not in search_opts:
  220. # No 'changes-since' or 'changes-before', so we only
  221. # want non-deleted servers
  222. search_opts['deleted'] = False
  223. else:
  224. # Convert deleted filter value to a valid boolean.
  225. # Return non-deleted servers if an invalid value
  226. # is passed with deleted filter.
  227. search_opts['deleted'] = strutils.bool_from_string(
  228. search_opts['deleted'], default=False)
  229. if search_opts.get("vm_state") == ['deleted']:
  230. if context.is_admin:
  231. search_opts['deleted'] = True
  232. else:
  233. msg = _("Only administrators may list deleted instances")
  234. raise exc.HTTPForbidden(explanation=msg)
  235. if api_version_request.is_supported(req, min_version='2.26'):
  236. for tag_filter in TAG_SEARCH_FILTERS:
  237. if tag_filter in search_opts:
  238. search_opts[tag_filter] = search_opts[
  239. tag_filter].split(',')
  240. all_tenants = common.is_all_tenants(search_opts)
  241. # use the boolean from here on out so remove the entry from search_opts
  242. # if it's present.
  243. # NOTE(tssurya): In case we support handling down cells
  244. # we need to know further down the stack whether the 'all_tenants'
  245. # filter was passed with the true value or not, so we pass the flag
  246. # further down the stack.
  247. search_opts.pop('all_tenants', None)
  248. if 'locked' in search_opts:
  249. search_opts['locked'] = common.is_locked(search_opts)
  250. elevated = None
  251. if all_tenants:
  252. if is_detail:
  253. context.can(server_policies.SERVERS % 'detail:get_all_tenants')
  254. else:
  255. context.can(server_policies.SERVERS % 'index:get_all_tenants')
  256. elevated = context.elevated()
  257. else:
  258. # As explained in lp:#1185290, if `all_tenants` is not passed
  259. # we must ignore the `tenant_id` search option.
  260. search_opts.pop('tenant_id', None)
  261. if context.project_id:
  262. search_opts['project_id'] = context.project_id
  263. else:
  264. search_opts['user_id'] = context.user_id
  265. limit, marker = common.get_limit_and_marker(req)
  266. sort_keys, sort_dirs = common.get_sort_params(req.params)
  267. blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY
  268. if api_version_request.is_supported(req, min_version='2.73'):
  269. blacklist = schema_servers.SERVER_LIST_IGNORE_SORT_KEY_V273
  270. sort_keys, sort_dirs = remove_invalid_sort_keys(
  271. context, sort_keys, sort_dirs, blacklist, ('host', 'node'))
  272. expected_attrs = []
  273. if is_detail:
  274. if api_version_request.is_supported(req, '2.16'):
  275. expected_attrs.append('services')
  276. if api_version_request.is_supported(req, '2.26'):
  277. expected_attrs.append("tags")
  278. if api_version_request.is_supported(req, '2.63'):
  279. expected_attrs.append("trusted_certs")
  280. if api_version_request.is_supported(req, '2.73'):
  281. expected_attrs.append("system_metadata")
  282. # merge our expected attrs with what the view builder needs for
  283. # showing details
  284. expected_attrs = self._view_builder.get_show_expected_attrs(
  285. expected_attrs)
  286. try:
  287. instance_list = self.compute_api.get_all(elevated or context,
  288. search_opts=search_opts, limit=limit, marker=marker,
  289. expected_attrs=expected_attrs, sort_keys=sort_keys,
  290. sort_dirs=sort_dirs, cell_down_support=cell_down_support,
  291. all_tenants=all_tenants)
  292. except exception.MarkerNotFound:
  293. msg = _('marker [%s] not found') % marker
  294. raise exc.HTTPBadRequest(explanation=msg)
  295. except exception.FlavorNotFound:
  296. LOG.debug("Flavor '%s' could not be found ",
  297. search_opts['flavor'])
  298. instance_list = objects.InstanceList()
  299. if is_detail:
  300. instance_list._context = context
  301. instance_list.fill_faults()
  302. response = self._view_builder.detail(
  303. req, instance_list, cell_down_support=cell_down_support)
  304. else:
  305. response = self._view_builder.index(
  306. req, instance_list, cell_down_support=cell_down_support)
  307. return response
  308. def _get_server(self, context, req, instance_uuid, is_detail=False,
  309. cell_down_support=False):
  310. """Utility function for looking up an instance by uuid.
  311. :param context: request context for auth
  312. :param req: HTTP request.
  313. :param instance_uuid: UUID of the server instance to get
  314. :param is_detail: True if you plan on showing the details of the
  315. instance in the response, False otherwise.
  316. :param cell_down_support: True if the API (and caller) support
  317. returning a minimal instance
  318. construct if the relevant cell is
  319. down.
  320. """
  321. expected_attrs = ['flavor', 'numa_topology']
  322. if is_detail:
  323. if api_version_request.is_supported(req, '2.26'):
  324. expected_attrs.append("tags")
  325. if api_version_request.is_supported(req, '2.63'):
  326. expected_attrs.append("trusted_certs")
  327. expected_attrs = self._view_builder.get_show_expected_attrs(
  328. expected_attrs)
  329. instance = common.get_instance(self.compute_api, context,
  330. instance_uuid,
  331. expected_attrs=expected_attrs,
  332. cell_down_support=cell_down_support)
  333. return instance
  334. @staticmethod
  335. def _validate_network_id(net_id, network_uuids):
  336. """Validates that a requested network id.
  337. This method performs two checks:
  338. 1. That the network id is in the proper uuid format.
  339. 2. That the network is not a duplicate when using nova-network.
  340. :param net_id: The network id to validate.
  341. :param network_uuids: A running list of requested network IDs that have
  342. passed validation already.
  343. :raises: webob.exc.HTTPBadRequest if validation fails
  344. """
  345. if not uuidutils.is_uuid_like(net_id):
  346. msg = _("Bad networks format: network uuid is "
  347. "not in proper format (%s)") % net_id
  348. raise exc.HTTPBadRequest(explanation=msg)
  349. # duplicate networks are allowed only for neutron v2.0
  350. if net_id in network_uuids and not utils.is_neutron():
  351. expl = _("Duplicate networks (%s) are not allowed") % net_id
  352. raise exc.HTTPBadRequest(explanation=expl)
  353. def _get_requested_networks(self, requested_networks):
  354. """Create a list of requested networks from the networks attribute."""
  355. # Starting in the 2.37 microversion, requested_networks is either a
  356. # list or a string enum with value 'auto' or 'none'. The auto/none
  357. # values are verified via jsonschema so we don't check them again here.
  358. if isinstance(requested_networks, six.string_types):
  359. return objects.NetworkRequestList(
  360. objects=[objects.NetworkRequest(
  361. network_id=requested_networks)])
  362. networks = []
  363. network_uuids = []
  364. for network in requested_networks:
  365. request = objects.NetworkRequest()
  366. try:
  367. # fixed IP address is optional
  368. # if the fixed IP address is not provided then
  369. # it will use one of the available IP address from the network
  370. request.address = network.get('fixed_ip', None)
  371. request.port_id = network.get('port', None)
  372. request.tag = network.get('tag', None)
  373. if request.port_id:
  374. request.network_id = None
  375. if not utils.is_neutron():
  376. # port parameter is only for neutron v2.0
  377. msg = _("Unknown argument: port")
  378. raise exc.HTTPBadRequest(explanation=msg)
  379. if request.address is not None:
  380. msg = _("Specified Fixed IP '%(addr)s' cannot be used "
  381. "with port '%(port)s': the two cannot be "
  382. "specified together.") % {
  383. "addr": request.address,
  384. "port": request.port_id}
  385. raise exc.HTTPBadRequest(explanation=msg)
  386. else:
  387. request.network_id = network['uuid']
  388. self._validate_network_id(
  389. request.network_id, network_uuids)
  390. network_uuids.append(request.network_id)
  391. networks.append(request)
  392. except KeyError as key:
  393. expl = _('Bad network format: missing %s') % key
  394. raise exc.HTTPBadRequest(explanation=expl)
  395. except TypeError:
  396. expl = _('Bad networks format')
  397. raise exc.HTTPBadRequest(explanation=expl)
  398. return objects.NetworkRequestList(objects=networks)
  399. @wsgi.expected_errors(404)
  400. def show(self, req, id):
  401. """Returns server details by server id."""
  402. context = req.environ['nova.context']
  403. context.can(server_policies.SERVERS % 'show')
  404. cell_down_support = api_version_request.is_supported(
  405. req, min_version=PARTIAL_CONSTRUCT_FOR_CELL_DOWN_MIN_VERSION)
  406. show_server_groups = api_version_request.is_supported(
  407. req, min_version='2.71')
  408. instance = self._get_server(
  409. context, req, id, is_detail=True,
  410. cell_down_support=cell_down_support)
  411. return self._view_builder.show(
  412. req, instance, cell_down_support=cell_down_support,
  413. show_server_groups=show_server_groups)
  414. @staticmethod
  415. def _process_bdms_for_create(
  416. context, target, server_dict, create_kwargs):
  417. """Processes block_device_mapping(_v2) req parameters for server create
  418. :param context: The nova auth request context
  419. :param target: The target dict for ``context.can`` policy checks
  420. :param server_dict: The POST /servers request body "server" entry
  421. :param create_kwargs: dict that gets populated by this method and
  422. passed to nova.comptue.api.API.create()
  423. :raises: webob.exc.HTTPBadRequest if the request parameters are invalid
  424. :raises: nova.exception.Forbidden if a policy check fails
  425. """
  426. block_device_mapping_legacy = server_dict.get('block_device_mapping',
  427. [])
  428. block_device_mapping_v2 = server_dict.get('block_device_mapping_v2',
  429. [])
  430. if block_device_mapping_legacy and block_device_mapping_v2:
  431. expl = _('Using different block_device_mapping syntaxes '
  432. 'is not allowed in the same request.')
  433. raise exc.HTTPBadRequest(explanation=expl)
  434. if block_device_mapping_legacy:
  435. for bdm in block_device_mapping_legacy:
  436. if 'delete_on_termination' in bdm:
  437. bdm['delete_on_termination'] = strutils.bool_from_string(
  438. bdm['delete_on_termination'])
  439. create_kwargs[
  440. 'block_device_mapping'] = block_device_mapping_legacy
  441. # Sets the legacy_bdm flag if we got a legacy block device mapping.
  442. create_kwargs['legacy_bdm'] = True
  443. elif block_device_mapping_v2:
  444. # Have to check whether --image is given, see bug 1433609
  445. image_href = server_dict.get('imageRef')
  446. image_uuid_specified = image_href is not None
  447. try:
  448. block_device_mapping = [
  449. block_device.BlockDeviceDict.from_api(bdm_dict,
  450. image_uuid_specified)
  451. for bdm_dict in block_device_mapping_v2]
  452. except exception.InvalidBDMFormat as e:
  453. raise exc.HTTPBadRequest(explanation=e.format_message())
  454. create_kwargs['block_device_mapping'] = block_device_mapping
  455. # Unset the legacy_bdm flag if we got a block device mapping.
  456. create_kwargs['legacy_bdm'] = False
  457. block_device_mapping = create_kwargs.get("block_device_mapping")
  458. if block_device_mapping:
  459. context.can(server_policies.SERVERS % 'create:attach_volume',
  460. target)
  461. def _process_networks_for_create(
  462. self, context, target, server_dict, create_kwargs):
  463. """Processes networks request parameter for server create
  464. :param context: The nova auth request context
  465. :param target: The target dict for ``context.can`` policy checks
  466. :param server_dict: The POST /servers request body "server" entry
  467. :param create_kwargs: dict that gets populated by this method and
  468. passed to nova.comptue.api.API.create()
  469. :raises: webob.exc.HTTPBadRequest if the request parameters are invalid
  470. :raises: nova.exception.Forbidden if a policy check fails
  471. """
  472. requested_networks = server_dict.get('networks', None)
  473. if requested_networks is not None:
  474. requested_networks = self._get_requested_networks(
  475. requested_networks)
  476. # Skip policy check for 'create:attach_network' if there is no
  477. # network allocation request.
  478. if requested_networks and len(requested_networks) and \
  479. not requested_networks.no_allocate:
  480. context.can(server_policies.SERVERS % 'create:attach_network',
  481. target)
  482. create_kwargs['requested_networks'] = requested_networks
  483. @staticmethod
  484. def _process_hosts_for_create(
  485. context, target, server_dict, create_kwargs, host, node):
  486. """Processes hosts request parameter for server create
  487. :param context: The nova auth request context
  488. :param target: The target dict for ``context.can`` policy checks
  489. :param server_dict: The POST /servers request body "server" entry
  490. :param create_kwargs: dict that gets populated by this method and
  491. passed to nova.comptue.api.API.create()
  492. :param host: Forced host of availability_zone
  493. :param node: Forced node of availability_zone
  494. :raise: webob.exc.HTTPBadRequest if the request parameters are invalid
  495. :raise: nova.exception.Forbidden if a policy check fails
  496. """
  497. requested_host = server_dict.get('host')
  498. requested_hypervisor_hostname = server_dict.get('hypervisor_hostname')
  499. if requested_host or requested_hypervisor_hostname:
  500. # If the policy check fails, this will raise Forbidden exception.
  501. context.can(server_policies.REQUESTED_DESTINATION, target=target)
  502. if host or node:
  503. msg = _("One mechanism with host and/or "
  504. "hypervisor_hostname and another mechanism "
  505. "with zone:host:node are mutually exclusive.")
  506. raise exc.HTTPBadRequest(explanation=msg)
  507. create_kwargs['requested_host'] = requested_host
  508. create_kwargs['requested_hypervisor_hostname'] = (
  509. requested_hypervisor_hostname)
  510. @wsgi.response(202)
  511. @wsgi.expected_errors((400, 403, 409))
  512. @validation.schema(schema_servers.base_create_v20, '2.0', '2.0')
  513. @validation.schema(schema_servers.base_create, '2.1', '2.18')
  514. @validation.schema(schema_servers.base_create_v219, '2.19', '2.31')
  515. @validation.schema(schema_servers.base_create_v232, '2.32', '2.32')
  516. @validation.schema(schema_servers.base_create_v233, '2.33', '2.36')
  517. @validation.schema(schema_servers.base_create_v237, '2.37', '2.41')
  518. @validation.schema(schema_servers.base_create_v242, '2.42', '2.51')
  519. @validation.schema(schema_servers.base_create_v252, '2.52', '2.56')
  520. @validation.schema(schema_servers.base_create_v257, '2.57', '2.62')
  521. @validation.schema(schema_servers.base_create_v263, '2.63', '2.66')
  522. @validation.schema(schema_servers.base_create_v267, '2.67', '2.73')
  523. @validation.schema(schema_servers.base_create_v274, '2.74')
  524. def create(self, req, body):
  525. """Creates a new server for a given user."""
  526. context = req.environ['nova.context']
  527. server_dict = body['server']
  528. password = self._get_server_admin_password(server_dict)
  529. name = common.normalize_name(server_dict['name'])
  530. description = name
  531. if api_version_request.is_supported(req, min_version='2.19'):
  532. description = server_dict.get('description')
  533. # Arguments to be passed to instance create function
  534. create_kwargs = {}
  535. create_kwargs['user_data'] = server_dict.get('user_data')
  536. # NOTE(alex_xu): The v2.1 API compat mode, we strip the spaces for
  537. # keypair create. But we didn't strip spaces at here for
  538. # backward-compatible some users already created keypair and name with
  539. # leading/trailing spaces by legacy v2 API.
  540. create_kwargs['key_name'] = server_dict.get('key_name')
  541. create_kwargs['config_drive'] = server_dict.get('config_drive')
  542. security_groups = server_dict.get('security_groups')
  543. if security_groups is not None:
  544. create_kwargs['security_groups'] = [
  545. sg['name'] for sg in security_groups if sg.get('name')]
  546. create_kwargs['security_groups'] = list(
  547. set(create_kwargs['security_groups']))
  548. scheduler_hints = {}
  549. if 'os:scheduler_hints' in body:
  550. scheduler_hints = body['os:scheduler_hints']
  551. elif 'OS-SCH-HNT:scheduler_hints' in body:
  552. scheduler_hints = body['OS-SCH-HNT:scheduler_hints']
  553. create_kwargs['scheduler_hints'] = scheduler_hints
  554. # min_count and max_count are optional. If they exist, they may come
  555. # in as strings. Verify that they are valid integers and > 0.
  556. # Also, we want to default 'min_count' to 1, and default
  557. # 'max_count' to be 'min_count'.
  558. min_count = int(server_dict.get('min_count', 1))
  559. max_count = int(server_dict.get('max_count', min_count))
  560. if min_count > max_count:
  561. msg = _('min_count must be <= max_count')
  562. raise exc.HTTPBadRequest(explanation=msg)
  563. create_kwargs['min_count'] = min_count
  564. create_kwargs['max_count'] = max_count
  565. availability_zone = server_dict.pop("availability_zone", None)
  566. if api_version_request.is_supported(req, min_version='2.52'):
  567. create_kwargs['tags'] = server_dict.get('tags')
  568. helpers.translate_attributes(helpers.CREATE,
  569. server_dict, create_kwargs)
  570. target = {
  571. 'project_id': context.project_id,
  572. 'user_id': context.user_id,
  573. 'availability_zone': availability_zone}
  574. context.can(server_policies.SERVERS % 'create', target)
  575. # Skip policy check for 'create:trusted_certs' if no trusted
  576. # certificate IDs were provided.
  577. trusted_certs = server_dict.get('trusted_image_certificates', None)
  578. if trusted_certs:
  579. create_kwargs['trusted_certs'] = trusted_certs
  580. context.can(server_policies.SERVERS % 'create:trusted_certs',
  581. target=target)
  582. parse_az = self.compute_api.parse_availability_zone
  583. try:
  584. availability_zone, host, node = parse_az(context,
  585. availability_zone)
  586. except exception.InvalidInput as err:
  587. raise exc.HTTPBadRequest(explanation=six.text_type(err))
  588. if host or node:
  589. context.can(server_policies.SERVERS % 'create:forced_host', {})
  590. if api_version_request.is_supported(req, min_version='2.74'):
  591. self._process_hosts_for_create(context, target, server_dict,
  592. create_kwargs, host, node)
  593. self._process_bdms_for_create(
  594. context, target, server_dict, create_kwargs)
  595. image_uuid = self._image_from_req_data(server_dict, create_kwargs)
  596. self._process_networks_for_create(
  597. context, target, server_dict, create_kwargs)
  598. flavor_id = self._flavor_id_from_req_data(body)
  599. try:
  600. inst_type = flavors.get_flavor_by_flavor_id(
  601. flavor_id, ctxt=context, read_deleted="no")
  602. supports_multiattach = common.supports_multiattach_volume(req)
  603. supports_port_resource_request = \
  604. common.supports_port_resource_request(req)
  605. (instances, resv_id) = self.compute_api.create(context,
  606. inst_type,
  607. image_uuid,
  608. display_name=name,
  609. display_description=description,
  610. availability_zone=availability_zone,
  611. forced_host=host, forced_node=node,
  612. metadata=server_dict.get('metadata', {}),
  613. admin_password=password,
  614. check_server_group_quota=True,
  615. supports_multiattach=supports_multiattach,
  616. supports_port_resource_request=supports_port_resource_request,
  617. **create_kwargs)
  618. except (exception.QuotaError,
  619. exception.PortLimitExceeded) as error:
  620. raise exc.HTTPForbidden(
  621. explanation=error.format_message())
  622. except exception.ImageNotFound:
  623. msg = _("Can not find requested image")
  624. raise exc.HTTPBadRequest(explanation=msg)
  625. except exception.KeypairNotFound:
  626. msg = _("Invalid key_name provided.")
  627. raise exc.HTTPBadRequest(explanation=msg)
  628. except exception.ConfigDriveInvalidValue:
  629. msg = _("Invalid config_drive provided.")
  630. raise exc.HTTPBadRequest(explanation=msg)
  631. except (exception.BootFromVolumeRequiredForZeroDiskFlavor,
  632. exception.ExternalNetworkAttachForbidden) as error:
  633. raise exc.HTTPForbidden(explanation=error.format_message())
  634. except messaging.RemoteError as err:
  635. msg = "%(err_type)s: %(err_msg)s" % {'err_type': err.exc_type,
  636. 'err_msg': err.value}
  637. raise exc.HTTPBadRequest(explanation=msg)
  638. except UnicodeDecodeError as error:
  639. msg = "UnicodeError: %s" % error
  640. raise exc.HTTPBadRequest(explanation=msg)
  641. except (exception.ImageNotActive,
  642. exception.ImageBadRequest,
  643. exception.ImageNotAuthorized,
  644. exception.FixedIpNotFoundForAddress,
  645. exception.FlavorNotFound,
  646. exception.FlavorDiskTooSmall,
  647. exception.FlavorMemoryTooSmall,
  648. exception.InvalidMetadata,
  649. exception.InvalidVolume,
  650. exception.MismatchVolumeAZException,
  651. exception.MultiplePortsNotApplicable,
  652. exception.InvalidFixedIpAndMaxCountRequest,
  653. exception.InstanceUserDataMalformed,
  654. exception.PortNotFound,
  655. exception.FixedIpAlreadyInUse,
  656. exception.SecurityGroupNotFound,
  657. exception.PortRequiresFixedIP,
  658. exception.NetworkRequiresSubnet,
  659. exception.NetworkNotFound,
  660. exception.InvalidBDM,
  661. exception.InvalidBDMSnapshot,
  662. exception.InvalidBDMVolume,
  663. exception.InvalidBDMImage,
  664. exception.InvalidBDMBootSequence,
  665. exception.InvalidBDMLocalsLimit,
  666. exception.InvalidBDMVolumeNotBootable,
  667. exception.InvalidBDMEphemeralSize,
  668. exception.InvalidBDMFormat,
  669. exception.InvalidBDMSwapSize,
  670. exception.VolumeTypeNotFound,
  671. exception.AutoDiskConfigDisabledByImage,
  672. exception.InstanceGroupNotFound,
  673. exception.SnapshotNotFound,
  674. exception.UnableToAutoAllocateNetwork,
  675. exception.MultiattachNotSupportedOldMicroversion,
  676. exception.CertificateValidationFailed,
  677. exception.CreateWithPortResourceRequestOldVersion,
  678. exception.ComputeHostNotFound) as error:
  679. raise exc.HTTPBadRequest(explanation=error.format_message())
  680. except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error:
  681. raise exc.HTTPBadRequest(explanation=error.format_message())
  682. except (exception.PortInUse,
  683. exception.InstanceExists,
  684. exception.NetworkAmbiguous,
  685. exception.NoUniqueMatch) as error:
  686. raise exc.HTTPConflict(explanation=error.format_message())
  687. # If the caller wanted a reservation_id, return it
  688. if server_dict.get('return_reservation_id', False):
  689. return wsgi.ResponseObject({'reservation_id': resv_id})
  690. server = self._view_builder.create(req, instances[0])
  691. if CONF.api.enable_instance_password:
  692. server['server']['adminPass'] = password
  693. robj = wsgi.ResponseObject(server)
  694. return self._add_location(robj)
  695. def _delete(self, context, req, instance_uuid):
  696. instance = self._get_server(context, req, instance_uuid)
  697. context.can(server_policies.SERVERS % 'delete',
  698. target={'user_id': instance.user_id,
  699. 'project_id': instance.project_id})
  700. if CONF.reclaim_instance_interval:
  701. try:
  702. self.compute_api.soft_delete(context, instance)
  703. except exception.InstanceInvalidState:
  704. # Note(yufang521247): instance which has never been active
  705. # is not allowed to be soft_deleted. Thus we have to call
  706. # delete() to clean up the instance.
  707. self.compute_api.delete(context, instance)
  708. else:
  709. self.compute_api.delete(context, instance)
  710. @wsgi.expected_errors(404)
  711. @validation.schema(schema_servers.base_update_v20, '2.0', '2.0')
  712. @validation.schema(schema_servers.base_update, '2.1', '2.18')
  713. @validation.schema(schema_servers.base_update_v219, '2.19')
  714. def update(self, req, id, body):
  715. """Update server then pass on to version-specific controller."""
  716. ctxt = req.environ['nova.context']
  717. update_dict = {}
  718. instance = self._get_server(ctxt, req, id, is_detail=True)
  719. ctxt.can(server_policies.SERVERS % 'update',
  720. target={'user_id': instance.user_id,
  721. 'project_id': instance.project_id})
  722. show_server_groups = api_version_request.is_supported(
  723. req, min_version='2.71')
  724. server = body['server']
  725. if 'name' in server:
  726. update_dict['display_name'] = common.normalize_name(
  727. server['name'])
  728. if 'description' in server:
  729. # This is allowed to be None (remove description)
  730. update_dict['display_description'] = server['description']
  731. helpers.translate_attributes(helpers.UPDATE, server, update_dict)
  732. try:
  733. instance = self.compute_api.update_instance(ctxt, instance,
  734. update_dict)
  735. # NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild
  736. # API response will show all attributes like GET /servers API.
  737. show_all_attributes = api_version_request.is_supported(
  738. req, min_version='2.75')
  739. extend_address = show_all_attributes
  740. show_AZ = show_all_attributes
  741. show_config_drive = show_all_attributes
  742. show_keypair = show_all_attributes
  743. show_srv_usg = show_all_attributes
  744. show_sec_grp = show_all_attributes
  745. show_extended_status = show_all_attributes
  746. show_extended_volumes = show_all_attributes
  747. # NOTE(gmann): Below attributes need to be added in response
  748. # if respective policy allows.So setting these as None
  749. # to perform the policy check in view builder.
  750. show_extended_attr = None if show_all_attributes else False
  751. show_host_status = None if show_all_attributes else False
  752. return self._view_builder.show(
  753. req, instance,
  754. extend_address=extend_address,
  755. show_AZ=show_AZ,
  756. show_config_drive=show_config_drive,
  757. show_extended_attr=show_extended_attr,
  758. show_host_status=show_host_status,
  759. show_keypair=show_keypair,
  760. show_srv_usg=show_srv_usg,
  761. show_sec_grp=show_sec_grp,
  762. show_extended_status=show_extended_status,
  763. show_extended_volumes=show_extended_volumes,
  764. show_server_groups=show_server_groups)
  765. except exception.InstanceNotFound:
  766. msg = _("Instance could not be found")
  767. raise exc.HTTPNotFound(explanation=msg)
  768. # NOTE(gmann): Returns 204 for backwards compatibility but should be 202
  769. # for representing async API as this API just accepts the request and
  770. # request hypervisor driver to complete the same in async mode.
  771. @wsgi.response(204)
  772. @wsgi.expected_errors((400, 404, 409))
  773. @wsgi.action('confirmResize')
  774. def _action_confirm_resize(self, req, id, body):
  775. context = req.environ['nova.context']
  776. context.can(server_policies.SERVERS % 'confirm_resize')
  777. instance = self._get_server(context, req, id)
  778. try:
  779. self.compute_api.confirm_resize(context, instance)
  780. except exception.MigrationNotFound:
  781. msg = _("Instance has not been resized.")
  782. raise exc.HTTPBadRequest(explanation=msg)
  783. except exception.InstanceIsLocked as e:
  784. raise exc.HTTPConflict(explanation=e.format_message())
  785. except exception.InstanceInvalidState as state_error:
  786. common.raise_http_conflict_for_instance_invalid_state(state_error,
  787. 'confirmResize', id)
  788. @wsgi.response(202)
  789. @wsgi.expected_errors((400, 404, 409))
  790. @wsgi.action('revertResize')
  791. def _action_revert_resize(self, req, id, body):
  792. context = req.environ['nova.context']
  793. context.can(server_policies.SERVERS % 'revert_resize')
  794. instance = self._get_server(context, req, id)
  795. try:
  796. self.compute_api.revert_resize(context, instance)
  797. except exception.MigrationNotFound:
  798. msg = _("Instance has not been resized.")
  799. raise exc.HTTPBadRequest(explanation=msg)
  800. except exception.FlavorNotFound:
  801. msg = _("Flavor used by the instance could not be found.")
  802. raise exc.HTTPBadRequest(explanation=msg)
  803. except exception.InstanceIsLocked as e:
  804. raise exc.HTTPConflict(explanation=e.format_message())
  805. except exception.InstanceInvalidState as state_error:
  806. common.raise_http_conflict_for_instance_invalid_state(state_error,
  807. 'revertResize', id)
  808. @wsgi.response(202)
  809. @wsgi.expected_errors((404, 409))
  810. @wsgi.action('reboot')
  811. @validation.schema(schema_servers.reboot)
  812. def _action_reboot(self, req, id, body):
  813. reboot_type = body['reboot']['type'].upper()
  814. context = req.environ['nova.context']
  815. context.can(server_policies.SERVERS % 'reboot')
  816. instance = self._get_server(context, req, id)
  817. try:
  818. self.compute_api.reboot(context, instance, reboot_type)
  819. except exception.InstanceIsLocked as e:
  820. raise exc.HTTPConflict(explanation=e.format_message())
  821. except exception.InstanceInvalidState as state_error:
  822. common.raise_http_conflict_for_instance_invalid_state(state_error,
  823. 'reboot', id)
  824. def _resize(self, req, instance_id, flavor_id, auto_disk_config=None):
  825. """Begin the resize process with given instance/flavor."""
  826. context = req.environ["nova.context"]
  827. instance = self._get_server(context, req, instance_id)
  828. context.can(server_policies.SERVERS % 'resize',
  829. target={'user_id': instance.user_id,
  830. 'project_id': instance.project_id})
  831. if common.instance_has_port_with_resource_request(
  832. instance_id, self.network_api):
  833. # TODO(gibi): Remove when nova only supports compute newer than
  834. # Train
  835. source_service = objects.Service.get_by_host_and_binary(
  836. context, instance.host, 'nova-compute')
  837. if source_service.version < MIN_COMPUTE_MOVE_BANDWIDTH:
  838. msg = _("The resize action on a server with ports having "
  839. "resource requests, like a port with a QoS "
  840. "minimum bandwidth policy, is not yet supported.")
  841. raise exc.HTTPConflict(explanation=msg)
  842. try:
  843. self.compute_api.resize(context, instance, flavor_id,
  844. auto_disk_config=auto_disk_config)
  845. except exception.QuotaError as error:
  846. raise exc.HTTPForbidden(
  847. explanation=error.format_message())
  848. except exception.InstanceIsLocked as e:
  849. raise exc.HTTPConflict(explanation=e.format_message())
  850. except exception.InstanceInvalidState as state_error:
  851. common.raise_http_conflict_for_instance_invalid_state(state_error,
  852. 'resize', instance_id)
  853. except exception.ImageNotAuthorized:
  854. msg = _("You are not authorized to access the image "
  855. "the instance was started with.")
  856. raise exc.HTTPUnauthorized(explanation=msg)
  857. except exception.ImageNotFound:
  858. msg = _("Image that the instance was started "
  859. "with could not be found.")
  860. raise exc.HTTPBadRequest(explanation=msg)
  861. except (exception.AutoDiskConfigDisabledByImage,
  862. exception.CannotResizeDisk,
  863. exception.CannotResizeToSameFlavor,
  864. exception.FlavorNotFound) as e:
  865. raise exc.HTTPBadRequest(explanation=e.format_message())
  866. except INVALID_FLAVOR_IMAGE_EXCEPTIONS as e:
  867. raise exc.HTTPBadRequest(explanation=e.format_message())
  868. except exception.Invalid:
  869. msg = _("Invalid instance image.")
  870. raise exc.HTTPBadRequest(explanation=msg)
  871. @wsgi.response(204)
  872. @wsgi.expected_errors((404, 409))
  873. def delete(self, req, id):
  874. """Destroys a server."""
  875. try:
  876. self._delete(req.environ['nova.context'], req, id)
  877. except exception.InstanceNotFound:
  878. msg = _("Instance could not be found")
  879. raise exc.HTTPNotFound(explanation=msg)
  880. except (exception.InstanceIsLocked,
  881. exception.AllocationDeleteFailed) as e:
  882. raise exc.HTTPConflict(explanation=e.format_message())
  883. except exception.InstanceInvalidState as state_error:
  884. common.raise_http_conflict_for_instance_invalid_state(state_error,
  885. 'delete', id)
  886. def _image_from_req_data(self, server_dict, create_kwargs):
  887. """Get image data from the request or raise appropriate
  888. exceptions.
  889. The field imageRef is mandatory when no block devices have been
  890. defined and must be a proper uuid when present.
  891. """
  892. image_href = server_dict.get('imageRef')
  893. if not image_href and create_kwargs.get('block_device_mapping'):
  894. return ''
  895. elif image_href:
  896. return image_href
  897. else:
  898. msg = _("Missing imageRef attribute")
  899. raise exc.HTTPBadRequest(explanation=msg)
  900. def _flavor_id_from_req_data(self, data):
  901. flavor_ref = data['server']['flavorRef']
  902. return common.get_id_from_href(flavor_ref)
  903. @wsgi.response(202)
  904. @wsgi.expected_errors((400, 401, 403, 404, 409))
  905. @wsgi.action('resize')
  906. @validation.schema(schema_servers.resize)
  907. def _action_resize(self, req, id, body):
  908. """Resizes a given instance to the flavor size requested."""
  909. resize_dict = body['resize']
  910. flavor_ref = str(resize_dict["flavorRef"])
  911. kwargs = {}
  912. helpers.translate_attributes(helpers.RESIZE, resize_dict, kwargs)
  913. self._resize(req, id, flavor_ref, **kwargs)
  914. @wsgi.response(202)
  915. @wsgi.expected_errors((400, 403, 404, 409))
  916. @wsgi.action('rebuild')
  917. @validation.schema(schema_servers.base_rebuild_v20, '2.0', '2.0')
  918. @validation.schema(schema_servers.base_rebuild, '2.1', '2.18')
  919. @validation.schema(schema_servers.base_rebuild_v219, '2.19', '2.53')
  920. @validation.schema(schema_servers.base_rebuild_v254, '2.54', '2.56')
  921. @validation.schema(schema_servers.base_rebuild_v257, '2.57', '2.62')
  922. @validation.schema(schema_servers.base_rebuild_v263, '2.63')
  923. def _action_rebuild(self, req, id, body):
  924. """Rebuild an instance with the given attributes."""
  925. rebuild_dict = body['rebuild']
  926. image_href = rebuild_dict["imageRef"]
  927. password = self._get_server_admin_password(rebuild_dict)
  928. context = req.environ['nova.context']
  929. instance = self._get_server(context, req, id)
  930. target = {'user_id': instance.user_id,
  931. 'project_id': instance.project_id}
  932. context.can(server_policies.SERVERS % 'rebuild', target=target)
  933. attr_map = {
  934. 'name': 'display_name',
  935. 'description': 'display_description',
  936. 'metadata': 'metadata',
  937. }
  938. kwargs = {}
  939. helpers.translate_attributes(helpers.REBUILD, rebuild_dict, kwargs)
  940. if (api_version_request.is_supported(req, min_version='2.54') and
  941. 'key_name' in rebuild_dict):
  942. kwargs['key_name'] = rebuild_dict.get('key_name')
  943. # If user_data is not specified, we don't include it in kwargs because
  944. # we don't want to overwrite the existing user_data.
  945. include_user_data = api_version_request.is_supported(
  946. req, min_version='2.57')
  947. if include_user_data and 'user_data' in rebuild_dict:
  948. kwargs['user_data'] = rebuild_dict['user_data']
  949. # Skip policy check for 'rebuild:trusted_certs' if no trusted
  950. # certificate IDs were provided.
  951. if ((api_version_request.is_supported(req, min_version='2.63')) and
  952. # Note that this is different from server create since with
  953. # rebuild a user can unset/reset the trusted certs by
  954. # specifying trusted_image_certificates=None, similar to
  955. # key_name.
  956. ('trusted_image_certificates' in rebuild_dict)):
  957. kwargs['trusted_certs'] = rebuild_dict.get(
  958. 'trusted_image_certificates')
  959. context.can(server_policies.SERVERS % 'rebuild:trusted_certs',
  960. target=target)
  961. for request_attribute, instance_attribute in attr_map.items():
  962. try:
  963. if request_attribute == 'name':
  964. kwargs[instance_attribute] = common.normalize_name(
  965. rebuild_dict[request_attribute])
  966. else:
  967. kwargs[instance_attribute] = rebuild_dict[
  968. request_attribute]
  969. except (KeyError, TypeError):
  970. pass
  971. try:
  972. self.compute_api.rebuild(context,
  973. instance,
  974. image_href,
  975. password,
  976. **kwargs)
  977. except exception.InstanceIsLocked as e:
  978. raise exc.HTTPConflict(explanation=e.format_message())
  979. except exception.InstanceInvalidState as state_error:
  980. common.raise_http_conflict_for_instance_invalid_state(state_error,
  981. 'rebuild', id)
  982. except exception.InstanceNotFound:
  983. msg = _("Instance could not be found")
  984. raise exc.HTTPNotFound(explanation=msg)
  985. except exception.ImageNotFound:
  986. msg = _("Cannot find image for rebuild")
  987. raise exc.HTTPBadRequest(explanation=msg)
  988. except exception.KeypairNotFound:
  989. msg = _("Invalid key_name provided.")
  990. raise exc.HTTPBadRequest(explanation=msg)
  991. except exception.QuotaError as error:
  992. raise exc.HTTPForbidden(explanation=error.format_message())
  993. except (exception.AutoDiskConfigDisabledByImage,
  994. exception.CertificateValidationFailed,
  995. exception.FlavorDiskTooSmall,
  996. exception.FlavorMemoryTooSmall,
  997. exception.ImageNotActive,
  998. exception.ImageUnacceptable,
  999. exception.InvalidMetadata,
  1000. ) as error:
  1001. raise exc.HTTPBadRequest(explanation=error.format_message())
  1002. except INVALID_FLAVOR_IMAGE_EXCEPTIONS as error:
  1003. raise exc.HTTPBadRequest(explanation=error.format_message())
  1004. instance = self._get_server(context, req, id, is_detail=True)
  1005. # NOTE(liuyulong): set the new key_name for the API response.
  1006. # from microversion 2.54 onwards.
  1007. show_keypair = api_version_request.is_supported(
  1008. req, min_version='2.54')
  1009. show_server_groups = api_version_request.is_supported(
  1010. req, min_version='2.71')
  1011. # NOTE(gmann): Starting from microversion 2.75, PUT and Rebuild
  1012. # API response will show all attributes like GET /servers API.
  1013. show_all_attributes = api_version_request.is_supported(
  1014. req, min_version='2.75')
  1015. extend_address = show_all_attributes
  1016. show_AZ = show_all_attributes
  1017. show_config_drive = show_all_attributes
  1018. show_srv_usg = show_all_attributes
  1019. show_sec_grp = show_all_attributes
  1020. show_extended_status = show_all_attributes
  1021. show_extended_volumes = show_all_attributes
  1022. # NOTE(gmann): Below attributes need to be added in response
  1023. # if respective policy allows.So setting these as None
  1024. # to perform the policy check in view builder.
  1025. show_extended_attr = None if show_all_attributes else False
  1026. show_host_status = None if show_all_attributes else False
  1027. view = self._view_builder.show(
  1028. req, instance,
  1029. extend_address=extend_address,
  1030. show_AZ=show_AZ,
  1031. show_config_drive=show_config_drive,
  1032. show_extended_attr=show_extended_attr,
  1033. show_host_status=show_host_status,
  1034. show_keypair=show_keypair,
  1035. show_srv_usg=show_srv_usg,
  1036. show_sec_grp=show_sec_grp,
  1037. show_extended_status=show_extended_status,
  1038. show_extended_volumes=show_extended_volumes,
  1039. show_server_groups=show_server_groups,
  1040. # NOTE(gmann): user_data has been added in response (by code at
  1041. # the end of this API method) since microversion 2.57 so tell
  1042. # view builder not to include it.
  1043. show_user_data=False)
  1044. # Add on the admin_password attribute since the view doesn't do it
  1045. # unless instance passwords are disabled
  1046. if CONF.api.enable_instance_password:
  1047. view['server']['adminPass'] = password
  1048. if include_user_data:
  1049. view['server']['user_data'] = instance.user_data
  1050. robj = wsgi.ResponseObject(view)
  1051. return self._add_location(robj)
  1052. @wsgi.response(202)
  1053. @wsgi.expected_errors((400, 403, 404, 409))
  1054. @wsgi.action('createImage')
  1055. @validation.schema(schema_servers.create_image, '2.0', '2.0')
  1056. @validation.schema(schema_servers.create_image, '2.1')
  1057. def _action_create_image(self, req, id, body):
  1058. """Snapshot a server instance."""
  1059. context = req.environ['nova.context']
  1060. context.can(server_policies.SERVERS % 'create_image')
  1061. entity = body["createImage"]
  1062. image_name = common.normalize_name(entity["name"])
  1063. metadata = entity.get('metadata', {})
  1064. # Starting from microversion 2.39 we don't check quotas on createImage
  1065. if api_version_request.is_supported(
  1066. req, max_version=
  1067. api_version_request.MAX_IMAGE_META_PROXY_API_VERSION):
  1068. common.check_img_metadata_properties_quota(context, metadata)
  1069. instance = self._get_server(context, req, id)
  1070. bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
  1071. context, instance.uuid)
  1072. try:
  1073. if compute_utils.is_volume_backed_instance(context, instance,
  1074. bdms):
  1075. context.can(server_policies.SERVERS %
  1076. 'create_image:allow_volume_backed')
  1077. image = self.compute_api.snapshot_volume_backed(
  1078. context,
  1079. instance,
  1080. image_name,
  1081. extra_properties=
  1082. metadata)
  1083. else:
  1084. image = self.compute_api.snapshot(context,
  1085. instance,
  1086. image_name,
  1087. extra_properties=metadata)
  1088. except exception.InstanceInvalidState as state_error:
  1089. common.raise_http_conflict_for_instance_invalid_state(state_error,
  1090. 'createImage', id)
  1091. except exception.Invalid as err:
  1092. raise exc.HTTPBadRequest(explanation=err.format_message())
  1093. except exception.OverQuota as e:
  1094. raise exc.HTTPForbidden(explanation=e.format_message())
  1095. # Starting with microversion 2.45 we return a response body containing
  1096. # the snapshot image id without the Location header.
  1097. if api_version_request.is_supported(req, '2.45'):
  1098. return {'image_id': image['id']}
  1099. # build location of newly-created image entity
  1100. image_id = str(image['id'])
  1101. image_ref = image_api.API().generate_image_url(image_id, context)
  1102. resp = webob.Response(status_int=202)
  1103. resp.headers['Location'] = image_ref
  1104. return resp
  1105. def _get_server_admin_password(self, server):
  1106. """Determine the admin password for a server on creation."""
  1107. if 'adminPass' in server:
  1108. password = server['adminPass']
  1109. else:
  1110. password = utils.generate_password()
  1111. return password
  1112. def _get_server_search_options(self, req):
  1113. """Return server search options allowed by non-admin."""
  1114. # NOTE(mriedem): all_tenants is admin-only by default but because of
  1115. # tight-coupling between this method, the remove_invalid_options method
  1116. # and how _get_servers uses them, we include all_tenants here but it
  1117. # will be removed later for non-admins. Fixing this would be nice but
  1118. # probably not trivial.
  1119. opt_list = ('reservation_id', 'name', 'status', 'image', 'flavor',
  1120. 'ip', 'changes-since', 'all_tenants')
  1121. if api_version_request.is_supported(req, min_version='2.5'):
  1122. opt_list += ('ip6',)
  1123. if api_version_request.is_supported(req, min_version='2.26'):
  1124. opt_list += TAG_SEARCH_FILTERS
  1125. if api_version_request.is_supported(req, min_version='2.66'):
  1126. opt_list += ('changes-before',)
  1127. if api_version_request.is_supported(req, min_version='2.73'):
  1128. opt_list += ('locked',)
  1129. return opt_list
  1130. def _get_instance(self, context, instance_uuid):
  1131. try:
  1132. attrs = ['system_metadata', 'metadata']
  1133. mapping = objects.InstanceMapping.get_by_instance_uuid(
  1134. context, instance_uuid)
  1135. nova_context.set_target_cell(context, mapping.cell_mapping)
  1136. return objects.Instance.get_by_uuid(
  1137. context, instance_uuid, expected_attrs=attrs)
  1138. except (exception.InstanceNotFound,
  1139. exception.InstanceMappingNotFound) as e:
  1140. raise webob.exc.HTTPNotFound(explanation=e.format_message())
  1141. @wsgi.response(202)
  1142. @wsgi.expected_errors((404, 409))
  1143. @wsgi.action('os-start')
  1144. def _start_server(self, req, id, body):
  1145. """Start an instance."""
  1146. context = req.environ['nova.context']
  1147. instance = self._get_instance(context, id)
  1148. context.can(server_policies.SERVERS % 'start', instance)
  1149. try:
  1150. self.compute_api.start(context, instance)
  1151. except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
  1152. raise webob.exc.HTTPConflict(explanation=e.format_message())
  1153. except exception.InstanceInvalidState as state_error:
  1154. common.raise_http_conflict_for_instance_invalid_state(state_error,
  1155. 'start', id)
  1156. @wsgi.response(202)
  1157. @wsgi.expected_errors((404, 409))
  1158. @wsgi.action('os-stop')
  1159. def _stop_server(self, req, id, body):
  1160. """Stop an instance."""
  1161. context = req.environ['nova.context']
  1162. instance = self._get_instance(context, id)
  1163. context.can(server_policies.SERVERS % 'stop',
  1164. target={'user_id': instance.user_id,
  1165. 'project_id': instance.project_id})
  1166. try:
  1167. self.compute_api.stop(context, instance)
  1168. except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
  1169. raise webob.exc.HTTPConflict(explanation=e.format_message())
  1170. except exception.InstanceInvalidState as state_error:
  1171. common.raise_http_conflict_for_instance_invalid_state(state_error,
  1172. 'stop', id)
  1173. @wsgi.Controller.api_version("2.17")
  1174. @wsgi.response(202)
  1175. @wsgi.expected_errors((400, 404, 409))
  1176. @wsgi.action('trigger_crash_dump')
  1177. @validation.schema(schema_servers.trigger_crash_dump)
  1178. def _action_trigger_crash_dump(self, req, id, body):
  1179. """Trigger crash dump in an instance"""
  1180. context = req.environ['nova.context']
  1181. instance = self._get_instance(context, id)
  1182. context.can(server_policies.SERVERS % 'trigger_crash_dump',
  1183. target={'user_id': instance.user_id,
  1184. 'project_id': instance.project_id})
  1185. try:
  1186. self.compute_api.trigger_crash_dump(context, instance)
  1187. except (exception.InstanceNotReady, exception.InstanceIsLocked) as e:
  1188. raise webob.exc.HTTPConflict(explanation=e.format_message())
  1189. except exception.InstanceInvalidState as state_error:
  1190. common.raise_http_conflict_for_instance_invalid_state(state_error,
  1191. 'trigger_crash_dump', id)
  1192. def remove_invalid_options(context, search_options, allowed_search_options):
  1193. """Remove search options that are not permitted unless policy allows."""
  1194. if context.can(server_policies.SERVERS % 'allow_all_filters',
  1195. fatal=False):
  1196. # Only remove parameters for sorting and pagination
  1197. for key in PAGING_SORTING_PARAMS:
  1198. search_options.pop(key, None)
  1199. return
  1200. # Otherwise, strip out all unknown options
  1201. unknown_options = [opt for opt in search_options
  1202. if opt not in allowed_search_options]
  1203. if unknown_options:
  1204. LOG.debug("Removing options '%s' from query",
  1205. ", ".join(unknown_options))
  1206. for opt in unknown_options:
  1207. search_options.pop(opt, None)
  1208. def remove_invalid_sort_keys(context, sort_keys, sort_dirs,
  1209. blacklist, admin_only_fields):
  1210. key_list = copy.deepcopy(sort_keys)
  1211. for key in key_list:
  1212. # NOTE(Kevin Zheng): We are intend to remove the sort_key
  1213. # in the blacklist and its' corresponding sort_dir, since
  1214. # the sort_key and sort_dir are not strict to be provide
  1215. # in pairs in the current implement, sort_dirs could be
  1216. # less than sort_keys, in order to avoid IndexError, we
  1217. # only pop sort_dir when number of sort_dirs is no less
  1218. # than the sort_key index.
  1219. if key in blacklist:
  1220. if len(sort_dirs) > sort_keys.index(key):
  1221. sort_dirs.pop(sort_keys.index(key))
  1222. sort_keys.pop(sort_keys.index(key))
  1223. elif key in admin_only_fields and not context.is_admin:
  1224. msg = _("Only administrators can sort servers "
  1225. "by %s") % key
  1226. raise exc.HTTPForbidden(explanation=msg)
  1227. return sort_keys, sort_dirs