From bbe0f313bdfd30cc1c740709543b679567b42f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Gagne=CC=81?= Date: Wed, 12 Jul 2017 10:59:55 -0400 Subject: [PATCH] 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 Blueprint: nova-support-attached-volume-extend Change-Id: If10cffd0dc4c9879f6754ce39bee5fae1d04f474 --- api-ref/source/os-instance-actions.inc | 10 +- api-ref/source/parameters.yaml | 26 +++- .../instance-action-get-non-admin-resp.json | 25 ++++ .../v2.51/instance-action-get-resp.json | 27 +++++ .../v2.51/instance-actions-list-resp.json | 22 ++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- doc/source/support-matrix.ini | 26 ++++ nova/api/openstack/api_version_request.py | 5 +- .../api/openstack/compute/instance_actions.py | 25 +++- .../compute/rest_api_version_history.rst | 17 ++- .../compute/schemas/server_external_events.py | 12 +- .../compute/server_external_events.py | 19 ++- nova/compute/api.py | 7 ++ nova/compute/instance_actions.py | 8 ++ nova/compute/manager.py | 52 ++++++++ nova/exception.py | 4 + nova/objects/external_event.py | 5 +- ...nstance-action-get-non-admin-resp.json.tpl | 25 ++++ .../v2.51/instance-action-get-resp.json.tpl | 27 +++++ .../v2.51/instance-actions-list-resp.json.tpl | 22 ++++ .../api_sample_tests/test_instance_actions.py | 35 +++++- .../compute/test_server_external_events.py | 21 +++- nova/tests/unit/compute/test_compute_api.py | 31 +++-- nova/tests/unit/compute/test_compute_mgr.py | 111 +++++++++++++++++- nova/tests/unit/objects/test_objects.py | 2 +- nova/tests/unit/virt/libvirt/test_driver.py | 102 ++++++++++++++++ .../virt/libvirt/volume/test_fibrechannel.py | 14 +++ .../unit/virt/libvirt/volume/test_iscsi.py | 13 ++ nova/virt/driver.py | 13 ++ nova/virt/fake.py | 9 +- nova/virt/libvirt/driver.py | 35 ++++++ nova/virt/libvirt/volume/fibrechannel.py | 9 ++ nova/virt/libvirt/volume/iscsi.py | 9 ++ nova/virt/libvirt/volume/volume.py | 4 + ...tached-volume-extend-88ce16ce41aa6d41.yaml | 17 +++ 36 files changed, 759 insertions(+), 34 deletions(-) create mode 100644 doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json.tpl create mode 100644 releasenotes/notes/nova-support-attached-volume-extend-88ce16ce41aa6d41.yaml diff --git a/api-ref/source/os-instance-actions.inc b/api-ref/source/os-instance-actions.inc index 99045499c0de..c2ac18b5f780 100644 --- a/api-ref/source/os-instance-actions.inc +++ b/api-ref/source/os-instance-actions.inc @@ -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 diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 49e3ecfeca2b..150395fbf527 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -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. diff --git a/doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json b/doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json new file mode 100644 index 000000000000..7043b4531f9e --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json @@ -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" + } +} diff --git a/doc/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json b/doc/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json new file mode 100644 index 000000000000..cbb6236c4eef --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json @@ -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" + } +} diff --git a/doc/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json b/doc/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json new file mode 100644 index 000000000000..b9ae31ac53db --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json @@ -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" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 31940e755376..2d9451e7e476 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.50", + "version": "2.51", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index e410a26fb0ae..44b0ef5e4214 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.50", + "version": "2.51", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/source/support-matrix.ini b/doc/source/support-matrix.ini index 54aff6222212..caa02bc2e25e 100644 --- a/doc/source/support-matrix.ini +++ b/doc/source/support-matrix.ini @@ -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 +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 diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index 8dd63cc28e55..e14dcac3fb27 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -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 diff --git a/nova/api/openstack/compute/instance_actions.py b/nova/api/openstack/compute/instance_actions.py index 1142cc61b810..7f2ff0df9a13 100644 --- a/nova/api/openstack/compute/instance_actions.py +++ b/nova/api/openstack/compute/instance_actions.py @@ -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} diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 3c33a648c3b3..dece5f169903 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -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. diff --git a/nova/api/openstack/compute/schemas/server_external_events.py b/nova/api/openstack/compute/schemas/server_external_events.py index c8c7ebdc238b..38435e0dc5cd 100644 --- a/nova/api/openstack/compute/schemas/server_external_events.py +++ b/nova/api/openstack/compute/schemas/server_external_events.py @@ -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') diff --git a/nova/api/openstack/compute/server_external_events.py b/nova/api/openstack/compute/server_external_events.py index 0af8d0aa612f..483ea975c1ea 100644 --- a/nova/api/openstack/compute/server_external_events.py +++ b/nova/api/openstack/compute/server_external_events.py @@ -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 ' diff --git a/nova/compute/api.py b/nova/compute/api.py index ebfe72012481..1eb2d337382a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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) diff --git a/nova/compute/instance_actions.py b/nova/compute/instance_actions.py index d8be784bf069..2faf1c4fa3ac 100644 --- a/nova/compute/instance_actions.py +++ b/nova/compute/instance_actions.py @@ -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' diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 73dd64b8a7d1..d8723bb9f7a9 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -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) diff --git a/nova/exception.py b/nova/exception.py index ff7fd9afed9a..8c28c1ac7f80 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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") diff --git a/nova/objects/external_event.py b/nova/objects/external_event.py index f31c4dafe9f9..cfcc01c02725 100644 --- a/nova/objects/external_event.py +++ b/nova/objects/external_event.py @@ -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(), diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json.tpl new file mode 100644 index 000000000000..a8f6c48e4005 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-non-admin-resp.json.tpl @@ -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" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json.tpl new file mode 100644 index 000000000000..fb7f780cad91 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-action-get-resp.json.tpl @@ -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": "" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json.tpl new file mode 100644 index 000000000000..0fdc33916a80 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.51/instance-actions-list-resp.json.tpl @@ -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": "" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_instance_actions.py b/nova/tests/functional/api_sample_tests/test_instance_actions.py index 04e4b60e49ef..36c93fa45325 100644 --- a/nova/tests/functional/api_sample_tests/test_instance_actions.py +++ b/nova/tests/functional/api_sample_tests/test_instance_actions.py @@ -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'})] diff --git a/nova/tests/unit/api/openstack/compute/test_server_external_events.py b/nova/tests/unit/api/openstack/compute/test_server_external_events.py index e9236eb54dbd..645a326b647c 100644 --- a/nova/tests/unit/api/openstack/compute/test_server_external_events.py +++ b/nova/tests/unit/api/openstack/compute/test_server_external_events.py @@ -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) diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index 2c441abbc0ce..f7d48a662890 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -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): diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 5e5a32161b3c..106d021e90df 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -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): diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 6263ef8a8d4f..07c360d589ce 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -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', diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 064290faff81..543a3fc01006 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -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 = "" + 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) diff --git a/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py b/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py index 64e73fe8a6f8..338bb6b58dd8 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py +++ b/nova/tests/unit/virt/libvirt/volume/test_fibrechannel.py @@ -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']) diff --git a/nova/tests/unit/virt/libvirt/volume/test_iscsi.py b/nova/tests/unit/virt/libvirt/volume/test_iscsi.py index 4694e5f17a95..8192bcfc0702 100644 --- a/nova/tests/unit/virt/libvirt/volume/test_iscsi.py +++ b/nova/tests/unit/virt/libvirt/volume/test_iscsi.py @@ -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']) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index c04340420272..87cb782eff47 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -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. diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 5b551187010b..61ed67fe3cd9 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -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( diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 2074a875e2be..640ad7dff5dd 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -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.', + instance=instance) + def attach_interface(self, context, instance, image_meta, vif): guest = self._host.get_guest(instance) diff --git a/nova/virt/libvirt/volume/fibrechannel.py b/nova/virt/libvirt/volume/fibrechannel.py index 0474215f7524..1d0523f50fb1 100644 --- a/nova/virt/libvirt/volume/fibrechannel.py +++ b/nova/virt/libvirt/volume/fibrechannel.py @@ -73,3 +73,12 @@ class LibvirtFibreChannelVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver): super(LibvirtFibreChannelVolumeDriver, self).disconnect_volume(connection_info, disk_dev, instance) + + def extend_volume(self, connection_info, instance): + """Extend the volume.""" + LOG.debug("calling os-brick to extend FC Volume", instance=instance) + new_size = self.connector.extend_volume(connection_info['data']) + LOG.debug("Extend FC Volume %s; new_size=%s", + connection_info['data']['device_path'], + new_size, instance=instance) + return new_size diff --git a/nova/virt/libvirt/volume/iscsi.py b/nova/virt/libvirt/volume/iscsi.py index 44a7d80a5f73..2f79a56571aa 100644 --- a/nova/virt/libvirt/volume/iscsi.py +++ b/nova/virt/libvirt/volume/iscsi.py @@ -78,3 +78,12 @@ class LibvirtISCSIVolumeDriver(libvirt_volume.LibvirtBaseVolumeDriver): super(LibvirtISCSIVolumeDriver, self).disconnect_volume(connection_info, disk_dev, instance) + + def extend_volume(self, connection_info, instance): + """Extend the volume.""" + LOG.debug("calling os-brick to extend iSCSI Volume", instance=instance) + new_size = self.connector.extend_volume(connection_info['data']) + LOG.debug("Extend iSCSI Volume %s; new_size=%s", + connection_info['data']['device_path'], + new_size, instance=instance) + return new_size diff --git a/nova/virt/libvirt/volume/volume.py b/nova/virt/libvirt/volume/volume.py index 3498788ca4c4..327aeca050b2 100644 --- a/nova/virt/libvirt/volume/volume.py +++ b/nova/virt/libvirt/volume/volume.py @@ -114,6 +114,10 @@ class LibvirtBaseVolumeDriver(object): """Disconnect the volume.""" pass + def extend_volume(self, connection_info, instance): + """Extend the volume.""" + raise NotImplementedError() + class LibvirtVolumeDriver(LibvirtBaseVolumeDriver): """Class for volumes backed by local file.""" diff --git a/releasenotes/notes/nova-support-attached-volume-extend-88ce16ce41aa6d41.yaml b/releasenotes/notes/nova-support-attached-volume-extend-88ce16ce41aa6d41.yaml new file mode 100644 index 000000000000..6c8650280146 --- /dev/null +++ b/releasenotes/notes/nova-support-attached-volume-extend-88ce16ce41aa6d41.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + It is now possible to signal and perform an online volume size change + as of the 2.51 microversion using the ``volume-extended`` external event. + Nova will perform the volume extension so the host can detect its new size. + It will also resize the device in QEMU so instance can detect + the new disk size without rebooting. + + Currently only the libvirt compute driver with iSCSI and FC volumes + supports the online volume size change. + - | + The 2.51 microversion exposes 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.