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.