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:
Mathieu Gagné 2017-07-12 10:59:55 -04:00 committed by Matt Riedemann
parent d2d84eb102
commit bbe0f313bd
36 changed files with 759 additions and 34 deletions

View File

@ -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

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.50",
"version": "2.51",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.50",
"version": "2.51",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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')

View File

@ -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 '

View File

@ -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)

View File

@ -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'

View File

@ -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)

View File

@ -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")

View File

@ -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(),

View File

@ -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"
}
]
}
}

View File

@ -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": ""
}
]
}
}

View File

@ -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": ""
}
]
}

View File

@ -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'})]

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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',

View File

@ -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)

View File

@ -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'])

View File

@ -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'])

View File

@ -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.

View File

@ -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(

View File

@ -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.',