Merge "Snapshot support for XenAPINFS"
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import MetaData, String, Table
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
snapshots = Table('snapshots', meta, autoload=True)
|
||||
provider_location = Column('provider_location', String(255))
|
||||
snapshots.create_column(provider_location)
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
snapshots = Table('snapshots', meta, autoload=True)
|
||||
provider_location = snapshots.columns.provider_location
|
||||
provider_location.drop()
|
||||
@@ -0,0 +1,41 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from sqlalchemy import MetaData, Table
|
||||
from migrate.changeset.constraint import ForeignKeyConstraint
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
snapshots = Table('snapshots', meta, autoload=True)
|
||||
volumes = Table('volumes', meta, autoload=True)
|
||||
|
||||
ForeignKeyConstraint(
|
||||
columns=[snapshots.c.volume_id],
|
||||
refcolumns=[volumes.c.id]).create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
meta = MetaData()
|
||||
meta.bind = migrate_engine
|
||||
|
||||
snapshots = Table('snapshots', meta, autoload=True)
|
||||
volumes = Table('volumes', meta, autoload=True)
|
||||
|
||||
ForeignKeyConstraint(
|
||||
columns=[snapshots.c.volume_id],
|
||||
refcolumns=[volumes.c.id]).drop()
|
||||
@@ -0,0 +1,32 @@
|
||||
-- As sqlite does not support the DROP FOREIGN KEY, we need to create
|
||||
-- the table, and move all the data to it.
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
CREATE TABLE snapshots_v6 (
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
deleted_at DATETIME,
|
||||
deleted BOOLEAN,
|
||||
id VARCHAR(36) NOT NULL,
|
||||
volume_id VARCHAR(36) NOT NULL,
|
||||
user_id VARCHAR(255),
|
||||
project_id VARCHAR(255),
|
||||
status VARCHAR(255),
|
||||
progress VARCHAR(255),
|
||||
volume_size INTEGER,
|
||||
scheduled_at DATETIME,
|
||||
display_name VARCHAR(255),
|
||||
display_description VARCHAR(255),
|
||||
provider_location VARCHAR(255),
|
||||
PRIMARY KEY (id),
|
||||
CHECK (deleted IN (0, 1))
|
||||
);
|
||||
|
||||
INSERT INTO snapshots_v6 SELECT * FROM snapshots;
|
||||
|
||||
DROP TABLE snapshots;
|
||||
|
||||
ALTER TABLE snapshots_v6 RENAME TO snapshots;
|
||||
|
||||
COMMIT;
|
||||
@@ -316,6 +316,14 @@ class Snapshot(BASE, CinderBase):
|
||||
display_name = Column(String(255))
|
||||
display_description = Column(String(255))
|
||||
|
||||
provider_location = Column(String(255))
|
||||
|
||||
volume = relationship(Volume, backref="snapshots",
|
||||
foreign_keys=volume_id,
|
||||
primaryjoin='and_('
|
||||
'Snapshot.volume_id == Volume.id,'
|
||||
'Snapshot.deleted == False)')
|
||||
|
||||
|
||||
class IscsiTarget(BASE, CinderBase):
|
||||
"""Represents an iscsi target for a given host."""
|
||||
|
||||
@@ -350,3 +350,53 @@ class TestMigrations(test.TestCase):
|
||||
autoload=True)
|
||||
self.assertTrue(isinstance(volumes.c.source_volid.type,
|
||||
sqlalchemy.types.VARCHAR))
|
||||
|
||||
def _metadatas(self, upgrade_to, downgrade_to=None):
|
||||
for (key, engine) in self.engines.items():
|
||||
migration_api.version_control(engine,
|
||||
TestMigrations.REPOSITORY,
|
||||
migration.INIT_VERSION)
|
||||
migration_api.upgrade(engine,
|
||||
TestMigrations.REPOSITORY,
|
||||
upgrade_to)
|
||||
|
||||
if downgrade_to is not None:
|
||||
migration_api.downgrade(
|
||||
engine, TestMigrations.REPOSITORY, downgrade_to)
|
||||
|
||||
metadata = sqlalchemy.schema.MetaData()
|
||||
metadata.bind = engine
|
||||
yield metadata
|
||||
|
||||
def metadatas_upgraded_to(self, revision):
|
||||
return self._metadatas(revision)
|
||||
|
||||
def metadatas_downgraded_from(self, revision):
|
||||
return self._metadatas(revision, revision - 1)
|
||||
|
||||
def test_upgrade_006_adds_provider_location(self):
|
||||
for metadata in self.metadatas_upgraded_to(6):
|
||||
snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
|
||||
self.assertTrue(isinstance(snapshots.c.provider_location.type,
|
||||
sqlalchemy.types.VARCHAR))
|
||||
|
||||
def test_downgrade_006_removes_provider_location(self):
|
||||
for metadata in self.metadatas_downgraded_from(6):
|
||||
snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
|
||||
|
||||
self.assertTrue('provider_location' not in snapshots.c)
|
||||
|
||||
def test_upgrade_007_adds_fk(self):
|
||||
for metadata in self.metadatas_upgraded_to(7):
|
||||
snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
|
||||
volumes = sqlalchemy.Table('volumes', metadata, autoload=True)
|
||||
|
||||
fkey, = snapshots.c.volume_id.foreign_keys
|
||||
|
||||
self.assertEquals(volumes.c.id, fkey.column)
|
||||
|
||||
def test_downgrade_007_removes_fk(self):
|
||||
for metadata in self.metadatas_downgraded_from(7):
|
||||
snapshots = sqlalchemy.Table('snapshots', metadata, autoload=True)
|
||||
|
||||
self.assertEquals(0, len(snapshots.c.volume_id.foreign_keys))
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from cinder.db import api as db_api
|
||||
from cinder.volume.drivers.xenapi import lib
|
||||
from cinder.volume.drivers.xenapi import sm as driver
|
||||
import mox
|
||||
@@ -182,3 +183,72 @@ class DriverTestCase(unittest.TestCase):
|
||||
),
|
||||
result
|
||||
)
|
||||
|
||||
def _setup_for_snapshots(self, server, serverpath):
|
||||
mock = mox.Mox()
|
||||
|
||||
drv = driver.XenAPINFSDriver()
|
||||
ops = mock.CreateMock(lib.NFSBasedVolumeOperations)
|
||||
db = mock.CreateMock(db_api)
|
||||
drv.nfs_ops = ops
|
||||
drv.db = db
|
||||
|
||||
mock.StubOutWithMock(driver, 'FLAGS')
|
||||
driver.FLAGS.xenapi_nfs_server = server
|
||||
driver.FLAGS.xenapi_nfs_serverpath = serverpath
|
||||
|
||||
return mock, drv
|
||||
|
||||
def test_create_snapshot(self):
|
||||
mock, drv = self._setup_for_snapshots('server', 'serverpath')
|
||||
|
||||
snapshot = dict(
|
||||
volume_id="volume-id",
|
||||
display_name="snapshot-name",
|
||||
display_description="snapshot-desc",
|
||||
volume=dict(provider_location="sr-uuid/vdi-uuid"))
|
||||
|
||||
drv.nfs_ops.copy_volume(
|
||||
"server", "serverpath", "sr-uuid", "vdi-uuid",
|
||||
"snapshot-name", "snapshot-desc"
|
||||
).AndReturn(dict(sr_uuid="copied-sr", vdi_uuid="copied-vdi"))
|
||||
|
||||
mock.ReplayAll()
|
||||
result = drv.create_snapshot(snapshot)
|
||||
mock.VerifyAll()
|
||||
self.assertEquals(
|
||||
dict(provider_location="copied-sr/copied-vdi"),
|
||||
result)
|
||||
|
||||
def test_create_volume_from_snapshot(self):
|
||||
mock, drv = self._setup_for_snapshots('server', 'serverpath')
|
||||
|
||||
snapshot = dict(
|
||||
provider_location='src-sr-uuid/src-vdi-uuid')
|
||||
volume = dict(
|
||||
display_name='tgt-name', name_description='tgt-desc')
|
||||
|
||||
drv.nfs_ops.copy_volume(
|
||||
"server", "serverpath", "src-sr-uuid", "src-vdi-uuid",
|
||||
"tgt-name", "tgt-desc"
|
||||
).AndReturn(dict(sr_uuid="copied-sr", vdi_uuid="copied-vdi"))
|
||||
|
||||
mock.ReplayAll()
|
||||
result = drv.create_volume_from_snapshot(volume, snapshot)
|
||||
mock.VerifyAll()
|
||||
|
||||
self.assertEquals(
|
||||
dict(provider_location='copied-sr/copied-vdi'), result)
|
||||
|
||||
def test_delete_snapshot(self):
|
||||
mock, drv = self._setup_for_snapshots('server', 'serverpath')
|
||||
|
||||
snapshot = dict(
|
||||
provider_location='src-sr-uuid/src-vdi-uuid')
|
||||
|
||||
drv.nfs_ops.delete_volume(
|
||||
"server", "serverpath", "src-sr-uuid", "src-vdi-uuid")
|
||||
|
||||
mock.ReplayAll()
|
||||
drv.delete_snapshot(snapshot)
|
||||
mock.VerifyAll()
|
||||
|
||||
@@ -135,6 +135,9 @@ class VdiOperations(OperationsBase):
|
||||
def destroy(self, vdi_ref):
|
||||
self.call_xenapi('VDI.destroy', vdi_ref)
|
||||
|
||||
def copy(self, vdi_ref, sr_ref):
|
||||
return self.call_xenapi('VDI.copy', vdi_ref, sr_ref)
|
||||
|
||||
|
||||
class HostOperations(OperationsBase):
|
||||
def get_record(self, host_ref):
|
||||
@@ -255,6 +258,9 @@ class NFSOperationsMixIn(CompoundOperations):
|
||||
vdi_ref = self.VDI.get_by_uuid(vdi_uuid)
|
||||
return dict(sr_ref=sr_ref, vdi_ref=vdi_ref)
|
||||
|
||||
def copy_vdi_to_sr(self, vdi_ref, sr_ref):
|
||||
return self.VDI.copy(vdi_ref, sr_ref)
|
||||
|
||||
|
||||
class ContextAwareSession(XenAPISession):
|
||||
def __enter__(self):
|
||||
@@ -326,3 +332,26 @@ class NFSBasedVolumeOperations(object):
|
||||
vdi_rec = session.VDI.get_record(vdi_ref)
|
||||
sr_ref = vdi_rec['SR']
|
||||
session.unplug_pbds_and_forget_sr(sr_ref)
|
||||
|
||||
def copy_volume(self, server, serverpath, sr_uuid, vdi_uuid,
|
||||
name=None, description=None):
|
||||
with self._session_factory.get_session() as session:
|
||||
src_refs = session.connect_volume(
|
||||
server, serverpath, sr_uuid, vdi_uuid)
|
||||
try:
|
||||
host_ref = session.get_this_host()
|
||||
|
||||
with session.new_sr_on_nfs(host_ref, server, serverpath,
|
||||
name, description) as target_sr_ref:
|
||||
target_vdi_ref = session.copy_vdi_to_sr(
|
||||
src_refs['vdi_ref'], target_sr_ref)
|
||||
|
||||
dst_refs = dict(
|
||||
sr_uuid=session.SR.get_uuid(target_sr_ref),
|
||||
vdi_uuid=session.VDI.get_uuid(target_vdi_ref)
|
||||
)
|
||||
|
||||
finally:
|
||||
session.unplug_pbds_and_forget_sr(src_refs['sr_ref'])
|
||||
|
||||
return dst_refs
|
||||
|
||||
@@ -112,13 +112,31 @@ class XenAPINFSDriver(driver.VolumeDriver):
|
||||
"""To override superclass' method"""
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
raise NotImplementedError()
|
||||
return self._copy_volume(
|
||||
snapshot, volume['display_name'], volume['name_description'])
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
raise NotImplementedError()
|
||||
volume_id = snapshot['volume_id']
|
||||
volume = snapshot['volume']
|
||||
return self._copy_volume(
|
||||
volume, snapshot['display_name'], snapshot['display_description'])
|
||||
|
||||
def _copy_volume(self, volume, target_name, target_desc):
|
||||
sr_uuid, vdi_uuid = volume['provider_location'].split('/')
|
||||
|
||||
volume_details = self.nfs_ops.copy_volume(
|
||||
FLAGS.xenapi_nfs_server,
|
||||
FLAGS.xenapi_nfs_serverpath,
|
||||
sr_uuid,
|
||||
vdi_uuid,
|
||||
target_name,
|
||||
target_desc
|
||||
)
|
||||
location = "%(sr_uuid)s/%(vdi_uuid)s" % volume_details
|
||||
return dict(provider_location=location)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
raise NotImplementedError()
|
||||
self.delete_volume(snapshot)
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user