Federico Simoncelli has uploaded a new change for review.
Change subject: qemuimg: add support for convert progress
......................................................................
qemuimg: add support for convert progress
Change-Id: Id0b53e418c62bb2e91444ba5f351c916ca417299
Signed-off-by: Federico Simoncelli <fsimonce(a)redhat.com>
---
M lib/vdsm/qemuimg.py
M tests/qemuimgTests.py
M vdsm/storage/image.py
3 files changed, 172 insertions(+), 58 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/vdsm refs/changes/10/33910/1
diff --git a/lib/vdsm/qemuimg.py b/lib/vdsm/qemuimg.py
index 39cd394..9ca9c58 100644
--- a/lib/vdsm/qemuimg.py
+++ b/lib/vdsm/qemuimg.py
@@ -160,9 +160,9 @@
return check
-def convert(srcImage, dstImage, stop, srcFormat=None, dstFormat=None,
+def convert(srcImage, dstImage, srcFormat=None, dstFormat=None,
backing=None, backingFormat=None):
- cmd = [_qemuimg.cmd, "convert", "-t", "none"]
+ cmd = [_qemuimg.cmd, "convert", "-p", "-t",
"none"]
options = []
cwdPath = None
@@ -190,14 +190,66 @@
cmd.append(dstImage)
- (rc, out, err) = utils.watchCmd(
- cmd, cwd=cwdPath, stop=stop, nice=utils.NICENESS.HIGH,
- ioclass=utils.IOCLASS.IDLE)
+ return QemuImgProcess(cmd, cwd=cwdPath)
- if rc != 0:
- raise QImgError(rc, out, err)
- return (rc, out, err)
+class QemuImgProcess(object):
+ REGEXPR = re.compile(r'\(([\d.]+)/100%\)')
+
+ def __init__(self, cmd, cwd=None):
+ self.progress = 0.0
+
+ self._stdout = bytearray()
+ self._stderr = bytearray()
+
+ cmd = utils.ionice_cmd(cmd, utils.IOCLASS.IDLE)
+ cmd = utils.nice_cmd(cmd, utils.NICENESS.HIGH)
+
+ self.execution = utils.CommandStream(
+ cmd, self._recvstdout, self._recvstderr, cwd=cwd,
+ deathSignal=signal.SIGKILL)
+
+ def _recvstderr(self, buffer):
+ self._stderr += buffer
+
+ def _recvstdout(self, buffer):
+ last_progress = None
+ self._stdout += buffer
+
+ while True:
+ try:
+ idx = self._stdout.index('\r')
+ except ValueError:
+ break
+
+ last_progress = self._stdout[:idx]
+ del self._stdout[:idx + 1]
+
+ if last_progress:
+ m = self.REGEXPR.match(last_progress.strip())
+ if m is None:
+ raise ValueError(
+ 'Unable to parse: "%s"' % last_progress)
+ self.progress = float(m.group(1))
+
+ @property
+ def stderr(self):
+ return str(self._stderr)
+
+ def wait(self, timeout=None):
+ returncode = self.execution.wait(timeout=timeout)
+
+ if (self.execution.returncode is not None
+ and self.execution.returncode != 0):
+ raise QImgError(returncode, "", self.stderr)
+
+ return returncode
+
+ def terminate(self):
+ self.execution.terminate()
+
+ def kill(self):
+ self.execution.kill()
def resize(image, newSize, format=None):
diff --git a/tests/qemuimgTests.py b/tests/qemuimgTests.py
index abc0750..9e61de0 100644
--- a/tests/qemuimgTests.py
+++ b/tests/qemuimgTests.py
@@ -163,12 +163,11 @@
def test_no_format(self):
def convert(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'src', 'dst']
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'src', 'dst']
self.assertEqual(cmd, expected)
- return 0, '', ''
- with FakeCmd(utils, 'watchCmd', convert):
- qemuimg.convert('src', 'dst', True)
+ with FakeCmd(qemuimg, 'QemuImgProcess', convert):
+ qemuimg.convert('src', 'dst')
def test_qcow2_compat_unsupported(self):
def qcow2_compat_unsupported(cmd, **kw):
@@ -176,14 +175,13 @@
return 0, 'Supported options:\nsize ...\n', ''
def convert(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'src', '-O',
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'src', '-O',
'qcow2', 'dst']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', qcow2_compat_unsupported):
- with FakeCmd(utils, 'watchCmd', convert):
- qemuimg.convert('src', 'dst', True,
dstFormat='qcow2')
+ with FakeCmd(qemuimg, 'QemuImgProcess', convert):
+ qemuimg.convert('src', 'dst', dstFormat='qcow2')
def qcow2_compat_supported(self, cmd, **kw):
self.check_supports_qcow2_compat(cmd, **kw)
@@ -191,14 +189,13 @@
def test_qcow2_compat_supported(self):
def convert(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'src', '-O',
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'src', '-O',
'qcow2', '-o', 'compat=0.10',
'dst']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', self.qcow2_compat_supported):
- with FakeCmd(utils, 'watchCmd', convert):
- qemuimg.convert('src', 'dst', True,
dstFormat='qcow2')
+ with FakeCmd(qemuimg, 'QemuImgProcess', convert):
+ qemuimg.convert('src', 'dst', dstFormat='qcow2')
def check_supports_qcow2_compat(self, cmd, **kw):
expected = [QEMU_IMG, 'convert', '-O', 'qcow2',
'-o', '?', '/dev/null',
@@ -207,49 +204,95 @@
def test_qcow2_no_backing_file(self):
def qcow2_no_backing_file(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'source', '-O',
- 'qcow2', '-o', 'compat=0.10',
'target']
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'source',
+ '-O', 'qcow2', '-o',
'compat=0.10', 'target']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', self.qcow2_compat_supported):
- with FakeCmd(utils, 'watchCmd', qcow2_no_backing_file):
- qemuimg.convert('source', 'target', None,
dstFormat='qcow2')
+ with FakeCmd(qemuimg, 'QemuImgProcess', qcow2_no_backing_file):
+ qemuimg.convert('source', 'target',
dstFormat='qcow2')
def test_qcow2_backing_file(self):
def qcow2_backing_file(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'source', '-O',
- 'qcow2', '-o',
'compat=0.10,backing_file=backing',
- 'target']
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'source',
+ '-O', 'qcow2',
+ '-o', 'compat=0.10,backing_file=backing',
'target']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', self.qcow2_compat_supported):
- with FakeCmd(utils, 'watchCmd', qcow2_backing_file):
- qemuimg.convert('source', 'target', None,
dstFormat='qcow2',
+ with FakeCmd(qemuimg, 'QemuImgProcess', qcow2_backing_file):
+ qemuimg.convert('source', 'target',
dstFormat='qcow2',
backing='backing')
def test_qcow2_backing_format(self):
def qcow2_backing_format(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'source', '-O',
- 'qcow2', '-o', 'compat=0.10',
'target']
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'source',
+ '-O', 'qcow2', '-o',
'compat=0.10', 'target']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', self.qcow2_compat_supported):
- with FakeCmd(utils, 'watchCmd', qcow2_backing_format):
- qemuimg.convert('source', 'target', None,
dstFormat='qcow2',
+ with FakeCmd(qemuimg, 'QemuImgProcess', qcow2_backing_format):
+ qemuimg.convert('source', 'target',
dstFormat='qcow2',
backingFormat='qcow2')
def test_qcow2_backing_file_and_format(self):
def qcow2_backing_format(cmd, **kw):
- expected = [QEMU_IMG, 'convert', '-t', 'none',
'source', '-O',
- 'qcow2', '-o',
'compat=0.10,backing_file=backing,'
+ expected = [QEMU_IMG, 'convert', '-p', '-t',
'none', 'source',
+ '-O', 'qcow2',
+ '-o', 'compat=0.10,backing_file=backing,'
'backing_fmt=qcow2', 'target']
self.assertEqual(cmd, expected)
- return 0, '', ''
with FakeCmd(utils, 'execCmd', self.qcow2_compat_supported):
- with FakeCmd(utils, 'watchCmd', qcow2_backing_format):
- qemuimg.convert('source', 'target', None,
dstFormat='qcow2',
+ with FakeCmd(qemuimg, 'QemuImgProcess', qcow2_backing_format):
+ qemuimg.convert('source', 'target',
dstFormat='qcow2',
backing='backing',
backingFormat='qcow2')
+
+
+class QemuImgProcessTests(TestCaseBase):
+ PROGRESS_FORMAT = " (%.2f/100%%)\r"
+
+ @staticmethod
+ def _progress_iterator():
+ return map(lambda x: x / 100.0, xrange(0, 10000, 1))
+
+ def test_progress_simple(self):
+ p = qemuimg.QemuImgProcess([])
+
+ for progress in self._progress_iterator():
+ p._recvstdout(self.PROGRESS_FORMAT % progress)
+ self.assertEquals(p.progress, progress)
+
+ self.assertEquals(p.wait(), 0)
+
+ def test_progress_incomplete(self):
+ p = qemuimg.QemuImgProcess([])
+
+ for progress in self._progress_iterator():
+ stdout = self.PROGRESS_FORMAT % progress
+ p._recvstdout(stdout[:12])
+ p._recvstdout(stdout[12:])
+ self.assertEquals(p.progress, progress)
+
+ self.assertEquals(p.wait(), 0)
+
+ def test_progress_batch(self):
+ p = qemuimg.QemuImgProcess([])
+
+ p._recvstdout(
+ (self.PROGRESS_FORMAT % 10.00) +
+ (self.PROGRESS_FORMAT % 25.00) +
+ (self.PROGRESS_FORMAT % 33.33))
+
+ self.assertEquals(p.progress, 33.33)
+ self.assertEquals(p.wait(), 0)
+
+ def test_unexpected_output(self):
+ p = qemuimg.QemuImgProcess([])
+
+ self.assertRaises(ValueError, p._recvstdout, "Hello World\r")
+
+ p._recvstdout("Hello ")
+ self.assertRaises(ValueError, p._recvstdout, "World\r")
+
+ self.assertEquals(p.wait(), 0)
diff --git a/vdsm/storage/image.py b/vdsm/storage/image.py
index 41e3f30..cdcd82a 100644
--- a/vdsm/storage/image.py
+++ b/vdsm/storage/image.py
@@ -93,6 +93,7 @@
"""
log = logging.getLogger('Storage.Image')
_fakeTemplateLock = threading.Lock()
+ _QEMU_LOGGING_INTERVAL = 60.0
@classmethod
def createImageRollback(cls, taskObj, imageDir):
@@ -109,6 +110,24 @@
def __init__(self, repoPath):
self.repoPath = repoPath
+
+ def qemuImgConvert(self, *args, **kwargs):
+ self.log.debug('starting qemu-img operation')
+ command = qemuimg.convert(*args, **kwargs)
+
+ def abortImgConversion():
+ self.log.info('aborting ongoing qemu-img operation')
+ command.terminate()
+
+ retcode = None
+
+ with vars.task.abort_callback(abortImgConversion):
+ while retcode is None:
+ retcode = command.wait(self._QEMU_LOGGING_INTERVAL)
+ self.log.debug('qemu-img operation progress: %s%%',
+ command.progress)
+
+ self.log.debug('qemu-img operation has completed: %s', retcode)
def create(self, sdUUID, imgUUID):
"""Create placeholder for image's volumes
@@ -444,13 +463,12 @@
backingFormat = None
self.log.debug("start qemu convert")
- qemuimg.convert(srcVol.getVolumePath(),
- dstVol.getVolumePath(),
- vars.task.aborting,
- srcFormat=srcFormat,
- dstFormat=dstFormat,
- backing=backing,
- backingFormat=backingFormat)
+ self.qemuImgConvert(srcVol.getVolumePath(),
+ dstVol.getVolumePath(),
+ srcFormat=srcFormat,
+ dstFormat=dstFormat,
+ backing=backing,
+ backingFormat=backingFormat)
except ActionStopped:
raise
except se.StorageException:
@@ -830,10 +848,11 @@
dstVol.prepare(rw=True, setrw=True)
try:
- qemuimg.convert(volParams['path'], dstPath,
- vars.task.aborting,
- volume.fmt2str(volParams['volFormat']),
- volume.fmt2str(dstVolFormat))
+ self.qemuImgConvert(
+ volParams['path'],
+ dstPath,
+ srcFormat=volume.fmt2str(volParams['volFormat']),
+ dstFormat=volume.fmt2str(dstVolFormat))
except ActionStopped:
raise
except qemuimg.QImgError:
@@ -1045,7 +1064,7 @@
# volume and rebase successor's children (if exists) on top of it.
# Step 1: Create an empty volume named sucessor_MERGE similar to
# ancestor volume.
- # Step 2: qemuimg.convert successor -> sucessor_MERGE
+ # Step 2: qemuImgConvert successor -> sucessor_MERGE
# Step 3: Rename successor to _remove_me__successor
# Step 4: Rename successor_MERGE to successor
# Step 5: Unsafely rebase successor's children on top of temporary
@@ -1071,11 +1090,11 @@
# Step 2: Convert successor to new volume
# qemu-img convert -f qcow2 successor -O raw newUUID
try:
- qemuimg.convert(srcVolParams['path'],
- newVol.getVolumePath(),
- vars.task.aborting,
- volume.fmt2str(srcVolParams['volFormat']),
- volume.fmt2str(volParams['volFormat']))
+ self.qemuImgConvert(
+ srcVolParams['path'],
+ newVol.getVolumePath(),
+ srcFormat=volume.fmt2str(srcVolParams['volFormat']),
+ dstFormat=volume.fmt2str(volParams['volFormat']))
except qemuimg.QImgError:
self.log.exception('conversion failure for volume %s',
srcVol.volUUID)
--
To view, visit
http://gerrit.ovirt.org/33910
To unsubscribe, visit
http://gerrit.ovirt.org/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Id0b53e418c62bb2e91444ba5f351c916ca417299
Gerrit-PatchSet: 1
Gerrit-Project: vdsm
Gerrit-Branch: master
Gerrit-Owner: Federico Simoncelli <fsimonce(a)redhat.com>