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