Add some real-world testing on DiskPartitioner

This patch adds some integration tests for DiskPartitioner by using
it on a temporary file. As a side effect, this patch adds
disk_partitioner.list_partitions() helper, which is used by the test,
but can by used for anything else later.

The new tests are currently skipped on gate-ironic-python26, as
there is no `parted` utility there.
Also these tests do not cover destroy_disk_metadata, as it can't
be executed on a file.

Change-Id: I53bf2d207b7d15a0673be6a6786c47500b9eb499
Closes-Bug: #1306153
This commit is contained in:
Dmitry Tantsur 2014-05-21 15:46:05 +02:00
parent 2a9c6acd57
commit fc7dfcc543
3 changed files with 149 additions and 0 deletions

View File

@ -13,9 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import re
import time
from ironic.common import utils
from ironic.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class DiskPartitioner(object):
@ -96,3 +101,35 @@ class DiskPartitioner(object):
# the "device is busy" problem. lsof, fuser...
# avoid "device is busy"
time.sleep(3)
_PARTED_PRINT_RE = re.compile(r"^\d+:([\d\.]+)MiB:"
"([\d\.]+)MiB:([\d\.]+)MiB:(\w*)::(\w*)")
def list_partitions(device):
"""Get partitions information from given device.
:param device: The device path.
:returns: list of dictionaries (one per partition) with keys:
start, end, size (in MiB), filesystem, flags
"""
output = utils.execute(
'parted', '-s', '-m', device, 'unit', 'MiB', 'print')[0]
lines = [line for line in output.split('\n') if line.strip()][2:]
# Example of line: 1:1.00MiB:501MiB:500MiB:ext4::boot
fields = ('start', 'end', 'size', 'filesystem', 'flags')
result = []
for line in lines:
match = _PARTED_PRINT_RE.match(line)
if match is None:
LOG.warn(_("Partition information from parted for device "
"%(device)s does not match "
"expected format: %(line)s"),
dict(device=device, line=line))
continue
# Cast int fields to ints (some are floats and we round them down)
groups = [int(float(x)) if i < 3 else x
for i, x in enumerate(match.groups())]
result.append(dict(zip(fields, groups)))
return result

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import itertools
import time
import fixtures
@ -585,3 +586,80 @@ class GetDeviceBlockSizeTestCase(tests_base.TestCase):
run_as_root=True, check_exit_code=[0])]
utils.get_dev_block_size(self.dev)
mock_exec.assert_has_calls(expected_call)
@mock.patch.object(utils, 'is_block_device', lambda d: True)
@mock.patch.object(utils, 'block_uuid', lambda p: 'uuid')
@mock.patch.object(utils, 'dd', lambda *_: None)
@mock.patch.object(common_utils, 'mkfs', lambda *_: None)
# TODO(dtantsur): remove once https://review.openstack.org/90126 lands
@mock.patch.object(time, 'sleep', lambda *_: None)
# NOTE(dtantsur): destroy_disk_metadata resets file size, disabling it
@mock.patch.object(utils, 'destroy_disk_metadata', lambda *_: None)
class RealFilePartitioningTestCase(tests_base.TestCase):
"""This test applies some real-world partitioning scenario to a file.
This test covers the whole partitioning, mocking everything not possible
on a file. That helps us assure, that we do all partitioning math properly
and also conducts integration testing of DiskPartitioner.
"""
def setUp(self):
super(RealFilePartitioningTestCase, self).setUp()
# NOTE(dtantsur): no parted utility on gate-ironic-python26
try:
common_utils.execute('parted', '--version')
except OSError as exc:
self.skipTest('parted utility was not found: %s' % exc)
self.file = tempfile.NamedTemporaryFile()
self.addCleanup(lambda: self.file.close())
# NOTE(dtantsur): 20 MiB file with zeros
common_utils.execute('dd', 'if=/dev/zero', 'of=%s' % self.file.name,
'bs=1', 'count=0', 'seek=20MiB')
@staticmethod
def _run_without_root(func, *args, **kwargs):
"""Make sure root is not required when using utils.execute."""
real_execute = common_utils.execute
def fake_execute(*cmd, **kwargs):
kwargs['run_as_root'] = False
return real_execute(*cmd, **kwargs)
with mock.patch.object(common_utils, 'execute', fake_execute):
return func(*args, **kwargs)
def test_different_sizes(self):
# NOTE(dtantsur): Keep this list in order with expected partitioning
fields = ['ephemeral_mb', 'swap_mb', 'root_mb']
variants = itertools.product([0, 1, 5], repeat=3)
for variant in variants:
if variant == (0, 0, 0):
continue
kwargs = dict(zip(fields, variant))
self._run_without_root(utils.work_on_disk, self.file.name,
ephemeral_format='ext4', node_uuid='',
image_path='path', **kwargs)
part_table = self._run_without_root(
disk_partitioner.list_partitions, self.file.name)
for part, expected_size in zip(part_table, filter(None, variant)):
self.assertEqual(expected_size, part['size'],
"comparison failed for %s" % list(variant))
def test_whole_disk(self):
# 6 MiB ephemeral + 3 MiB swap + 9 MiB root + 1 MiB for MBR
# + 1 MiB MAGIC == 20 MiB whole disk
# TODO(dtantsur): figure out why we need 'magic' 1 more MiB
# and why the is different on Ubuntu and Fedora (see below)
self._run_without_root(utils.work_on_disk, self.file.name,
root_mb=9, ephemeral_mb=6, swap_mb=3,
ephemeral_format='ext4', node_uuid='',
image_path='path')
part_table = self._run_without_root(
disk_partitioner.list_partitions, self.file.name)
sizes = [part['size'] for part in part_table]
# NOTE(dtantsur): parted in Ubuntu 12.04 will occupy the last MiB,
# parted in Fedora 20 won't - thus two possible variants for last part
self.assertEqual([6, 3], sizes[:2],
"unexpected partitioning %s" % part_table)
self.assertIn(sizes[2], (9, 10))

View File

@ -66,3 +66,37 @@ class DiskPartitionerTestCase(base.TestCase):
'mkpart', 'fake-type', 'fake-fs-type', '1', '2',
'mkpart', 'fake-type', 'fake-fs-type', '2', '3',
'set', '2', 'boot', 'on')
@mock.patch.object(utils, 'execute')
class ListPartitionsTestCase(base.TestCase):
def test_correct(self, execute_mock):
output = """
BYT;
/dev/sda:500107862016B:scsi:512:4096:msdos:ATA HGST HTS725050A7:;
1:1.00MiB:501MiB:500MiB:ext4::boot;
2:501MiB:476940MiB:476439MiB:::;
"""
expected = [
{'start': 1, 'end': 501, 'size': 500,
'filesystem': 'ext4', 'flags': 'boot'},
{'start': 501, 'end': 476940, 'size': 476439,
'filesystem': '', 'flags': ''},
]
execute_mock.return_value = (output, '')
result = disk_partitioner.list_partitions('/dev/fake')
self.assertEqual(expected, result)
execute_mock.assert_called_once_with(
'parted', '-s', '-m', '/dev/fake', 'unit', 'MiB', 'print')
@mock.patch.object(disk_partitioner.LOG, 'warn')
def test_incorrect(self, log_mock, execute_mock):
output = """
BYT;
/dev/sda:500107862016B:scsi:512:4096:msdos:ATA HGST HTS725050A7:;
1:XX1076MiB:---:524MiB:ext4::boot;
"""
execute_mock.return_value = (output, '')
self.assertEqual([], disk_partitioner.list_partitions('/dev/fake'))
self.assertEqual(1, log_mock.call_count)