update['attach_status'] = body['attach_status']
return update
+ @wsgi.action('os-force_detach')
+ def _force_detach(self, req, id, body):
+ """
+ Roll back a bad detach after the volume been disconnected from
+ the hypervisor.
+ """
+ context = req.environ['cinder.context']
+ self.authorize(context, 'force_detach')
+ try:
+ volume = self._get(context, id)
+ except exception.NotFound:
+ raise exc.HTTPNotFound()
+ self.volume_api.terminate_connection(context, volume,
+ {}, force=True)
+ self.volume_api.detach(context, volume)
+ return webob.Response(status_int=202)
+
class SnapshotAdminController(AdminController):
"""AdminController for Snapshots."""
from cinder import db
from cinder import exception
from cinder import test
+from cinder.volume import api as volume_api
from cinder.openstack.common import jsonutils
from cinder.tests.api.openstack import fakes
def setUp(self):
super(AdminActionsTest, self).setUp()
self.flags(rpc_backend='cinder.openstack.common.rpc.impl_fake')
+ self.volume_api = volume_api.API()
def test_reset_status_as_admin(self):
# admin context
# snapshot is deleted
self.assertRaises(exception.NotFound, db.snapshot_get, ctx,
snapshot['id'])
+
+ def test_force_detach_volume(self):
+ # admin context
+ ctx = context.RequestContext('admin', 'fake', True)
+ # current status is available
+ volume = db.volume_create(ctx, {'status': 'available', 'host': 'test',
+ 'provider_location': ''})
+ # start service to handle rpc messages for attach requests
+ self.start_service('volume', host='test')
+ self.volume_api.reserve_volume(ctx, volume)
+ self.volume_api.initialize_connection(ctx, volume, {})
+ mountpoint = '/dev/vbd'
+ self.volume_api.attach(ctx, volume, fakes.FAKE_UUID, mountpoint)
+ # volume is attached
+ volume = db.volume_get(ctx, volume['id'])
+ self.assertEquals(volume['status'], 'in-use')
+ self.assertEquals(volume['instance_uuid'], fakes.FAKE_UUID)
+ self.assertEquals(volume['mountpoint'], mountpoint)
+ self.assertEquals(volume['attach_status'], 'attached')
+ # build request to force detach
+ req = webob.Request.blank('/v1/fake/volumes/%s/action' % volume['id'])
+ req.method = 'POST'
+ req.headers['content-type'] = 'application/json'
+ # request status of 'error'
+ req.body = jsonutils.dumps({'os-force_detach': None})
+ # attach admin context to request
+ req.environ['cinder.context'] = ctx
+ # make request
+ resp = req.get_response(app())
+ # request is accepted
+ self.assertEquals(resp.status_int, 202)
+ volume = db.volume_get(ctx, volume['id'])
+ # status changed to 'available'
+ self.assertEquals(volume['status'], 'available')
+ self.assertEquals(volume['instance_uuid'], None)
+ self.assertEquals(volume['mountpoint'], None)
+ self.assertEquals(volume['attach_status'], 'detached')
'data': {}
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
pass
@staticmethod
conf.set_default('default_volume_type', def_vol_type)
conf.set_default('volume_driver',
'cinder.tests.fake_driver.FakeISCSIDriver')
+ conf.set_default('iscsi_helper', 'fake')
conf.set_default('connection_type', 'fake')
conf.set_default('fake_rabbit', True)
conf.set_default('rpc_backend', 'cinder.openstack.common.rpc.impl_fake')
"volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]],
"volume_extension:volume_admin_actions:force_delete": [["rule:admin_api"]],
"volume_extension:snapshot_admin_actions:force_delete": [["rule:admin_api"]],
+ "volume_extension:volume_admin_actions:force_detach": [["rule:admin_api"]],
"volume_extension:volume_actions:upload_image": [],
"volume_extension:types_manage": [],
"volume_extension:types_extra_specs": [],
"connector": connector}})
@wrap_check_policy
- def terminate_connection(self, context, volume, connector):
+ def terminate_connection(self, context, volume, connector, force=False):
self.unreserve_volume(context, volume)
host = volume['host']
queue = rpc.queue_get_for(context, FLAGS.volume_topic, host)
return rpc.call(context, queue,
{"method": "terminate_connection",
"args": {"volume_id": volume['id'],
- "connector": connector}})
+ "connector": connector, 'force': force}})
def _create_snapshot(self, context, volume, name, description,
force=False):
"""Allow connection to connector and return connection info."""
raise NotImplementedError()
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, force=False, **kwargs):
"""Disallow connection from connector"""
raise NotImplementedError()
'data': iscsi_properties
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
pass
def copy_image_to_volume(self, context, volume, image_service, image_id):
image_service.update(context, image_id, {}, volume_file)
+class FakeISCSIDriver(ISCSIDriver):
+ """Logs calls instead of executing."""
+ def __init__(self, *args, **kwargs):
+ super(FakeISCSIDriver, self).__init__(execute=self.fake_execute,
+ *args, **kwargs)
+
+ def check_for_setup_error(self):
+ """No setup necessary in fake mode."""
+ pass
+
+ def initialize_connection(self, volume, connector):
+ return {
+ 'driver_volume_type': 'iscsi',
+ 'data': {}
+ }
+
+ def terminate_connection(self, volume, connector, **kwargs):
+ pass
+
+ @staticmethod
+ def fake_execute(cmd, *_args, **_kwargs):
+ """Execute that simply logs the command."""
+ LOG.debug(_("FAKE ISCSI: %s"), cmd)
+ return (None, None)
+
+
def _iscsi_location(ip, target, iqn, lun=None):
return "%s:%s,%s %s %s" % (ip, FLAGS.iscsi_port, target, iqn, lun)
}
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
pass
def _parse_location(self, location):
}
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
pass
**kwargs)
+class FakeIscsiHelper(object):
+
+ def __init__(self):
+ self.tid = 1
+
+ def set_execute(self, execute):
+ self._execute = execute
+
+ def create_iscsi_target(self, *args, **kwargs):
+ self.tid += 1
+ return self.tid
+
+
def get_target_admin():
if FLAGS.iscsi_helper == 'tgtadm':
return TgtAdm()
+ elif FLAGS.iscsi_helper == 'fake':
+ return FakeIscsiHelper()
else:
return IetAdm()
volume_ref = self.db.volume_get(context, volume_id)
return self.driver.initialize_connection(volume_ref, connector)
- def terminate_connection(self, context, volume_id, connector):
+ def terminate_connection(self, context, volume_id, connector, force=False):
"""Cleanup connection from host represented by connector.
The format of connector is the same as for initialize_connection.
"""
volume_ref = self.db.volume_get(context, volume_id)
- self.driver.terminate_connection(volume_ref, connector)
+ self.driver.terminate_connection(volume_ref, connector, force=force)
def _volume_stats_changed(self, stat1, stat2):
if FLAGS.volume_force_update_capabilities:
'data': properties,
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Driver entry point to unattach a volume from an instance.
Unmask the LUN on the storage system so the given intiator can no
'data': properties,
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Driver entry point to unattach a volume from an instance.
Unmask the LUN on the storage system so the given intiator can no
'data': data
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Disallow connection from connector"""
pass
'data': iscsi_properties
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
cliq_args = {}
cliq_args['volumeName'] = volume['name']
return {'driver_volume_type': 'iscsi', 'data': properties, }
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Cleanup after an iSCSI connection has been terminated.
When we clean up a terminated connection between a given iSCSI name
'data': properties,
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Driver entry point to unattach a volume from an instance.
Unmask the LUN on the storage system so the given intiator can no
'data': xensm_properties
}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
pass
volume,
connector)
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""Terminate a connection to a volume."""
return self.xiv_proxy.terminate_connection(
return {'driver_volume_type': 'iscsi',
'data': properties}
- def terminate_connection(self, volume, connector):
+ def terminate_connection(self, volume, connector, **kwargs):
"""
Detach volume from the initiator.
"""