summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Gracik <mgracik@redhat.com>2009-03-24 11:05:40 +0100
committerMartin Gracik <mgracik@redhat.com>2009-03-24 14:41:55 +0100
commitac83df7815ff3db042875db51a43c107e982ee70 (patch)
treea8b67d04cf0b00ea92b6fa1d4c162123dc9fa0e3
downloadanate-ac83df7815ff3db042875db51a43c107e982ee70.tar.gz
anate-ac83df7815ff3db042875db51a43c107e982ee70.tar.xz
anate-ac83df7815ff3db042875db51a43c107e982ee70.zip
Initial commitHEADmaster
-rw-r--r--anate/__init__.py0
-rw-r--r--anate/baseutils.py44
-rw-r--r--anate/devices.py234
-rw-r--r--anate/vm.py135
-rwxr-xr-xrun_test.py107
-rw-r--r--tests/__init__.py20
-rw-r--r--tests/devices.py96
-rw-r--r--tests/vm.py55
8 files changed, 691 insertions, 0 deletions
diff --git a/anate/__init__.py b/anate/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/anate/__init__.py
diff --git a/anate/baseutils.py b/anate/baseutils.py
new file mode 100644
index 0000000..f084e64
--- /dev/null
+++ b/anate/baseutils.py
@@ -0,0 +1,44 @@
+import logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger('anate.baseutils')
+
+import os
+import subprocess
+
+
+class Subprocess(object):
+ def __init__(self, cmd, *args):
+ self._cmd = [cmd]
+ self._args = map(str, list(args))
+
+ self._rc = None
+ self._stdout = []
+ self._stderr = []
+
+ def run(self):
+ logger.info('running command %s', self._cmd + self._args)
+ proc = subprocess.Popen(self._cmd + self._args,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ while True:
+ (out_str, err_str) = proc.communicate()
+ if out_str:
+ self._stdout.append(out_str)
+ if err_str:
+ self._stderr.append(err_str)
+
+ if proc.returncode is not None:
+ self._rc = proc.returncode
+ break
+
+ @property
+ def rc(self):
+ return self._rc
+
+ @property
+ def stdout(self):
+ return self._stdout
+
+ @property
+ def stderr(self):
+ return self._stderr
diff --git a/anate/devices.py b/anate/devices.py
new file mode 100644
index 0000000..cdc845e
--- /dev/null
+++ b/anate/devices.py
@@ -0,0 +1,234 @@
+import logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger('anate.devices')
+
+import os
+import stat
+import pwd
+import grp
+import tempfile
+from baseutils import Subprocess
+
+
+def getDeviceNumbers(path):
+ try:
+ statinfo = os.stat(path)
+ except OSError:
+ return (None, None)
+
+ return (os.major(statinfo.st_rdev), os.minor(statinfo.st_rdev))
+
+
+class BlockDeviceNode(object):
+ def __init__(self, path, major, minor):
+ self._path = path
+ self._major = major
+ self._minor = minor
+
+ def create(self, mode=0660, owner='root', group='disk'):
+ try:
+ os.mknod(self.path, mode | stat.S_IFBLK, os.makedev(self.major, self.minor))
+ except OSError:
+ logger.error('unable to create node %s', self.path)
+ return
+
+ try:
+ uid = pwd.getpwnam(owner)[2]
+ except KeyError:
+ logger.warning('user %s does not exist, not changing the owner', owner)
+ uid = -1
+
+ try:
+ gid = grp.getgrnam(group)[2]
+ except KeyError:
+ logger.warning('group %s does not exist, not changing the group', group)
+ gid = -1
+
+ try:
+ os.chown(self.path, uid, gid)
+ except OSError:
+ logger.warning('unable to change the owner and group of node %s', self.path)
+
+ def unlink(self):
+ try:
+ os.unlink(self.path)
+ except OSError:
+ logger.error('unable to unlink node %s', self.path)
+
+ @property
+ def path(self):
+ return self._path
+
+ @property
+ def major(self):
+ return self._major
+
+ @major.setter
+ def major(self, value):
+ self._major = value
+
+ @property
+ def minor(self):
+ return self._minor
+
+ @minor.setter
+ def minor(self, value):
+ self._minor = value
+
+ @property
+ def available(self):
+ return os.access(self.path, os.R_OK | os.W_OK)
+
+
+class BlockDevice(object):
+ def __init__(self, name, node):
+ if not isinstance(node, BlockDeviceNode):
+ raise TypeError, 'node must be an instance of BlockDeviceNode, got %s' % type(node)
+
+ self._name = name
+ self._node = node
+ self._active = False
+
+ def initialize(self):
+ self._active = True
+
+ def destroy(self):
+ self._active = False
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def path(self):
+ return self._node.path
+
+ @property
+ def major(self):
+ return self._node.major
+
+ @property
+ def minor(self):
+ return self._node.minor
+
+ @property
+ def active(self):
+ return self._active
+
+
+class StorageDevice(BlockDevice):
+ def __init__(self, name, node, size=0):
+ BlockDevice.__init__(self, name, node)
+ self._size = size
+
+ @property
+ def size(self):
+ return self._size
+
+
+class LoopDevice(StorageDevice):
+ def __init__(self, name, node, destination_file, size):
+ StorageDevice.__init__(self, name, node, size)
+ self._file = destination_file
+
+ def initialize(self):
+ proc = Subprocess('dd', 'if=/dev/zero', 'of=%s' % self._file, 'bs=1k', 'seek=%d' % (self.size * 1024 - 1), 'count=1')
+ proc.run()
+ if proc.rc:
+ logger.error('%s', proc.stderr)
+ raise OSError, 'dd failed when creating file %s' % self._file
+
+ proc = Subprocess('losetup', self.path, self._file)
+ proc.run()
+ if proc.rc:
+ logger.error('%s', proc.stderr)
+ raise OSError, 'losetup failed when setting up loop device %s' % self.path
+
+ self._active = True
+
+ def destroy(self):
+ proc = Subprocess('losetup', '-d', self.path)
+ proc.run()
+ if proc.rc:
+ logger.error('%s', proc.stderr)
+ raise OSError, 'losetup failed when removing loop device %s' % self.path
+
+ os.unlink(self._file)
+
+ self._active = False
+
+
+class DMDevice(StorageDevice):
+
+ _DM_DEVICES_ROOT_PATH = '/dev/mapper/'
+ _SECTORS_PER_MB = 2048
+
+ def __init__(self, name, destination_device, size=None):
+ if not isinstance(destination_device, StorageDevice):
+ raise TypeError, 'destination_device must be an instance of StorageDevice, got %s' % type(destination_device)
+ if not destination_device.active:
+ destination_device.initialize()
+
+ usable_size = destination_device.size
+ if not size:
+ size = usable_size
+ elif size > usable_size:
+ logger.warning('creating dm device which is bigger than the destination device')
+ else:
+ usable_size = size
+
+ node = BlockDeviceNode(os.path.join(self._DM_DEVICES_ROOT_PATH, name), major=None, minor=None)
+
+ StorageDevice.__init__(self, name, node, size)
+ self._destination_device = destination_device
+ self._usable_size = usable_size
+
+ def initialize(self):
+ table_file = tempfile.NamedTemporaryFile()
+
+ map = {'logical_start_sector': 0,
+ 'num_sectors': self.usable_size * self._SECTORS_PER_MB,
+ 'target_type': 'linear',
+ 'destination_device': self._destination_device.path,
+ 'destination_device_start_sector': 0}
+ string = '%(logical_start_sector)d %(num_sectors)d %(target_type)s %(destination_device)s %(destination_device_start_sector)d\n' % map
+ table_file.write(string)
+ table_file.flush()
+ logger.info('table_file: %s', string.strip())
+
+ if self.size > self.usable_size:
+ map = {'logical_start_sector': self.usable_size * self._SECTORS_PER_MB,
+ 'num_sectors': (self.size - self.usable_size) * self._SECTORS_PER_MB,
+ 'target_type': 'zero'}
+ string = '%(logical_start_sector)d %(num_sectors)d %(target_type)s\n' % map
+ table_file.write(string)
+ table_file.flush()
+ logger.info('table_file: %s', string.strip())
+
+ proc = Subprocess('dmsetup', 'create', self.name, table_file.name)
+ proc.run()
+
+ table_file.close()
+
+ if proc.rc:
+ logger.error('%s', proc.stderr)
+ raise OSError, 'dmsetup failed when setting up dm device %s' % self.name
+
+ major, minor = getDeviceNumbers(self._node.path)
+ self._node.major = major
+ self._node.minor = minor
+
+ self._active = True
+
+ def destroy(self):
+ proc = Subprocess('dmsetup', 'remove', '--force', self.name)
+ proc.run()
+ if proc.rc:
+ logger.error('%s', proc.stderr)
+ raise OSError, 'dmsetup failed when removing dm device %s' % self.name
+
+ self._active = False
+
+ @property
+ def usable_size(self):
+ return self._usable_size
diff --git a/anate/vm.py b/anate/vm.py
new file mode 100644
index 0000000..71fcabc
--- /dev/null
+++ b/anate/vm.py
@@ -0,0 +1,135 @@
+import logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger('anate.vm')
+
+import os
+import devices
+
+
+class VirtualDisk(devices.DMDevice):
+
+ _LOOP_DEV_MAJOR = 7
+ _AVAILABLE_MINORS = [0, 1, 2, 3, 4, 5, 6, 7]
+
+ def getAvailableMinor(self):
+ return self._AVAILABLE_MINORS[0]
+
+ def useMinor(self, minor):
+ self._AVAILABLE_MINORS.remove(minor)
+
+ def freeMinor(self, minor):
+ self._AVAILABLE_MINORS.append(minor)
+ self._AVAILABLE_MINORS.sort()
+
+ def __init__(self, name, size, usable_size=None):
+ if not usable_size:
+ usable_size = size
+ elif usable_size > size:
+ logger.error('usable_size cannot be greater than size')
+ return
+
+ self._remove_node = False
+ self._loop_node = self._createLoopNode(name)
+ if not self._loop_node:
+ raise OSError, 'node error'
+
+ self._loop_dev = self._createLoopDev(name, self._loop_node, usable_size)
+
+ devices.DMDevice.__init__(self, name, self._loop_dev, size)
+
+ def _createLoopNode(self, name):
+ loop_node = devices.BlockDeviceNode(os.path.join('/dev/', name), major=None, minor=None)
+ if loop_node.available:
+ logger.info('using existing node %s', loop_node.path)
+ loop_node.major, loop_node.minor = devices.getDeviceNumbers(loop_node.path)
+ if loop_node.major != self._LOOP_DEV_MAJOR:
+ logger.error('not a loop device node')
+ return
+ if loop_node.minor not in self._AVAILABLE_MINORS:
+ logger.error('node not available')
+ return
+ else:
+ logger.info('creating new node %s', loop_node.path)
+ loop_node.major = self._LOOP_DEV_MAJOR
+ loop_node.minor = self.getAvailableMinor()
+ loop_node.create()
+ self._remove_node = True
+
+ self.useMinor(loop_node.minor)
+ logger.info('using minor %d', loop_node.minor)
+ logger.info('available minors: %s', self._AVAILABLE_MINORS)
+
+ if not loop_node.available:
+ raise OSError, 'node error'
+
+ return loop_node
+
+ def _createLoopDev(self, name, loop_node, size):
+ loop_dev = devices.LoopDevice(name, loop_node, os.path.join('/tmp/', name), size)
+ loop_dev.initialize()
+ if not loop_dev.active:
+ raise OSError, 'loop device error'
+
+ return loop_dev
+
+ def create(self):
+ self.initialize()
+ if not self.active:
+ raise OSError, 'dm device error'
+
+ def remove(self):
+ self.destroy()
+ if self.active:
+ raise OSError, 'dm device error'
+
+ self._loop_dev.destroy()
+ if self._loop_dev.active:
+ raise OSError, 'loop device error'
+
+ if self._remove_node:
+ logger.info('removing node %s', self._loop_node.path)
+ self._loop_node.unlink()
+ if self._loop_node.available:
+ raise OSError, 'node error'
+
+ self.freeMinor(self._loop_node.minor)
+ logger.info('freeing minor %d', self._loop_node.minor)
+ logger.info('available minors: %s', self._AVAILABLE_MINORS)
+
+
+class VM(object):
+ def __init__(self):
+ self._disks = {}
+
+ def addDisk(self, name, size, usable_size=None):
+ if name in self.disks:
+ logger.error('disk %s already exists', name)
+ return
+
+ disk = VirtualDisk(name, size=size, usable_size=usable_size)
+ disk.create()
+ self._disks[name] = disk
+
+ def removeDisk(self, name):
+ if name not in self.disks:
+ logger.error('disk %s does not exist', name)
+ return
+
+ disk = self.getDisk(name)
+ if disk.active:
+ disk.remove()
+ del self._disks[name]
+
+ def halt(self):
+ for disk in self.disks.values():
+ if disk.active:
+ disk.remove()
+
+ self._disks = {}
+
+ @property
+ def disks(self):
+ return self._disks
+
+ def getDisk(self, name):
+ return self.disks.get(name, None)
diff --git a/run_test.py b/run_test.py
new file mode 100755
index 0000000..06ad641
--- /dev/null
+++ b/run_test.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python
+
+import sys
+import unittest
+import tests
+import string
+from optparse import OptionParser
+
+
+def getFullSuiteName(suite, full_suite_names):
+ suites = []
+ for full_suite_name in full_suite_names:
+ if full_suite_name.lower().find(suite) != -1:
+ suites.append(full_suite_name)
+
+ return suites
+
+
+def main():
+ usage = 'Usage: %prog [options] [arg1 arg2 ... argN]'
+ version = '%prog 0.1'
+ parser = OptionParser(usage=usage, version=version)
+ parser.add_option('-l', '--list', action='store_true', default=False,
+ help='print all available test suites and exit')
+ parser.add_option('-i', '--interactive', action='store_true', default=False,
+ help='run in the interactive mode')
+
+ (options, args) = parser.parse_args(sys.argv[1:])
+ if not args and not (options.list or options.interactive):
+ parser.error('you must specify either an option or an argument\n'
+ 'Try "%s --help" for more information' % sys.argv[0])
+
+ print 'Searching for test suites'
+ available_suites = tests.getAvailableSuites()
+ if not available_suites:
+ print 'No test suites available, exiting'
+ sys.exit(1)
+
+ suite_names = available_suites.keys()
+ suite_names.sort()
+
+ if options.list and not options.interactive:
+ print '\nAvailable test suites:'
+ for suite_name in suite_names:
+ print suite_name
+ sys.exit(0)
+
+ suites_to_run = []
+ if options.interactive:
+ print '\nAvailable test suites:'
+ suite_num = 0
+ for suite_name in suite_names:
+ print '[%3d] %s' % (suite_num, suite_name)
+ suite_num += 1
+
+ try:
+ input_string = raw_input('\nType in the test you want to run, '
+ 'or "all" to run all listed tests: ')
+ except KeyboardInterrupt:
+ print '\nAborted by user'
+ sys.exit(1)
+
+ for arg in input_string.split():
+ if arg.isdigit():
+ arg = int(arg)
+ try:
+ args.append(suite_names[arg])
+ except KeyError:
+ pass
+ else:
+ args.append(arg)
+
+ args = map(string.lower, args)
+ if 'all' in args:
+ suites_to_run = suite_names[:]
+ else:
+ for arg in args:
+ matching_suites = getFullSuiteName(arg, suite_names)
+ suites_to_run.extend(filter(lambda suite: suite not in suites_to_run,
+ matching_suites))
+
+ if suites_to_run:
+ suites_to_run.sort()
+ print 'Running test suites: %s' % suites_to_run
+ suite = unittest.TestSuite([available_suites[suite_name]
+ for suite_name in suites_to_run])
+
+ try:
+ result = unittest.TextTestRunner(verbosity=2).run(suite)
+ except KeyboardInterrupt:
+ print '\nAborted by user'
+ sys.exit(1)
+
+ if result.wasSuccessful():
+ print '\nAll test suites OK'
+ sys.exit(0)
+ else:
+ print '\nTest suites finished with %d errors and %d failures' % (len(result.errors),
+ len(result.failures))
+ sys.exit(2)
+ else:
+ print 'No test suites matching your criteria found, exiting'
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..99ee679
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,20 @@
+import os
+
+def getAvailableSuites():
+ root, tests_dir = os.path.split(os.path.dirname(__file__))
+ modules = []
+
+ for root, dirs, files in os.walk(tests_dir):
+ for filename in files:
+ if filename.endswith('.py') and filename != '__init__.py':
+ basename, extension = os.path.splitext(filename)
+ modules.append(os.path.join(root, basename).replace('/', '.'))
+
+ available_suites = {}
+ for module in modules:
+ imported = __import__(module, globals(), locals(), [module], -1)
+ suite = getattr(imported, 'suite', None)
+ if callable(suite):
+ available_suites[module] = suite()
+
+ return available_suites
diff --git a/tests/devices.py b/tests/devices.py
new file mode 100644
index 0000000..5805035
--- /dev/null
+++ b/tests/devices.py
@@ -0,0 +1,96 @@
+import unittest
+import anate.devices as devices
+
+
+class DevicesTestCase(unittest.TestCase):
+ def setUp(self):
+ self._remove_node = False
+ self.device_node = devices.BlockDeviceNode('/dev/loop0', major=7, minor=0)
+ if not self.device_node.available:
+ self.device_node.create()
+ self._remove_node = True
+
+ def tearDown(self):
+ if self._remove_node:
+ self.device_node.unlink()
+
+ def testBlockDeviceClass(self):
+ device_object = devices.BlockDevice('device0', self.device_node)
+ self.assertEqual(device_object.name, 'device0')
+ self.assertEqual(device_object.path, '/dev/loop0')
+ self.assertEqual(device_object.active, False)
+ self.assertEqual(device_object.major, 7)
+ self.assertEqual(device_object.minor, 0)
+
+ device_object.initialize()
+ self.assertEqual(device_object.active, True)
+
+ device_object.destroy()
+ self.assertEqual(device_object.active, False)
+
+ def testStorageDeviceClass(self):
+ storage_object = devices.StorageDevice('storage0', self.device_node, size=20)
+ self.assertEqual(storage_object.name, 'storage0')
+ self.assertEqual(storage_object.path, '/dev/loop0')
+ self.assertEqual(storage_object.size, 20)
+ self.assertEqual(storage_object.active, False)
+ self.assertEqual(storage_object.major, 7)
+ self.assertEqual(storage_object.minor, 0)
+
+ storage_object.initialize()
+ self.assertEqual(storage_object.active, True)
+
+ storage_object.destroy()
+ self.assertEqual(storage_object.active, False)
+
+ def testLoopDeviceClass(self):
+ loop_object = devices.LoopDevice('loop0', self.device_node, '/tmp/file0', size=20)
+ self.assertEqual(loop_object.name, 'loop0')
+ self.assertEqual(loop_object.path, '/dev/loop0')
+ self.assertEqual(loop_object.active, False)
+ self.assertEqual(loop_object.major, 7)
+ self.assertEqual(loop_object.minor, 0)
+
+ loop_object.initialize()
+ self.assertEqual(loop_object.active, True)
+
+ loop_object.destroy()
+ self.assertEqual(loop_object.active, False)
+
+ def testDMDeviceClass(self):
+ loop_object = devices.LoopDevice('loop0', self.device_node, '/tmp/file0', size=20)
+ loop_object.initialize()
+
+ dm_object = devices.DMDevice('mydm0', loop_object)
+ self.assertEqual(dm_object.name, 'mydm0')
+ self.assertEqual(dm_object.path, '/dev/mapper/mydm0')
+ self.assertEqual(dm_object.size, loop_object.size)
+ self.assertEqual(dm_object.usable_size, loop_object.size)
+ self.assertEqual(dm_object.active, False)
+
+ dm_object.initialize()
+ self.assertEqual(dm_object.active, True)
+
+ major, minor = devices.getDeviceNumbers(dm_object.path)
+ self.assertEqual(dm_object.major, major)
+ self.assertEqual(dm_object.minor, minor)
+
+ dm_object.destroy()
+ self.assertEqual(dm_object.active, False)
+
+ dm_object = devices.DMDevice('mydm0', loop_object, size=40)
+ self.assertEqual(dm_object.size, 40)
+ self.assertEqual(dm_object.usable_size, loop_object.size)
+
+ dm_object.initialize()
+ dm_object.destroy()
+
+ loop_object.destroy()
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(DevicesTestCase)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/vm.py b/tests/vm.py
new file mode 100644
index 0000000..62fc3c3
--- /dev/null
+++ b/tests/vm.py
@@ -0,0 +1,55 @@
+import unittest
+import anate.vm as vm
+
+
+class VMTestCase(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def testVirtualDisk(self):
+ disk = vm.VirtualDisk('mydisk1', 10000, 100)
+ self.assertEqual(disk.name, 'mydisk1')
+ self.assertEqual(disk.path, '/dev/mapper/mydisk1')
+ self.assertEqual(disk.size, 10000)
+ self.assertEqual(disk.usable_size, 100)
+ self.assertFalse(disk.active)
+
+ disk.create()
+ self.assertTrue(disk.active)
+ self.assertEqual(disk._loop_dev.major, 7)
+ self.assertEqual(disk._loop_dev.minor, 0)
+
+ disk.remove()
+ self.assertFalse(disk.active)
+
+ def testVM(self):
+ machine = vm.VM()
+
+ machine.addDisk('mydisk1', 10000, 100)
+ machine.addDisk('mydisk2', 10000, 100)
+ self.assertEqual(len(machine.disks), 2)
+
+ for name in ('mydisk1', 'mydisk2'):
+ disk = machine.getDisk(name)
+ self.assertEqual(disk.name, name)
+ self.assertEqual(disk.path, '/dev/mapper/' + name)
+ self.assertEqual(disk.size, 10000)
+ self.assertEqual(disk.usable_size, 100)
+ self.assertTrue(disk.active)
+
+ machine.removeDisk('mydisk2')
+ machine.removeDisk('mydisk1')
+ self.assertEqual(machine.disks, {})
+
+ machine.halt()
+
+
+def suite():
+ return unittest.TestLoader().loadTestsFromTestCase(VMTestCase)
+
+
+if __name__ == '__main__':
+ unittest.main()