644 lines
25 KiB
Python
644 lines
25 KiB
Python
# Copyright 2016 Huawei Technologies Co.,LTD.
|
|
# 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 datetime
|
|
|
|
from oslo_log import log
|
|
from oslo_utils import netutils
|
|
import pecan
|
|
from pecan import rest
|
|
from six.moves import http_client
|
|
import wsme
|
|
from wsme import types as wtypes
|
|
|
|
from mogan.api.controllers import base
|
|
from mogan.api.controllers import link
|
|
from mogan.api.controllers.v1.schemas import floating_ips as fip_schemas
|
|
from mogan.api.controllers.v1.schemas import instances as inst_schemas
|
|
from mogan.api.controllers.v1 import types
|
|
from mogan.api.controllers.v1 import utils as api_utils
|
|
from mogan.api import expose
|
|
from mogan.api import validation
|
|
from mogan.common import exception
|
|
from mogan.common.i18n import _
|
|
from mogan.common.i18n import _LW
|
|
from mogan.common import policy
|
|
from mogan.common import states
|
|
from mogan import network
|
|
from mogan import objects
|
|
|
|
_DEFAULT_INSTANCE_RETURN_FIELDS = ('uuid', 'name', 'description',
|
|
'status', 'power_state')
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
class InstanceStates(base.APIBase):
|
|
"""API representation of the states of a instance."""
|
|
|
|
power_state = wtypes.text
|
|
"""Represent the current power state of the instance"""
|
|
|
|
status = wtypes.text
|
|
"""Represent the current status of the instance"""
|
|
|
|
locked = types.boolean
|
|
"""Represent the current lock state of the instance"""
|
|
|
|
@classmethod
|
|
def sample(cls):
|
|
sample = cls(power_state=states.POWER_ON,
|
|
status=states.ACTIVE, locked=False)
|
|
return sample
|
|
|
|
|
|
class InstanceControllerBase(rest.RestController):
|
|
_resource = None
|
|
|
|
# This _resource is used for authorization.
|
|
def _get_resource(self, uuid, *args, **kwargs):
|
|
self._resource = objects.Instance.get(pecan.request.context, uuid)
|
|
return self._resource
|
|
|
|
|
|
class InstanceStatesController(InstanceControllerBase):
|
|
|
|
_custom_actions = {
|
|
'power': ['PUT'],
|
|
'lock': ['PUT'],
|
|
'provision': ['PUT'],
|
|
}
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "get_states")
|
|
@expose.expose(InstanceStates, types.uuid)
|
|
def get(self, instance_uuid):
|
|
"""List the states of the instance, just support power state at present.
|
|
|
|
:param instance_uuid: the UUID of a instance.
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
|
|
return InstanceStates(power_state=rpc_instance.power_state,
|
|
status=rpc_instance.status,
|
|
locked=rpc_instance.locked)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "set_power_state")
|
|
@expose.expose(None, types.uuid, wtypes.text,
|
|
status_code=http_client.ACCEPTED)
|
|
def power(self, instance_uuid, target):
|
|
"""Set the power state of the instance.
|
|
|
|
:param instance_uuid: the UUID of a instance.
|
|
:param target: the desired target to change power state,
|
|
on, off or reboot.
|
|
:raises: Conflict (HTTP 409) if a power operation is
|
|
already in progress.
|
|
:raises: BadRequest (HTTP 400) if the requested target
|
|
state is not valid or if the instance is in CLEANING state.
|
|
|
|
"""
|
|
if target not in ["on", "off", "reboot", "soft_off", "soft_reboot"]:
|
|
# ironic will throw InvalidStateRequested
|
|
raise exception.InvalidActionParameterValue(
|
|
value=target, action="power",
|
|
instance=instance_uuid)
|
|
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
pecan.request.engine_api.power(
|
|
pecan.request.context, rpc_instance, target)
|
|
# At present we do not catch the Exception from ironicclient.
|
|
# Such as Conflict and BadRequest.
|
|
# varify provision_state, if instance is being cleaned,
|
|
# don't change power state?
|
|
|
|
# Set the HTTP Location Header, user can get the power_state
|
|
# by locaton.
|
|
url_args = '/'.join([instance_uuid, 'states'])
|
|
pecan.response.location = link.build_url('instances', url_args)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "set_lock_state")
|
|
@expose.expose(None, types.uuid, types.boolean,
|
|
status_code=http_client.ACCEPTED)
|
|
def lock(self, instance_uuid, target):
|
|
"""Set the lock state of the instance.
|
|
|
|
:param instance_uuid: the UUID of a instance.
|
|
:param target: the desired target to change lock state,
|
|
true or false
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
context = pecan.request.context
|
|
|
|
# Target is True, means lock an instance
|
|
if target:
|
|
pecan.request.engine_api.lock(context, rpc_instance)
|
|
|
|
# Else, unlock the instance
|
|
else:
|
|
# Try to unlock an instance with non-admin or non-owner
|
|
if not pecan.request.engine_api.is_expected_locked_by(
|
|
context, rpc_instance):
|
|
raise exception.Forbidden()
|
|
pecan.request.engine_api.unlock(context, rpc_instance)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "set_provision_state")
|
|
@expose.expose(None, types.uuid, wtypes.text,
|
|
status_code=http_client.ACCEPTED)
|
|
def provision(self, instance_uuid, target):
|
|
"""Asynchronous trigger the provisioning of the instance.
|
|
|
|
This will set the target provision state of the instance, and
|
|
a background task will begin which actually applies the state
|
|
change. This call will return a 202 (Accepted) indicating the
|
|
request was accepted and is in progress; the client should
|
|
continue to GET the status of this instance to observe the
|
|
status of the requested action.
|
|
|
|
:param instance_uuid: UUID of an instance.
|
|
:param target: The desired provision state of the instance or verb.
|
|
"""
|
|
|
|
# Currently we only support rebuild target
|
|
if target not in (states.REBUILD,):
|
|
raise exception.InvalidActionParameterValue(
|
|
value=target, action="provision",
|
|
instance=instance_uuid)
|
|
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
if target == states.REBUILD:
|
|
try:
|
|
pecan.request.engine_api.rebuild(pecan.request.context,
|
|
rpc_instance)
|
|
except exception.InstanceNotFound:
|
|
msg = (_("Instance %s could not be found") %
|
|
instance_uuid)
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.NOT_FOUND)
|
|
|
|
# Set the HTTP Location Header
|
|
url_args = '/'.join([instance_uuid, 'states'])
|
|
pecan.response.location = link.build_url('instances', url_args)
|
|
|
|
|
|
class FloatingIPController(InstanceControllerBase):
|
|
"""REST controller for Instance floatingips."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(FloatingIPController, self).__init__(*args, **kwargs)
|
|
self.network_api = network.API()
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "associate_floatingip", False)
|
|
@expose.expose(None, types.uuid, body=types.jsontype,
|
|
status_code=http_client.NO_CONTENT)
|
|
def post(self, instance_uuid, floatingip):
|
|
"""Add(Associate) Floating Ip.
|
|
|
|
:param instance_uuid: UUID of a instance.
|
|
:param floatingip: The floating IP within the request body.
|
|
"""
|
|
validation.check_schema(floatingip, fip_schemas.add_floating_ip)
|
|
|
|
instance = self._resource or self._get_resource(instance_uuid)
|
|
address = floatingip['address']
|
|
instance_nics = instance.nics
|
|
|
|
if not instance_nics:
|
|
msg = _('No ports associated to instance')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
|
|
fixed_address = None
|
|
if 'fixed_address' in floatingip:
|
|
fixed_address = floatingip['fixed_address']
|
|
for nic in instance_nics:
|
|
for port_address in nic.fixed_ips:
|
|
if port_address['ip_address'] == fixed_address:
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
else:
|
|
msg = _('Specified fixed address not assigned to instance')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
|
|
if not fixed_address:
|
|
for nic in instance_nics:
|
|
for port_address in nic.fixed_ips:
|
|
if netutils.is_valid_ipv4(port_address['ip_address']):
|
|
fixed_address = port_address['ip_address']
|
|
break
|
|
else:
|
|
continue
|
|
break
|
|
else:
|
|
msg = _('Unable to associate floating IP %(address)s '
|
|
'to any fixed IPs for instance %(id)s. '
|
|
'Instance has no fixed IPv4 addresses to '
|
|
'associate.') % ({'address': address,
|
|
'id': instance.uuid})
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
if len(instance_nics) > 1:
|
|
LOG.warning(_LW('multiple ports exist, using the first '
|
|
'IPv4 fixed_ip: %s'), fixed_address)
|
|
|
|
try:
|
|
self.network_api.associate_floating_ip(
|
|
pecan.request.context, floating_address=address,
|
|
port_id=nic.port_id, fixed_address=fixed_address)
|
|
except exception.FloatingIpNotFoundForAddress as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.NOT_FOUND)
|
|
except exception.Forbidden as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.FORBIDDEN)
|
|
except Exception as e:
|
|
msg = _('Unable to associate floating IP %(address)s to '
|
|
'fixed IP %(fixed_address)s for instance %(id)s. '
|
|
'Error: %(error)s') % ({'address': address,
|
|
'fixed_address': fixed_address,
|
|
'id': instance.uuid, 'error': e})
|
|
LOG.exception(msg)
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "disassociate_floatingip")
|
|
@expose.expose(None, types.uuid, wtypes.text,
|
|
status_code=http_client.NO_CONTENT)
|
|
def delete(self, instance_uuid, address):
|
|
"""Dissociate floating_ip from an instance.
|
|
|
|
:param instance_uuid: UUID of a instance.
|
|
:param floatingip: The floating IP within the request body.
|
|
"""
|
|
if not netutils.is_valid_ipv4(address):
|
|
msg = "Invalid IP address %s" % address
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
# get the floating ip object
|
|
try:
|
|
floating_ip = self.network_api.get_floating_ip_by_address(
|
|
pecan.request.context, address)
|
|
except exception.FloatingIpNotFoundForAddress:
|
|
msg = _("floating IP not found")
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.NOT_FOUND)
|
|
|
|
# get the associated instance object (if any)
|
|
try:
|
|
instance_id =\
|
|
self.network_api.get_instance_id_by_floating_address(
|
|
pecan.request.context, address)
|
|
except exception.FloatingIpNotFoundForAddress as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.NOT_FOUND)
|
|
except exception.FloatingIpMultipleFoundForAddress as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.CONFLICT)
|
|
|
|
# disassociate if associated
|
|
if (floating_ip.get('port_id') and instance_id == instance_uuid):
|
|
try:
|
|
self.network_api.disassociate_floating_ip(
|
|
pecan.request.context, address)
|
|
except exception.Forbidden as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.FORBIDDEN)
|
|
except exception.CannotDisassociateAutoAssignedFloatingIP:
|
|
msg = _('Cannot disassociate auto assigned floating IP')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.FORBIDDEN)
|
|
except exception.FloatingIpNotAssociated:
|
|
msg = _('Floating IP is not associated')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
else:
|
|
msg = _("Floating IP %(address)s is not associated with instance "
|
|
"%(id)s.") % {'address': address, 'id': instance_uuid}
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
|
|
|
|
class InstanceNetworks(base.APIBase):
|
|
"""API representation of the networks of an instance."""
|
|
|
|
ports = {wtypes.text: types.jsontype}
|
|
"""The network information of the instance"""
|
|
|
|
|
|
class InstanceNetworksController(InstanceControllerBase):
|
|
"""REST controller for Instance networks."""
|
|
|
|
floatingips = FloatingIPController()
|
|
"""Expose floatingip as a sub-element of networks"""
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "get_networks")
|
|
@expose.expose(InstanceNetworks, types.uuid)
|
|
def get(self, instance_uuid):
|
|
"""List the networks info of the instance.
|
|
|
|
:param instance_uuid: the UUID of a instance.
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
|
|
return InstanceNetworks(
|
|
ports=rpc_instance.instance_nics.to_legacy_dict())
|
|
|
|
|
|
class Instance(base.APIBase):
|
|
"""API representation of a instance.
|
|
|
|
This class enforces type checking and value constraints, and converts
|
|
between the internal object model and the API representation of
|
|
a instance.
|
|
"""
|
|
uuid = types.uuid
|
|
"""The UUID of the instance"""
|
|
|
|
name = wsme.wsattr(wtypes.text, mandatory=True)
|
|
"""The name of the instance"""
|
|
|
|
description = wtypes.text
|
|
"""The description of the instance"""
|
|
|
|
project_id = types.uuid
|
|
"""The project UUID of the instance"""
|
|
|
|
user_id = types.uuid
|
|
"""The user UUID of the instance"""
|
|
|
|
status = wtypes.text
|
|
"""The status of the instance"""
|
|
|
|
power_state = wtypes.text
|
|
"""The power state of the instance"""
|
|
|
|
availability_zone = wtypes.text
|
|
"""The availability zone of the instance"""
|
|
|
|
instance_type_uuid = types.uuid
|
|
"""The instance type UUID of the instance"""
|
|
|
|
image_uuid = types.uuid
|
|
"""The image UUID of the instance"""
|
|
|
|
network_info = {wtypes.text: types.jsontype}
|
|
"""The network information of the instance"""
|
|
|
|
links = wsme.wsattr([link.Link], readonly=True)
|
|
"""A list containing a self link"""
|
|
|
|
launched_at = datetime.datetime
|
|
"""The UTC date and time of the instance launched"""
|
|
|
|
extra = {wtypes.text: types.jsontype}
|
|
"""The meta data of the instance"""
|
|
|
|
def __init__(self, **kwargs):
|
|
super(Instance, self).__init__(**kwargs)
|
|
self.fields = []
|
|
for field in objects.Instance.fields:
|
|
# TODO(liusheng) workaround to keep the output of API request same
|
|
# as before
|
|
if field == 'nics':
|
|
network_info = kwargs.get(field, None)
|
|
if network_info is not None:
|
|
network_info = network_info.to_legacy_dict()
|
|
else:
|
|
network_info = {}
|
|
setattr(self, 'network_info', network_info)
|
|
# Skip fields we do not expose.
|
|
if not hasattr(self, field):
|
|
continue
|
|
self.fields.append(field)
|
|
setattr(self, field, kwargs.get(field, wtypes.Unset))
|
|
|
|
@classmethod
|
|
def convert_with_links(cls, instance_data, fields=None):
|
|
instance = Instance(**instance_data)
|
|
instance_uuid = instance.uuid
|
|
if fields is not None:
|
|
instance.unset_fields_except(fields)
|
|
url = pecan.request.public_url
|
|
instance.links = [link.Link.make_link('self',
|
|
url,
|
|
'instances', instance_uuid),
|
|
link.Link.make_link('bookmark',
|
|
url,
|
|
'instances', instance_uuid,
|
|
bookmark=True)
|
|
]
|
|
return instance
|
|
|
|
|
|
class InstancePatchType(types.JsonPatchType):
|
|
|
|
_api_base = Instance
|
|
|
|
@staticmethod
|
|
def internal_attrs():
|
|
defaults = types.JsonPatchType.internal_attrs()
|
|
return defaults + ['/project_id', '/user_id', '/status',
|
|
'/power_state', '/availability_zone',
|
|
'/instance_type_uuid', 'image_uuid',
|
|
'/isntance_nics', '/launched_at']
|
|
|
|
|
|
class InstanceCollection(base.APIBase):
|
|
"""API representation of a collection of instance."""
|
|
|
|
instances = [Instance]
|
|
"""A list containing instance objects"""
|
|
|
|
@staticmethod
|
|
def convert_with_links(instances_data, fields=None):
|
|
collection = InstanceCollection()
|
|
collection.instances = [Instance.convert_with_links(inst, fields)
|
|
for inst in instances_data]
|
|
return collection
|
|
|
|
|
|
class InstanceController(InstanceControllerBase):
|
|
"""REST controller for Instance."""
|
|
|
|
states = InstanceStatesController()
|
|
"""Expose the state controller action as a sub-element of instances"""
|
|
|
|
networks = InstanceNetworksController()
|
|
"""Expose the network controller action as a sub-element of instances"""
|
|
|
|
_custom_actions = {
|
|
'detail': ['GET']
|
|
}
|
|
|
|
def _get_instance_collection(self, fields=None, all_tenants=False):
|
|
context = pecan.request.context
|
|
project_only = True
|
|
if context.is_admin and all_tenants:
|
|
project_only = False
|
|
|
|
instances = objects.Instance.list(pecan.request.context,
|
|
project_only=project_only)
|
|
instances_data = [instance.as_dict() for instance in instances]
|
|
|
|
return InstanceCollection.convert_with_links(instances_data,
|
|
fields=fields)
|
|
|
|
@expose.expose(InstanceCollection, types.listtype, types.boolean)
|
|
def get_all(self, fields=None, all_tenants=None):
|
|
"""Retrieve a list of instance.
|
|
|
|
:param fields: Optional, a list with a specified set of fields
|
|
of the resource to be returned.
|
|
:param all_tenants: Optional, allows administrators to see the
|
|
servers owned by all tenants, otherwise only the
|
|
servers associated with the calling tenant are
|
|
included in the response.
|
|
"""
|
|
if fields is None:
|
|
fields = _DEFAULT_INSTANCE_RETURN_FIELDS
|
|
return self._get_instance_collection(fields=fields,
|
|
all_tenants=all_tenants)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "get")
|
|
@expose.expose(Instance, types.uuid, types.listtype)
|
|
def get_one(self, instance_uuid, fields=None):
|
|
"""Retrieve information about the given instance.
|
|
|
|
:param instance_uuid: UUID of a instance.
|
|
:param fields: Optional, a list with a specified set of fields
|
|
of the resource to be returned.
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
instance_data = rpc_instance.as_dict()
|
|
|
|
return Instance.convert_with_links(instance_data, fields=fields)
|
|
|
|
@expose.expose(InstanceCollection, types.boolean)
|
|
def detail(self, all_tenants=None):
|
|
"""Retrieve detail of a list of instances."""
|
|
# /detail should only work against collections
|
|
parent = pecan.request.path.split('/')[:-1][-1]
|
|
if parent != "instances":
|
|
raise exception.NotFound()
|
|
return self._get_instance_collection(all_tenants=all_tenants)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "create", False)
|
|
@expose.expose(Instance, body=types.jsontype,
|
|
status_code=http_client.CREATED)
|
|
def post(self, instance):
|
|
"""Create a new instance.
|
|
|
|
:param instance: a instance within the request body.
|
|
"""
|
|
validation.check_schema(instance, inst_schemas.create_instance)
|
|
|
|
min_count = instance.get('min_count', 1)
|
|
max_count = instance.get('max_count', min_count)
|
|
|
|
if min_count > max_count:
|
|
msg = _('min_count must be <= max_count')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
|
|
requested_networks = instance.pop('networks', None)
|
|
instance_type_uuid = instance.get('instance_type_uuid')
|
|
image_uuid = instance.get('image_uuid')
|
|
|
|
try:
|
|
instance_type = objects.InstanceType.get(pecan.request.context,
|
|
instance_type_uuid)
|
|
|
|
instances = pecan.request.engine_api.create(
|
|
pecan.request.context,
|
|
instance_type,
|
|
image_uuid=image_uuid,
|
|
name=instance.get('name'),
|
|
description=instance.get('description'),
|
|
availability_zone=instance.get('availability_zone'),
|
|
extra=instance.get('extra'),
|
|
requested_networks=requested_networks,
|
|
min_count=min_count,
|
|
max_count=max_count)
|
|
except exception.InstanceTypeNotFound:
|
|
msg = (_("InstanceType %s could not be found") %
|
|
instance_type_uuid)
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
except exception.ImageNotFound:
|
|
msg = (_("Requested image %s could not be found") % image_uuid)
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
except exception.PortLimitExceeded as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.FORBIDDEN)
|
|
except exception.AZNotFound:
|
|
msg = _('The requested availability zone is not available')
|
|
raise wsme.exc.ClientSideError(
|
|
msg, status_code=http_client.BAD_REQUEST)
|
|
except (exception.GlanceConnectionFailed,
|
|
exception.NetworkRequiresSubnet,
|
|
exception.NetworkNotFound) as e:
|
|
raise wsme.exc.ClientSideError(
|
|
e.message, status_code=http_client.BAD_REQUEST)
|
|
|
|
# Set the HTTP Location Header for the first instance.
|
|
pecan.response.location = link.build_url('instance', instances[0].uuid)
|
|
return Instance.convert_with_links(instances[0])
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "update")
|
|
@wsme.validate(types.uuid, [InstancePatchType])
|
|
@expose.expose(Instance, types.uuid, body=[InstancePatchType])
|
|
def patch(self, instance_uuid, patch):
|
|
"""Update an instance.
|
|
|
|
:param instance_uuid: UUID of an instance.
|
|
:param patch: a json PATCH document to apply to this instance.
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
try:
|
|
instance = Instance(
|
|
**api_utils.apply_jsonpatch(rpc_instance.as_dict(), patch))
|
|
|
|
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
|
raise exception.PatchError(patch=patch, reason=e)
|
|
|
|
# Update only the fields that have changed
|
|
for field in objects.Instance.fields:
|
|
try:
|
|
patch_val = getattr(instance, field)
|
|
except AttributeError:
|
|
# Ignore fields that aren't exposed in the API
|
|
continue
|
|
if patch_val == wtypes.Unset:
|
|
patch_val = None
|
|
if rpc_instance[field] != patch_val:
|
|
rpc_instance[field] = patch_val
|
|
|
|
rpc_instance.save()
|
|
|
|
return Instance.convert_with_links(rpc_instance)
|
|
|
|
@policy.authorize_wsgi("mogan:instance", "delete")
|
|
@expose.expose(None, types.uuid, status_code=http_client.NO_CONTENT)
|
|
def delete(self, instance_uuid):
|
|
"""Delete a instance.
|
|
|
|
:param instance_uuid: UUID of a instance.
|
|
"""
|
|
rpc_instance = self._resource or self._get_resource(instance_uuid)
|
|
pecan.request.engine_api.delete(pecan.request.context, rpc_instance)
|