diff --git a/etc/nova/nova.conf.sample b/etc/nova/nova.conf.sample index efa8f2432f80..a8cb0c42d2b8 100644 --- a/etc/nova/nova.conf.sample +++ b/etc/nova/nova.conf.sample @@ -2883,6 +2883,14 @@ # Deprecated group/name - [DEFAULT]/libvirt_sparse_logical_volumes #sparse_logical_volumes=false +# Method used to wipe old volumes (valid options are: none, +# zero, shred) (string value) +#volume_clear=zero + +# Size in MiB to wipe at start of old volumes. 0 => all +# (integer value) +#volume_clear_size=0 + # The RADOS pool in which rbd volumes are stored (string # value) # Deprecated group/name - [DEFAULT]/libvirt_images_rbd_pool diff --git a/etc/nova/rootwrap.d/compute.filters b/etc/nova/rootwrap.d/compute.filters index d51aa5f983db..131690de3fb0 100644 --- a/etc/nova/rootwrap.d/compute.filters +++ b/etc/nova/rootwrap.d/compute.filters @@ -217,6 +217,9 @@ rpc.mountd: CommandFilter, rpc.mountd, root # nova/virt/libvirt/utils.py: rbd: CommandFilter, rbd, root +# nova/virt/libvirt/utils.py: 'shred', '-n3', '-s%d' % volume_size, path +shred: CommandFilter, shred, root + # nova/virt/libvirt/volume.py: 'cp', '/dev/stdin', delete_control.. cp: CommandFilter, cp, root diff --git a/nova/tests/virt/libvirt/test_libvirt_utils.py b/nova/tests/virt/libvirt/test_libvirt_utils.py index c25a4e4b6cfa..0aa2db5f3ecc 100644 --- a/nova/tests/virt/libvirt/test_libvirt_utils.py +++ b/nova/tests/virt/libvirt/test_libvirt_utils.py @@ -17,12 +17,15 @@ import os import tempfile import fixtures +from oslo.config import cfg from nova.openstack.common import processutils from nova import test from nova import utils from nova.virt.libvirt import utils as libvirt_utils +CONF = cfg.CONF + class LibvirtUtilsTestCase(test.NoDBTestCase): def test_get_disk_type(self): @@ -56,6 +59,117 @@ blah BLAH: bb self.assertEqual(expected_commands, executes) self.assertEqual(size, 123456789) + def test_lvm_clear(self): + def fake_lvm_size(path): + return lvm_size + + def fake_execute(*cmd, **kwargs): + executes.append(cmd) + + self.stubs.Set(libvirt_utils, 'logical_volume_size', fake_lvm_size) + self.stubs.Set(utils, 'execute', fake_execute) + + # Test the correct dd commands are run for various sizes + lvm_size = 1 + executes = [] + expected_commands = [('dd', 'bs=1', 'if=/dev/zero', 'of=/dev/v1', + 'seek=0', 'count=1', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/v1') + self.assertEqual(expected_commands, executes) + + lvm_size = 1024 + executes = [] + expected_commands = [('dd', 'bs=1024', 'if=/dev/zero', 'of=/dev/v2', + 'seek=0', 'count=1', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/v2') + self.assertEqual(expected_commands, executes) + + lvm_size = 1025 + executes = [] + expected_commands = [('dd', 'bs=1024', 'if=/dev/zero', 'of=/dev/v3', + 'seek=0', 'count=1', 'conv=fdatasync')] + expected_commands += [('dd', 'bs=1', 'if=/dev/zero', 'of=/dev/v3', + 'seek=1024', 'count=1', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/v3') + self.assertEqual(expected_commands, executes) + + lvm_size = 1048576 + executes = [] + expected_commands = [('dd', 'bs=1048576', 'if=/dev/zero', 'of=/dev/v4', + 'seek=0', 'count=1', 'oflag=direct')] + libvirt_utils.clear_logical_volume('/dev/v4') + self.assertEqual(expected_commands, executes) + + lvm_size = 1048577 + executes = [] + expected_commands = [('dd', 'bs=1048576', 'if=/dev/zero', 'of=/dev/v5', + 'seek=0', 'count=1', 'oflag=direct')] + expected_commands += [('dd', 'bs=1', 'if=/dev/zero', 'of=/dev/v5', + 'seek=1048576', 'count=1', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/v5') + self.assertEqual(expected_commands, executes) + + lvm_size = 1234567 + executes = [] + expected_commands = [('dd', 'bs=1048576', 'if=/dev/zero', 'of=/dev/v6', + 'seek=0', 'count=1', 'oflag=direct')] + expected_commands += [('dd', 'bs=1024', 'if=/dev/zero', 'of=/dev/v6', + 'seek=1024', 'count=181', 'conv=fdatasync')] + expected_commands += [('dd', 'bs=1', 'if=/dev/zero', 'of=/dev/v6', + 'seek=1233920', 'count=647', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/v6') + self.assertEqual(expected_commands, executes) + + # Test volume_clear_size limits the size + lvm_size = 10485761 + CONF.set_override('volume_clear_size', '1', 'libvirt') + executes = [] + expected_commands = [('dd', 'bs=1048576', 'if=/dev/zero', 'of=/dev/v7', + 'seek=0', 'count=1', 'oflag=direct')] + libvirt_utils.clear_logical_volume('/dev/v7') + self.assertEqual(expected_commands, executes) + + CONF.set_override('volume_clear_size', '2', 'libvirt') + lvm_size = 1048576 + executes = [] + expected_commands = [('dd', 'bs=1048576', 'if=/dev/zero', 'of=/dev/v9', + 'seek=0', 'count=1', 'oflag=direct')] + libvirt_utils.clear_logical_volume('/dev/v9') + self.assertEqual(expected_commands, executes) + + # Test volume_clear=shred + CONF.set_override('volume_clear', 'shred', 'libvirt') + CONF.set_override('volume_clear_size', '0', 'libvirt') + lvm_size = 1048576 + executes = [] + expected_commands = [('shred', '-n3', '-s1048576', '/dev/va')] + libvirt_utils.clear_logical_volume('/dev/va') + self.assertEqual(expected_commands, executes) + + CONF.set_override('volume_clear', 'shred', 'libvirt') + CONF.set_override('volume_clear_size', '1', 'libvirt') + lvm_size = 10485761 + executes = [] + expected_commands = [('shred', '-n3', '-s1048576', '/dev/vb')] + libvirt_utils.clear_logical_volume('/dev/vb') + self.assertEqual(expected_commands, executes) + + # Test volume_clear=none does nothing + CONF.set_override('volume_clear', 'none', 'libvirt') + executes = [] + expected_commands = [] + libvirt_utils.clear_logical_volume('/dev/vc') + self.assertEqual(expected_commands, executes) + + # Test volume_clear=invalid falls back to the default 'zero' + CONF.set_override('volume_clear', 'invalid', 'libvirt') + lvm_size = 1 + executes = [] + expected_commands = [('dd', 'bs=1', 'if=/dev/zero', 'of=/dev/vd', + 'seek=0', 'count=1', 'conv=fdatasync')] + libvirt_utils.clear_logical_volume('/dev/vd') + self.assertEqual(expected_commands, executes) + def test_list_rbd_volumes(self): conf = '/etc/ceph/fake_ceph.conf' pool = 'fake_pool' diff --git a/nova/virt/libvirt/imagebackend.py b/nova/virt/libvirt/imagebackend.py index 33e064b1dcf5..76edbabb8ad5 100644 --- a/nova/virt/libvirt/imagebackend.py +++ b/nova/virt/libvirt/imagebackend.py @@ -62,6 +62,13 @@ __imagebackend_opts = [ ' if this flag is set to True.', deprecated_group='DEFAULT', deprecated_name='libvirt_sparse_logical_volumes'), + cfg.StrOpt('volume_clear', + default='zero', + help='Method used to wipe old volumes (valid options are: ' + 'none, zero, shred)'), + cfg.IntOpt('volume_clear_size', + default=0, + help='Size in MiB to wipe at start of old volumes. 0 => all'), cfg.StrOpt('images_rbd_pool', default='rbd', help='The RADOS pool in which rbd volumes are stored', diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py index e926d3d59b04..ae831e0df34f 100644 --- a/nova/virt/libvirt/utils.py +++ b/nova/virt/libvirt/utils.py @@ -361,26 +361,23 @@ def logical_volume_size(path): return int(out) -def clear_logical_volume(path): - """Obfuscate the logical volume. +def _zero_logical_volume(path, volume_size): + """Write zeros over the specified path :param path: logical volume path + :param size: number of zeros to write """ - # TODO(p-draigbrady): We currently overwrite with zeros - # but we may want to make this configurable in future - # for more or less security conscious setups. - - vol_size = logical_volume_size(path) bs = units.Mi direct_flags = ('oflag=direct',) sync_flags = () - remaining_bytes = vol_size + remaining_bytes = volume_size - # The loop caters for versions of dd that - # don't support the iflag=count_bytes option. + # The loop efficiently writes zeros using dd, + # and caters for versions of dd that don't have + # the easier to use iflag=count_bytes option. while remaining_bytes: zero_blocks = remaining_bytes / bs - seek_blocks = (vol_size - remaining_bytes) / bs + seek_blocks = (volume_size - remaining_bytes) / bs zero_cmd = ('dd', 'bs=%s' % bs, 'if=/dev/zero', 'of=%s' % path, 'seek=%s' % seek_blocks, 'count=%s' % zero_blocks) @@ -395,6 +392,39 @@ def clear_logical_volume(path): sync_flags = ('conv=fdatasync',) +def clear_logical_volume(path): + """Obfuscate the logical volume. + + :param path: logical volume path + """ + volume_clear = CONF.libvirt.volume_clear + + if volume_clear not in ('none', 'shred', 'zero'): + LOG.error(_("ignoring unrecognized volume_clear='%s' value"), + volume_clear) + volume_clear = 'zero' + + if volume_clear == 'none': + return + + volume_clear_size = int(CONF.libvirt.volume_clear_size) * units.Mi + volume_size = logical_volume_size(path) + + if volume_clear_size != 0 and volume_clear_size < volume_size: + volume_size = volume_clear_size + + if volume_clear == 'zero': + # NOTE(p-draigbrady): we could use shred to do the zeroing + # with -n0 -z, however only versions >= 8.22 perform as well as dd + _zero_logical_volume(path, volume_size) + elif volume_clear == 'shred': + utils.execute('shred', '-n3', '-s%d' % volume_size, path, + run_as_root=True) + else: + raise exception.Invalid(_("volume_clear='%s' is not handled") + % volume_clear) + + def remove_logical_volumes(*paths): """Remove one or more logical volume."""