Add ability to signal and perform online volume size change
Allow Cinder to use external events to signal a volume extension. 1) Nova will then call os-brick to perform the volume extension so the host can detect its new size. 2) Compute driver will resize the device in QEMU so instance can detect the new disk size without rebooting. This change: * Adds the 'volume-extended' external event. The event tag needs to be the extended volume id. * Bumps the latest microversion to 2.51. * Exposes non-traceback instance action event details for non-admins on the microversion. This is needed for the non-admin API user that initiated the volume extend operation to be able to tell when the nova-compute side is complete. Co-Authored-By: Matt Riedemann <mriedem.os@gmail.com> Blueprint: nova-support-attached-volume-extend Change-Id: If10cffd0dc4c9879f6754ce39bee5fae1d04f474
This commit is contained in:
parent
d2d84eb102
commit
bbe0f313bd
@ -95,14 +95,20 @@ Response
|
||||
- request_id: request_id_body
|
||||
- start_time: start_time
|
||||
- user_id: user_id
|
||||
- events: instance_action_events
|
||||
- events: instance_action_events_2_50
|
||||
- events: instance_action_events_2_51
|
||||
- events.event: event
|
||||
- events.start_time: event_start_time
|
||||
- events.finish_time: event_finish_time
|
||||
- events.result: event_result
|
||||
- events.traceback: event_traceback
|
||||
|
||||
**Example Show Server Action Details: JSON response**
|
||||
**Example Show Server Action Details For Admin (v2.1)**
|
||||
|
||||
.. literalinclude:: ../../doc/api_samples/os-instance-actions/instance-action-get-resp.json
|
||||
:language: javascript
|
||||
|
||||
**Example Show Server Action Details For Non-Admin (v2.51)**
|
||||
|
||||
.. literalinclude:: ../../doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json
|
||||
:language: javascript
|
||||
|
@ -1518,6 +1518,7 @@ code:
|
||||
The HTTP response code for the event. The following codes are currently used:
|
||||
|
||||
* 200 - successfully submitted event
|
||||
* 400 - the request is missing required parameter
|
||||
* 404 - the instance specified by ``server_uuid`` was not found
|
||||
* 422 - no host was found for the server specified by ``server_uuid``,
|
||||
so there is no route to this server.
|
||||
@ -2001,7 +2002,8 @@ event_finish_time:
|
||||
event_name:
|
||||
description: |
|
||||
The event name. A valid value is ``network-changed``, ``network-vif-plugged``,
|
||||
``network-vif-unplugged``, or ``network-vif-deleted``.
|
||||
``network-vif-unplugged``, ``network-vif-deleted``, or ``volume-extended``.
|
||||
The event name ``volume-extended`` is added since microversion ``2.51``.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
@ -2041,7 +2043,11 @@ event_tag:
|
||||
type: string
|
||||
event_traceback:
|
||||
description: |
|
||||
The traceback stack if error occurred in this event.
|
||||
The traceback stack if an error occurred in this event.
|
||||
|
||||
Policy defaults enable only users with the administrative role to see
|
||||
an instance action event traceback. Cloud providers can change these
|
||||
permissions through the ``policy.json`` file.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
@ -3095,9 +3101,9 @@ injectNetworkInfo:
|
||||
in: body
|
||||
required: true
|
||||
type: none
|
||||
instance_action_events:
|
||||
instance_action_events_2_50:
|
||||
description: |
|
||||
The events occurred in this action.
|
||||
The events which occurred in this action.
|
||||
|
||||
Policy defaults enable only users with the administrative role to see
|
||||
instance action event information. Cloud providers can change these
|
||||
@ -3105,6 +3111,18 @@ instance_action_events:
|
||||
in: body
|
||||
required: false
|
||||
type: array
|
||||
max_version: 2.50
|
||||
instance_action_events_2_51:
|
||||
description: |
|
||||
The events which occurred in this action.
|
||||
|
||||
Policy defaults enable only users with the administrative role or the owner
|
||||
of the server to see instance action event information. Cloud providers can
|
||||
change these permissions through the ``policy.json`` file.
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
min_version: 2.51
|
||||
instance_id_body:
|
||||
description: |
|
||||
The UUID of the server.
|
||||
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"instanceAction": {
|
||||
"action": "reboot",
|
||||
"events": [
|
||||
{
|
||||
"event": "schedule",
|
||||
"finish_time": "2012-12-05T01:02:00.000000",
|
||||
"result": "Success",
|
||||
"start_time": "2012-12-05T01:00:02.000000"
|
||||
},
|
||||
{
|
||||
"event": "compute_create",
|
||||
"finish_time": "2012-12-05T01:04:00.000000",
|
||||
"result": "Success",
|
||||
"start_time": "2012-12-05T01:03:00.000000"
|
||||
}
|
||||
],
|
||||
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
|
||||
"message": "",
|
||||
"project_id": "147",
|
||||
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
|
||||
"start_time": "2012-12-05T00:00:00.000000",
|
||||
"user_id": "789"
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"instanceAction": {
|
||||
"action": "reboot",
|
||||
"events": [
|
||||
{
|
||||
"event": "schedule",
|
||||
"finish_time": "2012-12-05T01:02:00.000000",
|
||||
"result": "Success",
|
||||
"start_time": "2012-12-05T01:00:02.000000",
|
||||
"traceback": ""
|
||||
},
|
||||
{
|
||||
"event": "compute_create",
|
||||
"finish_time": "2012-12-05T01:04:00.000000",
|
||||
"result": "Success",
|
||||
"start_time": "2012-12-05T01:03:00.000000",
|
||||
"traceback": ""
|
||||
}
|
||||
],
|
||||
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
|
||||
"message": "",
|
||||
"project_id": "147",
|
||||
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
|
||||
"start_time": "2012-12-05T00:00:00.000000",
|
||||
"user_id": "789"
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"instanceActions": [
|
||||
{
|
||||
"action": "resize",
|
||||
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
|
||||
"message": "",
|
||||
"project_id": "842",
|
||||
"request_id": "req-25517360-b757-47d3-be45-0e8d2a01b36a",
|
||||
"start_time": "2012-12-05T01:00:00.000000",
|
||||
"user_id": "789"
|
||||
},
|
||||
{
|
||||
"action": "reboot",
|
||||
"instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13",
|
||||
"message": "",
|
||||
"project_id": "147",
|
||||
"request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8",
|
||||
"start_time": "2012-12-05T00:00:00.000000",
|
||||
"user_id": "789"
|
||||
}
|
||||
]
|
||||
}
|
@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.50",
|
||||
"version": "2.51",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"status": "CURRENT",
|
||||
"version": "2.50",
|
||||
"version": "2.51",
|
||||
"min_version": "2.1",
|
||||
"updated": "2013-07-23T11:33:21Z"
|
||||
}
|
||||
|
@ -130,6 +130,32 @@ driver-impl-libvirt-vz-vm=complete
|
||||
driver-impl-libvirt-vz-ct=missing
|
||||
driver-impl-powervm=missing
|
||||
|
||||
[operation.extend-volume]
|
||||
title=Extend block volume attached to instance
|
||||
status=optional
|
||||
notes=The extend volume operation provides a means to extend
|
||||
the size of an attached volume. This allows volume size
|
||||
to be expanded without interruption of service.
|
||||
In a cloud model it would be more typical to just
|
||||
spin up a new instance with large storage, so the ability to
|
||||
extend the size of an attached volume is for those cases
|
||||
where the instance is considered to be more of a pet than cattle.
|
||||
Therefore this operation is not considered to be mandatory to support.
|
||||
cli=cinder extend <volume> <new_size>
|
||||
driver-impl-xenserver=missing
|
||||
driver-impl-libvirt-kvm-x86=complete
|
||||
driver-impl-libvirt-kvm-ppc64=unknown
|
||||
driver-impl-libvirt-kvm-s390x=unknown
|
||||
driver-impl-libvirt-qemu-x86=complete
|
||||
driver-impl-libvirt-lxc=missing
|
||||
driver-impl-libvirt-xen=unknown
|
||||
driver-impl-vmware=missing
|
||||
driver-impl-hyperv=missing
|
||||
driver-impl-ironic=missing
|
||||
driver-impl-libvirt-vz-vm=unknown
|
||||
driver-impl-libvirt-vz-ct=missing
|
||||
driver-impl-powervm=missing
|
||||
|
||||
[operation.attach-interface]
|
||||
title=Attach virtual network interface to instance
|
||||
status=optional
|
||||
|
@ -120,6 +120,9 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
GET & PUT ``os-quota-class-sets`` APIs response.
|
||||
Also filter out Network related quotas from
|
||||
``os-quota-class-sets`` API
|
||||
* 2.51 - Adds new event name to external-events (volume-extended). Also,
|
||||
non-admins can see instance action event details except for the
|
||||
traceback field.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
@ -128,7 +131,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
# Note(cyeoh): This only applies for the v2.1 API once microversions
|
||||
# support is fully merged. It does not affect the V2 API.
|
||||
_MIN_API_VERSION = "2.1"
|
||||
_MAX_API_VERSION = "2.50"
|
||||
_MAX_API_VERSION = "2.51"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
# Almost all proxy APIs which related to network, images and baremetal
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
from webob import exc
|
||||
|
||||
from nova.api.openstack import api_version_request
|
||||
from nova.api.openstack import common
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
@ -41,9 +42,12 @@ class InstanceActionsController(wsgi.Controller):
|
||||
action[key] = action_raw.get(key)
|
||||
return action
|
||||
|
||||
def _format_event(self, event_raw):
|
||||
def _format_event(self, event_raw, show_traceback=False):
|
||||
event = {}
|
||||
for key in EVENT_KEYS:
|
||||
# By default, non-admins are not allowed to see traceback details.
|
||||
if key == 'traceback' and not show_traceback:
|
||||
continue
|
||||
event[key] = event_raw.get(key)
|
||||
return event
|
||||
|
||||
@ -80,8 +84,25 @@ class InstanceActionsController(wsgi.Controller):
|
||||
|
||||
action_id = action['id']
|
||||
action = self._format_action(action)
|
||||
# Prior to microversion 2.51, events would only be returned in the
|
||||
# response for admins by default policy rules. Starting in
|
||||
# microversion 2.51, events are returned for admin_or_owner (of the
|
||||
# instance) but the "traceback" field is only shown for admin users
|
||||
# by default.
|
||||
show_events = False
|
||||
show_traceback = False
|
||||
if context.can(ia_policies.POLICY_ROOT % 'events', fatal=False):
|
||||
# For all microversions, the user can see all event details
|
||||
# including the traceback.
|
||||
show_events = show_traceback = True
|
||||
elif api_version_request.is_supported(req, '2.51'):
|
||||
# The user is not able to see all event details, but they can at
|
||||
# least see the non-traceback event details.
|
||||
show_events = True
|
||||
|
||||
if show_events:
|
||||
events_raw = self.action_api.action_events_get(context, instance,
|
||||
action_id)
|
||||
action['events'] = [self._format_event(evt) for evt in events_raw]
|
||||
action['events'] = [self._format_event(evt, show_traceback)
|
||||
for evt in events_raw]
|
||||
return {'instanceAction': action}
|
||||
|
@ -580,7 +580,6 @@ user documentation.
|
||||
|
||||
Tagged volume attachment is not supported for shelved-offloaded instances.
|
||||
|
||||
|
||||
2.50
|
||||
----
|
||||
|
||||
@ -595,3 +594,19 @@ user documentation.
|
||||
- "networks",
|
||||
- "security_group_rules"
|
||||
- "security_groups"
|
||||
|
||||
2.51
|
||||
----
|
||||
|
||||
There are two changes for the 2.51 microversion:
|
||||
|
||||
* Add ``volume-extended`` event name to the ``os-server-external-events``
|
||||
API. This will be used by the Block Storage service when extending the size
|
||||
of an attached volume. This signals the Compute service to perform any
|
||||
necessary actions on the compute host or hypervisor to adjust for the new
|
||||
volume block device size.
|
||||
* Expose the ``events`` field in the response body for the
|
||||
``GET /servers/{server_id}/os-instance-actions/{request_id}`` API. This is
|
||||
useful for API users to monitor when a volume extend operation completes
|
||||
for the given server instance. By default only users with the administrator
|
||||
role will be able to see event ``traceback`` details.
|
||||
|
@ -11,6 +11,7 @@
|
||||
# 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 nova.objects import external_event as external_event_obj
|
||||
|
||||
@ -27,7 +28,12 @@ create = {
|
||||
},
|
||||
'name': {
|
||||
'type': 'string',
|
||||
'enum': external_event_obj.EVENT_NAMES
|
||||
'enum': [
|
||||
'network-changed',
|
||||
'network-vif-plugged',
|
||||
'network-vif-unplugged',
|
||||
'network-vif-deleted'
|
||||
],
|
||||
},
|
||||
'status': {
|
||||
'type': 'string',
|
||||
@ -45,3 +51,7 @@ create = {
|
||||
'required': ['events'],
|
||||
'additionalProperties': False,
|
||||
}
|
||||
|
||||
create_v251 = copy.deepcopy(create)
|
||||
name = create_v251['properties']['events']['items']['properties']['name']
|
||||
name['enum'].append('volume-extended')
|
||||
|
@ -36,9 +36,16 @@ class ServerExternalEventsController(wsgi.Controller):
|
||||
self.compute_api = compute.API()
|
||||
super(ServerExternalEventsController, self).__init__()
|
||||
|
||||
@staticmethod
|
||||
def _is_event_tag_present_when_required(event):
|
||||
if event.name == 'volume-extended' and event.tag is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@extensions.expected_errors((400, 403, 404))
|
||||
@wsgi.response(200)
|
||||
@validation.schema(server_external_events.create)
|
||||
@validation.schema(server_external_events.create, '2.1', '2.50')
|
||||
@validation.schema(server_external_events.create_v251, '2.51')
|
||||
def create(self, req, body):
|
||||
"""Creates a new instance event."""
|
||||
context = req.environ['nova.context']
|
||||
@ -92,7 +99,15 @@ class ServerExternalEventsController(wsgi.Controller):
|
||||
# for which the event is sent is assigned to a host; otherwise
|
||||
# it will not be possible to dispatch the event
|
||||
if instance:
|
||||
if instance.host:
|
||||
if not self._is_event_tag_present_when_required(event):
|
||||
LOG.debug("Event tag is missing for instance "
|
||||
"%(instance)s. Dropping event %(event)s",
|
||||
{'instance': event.instance_uuid,
|
||||
'event': event.name})
|
||||
_event['status'] = 'failed'
|
||||
_event['code'] = 400
|
||||
result = 207
|
||||
elif instance.host:
|
||||
accepted_events.append(event)
|
||||
accepted_instances.add(instance)
|
||||
LOG.info('Creating event %(name)s:%(tag)s for '
|
||||
|
@ -4277,6 +4277,13 @@ class API(base.Base):
|
||||
hosts_by_instance[instance.uuid].append(host)
|
||||
|
||||
for event in events:
|
||||
if event.name == 'volume-extended':
|
||||
# Volume extend is a user-initiated operation starting in the
|
||||
# Block Storage service API. We record an instance action so
|
||||
# the user can monitor the operation to completion.
|
||||
objects.InstanceAction.action_start(
|
||||
context, event.instance_uuid,
|
||||
instance_actions.EXTEND_VOLUME, want_result=False)
|
||||
for host in hosts_by_instance[event.instance_uuid]:
|
||||
events_by_host[host].append(event)
|
||||
|
||||
|
@ -52,3 +52,11 @@ LIVE_MIGRATION = 'live-migration'
|
||||
LIVE_MIGRATION_CANCEL = 'live_migration_cancel'
|
||||
LIVE_MIGRATION_FORCE_COMPLETE = 'live_migration_force_complete'
|
||||
TRIGGER_CRASH_DUMP = 'trigger_crash_dump'
|
||||
# The extend_volume action is not like the traditional instance actions which
|
||||
# are driven directly through the compute API. The extend_volume action is
|
||||
# initiated by a Cinder volume extend (resize) action. Cinder will call the
|
||||
# server external events API after the volume extend is performed so that Nova
|
||||
# can perform any updates on the compute side. The instance actions framework
|
||||
# is used for tracking this asynchronous operation so the user/admin can know
|
||||
# when it is done in case they need/want to reboot the guest operating system.
|
||||
EXTEND_VOLUME = 'extend_volume'
|
||||
|
@ -6881,6 +6881,56 @@ class ComputeManager(manager.Manager):
|
||||
instance=instance)
|
||||
break
|
||||
|
||||
@wrap_instance_event(prefix='compute')
|
||||
@wrap_instance_fault
|
||||
def extend_volume(self, context, instance, extended_volume_id):
|
||||
|
||||
# If an attached volume is extended by cinder, it needs to
|
||||
# be extended by virt driver so host can detect its new size.
|
||||
# And bdm needs to be updated.
|
||||
LOG.debug('Handling volume-extended event for volume %(vol)s',
|
||||
{'vol': extended_volume_id}, instance=instance)
|
||||
|
||||
try:
|
||||
bdm = objects.BlockDeviceMapping.get_by_volume_and_instance(
|
||||
context, extended_volume_id, instance.uuid)
|
||||
except exception.NotFound:
|
||||
LOG.warning('Extend volume failed, '
|
||||
'volume %(vol)s is not attached to instance.',
|
||||
{'vol': extended_volume_id},
|
||||
instance=instance)
|
||||
return
|
||||
|
||||
LOG.info('Cinder extended volume %(vol)s; '
|
||||
'extending it to detect new size',
|
||||
{'vol': extended_volume_id},
|
||||
instance=instance)
|
||||
volume = self.volume_api.get(context, bdm.volume_id)
|
||||
|
||||
if bdm.connection_info is None:
|
||||
LOG.warning('Extend volume failed, '
|
||||
'attached volume %(vol)s has no connection_info',
|
||||
{'vol': extended_volume_id},
|
||||
instance=instance)
|
||||
return
|
||||
|
||||
connection_info = jsonutils.loads(bdm.connection_info)
|
||||
bdm.volume_size = volume['size']
|
||||
bdm.save()
|
||||
|
||||
if not self.driver.capabilities.get('supports_extend_volume', False):
|
||||
raise exception.ExtendVolumeNotSupported()
|
||||
|
||||
try:
|
||||
self.driver.extend_volume(connection_info,
|
||||
instance)
|
||||
except Exception as ex:
|
||||
LOG.warning('Extend volume failed, '
|
||||
'volume_id=%(volume_id)s, reason: %(msg)s',
|
||||
{'volume_id': extended_volume_id, 'msg': ex},
|
||||
instance=instance)
|
||||
raise
|
||||
|
||||
@wrap_exception()
|
||||
def external_instance_event(self, context, instances, events):
|
||||
# NOTE(danms): Some event types are handled by the manager, such
|
||||
@ -6911,6 +6961,8 @@ class ComputeManager(manager.Manager):
|
||||
'%(event)s due to: %(error)s',
|
||||
{'event': event.key, 'error': six.text_type(e)},
|
||||
instance=instance)
|
||||
elif event.name == 'volume-extended':
|
||||
self.extend_volume(context, instance, event.tag)
|
||||
else:
|
||||
self._process_instance_event(instance, event)
|
||||
|
||||
|
@ -264,6 +264,10 @@ class VolumeNotCreated(NovaException):
|
||||
" attempts. And its status is %(volume_status)s.")
|
||||
|
||||
|
||||
class ExtendVolumeNotSupported(Invalid):
|
||||
msg_fmt = _("Volume size extension is not supported by the hypervisor.")
|
||||
|
||||
|
||||
class VolumeEncryptionNotSupported(Invalid):
|
||||
msg_fmt = _("Volume encryption is not supported for %(volume_type)s "
|
||||
"volume %(volume_id)s")
|
||||
|
@ -24,6 +24,8 @@ EVENT_NAMES = [
|
||||
'network-vif-unplugged',
|
||||
'network-vif-deleted',
|
||||
|
||||
# Volume was extended for this instance, tag is volume_id
|
||||
'volume-extended',
|
||||
]
|
||||
|
||||
EVENT_STATUSES = ['failed', 'completed', 'in-progress']
|
||||
@ -34,7 +36,8 @@ class InstanceExternalEvent(obj_base.NovaObject):
|
||||
# Version 1.0: Initial version
|
||||
# Supports network-changed and vif-plugged
|
||||
# Version 1.1: adds network-vif-deleted event
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: adds volume-extended event
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
'instance_uuid': fields.UUIDField(),
|
||||
|
@ -0,0 +1,25 @@
|
||||
{
|
||||
"instanceAction": {
|
||||
"action": "%(action)s",
|
||||
"instance_uuid": "%(instance_uuid)s",
|
||||
"request_id": "%(request_id)s",
|
||||
"user_id": "%(integer_id)s",
|
||||
"project_id": "%(integer_id)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"message": "",
|
||||
"events": [
|
||||
{
|
||||
"event": "%(event)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"finish_time": "%(strtime)s",
|
||||
"result": "%(result)s"
|
||||
},
|
||||
{
|
||||
"event": "%(event)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"finish_time": "%(strtime)s",
|
||||
"result": "%(result)s"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
{
|
||||
"instanceAction": {
|
||||
"action": "%(action)s",
|
||||
"instance_uuid": "%(instance_uuid)s",
|
||||
"request_id": "%(request_id)s",
|
||||
"user_id": "%(integer_id)s",
|
||||
"project_id": "%(integer_id)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"message": "",
|
||||
"events": [
|
||||
{
|
||||
"event": "%(event)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"finish_time": "%(strtime)s",
|
||||
"result": "%(result)s",
|
||||
"traceback": ""
|
||||
},
|
||||
{
|
||||
"event": "%(event)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"finish_time": "%(strtime)s",
|
||||
"result": "%(result)s",
|
||||
"traceback": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"instanceActions": [
|
||||
{
|
||||
"action": "%(action)s",
|
||||
"instance_uuid": "%(uuid)s",
|
||||
"request_id": "%(request_id)s",
|
||||
"user_id": "%(integer_id)s",
|
||||
"project_id": "%(integer_id)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"message": ""
|
||||
},
|
||||
{
|
||||
"action": "%(action)s",
|
||||
"instance_uuid": "%(uuid)s",
|
||||
"request_id": "%(request_id)s",
|
||||
"user_id": "%(integer_id)s",
|
||||
"project_id": "%(integer_id)s",
|
||||
"start_time": "%(strtime)s",
|
||||
"message": ""
|
||||
}
|
||||
]
|
||||
}
|
@ -76,7 +76,13 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
subs['start_time'] = str(fake_action['start_time'])
|
||||
subs['result'] = '(Success)|(Error)'
|
||||
subs['event'] = '(schedule)|(compute_create)'
|
||||
self._verify_response('instance-action-get-resp', subs, response, 200)
|
||||
# Non-admins can see event details except for the "traceback" field
|
||||
# starting in the 2.51 microversion.
|
||||
if self.ADMIN_API:
|
||||
name = 'instance-action-get-resp'
|
||||
else:
|
||||
name = 'instance-action-get-non-admin-resp'
|
||||
self._verify_response(name, subs, response, 200)
|
||||
|
||||
def test_instance_actions_list(self):
|
||||
fake_uuid = fake_server_actions.FAKE_UUID
|
||||
@ -93,3 +99,30 @@ class ServerActionsSampleJsonTest(api_sample_base.ApiSampleTestBaseV21):
|
||||
class ServerActionsV221SampleJsonTest(ServerActionsSampleJsonTest):
|
||||
microversion = '2.21'
|
||||
scenarios = [('v2_21', {'api_major_version': 'v2.1'})]
|
||||
|
||||
|
||||
class ServerActionsV251AdminSampleJsonTest(ServerActionsSampleJsonTest):
|
||||
"""Tests the 2.51 microversion for the os-instance-actions API.
|
||||
|
||||
The 2.51 microversion allows non-admins to see instance action event
|
||||
details *except* for the traceback field.
|
||||
|
||||
The tests in this class are run as an admin user so all fields will be
|
||||
displayed.
|
||||
"""
|
||||
microversion = '2.51'
|
||||
scenarios = [('v2_51', {'api_major_version': 'v2.1'})]
|
||||
|
||||
|
||||
class ServerActionsV251NonAdminSampleJsonTest(ServerActionsSampleJsonTest):
|
||||
"""Tests the 2.51 microversion for the os-instance-actions API.
|
||||
|
||||
The 2.51 microversion allows non-admins to see instance action event
|
||||
details *except* for the traceback field.
|
||||
|
||||
The tests in this class are run as a non-admin user so all fields except
|
||||
for the ``traceback`` field will be displayed.
|
||||
"""
|
||||
ADMIN_API = False
|
||||
microversion = '2.51'
|
||||
scenarios = [('v2_51', {'api_major_version': 'v2.1'})]
|
||||
|
@ -55,6 +55,7 @@ def fake_get_by_uuid(cls, context, uuid, **kwargs):
|
||||
class ServerExternalEventsTestV21(test.NoDBTestCase):
|
||||
server_external_events = server_external_events_v21
|
||||
invalid_error = exception.ValidationError
|
||||
wsgi_api_version = '2.1'
|
||||
|
||||
def setUp(self):
|
||||
super(ServerExternalEventsTestV21, self).setUp()
|
||||
@ -74,7 +75,8 @@ class ServerExternalEventsTestV21(test.NoDBTestCase):
|
||||
self.resp_event_2['status'] = 'completed'
|
||||
self.default_resp_body = {'events': [self.resp_event_1,
|
||||
self.resp_event_2]}
|
||||
self.req = fakes.HTTPRequest.blank('', use_admin_context=True)
|
||||
self.req = fakes.HTTPRequest.blank('', use_admin_context=True,
|
||||
version=self.wsgi_api_version)
|
||||
|
||||
def _assert_call(self, body, expected_uuids, expected_events):
|
||||
with mock.patch.object(self.api.compute_api,
|
||||
@ -157,3 +159,20 @@ class ServerExternalEventsTestV21(test.NoDBTestCase):
|
||||
body = {'events': self.event_1}
|
||||
self.assertRaises(self.invalid_error,
|
||||
self.api.create, self.req, body=body)
|
||||
|
||||
|
||||
@mock.patch('nova.objects.instance.Instance.get_by_uuid', fake_get_by_uuid)
|
||||
class ServerExternalEventsTestV251(ServerExternalEventsTestV21):
|
||||
wsgi_api_version = '2.51'
|
||||
|
||||
def test_create_with_missing_tag(self):
|
||||
body = self.default_body
|
||||
body['events'][1]['name'] = 'volume-extended'
|
||||
result, code = self._assert_call(body,
|
||||
[fake_instance_uuids[0]],
|
||||
['network-vif-plugged'])
|
||||
self.assertEqual(200, result['events'][0]['code'])
|
||||
self.assertEqual('completed', result['events'][0]['status'])
|
||||
self.assertEqual(400, result['events'][1]['code'])
|
||||
self.assertEqual('failed', result['events'][1]['status'])
|
||||
self.assertEqual(207, code)
|
||||
|
@ -3377,7 +3377,8 @@ class _ComputeAPIUnitTestMixIn(object):
|
||||
cores=instance.flavor.vcpus, ram=instance.flavor.memory_mb,
|
||||
project_id=instance.project_id, user_id=instance.user_id)
|
||||
|
||||
def test_external_instance_event(self):
|
||||
@mock.patch.object(objects.InstanceAction, 'action_start')
|
||||
def test_external_instance_event(self, mock_action_start):
|
||||
instances = [
|
||||
objects.Instance(uuid=uuids.instance_1, host='host1',
|
||||
migration_context=None),
|
||||
@ -3385,17 +3386,27 @@ class _ComputeAPIUnitTestMixIn(object):
|
||||
migration_context=None),
|
||||
objects.Instance(uuid=uuids.instance_3, host='host2',
|
||||
migration_context=None),
|
||||
objects.Instance(uuid=uuids.instance_4, host='host2',
|
||||
migration_context=None),
|
||||
]
|
||||
mappings = {inst.uuid: objects.InstanceMapping.get_by_instance_uuid(
|
||||
self.context, inst.uuid)
|
||||
for inst in instances}
|
||||
volume_id = uuidutils.generate_uuid()
|
||||
events = [
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_1),
|
||||
instance_uuid=uuids.instance_1,
|
||||
name='network-changed'),
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_2),
|
||||
instance_uuid=uuids.instance_2,
|
||||
name='network-changed'),
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_3),
|
||||
instance_uuid=uuids.instance_3,
|
||||
name='network-changed'),
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_4,
|
||||
name='volume-extended',
|
||||
tag=volume_id),
|
||||
]
|
||||
self.compute_api.compute_rpcapi = mock.MagicMock()
|
||||
self.compute_api.external_instance_event(self.context,
|
||||
@ -3405,6 +3416,9 @@ class _ComputeAPIUnitTestMixIn(object):
|
||||
host='host1')
|
||||
method.assert_any_call(self.context, instances[2:], events[2:],
|
||||
host='host2')
|
||||
mock_action_start.assert_called_once_with(
|
||||
self.context, uuids.instance_4, instance_actions.EXTEND_VOLUME,
|
||||
want_result=False)
|
||||
self.assertEqual(2, method.call_count)
|
||||
|
||||
def test_external_instance_event_evacuating_instance(self):
|
||||
@ -3440,11 +3454,14 @@ class _ComputeAPIUnitTestMixIn(object):
|
||||
self.context, inst.uuid) for inst in instances}
|
||||
events = [
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_1),
|
||||
instance_uuid=uuids.instance_1,
|
||||
name='network-changed'),
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_2),
|
||||
instance_uuid=uuids.instance_2,
|
||||
name='network-changed'),
|
||||
objects.InstanceExternalEvent(
|
||||
instance_uuid=uuids.instance_3),
|
||||
instance_uuid=uuids.instance_3,
|
||||
name='network-changed'),
|
||||
]
|
||||
|
||||
with mock.patch('nova.db.sqlalchemy.api.migration_get', migration_get):
|
||||
|
@ -2261,11 +2261,92 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
|
||||
do_test()
|
||||
|
||||
def test_extend_volume(self):
|
||||
inst_obj = objects.Instance(id=3, uuid=uuids.instance)
|
||||
connection_info = {'foo': 'bar'}
|
||||
bdm = objects.BlockDeviceMapping(
|
||||
source_type='volume',
|
||||
destination_type='volume',
|
||||
volume_id=uuids.volume_id,
|
||||
volume_size=10,
|
||||
instance_uuid=uuids.instance,
|
||||
device_name='/dev/vda',
|
||||
connection_info=jsonutils.dumps(connection_info))
|
||||
|
||||
@mock.patch.object(self.compute, 'volume_api')
|
||||
@mock.patch.object(self.compute.driver, 'extend_volume')
|
||||
@mock.patch.object(objects.BlockDeviceMapping,
|
||||
'get_by_volume_and_instance')
|
||||
@mock.patch.object(objects.BlockDeviceMapping, 'save')
|
||||
def do_test(bdm_save, bdm_get_by_vol_and_inst, extend_volume,
|
||||
volume_api):
|
||||
bdm_get_by_vol_and_inst.return_value = bdm
|
||||
volume_api.get.return_value = {'size': 20}
|
||||
|
||||
self.compute.extend_volume(
|
||||
self.context, inst_obj, uuids.volume_id)
|
||||
bdm_save.assert_called_once_with()
|
||||
extend_volume.assert_called_once_with(
|
||||
connection_info, inst_obj)
|
||||
|
||||
do_test()
|
||||
|
||||
def test_extend_volume_not_implemented_error(self):
|
||||
"""Tests the case where driver.extend_volume raises
|
||||
NotImplementedError.
|
||||
"""
|
||||
inst_obj = objects.Instance(id=3, uuid=uuids.instance)
|
||||
connection_info = {'foo': 'bar'}
|
||||
bdm = objects.BlockDeviceMapping(
|
||||
source_type='volume',
|
||||
destination_type='volume',
|
||||
volume_id=uuids.volume_id,
|
||||
volume_size=10,
|
||||
instance_uuid=uuids.instance,
|
||||
device_name='/dev/vda',
|
||||
connection_info=jsonutils.dumps(connection_info))
|
||||
|
||||
@mock.patch.object(self.compute, 'volume_api')
|
||||
@mock.patch.object(objects.BlockDeviceMapping,
|
||||
'get_by_volume_and_instance')
|
||||
@mock.patch.object(objects.BlockDeviceMapping, 'save')
|
||||
@mock.patch.object(compute_utils, 'add_instance_fault_from_exc')
|
||||
def do_test(add_fault_mock, bdm_save, bdm_get_by_vol_and_inst,
|
||||
volume_api):
|
||||
bdm_get_by_vol_and_inst.return_value = bdm
|
||||
volume_api.get.return_value = {'size': 20}
|
||||
self.assertRaises(
|
||||
exception.ExtendVolumeNotSupported,
|
||||
self.compute.extend_volume,
|
||||
self.context, inst_obj, uuids.volume_id)
|
||||
add_fault_mock.assert_called_once_with(
|
||||
self.context, inst_obj, mock.ANY, mock.ANY)
|
||||
|
||||
with mock.patch.dict(self.compute.driver.capabilities,
|
||||
supports_extend_volume=False):
|
||||
do_test()
|
||||
|
||||
def test_extend_volume_volume_not_found(self):
|
||||
"""Tests the case where driver.extend_volume tries to extend
|
||||
a volume not attached to the specified instance.
|
||||
"""
|
||||
inst_obj = objects.Instance(id=3, uuid=uuids.instance)
|
||||
|
||||
@mock.patch.object(objects.BlockDeviceMapping,
|
||||
'get_by_volume_and_instance',
|
||||
side_effect=exception.NotFound())
|
||||
def do_test(bdm_get_by_vol_and_inst):
|
||||
self.compute.extend_volume(
|
||||
self.context, inst_obj, uuids.volume_id)
|
||||
|
||||
do_test()
|
||||
|
||||
def test_external_instance_event(self):
|
||||
instances = [
|
||||
objects.Instance(id=1, uuid=uuids.instance_1),
|
||||
objects.Instance(id=2, uuid=uuids.instance_2),
|
||||
objects.Instance(id=3, uuid=uuids.instance_3)]
|
||||
objects.Instance(id=3, uuid=uuids.instance_3),
|
||||
objects.Instance(id=4, uuid=uuids.instance_4)]
|
||||
events = [
|
||||
objects.InstanceExternalEvent(name='network-changed',
|
||||
tag='tag1',
|
||||
@ -2275,13 +2356,18 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
tag='tag2'),
|
||||
objects.InstanceExternalEvent(name='network-vif-deleted',
|
||||
instance_uuid=uuids.instance_3,
|
||||
tag='tag3')]
|
||||
tag='tag3'),
|
||||
objects.InstanceExternalEvent(name='volume-extended',
|
||||
instance_uuid=uuids.instance_4,
|
||||
tag='tag4')]
|
||||
|
||||
@mock.patch.object(self.compute,
|
||||
'extend_volume')
|
||||
@mock.patch.object(self.compute, '_process_instance_vif_deleted_event')
|
||||
@mock.patch.object(self.compute.network_api, 'get_instance_nw_info')
|
||||
@mock.patch.object(self.compute, '_process_instance_event')
|
||||
def do_test(_process_instance_event, get_instance_nw_info,
|
||||
_process_instance_vif_deleted_event):
|
||||
_process_instance_vif_deleted_event, extend_volume):
|
||||
self.compute.external_instance_event(self.context,
|
||||
instances, events)
|
||||
get_instance_nw_info.assert_called_once_with(self.context,
|
||||
@ -2290,6 +2376,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
events[1])
|
||||
_process_instance_vif_deleted_event.assert_called_once_with(
|
||||
self.context, instances[2], events[2].tag)
|
||||
extend_volume.assert_called_once_with(
|
||||
self.context, instances[3], events[3].tag)
|
||||
do_test()
|
||||
|
||||
def test_external_instance_event_with_exception(self):
|
||||
@ -2307,7 +2395,9 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
objects.Instance(id=3, uuid=uuids.instance_3),
|
||||
# instance_4 doesn't have info_cache set so it will be lazy-loaded
|
||||
# and blow up with an InstanceNotFound error.
|
||||
objects.Instance(id=4, uuid=uuids.instance_4)]
|
||||
objects.Instance(id=4, uuid=uuids.instance_4),
|
||||
objects.Instance(id=5, uuid=uuids.instance_5),
|
||||
]
|
||||
events = [
|
||||
objects.InstanceExternalEvent(name='network-changed',
|
||||
tag='tag1',
|
||||
@ -2321,10 +2411,17 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
objects.InstanceExternalEvent(name='network-vif-deleted',
|
||||
instance_uuid=uuids.instance_4,
|
||||
tag='tag4'),
|
||||
objects.InstanceExternalEvent(name='volume-extended',
|
||||
instance_uuid=uuids.instance_5,
|
||||
tag='tag5'),
|
||||
]
|
||||
|
||||
# Make sure all the four events are handled despite the exceptions in
|
||||
# processing events 1, 2, and 4.
|
||||
# processing events 1, 2, 4 and 5.
|
||||
@mock.patch.object(objects.BlockDeviceMapping,
|
||||
'get_by_volume_and_instance',
|
||||
side_effect=exception.InstanceNotFound(
|
||||
instance_id=uuids.instance_5))
|
||||
@mock.patch.object(instances[3], 'obj_load_attr',
|
||||
side_effect=exception.InstanceNotFound(
|
||||
instance_id=uuids.instance_4))
|
||||
@ -2338,7 +2435,7 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
@mock.patch.object(self.compute, '_process_instance_event')
|
||||
def do_test(_process_instance_event, get_instance_nw_info,
|
||||
detach_interface, update_instance_cache_with_nw_info,
|
||||
obj_load_attr):
|
||||
obj_load_attr, bdm_get_by_vol_and_inst):
|
||||
self.compute.external_instance_event(self.context,
|
||||
instances, events)
|
||||
get_instance_nw_info.assert_called_once_with(self.context,
|
||||
@ -2353,6 +2450,8 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase):
|
||||
_process_instance_event.assert_called_once_with(instances[2],
|
||||
events[2])
|
||||
obj_load_attr.assert_called_once_with('info_cache')
|
||||
bdm_get_by_vol_and_inst.assert_called_once_with(
|
||||
self.context, 'tag5', instances[4].uuid)
|
||||
do_test()
|
||||
|
||||
def test_cancel_all_events(self):
|
||||
|
@ -1106,7 +1106,7 @@ object_data = {
|
||||
'InstanceActionEventList': '1.1-13d92fb953030cdbfee56481756e02be',
|
||||
'InstanceActionList': '1.0-4a53826625cc280e15fae64a575e0879',
|
||||
'InstanceDeviceMetadata': '1.0-74d78dd36aa32d26d2769a1b57caf186',
|
||||
'InstanceExternalEvent': '1.1-6e446ceaae5f475ead255946dd443417',
|
||||
'InstanceExternalEvent': '1.2-23eb6ba79cde5cd06d3445f845ba4589',
|
||||
'InstanceFault': '1.2-7ef01f16f1084ad1304a513d6d410a38',
|
||||
'InstanceFaultList': '1.2-6bb72de2872fe49ded5eb937a93f2451',
|
||||
'InstanceGroup': '1.10-1a0c8c7447dc7ecb9da53849430c4a5f',
|
||||
|
@ -800,6 +800,10 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
||||
'Driver capabilities for '
|
||||
'\'supports_attach_interface\' '
|
||||
'is invalid')
|
||||
self.assertTrue(drvr.capabilities['supports_extend_volume'],
|
||||
'Driver capabilities for '
|
||||
'\'supports_extend_volume\' '
|
||||
'is invalid')
|
||||
|
||||
def create_fake_libvirt_mock(self, **kwargs):
|
||||
"""Defining mocks for LibvirtDriver(libvirt is not used)."""
|
||||
@ -6593,6 +6597,104 @@ class LibvirtConnTestCase(test.NoDBTestCase,
|
||||
mock.call.detach_encryptor(**encryption),
|
||||
mock.call.disconnect_volume(connection_info, 'vdc', instance)])
|
||||
|
||||
def test_extend_volume(self):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
instance = objects.Instance(**self.test_instance)
|
||||
connection_info = {
|
||||
'driver_volume_type': 'fake',
|
||||
'data': {'device_path': '/fake',
|
||||
'access_mode': 'rw'}
|
||||
}
|
||||
|
||||
new_size_in_kb = 20 * 1024 * 1024
|
||||
|
||||
guest = mock.Mock(spec='nova.virt.libvirt.guest.Guest')
|
||||
# block_device
|
||||
block_device = mock.Mock(
|
||||
spec='nova.virt.libvirt.guest.BlockDevice')
|
||||
block_device.resize = mock.Mock()
|
||||
guest.get_block_device = mock.Mock(return_value=block_device)
|
||||
drvr._host.get_guest = mock.Mock(return_value=guest)
|
||||
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
|
||||
|
||||
for state in (power_state.RUNNING, power_state.PAUSED):
|
||||
guest.get_power_state = mock.Mock(return_value=state)
|
||||
drvr.extend_volume(connection_info, instance)
|
||||
drvr._extend_volume.assert_called_with(connection_info,
|
||||
instance)
|
||||
guest.get_block_device.assert_called_with('/fake')
|
||||
block_device.resize.assert_called_with(20480)
|
||||
|
||||
def test_extend_volume_with_volume_driver_without_support(self):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
instance = objects.Instance(**self.test_instance)
|
||||
|
||||
with mock.patch.object(drvr, '_extend_volume',
|
||||
side_effect=NotImplementedError()):
|
||||
connection_info = {'driver_volume_type': 'fake'}
|
||||
self.assertRaises(exception.ExtendVolumeNotSupported,
|
||||
drvr.extend_volume,
|
||||
connection_info, instance)
|
||||
|
||||
def test_extend_volume_disk_not_found(self):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
instance = objects.Instance(**self.test_instance)
|
||||
connection_info = {
|
||||
'driver_volume_type': 'fake',
|
||||
'data': {'device_path': '/fake',
|
||||
'access_mode': 'rw'}
|
||||
}
|
||||
new_size_in_kb = 20 * 1024 * 1024
|
||||
|
||||
xml_no_disk = "<domain><devices></devices></domain>"
|
||||
dom = fakelibvirt.Domain(drvr._get_connection(), xml_no_disk, False)
|
||||
guest = libvirt_guest.Guest(dom)
|
||||
guest.get_power_state = mock.Mock(return_value=power_state.RUNNING)
|
||||
drvr._host.get_guest = mock.Mock(return_value=guest)
|
||||
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
|
||||
|
||||
drvr.extend_volume(connection_info, instance)
|
||||
|
||||
def test_extend_volume_with_instance_not_found(self):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
instance = objects.Instance(**self.test_instance)
|
||||
|
||||
with test.nested(
|
||||
mock.patch.object(host.Host, 'get_domain',
|
||||
side_effect=exception.InstanceNotFound(
|
||||
instance_id=instance.uuid)),
|
||||
mock.patch.object(drvr, '_extend_volume')
|
||||
) as (_get_domain, _extend_volume):
|
||||
connection_info = {'driver_volume_type': 'fake'}
|
||||
self.assertRaises(exception.InstanceNotFound,
|
||||
drvr.extend_volume,
|
||||
connection_info, instance)
|
||||
|
||||
def test_extend_volume_with_libvirt_error(self):
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False)
|
||||
instance = objects.Instance(**self.test_instance)
|
||||
connection_info = {
|
||||
'driver_volume_type': 'fake',
|
||||
'data': {'device_path': '/fake',
|
||||
'access_mode': 'rw'}
|
||||
}
|
||||
new_size_in_kb = 20 * 1024 * 1024
|
||||
|
||||
guest = mock.Mock(spec='nova.virt.libvirt.guest.Guest')
|
||||
guest.get_power_state = mock.Mock(return_value=power_state.RUNNING)
|
||||
# block_device
|
||||
block_device = mock.Mock(
|
||||
spec='nova.virt.libvirt.guest.BlockDevice')
|
||||
block_device.resize = mock.Mock(
|
||||
side_effect=fakelibvirt.libvirtError('ERR'))
|
||||
guest.get_block_device = mock.Mock(return_value=block_device)
|
||||
drvr._host.get_guest = mock.Mock(return_value=guest)
|
||||
drvr._extend_volume = mock.Mock(return_value=new_size_in_kb)
|
||||
|
||||
self.assertRaises(fakelibvirt.libvirtError,
|
||||
drvr.extend_volume,
|
||||
connection_info, instance)
|
||||
|
||||
def test_multi_nic(self):
|
||||
network_info = _fake_network_info(self, 2)
|
||||
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
|
||||
|
@ -59,3 +59,17 @@ class LibvirtFibreChannelVolumeDriverTestCase(
|
||||
self.assertEqual(device_path, tree.find('./source').get('dev'))
|
||||
self.assertEqual('raw', tree.find('./driver').get('type'))
|
||||
self.assertEqual('native', tree.find('./driver').get('io'))
|
||||
|
||||
def test_extend_volume(self):
|
||||
device_path = '/dev/fake-dev'
|
||||
connection_info = {'data': {'device_path': device_path}}
|
||||
|
||||
libvirt_driver = fibrechannel.LibvirtFibreChannelVolumeDriver(
|
||||
self.fake_host)
|
||||
libvirt_driver.connector.extend_volume = mock.MagicMock(return_value=1)
|
||||
new_size = libvirt_driver.extend_volume(connection_info,
|
||||
mock.sentinel.instance)
|
||||
|
||||
self.assertEqual(1, new_size)
|
||||
libvirt_driver.connector.extend_volume.assert_called_once_with(
|
||||
connection_info['data'])
|
||||
|
@ -55,3 +55,16 @@ class LibvirtISCSIVolumeDriverTestCase(
|
||||
|
||||
msg = mock_LOG_warning.call_args_list[0]
|
||||
self.assertIn('Ignoring VolumeDeviceNotFound', msg[0][0])
|
||||
|
||||
def test_extend_volume(self):
|
||||
device_path = '/dev/fake-dev'
|
||||
connection_info = {'data': {'device_path': device_path}}
|
||||
|
||||
libvirt_driver = iscsi.LibvirtISCSIVolumeDriver(self.fake_host)
|
||||
libvirt_driver.connector.extend_volume = mock.MagicMock(return_value=1)
|
||||
new_size = libvirt_driver.extend_volume(connection_info,
|
||||
mock.sentinel.instance)
|
||||
|
||||
self.assertEqual(1, new_size)
|
||||
libvirt_driver.connector.extend_volume.assert_called_once_with(
|
||||
connection_info['data'])
|
||||
|
@ -129,6 +129,7 @@ class ComputeDriver(object):
|
||||
"supports_device_tagging": False,
|
||||
"supports_tagged_attach_interface": False,
|
||||
"supports_tagged_attach_volume": False,
|
||||
"supports_extend_volume": False,
|
||||
}
|
||||
|
||||
def __init__(self, virtapi):
|
||||
@ -478,6 +479,18 @@ class ComputeDriver(object):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def extend_volume(self, connection_info, instance):
|
||||
"""Extend the disk attached to the instance.
|
||||
|
||||
:param dict connection_info:
|
||||
The connection for the extended volume.
|
||||
:param nova.objects.instance.Instance instance:
|
||||
The instance whose volume gets extended.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def attach_interface(self, context, instance, image_meta, vif):
|
||||
"""Use hotplug to add a network interface to a running instance.
|
||||
|
||||
|
@ -124,8 +124,9 @@ class FakeDriver(driver.ComputeDriver):
|
||||
"supports_migrate_to_same_host": True,
|
||||
"supports_attach_interface": True,
|
||||
"supports_tagged_attach_interface": True,
|
||||
"supports_tagged_attach_volume": True
|
||||
}
|
||||
"supports_tagged_attach_volume": True,
|
||||
"supports_extend_volume": True,
|
||||
}
|
||||
|
||||
# Since we don't have a real hypervisor, pretend we have lots of
|
||||
# disk and ram so this driver can be used to test large instances.
|
||||
@ -309,6 +310,10 @@ class FakeDriver(driver.ComputeDriver):
|
||||
self._mounts[instance_name] = {}
|
||||
self._mounts[instance_name][mountpoint] = new_connection_info
|
||||
|
||||
def extend_volume(self, connection_info, instance):
|
||||
"""Extend the disk attached to the instance."""
|
||||
pass
|
||||
|
||||
def attach_interface(self, context, instance, image_meta, vif):
|
||||
if vif['id'] in self._interfaces:
|
||||
raise exception.InterfaceAttachFailed(
|
||||
|
@ -301,6 +301,7 @@ class LibvirtDriver(driver.ComputeDriver):
|
||||
"supports_device_tagging": True,
|
||||
"supports_tagged_attach_interface": True,
|
||||
"supports_tagged_attach_volume": True,
|
||||
"supports_extend_volume": True,
|
||||
}
|
||||
|
||||
def __init__(self, virtapi, read_only=False):
|
||||
@ -1167,6 +1168,10 @@ class LibvirtDriver(driver.ComputeDriver):
|
||||
vol_driver = self._get_volume_driver(connection_info)
|
||||
vol_driver.disconnect_volume(connection_info, disk_dev, instance)
|
||||
|
||||
def _extend_volume(self, connection_info, instance):
|
||||
vol_driver = self._get_volume_driver(connection_info)
|
||||
return vol_driver.extend_volume(connection_info, instance)
|
||||
|
||||
def _get_volume_config(self, connection_info, disk_info):
|
||||
vol_driver = self._get_volume_driver(connection_info)
|
||||
return vol_driver.get_config(connection_info, disk_info)
|
||||
@ -1402,6 +1407,36 @@ class LibvirtDriver(driver.ComputeDriver):
|
||||
|
||||
self._disconnect_volume(connection_info, disk_dev, instance)
|
||||
|
||||
def extend_volume(self, connection_info, instance):
|
||||
try:
|
||||
new_size = self._extend_volume(connection_info, instance)
|
||||
except NotImplementedError:
|
||||
raise exception.ExtendVolumeNotSupported()
|
||||
|
||||
# Resize the device in QEMU so its size is updated and
|
||||
# detected by the instance without rebooting.
|
||||
try:
|
||||
guest = self._host.get_guest(instance)
|
||||
state = guest.get_power_state(self._host)
|
||||
active_state = state in (power_state.RUNNING, power_state.PAUSED)
|
||||
if active_state:
|
||||
disk_path = connection_info['data']['device_path']
|
||||
LOG.debug('resizing block device %(dev)s to %(size)u kb',
|
||||
{'dev': disk_path, 'size': new_size})
|
||||
dev = guest.get_block_device(disk_path)
|
||||
dev.resize(new_size // units.Ki)
|
||||
else:
|
||||
LOG.debug('Skipping block device resize, guest is not running',
|
||||
instance=instance)
|
||||
except exception.InstanceNotFound:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.warning('During extend_volume, instance disappeared.',
|
||||
instance=instance)
|
||||
except libvirt.libvirtError:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.exception('resizing block device failed.',
|
||||