Change in vdsm[master]: tests: new functional tests for vdsm storage

ykleinbe at redhat.com ykleinbe at redhat.com
Thu Sep 4 14:53:22 UTC 2014


Yoav Kleinberger has uploaded a new change for review.

Change subject: tests: new functional tests for vdsm storage
......................................................................

tests: new functional tests for vdsm storage

Ultimately, the purpose of this patch is to replace the existing
tests/functional/storageTests.py, henceforth "the old test".

The old test does not, in fact, verify VDSM behaviour. It only checks
for the return codes that VDSM returns to its caller.

This patch introduces a framework of "test contexts" that is extensible to various
storage backends. Each test context, be it iscsi, nfs or some other
storage type, knows how to tell VDSM to create its particular type of
storage domain, and also knows how to verify that observable actions
(e.g. the creation of a logical volume in an LVM volume group) have
actually been performed.

Currently, only localfs and iscsi are supported. Other storage backends
will be added later.

Change-Id: I1703e7c1dc223ff707775865cd14c7dd62314caf
Bug-Url: https://bugzilla.redhat.com/??????
Signed-off-by: Yoav Kleinberger <ykleinbe at redhat.com>
---
A tests/functional/basicStorageTest.py
A tests/functional/testlib/__init__.py
A tests/functional/testlib/controlvdsm.py
A tests/functional/testlib/testcontexts/__init__.py
A tests/functional/testlib/testcontexts/base.py
A tests/functional/testlib/testcontexts/iscsi.py
A tests/functional/testlib/testcontexts/localfs.py
A tests/run_functional_storage_tests.sh
8 files changed, 459 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/96/32496/1

