--- /dev/null
+# 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()
--- /dev/null
+# 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()
--- /dev/null
+-- 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;
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."""
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))
# 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
),
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()
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):
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):
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
"""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