diff --git a/tests/functional/basicStorageTest.py b/tests/functional/basicStorageTest.py
new file mode 100644
index 0000000..c78e4b2
--- /dev/null
+++ b/tests/functional/basicStorageTest.py
@@ -0,0 +1,42 @@
+import storage.volume
+import storage.image
+import logging
+from testlib import controlvdsm
+from testlib.testcontexts import localfs
+from testlib.testcontexts import iscsi
+
+class TestBasicStorageDomain:
+    @classmethod
+    def setup_class(cls):
+        logging.basicConfig(level=logging.DEBUG, format='%(asctime)s TEST %(levelname)s: %(message)s')
+
+    def setup(self):
+        controlVDSM = controlvdsm.ControlVDSM()
+        controlVDSM.cleanup()
+
+    def testCreateVolumeLocalFS(self):
+        self._testCreateVolume(localfs.LocalFS)
+
+    def testCreateVolumeISCSI(self):
+        self._testCreateVolume(iscsi.ISCSI)
+
+    def _testCreateVolume(self, storageContext):
+        with storageContext() as (vdsm, verify):
+            storageServerID = vdsm.connectStorageServer()
+            verify.storageServerConnected()
+
+            domainID = vdsm.createStorageDomain()
+            verify.storageDomainCreated(domainID)
+
+            poolID = vdsm.createStoragePool()
+            verify.storagePoolCreated(poolID, masterDomainID = domainID)
+
+            vdsm.connectStoragePool(poolID, masterDomainID = domainID)
+            vdsm.spmStart(poolID)
+            verify.spmStarted(poolID)
+
+            vdsm.activateStorageDomain(domainID, poolID)
+            GIGABYTE = 1024 ** 3
+            volumeInfo = vdsm.createVolume(1 * GIGABYTE)
+            verify.volumeCreated(volumeInfo)
+            vdsm.disconnectStorageServer()
diff --git a/tests/functional/testlib/__init__.py b/tests/functional/testlib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/functional/testlib/__init__.py
diff --git a/tests/functional/testlib/controlvdsm.py b/tests/functional/testlib/controlvdsm.py
new file mode 100644
index 0000000..a983bf3
--- /dev/null
+++ b/tests/functional/testlib/controlvdsm.py
@@ -0,0 +1,53 @@
+import subprocess
+import logging
+import vdsm.vdscli
+import socket
+import vdsm.config
+import time
+
+class ControlVDSM:
+    def cleanup(self):
+        self._stopService()
+        assert not self._serviceRunning()
+        self._brutallyCleanFiles()
+        self._restartService()
+        return self._checkConnection()
+
+    def _checkConnection(self):
+        useSSL = vdsm.config.config.getboolean('vars', 'ssl')
+        vdsmClient = vdsm.vdscli.connect(useSSL=useSSL)
+        RETRIES = 5
+        for _ in range(RETRIES):
+            try:
+                vdsmClient.getStorageDomainsList()
+                logging.info('connection to VDSM succeeded')
+                return
+            except socket.error as e:
+                logging.warning('could not talk to VDSM: %s' % e)
+                time.sleep(1)
+
+        raise Exception('could not connect to VDSM')
+
+    def _stopService(self):
+        self._run("sudo service vdsmd stop")
+
+    def _serviceRunning(self):
+        returnCode = subprocess.call('sudo service vdsmd status', shell=True, stdout=open('/dev/null','w'), stderr=open('/dev/null','w'))
+        logging.info('vdsm running: %s' % (returnCode == 0))
+        return returnCode == 0
+
+    def _restartService(self):
+        self._run("sudo vdsm-tool configure --force")
+        self._run("sudo service vdsmd start")
+
+    def _run(self, command):
+        logging.info('running: %s' % command)
+        returnCode = subprocess.call(command, shell=True, close_fds=True, stdout=open('/dev/null','w'), stderr=open('/dev/null','w'))
+        if returnCode != 0:
+            logging.warning('failure! command was: %s' % command)
+        else:
+            logging.info('finished.')
+
+    def _brutallyCleanFiles(self):
+        logging.warning('removing /rhev/data-center without asking too many questions')
+        self._run('sudo rm -fr /rhev/data-center/*')
diff --git a/tests/functional/testlib/testcontexts/__init__.py b/tests/functional/testlib/testcontexts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/functional/testlib/testcontexts/__init__.py
diff --git a/tests/functional/testlib/testcontexts/base.py b/tests/functional/testlib/testcontexts/base.py
new file mode 100644
index 0000000..bccebdf
--- /dev/null
+++ b/tests/functional/testlib/testcontexts/base.py
@@ -0,0 +1,101 @@
+import os
+import vdsm.vdscli
+import vdsm.config
+import random
+import uuid
+import logging
+import time
+
+class Verify:
+    def __init__(self, vdsm):
+        self._vdsm = vdsm
+
+    def assertPathExists(self, path, link = False):
+        logging.info('verifying path: %s' % path)
+        if link:
+            assert os.path.lexists(path)
+        else:
+            assert os.path.exists(path)
+
+    def assertPathDoesNotExist(self, path, link = False):
+        if link:
+            assert not os.path.lexists(path)
+        else:
+            assert not os.path.exists(path)
+
+    def waitForVDSMToFinishTask(self, duration = 1):
+        time.sleep(duration)
+
+    def storagePoolCreated(self, poolID, masterDomainID):
+        self.waitForVDSMToFinishTask()
+        linkToDomain = os.path.join('/rhev/data-center', poolID, masterDomainID)
+        masterAliasLink = os.path.join('/rhev/data-center', poolID, 'mastersd')
+        logging.info('verifying domain %s in pool %s' % (masterDomainID, poolID))
+        assert os.path.lexists(linkToDomain)
+        logging.info('verifying symlink to master domain')
+        assert os.path.lexists(masterAliasLink)
+
+    def waitFor(self, timeout, description, predicate, *args, **kwargs):
+        logging.info('waiting for "%s"' % description)
+        start = time.time()
+        for _ in xrange(timeout):
+            if predicate(*args, **kwargs):
+                logging.info('it took %s seconds' % (time.time() - start))
+                return
+            time.sleep(1)
+
+        assert False, 'waited %s seconds for "%s" but it did not happen' % (timeout, description)
+
+    def spmStarted(self, poolID):
+        self.waitForVDSMToFinishTask()
+        masterDomainDirectory = '/rhev/data-center/%s/mastersd' % poolID
+        master = os.path.join(masterDomainDirectory, 'master')
+        tasks = os.path.join(master, 'tasks')
+        vms = os.path.join(master, 'vms')
+        allExist = lambda: ( os.path.exists(master) and os.path.exists(tasks) and os.path.exists(vms) )
+        self.waitFor(60, 'SPM related subdirectories exist', allExist)
+
+    def waitOnVDSMTask(self, taskID, timeout):
+        taskFinished = lambda: (self._taskStatus(taskID)['taskState'] == 'finished')
+        self.waitFor(timeout, 'vdsm task to be finished', taskFinished)
+        taskStatus = self._taskStatus(taskID)
+        assert taskStatus['code'] == 0, taskStatus['message']
+
+    def _taskStatus(self, taskID):
+        result = self._vdsm.getTaskStatus(taskID)
+        assert result['status']['code'] == 0
+        taskStatus = result['taskStatus']
+        return taskStatus
+
+class StorageBackend:
+    def __init__(self):
+        useSSL = vdsm.config.config.getboolean('vars', 'ssl')
+        self._vdsm = vdsm.vdscli.connect(useSSL=useSSL)
+
+    def newUUID(self):
+        return str(uuid.uuid4())
+
+    def randomName(self, base):
+        return "%s_%04d" % (base, random.randint(1,10000))
+
+    def connectStoragePool(self, poolID, masterDomainID):
+        SCSI_KEY_DEPRECATED = 0
+        result = self._vdsm.connectStoragePool(poolID, 1, SCSI_KEY_DEPRECATED, masterDomainID, 1)
+        self.verifyVDSMSuccess(result)
+
+    def spmStart(self, poolID):
+        RECOVERY_MODE_DEPRECATED = 0
+        SCSI_FENCING_DEPRECATED = 0
+        result = self._vdsm.spmStart(poolID, -1, '-1', SCSI_FENCING_DEPRECATED, RECOVERY_MODE_DEPRECATED)
+        self.verifyVDSMSuccess(result)
+
+    def activateStorageDomain(self, domainID, poolID):
+        result = self._vdsm.activateStorageDomain(domainID,poolID)
+        self.verifyVDSMSuccess(result)
+
+    def stringForXMLRPC(self, number):
+        return str(number)
+
+    def verifyVDSMSuccess(self, result):
+        if result[ 'status' ][ 'code' ] != 0:
+            raise Exception('expected OK result from VDSM, got "%s" instead' % str(result))
diff --git a/tests/functional/testlib/testcontexts/iscsi.py b/tests/functional/testlib/testcontexts/iscsi.py
new file mode 100644
index 0000000..96d6958
--- /dev/null
+++ b/tests/functional/testlib/testcontexts/iscsi.py
@@ -0,0 +1,150 @@
+import os
+import random
+import logging
+import tempfile
+import shutil
+import subprocess
+import glob
+import storage.sd
+import storage.volume
+import storage.image
+from . import base
+
+class Verify(base.Verify):
+    def __init__(self, iqn, volumeGroup, vdsm, volumeID):
+        base.Verify.__init__(self, vdsm)
+        self._iqn = iqn
+        self._volumeGroup = volumeGroup
+        self._volumeID = volumeID
+
+    def storageServerConnected(self):
+        targetNameFiles = glob.glob('/sys/devices/platform/host*/session*/iscsi_session/*/targetname')
+        targetNames = [ open(file).read().strip() for file in targetNameFiles ]
+        assert self._iqn in targetNames
+
+    def storageDomainCreated(self, domainID):
+        self.waitFor(10, 'storage domain exists', self._storageDomainVolumesExist, domainID)
+
+    def _storageDomainVolumesExist(self, domainID):
+        for name in [ 'ids', 'inbox', 'leases', 'master', 'metadata', 'outbox' ]:
+            doubleDashed = domainID.replace('-', '--')
+            expectedPath = os.path.join('/dev/mapper', '%s-%s' % (doubleDashed, name))
+            logging.info('verifying: %s' % expectedPath)
+            if not os.path.lexists(expectedPath):
+                return False
+
+        return True
+
+    def volumeCreated(self, taskID):
+        self.waitOnVDSMTask(taskID, 10)
+        result = subprocess.call('sudo lvs %s | grep %s' % (self._volumeGroup['uuid'], self._volumeID), shell = True)
+        assert result == 0, "did not find logical volume in volume group"
+
+class ISCSI(base.StorageBackend):
+    _NULL_UUID = '00000000-0000-0000-0000-000000000000'
+    def __init__(self):
+        base.StorageBackend.__init__(self)
+        self._iqn = 'iqn.1970-01.functional.test:%04d' % random.randint(1,10000)
+        self._volumeGroup = { 'uuid': self.newUUID(), 'vgs_uuid': None }
+        self._poolID = self.newUUID()
+        self._connectionID = self.newUUID()
+        self._imageID = self.newUUID()
+        self._volumeID = self.newUUID()
+        logging.info( 'using the following attributes:' )
+        for name in [ '_volumeGroup', '_iqn', '_poolID', '_connectionID', '_imageID', '_volumeID' ]:
+            logging.info( '%s => %s' % ( name, getattr(self,name) ) )
+
+    def _targetcli(self, command):
+        commandAsList = command.split()
+        logging.info('running targetcli: targetcli %s' % commandAsList)
+        subprocess.check_call( [ 'targetcli' ] + commandAsList, close_fds = True )
+
+    def __enter__(self):
+        self._testDirectory = tempfile.mkdtemp()
+        self._storageFile = os.path.join(self._testDirectory, 'testfile')
+        self._fileioBackstore = self.randomName('backfile')
+        logging.info('using %s, %s' % (self._fileioBackstore, self._storageFile))
+        self._targetcli('/backstores/fileio create %s %s 10G' % (self._fileioBackstore, self._storageFile))
+        self._targetcli('/iscsi create %s' % self._iqn)
+        self._targetcli('/iscsi/%s/tpg1/luns create /backstores/fileio/%s' % (self._iqn, self._fileioBackstore))
+        self._targetcli('/iscsi/%s/tpg1 set attribute authentication=0 demo_mode_write_protect=0' % self._iqn)
+        self._targetcli('/iscsi/%s/tpg1 set attribute generate_node_acls=1 cache_dynamic_acls=1' % self._iqn)
+        return self, Verify(self._iqn, self._volumeGroup, self._vdsm, self._volumeID)
+
+    def __exit__(self, *args):
+        doubleDashed = self._volumeGroup['uuid'].replace('-', '--')
+        mapperDevices = glob.glob('/dev/mapper/%s*' % doubleDashed ) + [ '/dev/mapper/%s' % self._lunGUID ]
+        for device in mapperDevices:
+            logging.info('removing %s' % device)
+            result = subprocess.call('sudo dmsetup remove %s' % device, shell=True)
+            if result != 0:
+                logging.warning('could not remove %s' % device)
+        self._targetcli( '/iscsi delete %s' % self._iqn )
+        self._targetcli( '/backstores/fileio delete %s' % self._fileioBackstore )
+        shutil.rmtree(self._testDirectory)
+
+    def connectStorageServer(self):
+        connection = {'connection': '127.0.0.1', 'iqn': self._iqn, 'user': '', 'tpgt': '1', 'password': '', 'id': self._NULL_UUID, 'port': '3260'}
+        result = self._vdsm.connectStorageServer(
+                                storage.sd.ISCSI_DOMAIN,
+                                self._NULL_UUID,
+                                [ connection ])
+        self.verifyVDSMSuccess(result)
+        connectionDetails = result['statuslist'][0]
+        assert connectionDetails['status'] == 0
+        logging.info( 'got id %s', connectionDetails['id'] )
+
+    def _findLUN(self):
+        result = self._vdsm.getDeviceList(storage.sd.ISCSI_DOMAIN)
+        for device in result['devList']:
+            for path in device['pathlist']:
+                if path['iqn'] == self._iqn:
+                    logging.info( 'vdsm reports our LUN %s' % device )
+                    return device
+
+        raise Exception("LUN not found!")
+
+    def disconnectStorageServer(self):
+        connection = {'connection': '127.0.0.1', 'iqn': self._iqn, 'user': '', 'tpgt': '1', 'password': '', 'id': self._connectionID, 'port': '3260'}
+        result = self._vdsm.disconnectStorageServer(
+                                storage.sd.ISCSI_DOMAIN,
+                                self._poolID,
+                                [ connection ])
+        self.verifyVDSMSuccess(result)
+
+    def createStoragePool(self):
+        POOL_TYPE_DEPRECATED = 0
+        result = self._vdsm.createStoragePool(POOL_TYPE_DEPRECATED, self._poolID, self.randomName('pool'), self._domainID(), [ self._domainID() ], 1)
+        self.verifyVDSMSuccess(result)
+        return self._poolID
+
+    def _domainID(self):
+        return self._volumeGroup['uuid']
+
+    def _createVG(self):
+        lun = self._findLUN()
+        result = self._vdsm.createVG(self._volumeGroup['uuid'], [ lun['GUID'] ])
+        logging.info('createVG returned %s' % result)
+        self.verifyVDSMSuccess(result)
+        self._volumeGroup[ 'vgs_uuid' ] = result[ 'uuid' ]
+        self._lunGUID = lun['GUID']
+
+    def createStorageDomain(self):
+        self._createVG()
+        self._createStorageDomain()
+        return self._volumeGroup['uuid']
+
+    def _createStorageDomain(self):
+        domainID = self._volumeGroup['uuid']
+        DOMAIN_VERSION = 3
+        domainName = self.randomName( 'some_name' )
+        result = self._vdsm.createStorageDomain(storage.sd.ISCSI_DOMAIN, domainID, domainName, self._volumeGroup['vgs_uuid'], storage.sd.DATA_DOMAIN, DOMAIN_VERSION)
+        self.verifyVDSMSuccess(result)
+
+    def createVolume(self, size):
+        PREALLOCATE = 1
+        result = self._vdsm.createVolume(self._domainID(), self._poolID, self._imageID, 
+                    self.stringForXMLRPC(size), storage.volume.RAW_FORMAT, PREALLOCATE, storage.image.DATA_DISK_TYPE, self._volumeID, self.randomName('iscsi_description'))
+        logging.info( 'createVolume result: %s' % result)
+        self.verifyVDSMSuccess(result)
+        return result[ 'uuid' ]
diff --git a/tests/functional/testlib/testcontexts/localfs.py b/tests/functional/testlib/testcontexts/localfs.py
new file mode 100644
index 0000000..b498b6e
--- /dev/null
+++ b/tests/functional/testlib/testcontexts/localfs.py
@@ -0,0 +1,104 @@
+import tempfile
+import pwd
+import shutil
+import os
+import storage.sd
+import storage.volume
+import storage.image
+from . import base
+
+class Verify(base.Verify):
+    def __init__(self, directory, vdsm):
+        base.Verify.__init__(self, vdsm)
+        self._directory = directory
+
+    def storageServerConnected(self):
+        self.waitForVDSMToFinishTask(2)
+        transformedDirectory = self._directory.replace( '/', '_' )
+        expectedSymlink = os.path.join('/rhev/data-center/mnt/', transformedDirectory)
+        self.assertPathExists(expectedSymlink, link = True)
+
+    def storageDomainCreated(self, domainID):
+        self.waitForVDSMToFinishTask()
+        self._directIOTestFileExists()
+        domainRoot = os.path.join(self._directory, domainID)
+        expectedFiles = [ 'images', 'dom_md', 'dom_md/leases', 'dom_md/inbox', 'dom_md/outbox', 'dom_md/metadata', 'dom_md/ids' ]
+        expectedFullPaths = [ os.path.join(domainRoot, path) for path in expectedFiles ]
+        for path in expectedFullPaths:
+            assert os.path.exists(path)
+
+    def _directIOTestFileExists(self):
+        self.waitForVDSMToFinishTask()
+        directIOTestFile = os.path.join(self._directory, '__DIRECT_IO_TEST__')
+        self.assertPathExists(directIOTestFile)
+
+    def volumeCreated(self, volumeInfo):
+        taskID, domainID, imageID, volumeID = volumeInfo
+        self.waitOnVDSMTask(taskID, 20)
+        domain_directory = os.path.join(self._directory, domainID)
+        image_directory = os.path.join(domain_directory, 'images', imageID)
+        self.assertPathExists(image_directory)
+        volume_file = os.path.join(image_directory, volumeID)
+        volume_lease = '%s.lease' % volume_file
+        volume_meta = '%s.meta' % volume_file
+        for path in volume_file, volume_lease, volume_meta:
+            self.assertPathExists(path)
+
+class LocalFS(base.StorageBackend):
+    _NON_EXISTANT_POOL = '00000000-0000-0000-0000-000000000000'
+    def __init__(self):
+        base.StorageBackend.__init__(self)
+        self._domainID = self.newUUID()
+        self._poolID = self.newUUID()
+        self._imageID = self.newUUID()
+        self._volumeID = self.newUUID()
+        self._connectionID = self.newUUID()
+
+    def __enter__(self):
+        self._createDirectoryForLocalFSStorage()
+        return self, Verify(self._directory, self._vdsm)
+
+    def _createDirectoryForLocalFSStorage(self):
+        self._directory = tempfile.mkdtemp('', 'localfstest', '/var/tmp')
+        vdsm_user = pwd.getpwnam('vdsm')
+        os.chown(self._directory, vdsm_user.pw_uid, vdsm_user.pw_gid)
+        os.chmod(self._directory, 0755)
+
+    def __exit__(self, *args):
+        shutil.rmtree(self._directory)
+
+    def connectStorageServer(self):
+        localFilesystemConnection = {'connection': self._directory, 'iqn': '', 'user': '', 'tpgt': '1', 'password': '******', 'id': self._connectionID, 'port': ''}
+        result = self._vdsm.connectStorageServer(
+                                storage.sd.LOCALFS_DOMAIN,
+                                self._NON_EXISTANT_POOL,
+                                [ localFilesystemConnection ])
+        self.verifyVDSMSuccess(result)
+        assert result[ 'statuslist' ][ 0 ][ 'id' ] == self._connectionID
+
+    def disconnectStorageServer(self):
+        localFilesystemConnection = {'connection': self._directory, 'iqn': '', 'user': '', 'tpgt': '1', 'password': '******', 'id': self._connectionID, 'port': ''}
+        result = self._vdsm.disconnectStorageServer(
+                                storage.sd.LOCALFS_DOMAIN,
+                                self._NON_EXISTANT_POOL,
+                                [ localFilesystemConnection ])
+        self.verifyVDSMSuccess(result)
+
+    def createStoragePool(self):
+        POOL_TYPE_DEPRECATED = 0
+        result = self._vdsm.createStoragePool(POOL_TYPE_DEPRECATED, self._poolID, self.randomName('pool'), self._domainID, [ self._domainID ], 1)
+        self.verifyVDSMSuccess(result)
+        return self._poolID
+
+    def createStorageDomain(self):
+        DOMAIN_VERSION = 3
+        result = self._vdsm.createStorageDomain(storage.sd.LOCALFS_DOMAIN, self._domainID, 'some_name', self._directory, storage.sd.DATA_DOMAIN, DOMAIN_VERSION)
+        self.verifyVDSMSuccess(result)
+        return self._domainID
+
+    def createVolume(self, size):
+        PREALLOCATE = 1
+        result = self._vdsm.createVolume(self._domainID, self._poolID, self._imageID, self.stringForXMLRPC(size), storage.volume.RAW_FORMAT, PREALLOCATE, storage.image.DATA_DISK_TYPE, self._volumeID, self.randomName('localfs_description'))
+        self.verifyVDSMSuccess(result)
+        taskID = result[ 'uuid' ]
+        return taskID, self._domainID, self._imageID, self._volumeID
diff --git a/tests/run_functional_storage_tests.sh b/tests/run_functional_storage_tests.sh
new file mode 100755
index 0000000..cb544ee
--- /dev/null
+++ b/tests/run_functional_storage_tests.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+if [[ "$1" = "--help" ]]; then
+    echo "run_functional_storage_tests.sh [--verbose]"
+    exit -1
+fi
+if [[ "$1" = "--verbose" ]]; then
+    verbose="--nologcapture"
+fi
+sudo PYTHONPATH=../lib:../vdsm:functional nosetests -s $verbose functional/basicStorageTest.py


-- 
To view, visit http://gerrit.ovirt.org/32496
To unsubscribe, visit http://gerrit.ovirt.org/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I1703e7c1dc223ff707775865cd14c7dd62314caf
Gerrit-PatchSet: 1
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Yoav Kleinberger <ykleinbe at redhat.com>


More information about the vdsm-patches mailing list