diff options
| author | Daniel P. Berrange <berrange@redhat.com> | 2012-11-13 11:21:09 +0000 |
|---|---|---|
| committer | Daniel P. Berrange <berrange@redhat.com> | 2012-11-21 11:53:06 +0000 |
| commit | 72918415f2b6a241d6097a563aba55a849936591 (patch) | |
| tree | 1461b831d6c0a3b91117913fcf14353f3a861333 | |
| parent | 74e38f1baefb020de30aa19745964bef9ad7254a (diff) | |
Convert file injection code to use the VFS APIs
Remove the requirement to mount disk images on the host
filesystem for file injection, by switching over to use
the new VFS APIs. The mount code is now solely used for
setting up LXC disk images
blueprint: virt-disk-api-refactoring
Change-Id: I1335bf7a8266d3d1d410a66538969f1213766cb9
Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
| -rw-r--r-- | nova/tests/test_virt.py | 46 | ||||
| -rw-r--r-- | nova/tests/test_virt_disk.py | 163 | ||||
| -rw-r--r-- | nova/virt/disk/api.py | 211 | ||||
| -rw-r--r-- | nova/virt/disk/vfs/api.py | 25 | ||||
| -rw-r--r-- | nova/virt/xenapi/vm_utils.py | 6 |
5 files changed, 292 insertions, 159 deletions
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 252b0db55..2c5dd4734 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -149,49 +149,3 @@ class TestVirtDisk(test.TestCase): self.executes.pop() self.assertEqual(self.executes, expected_commands) - - -class TestVirtDiskPaths(test.TestCase): - def setUp(self): - super(TestVirtDiskPaths, self).setUp() - - real_execute = utils.execute - - def nonroot_execute(*cmd_parts, **kwargs): - kwargs.pop('run_as_root', None) - return real_execute(*cmd_parts, **kwargs) - - self.stubs.Set(utils, 'execute', nonroot_execute) - - def test_check_safe_path(self): - if tests.utils.is_osx(): - self.skipTest("Unable to test on OSX") - ret = disk_api._join_and_check_path_within_fs('/foo', 'etc', - 'something.conf') - self.assertEquals(ret, '/foo/etc/something.conf') - - def test_check_unsafe_path(self): - if tests.utils.is_osx(): - self.skipTest("Unable to test on OSX") - self.assertRaises(exception.Invalid, - disk_api._join_and_check_path_within_fs, - '/foo', 'etc/../../../something.conf') - - def test_inject_files_with_bad_path(self): - if tests.utils.is_osx(): - self.skipTest("Unable to test on OSX") - self.assertRaises(exception.Invalid, - disk_api._inject_file_into_fs, - '/tmp', '/etc/../../../../etc/passwd', - 'hax') - - def test_inject_metadata(self): - if tests.utils.is_osx(): - self.skipTest("Unable to test on OSX") - with utils.tempdir() as tmpdir: - meta_objs = [{"key": "foo", "value": "bar"}] - metadata = {"foo": "bar"} - disk_api._inject_metadata_into_fs(meta_objs, tmpdir) - json_file = os.path.join(tmpdir, 'meta.js') - json_data = jsonutils.loads(open(json_file).read()) - self.assertEqual(metadata, json_data) diff --git a/nova/tests/test_virt_disk.py b/nova/tests/test_virt_disk.py new file mode 100644 index 000000000..cc69462d7 --- /dev/null +++ b/nova/tests/test_virt_disk.py @@ -0,0 +1,163 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (C) 2012 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from nova import test + +from nova.tests import fakeguestfs +from nova.virt.disk import api as diskapi +from nova.virt.disk.vfs import api as vfsapi +from nova.virt.disk.vfs import guestfs as vfsguestfs + + +class VirtDiskTest(test.TestCase): + + def setUp(self): + super(VirtDiskTest, self).setUp() + sys.modules['guestfs'] = fakeguestfs + vfsguestfs.guestfs = fakeguestfs + + def test_inject_data_key(self): + + vfs = vfsguestfs.VFSGuestFS("/some/file", "qcow2") + vfs.setup() + + diskapi._inject_key_into_fs("mysshkey", vfs) + + self.assertTrue("/root/.ssh" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/root/.ssh"], + {'isdir': True, 'gid': 0, 'uid': 0, 'mode': 0700}) + self.assertTrue("/root/.ssh/authorized_keys" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/root/.ssh/authorized_keys"], + {'isdir': False, + 'content': "Hello World\n# The following ssh " + + "key was injected by Nova\nmysshkey\n", + 'gid': 100, + 'uid': 100, + 'mode': 0700}) + + vfs.teardown() + + def test_inject_data_key_with_selinux(self): + + vfs = vfsguestfs.VFSGuestFS("/some/file", "qcow2") + vfs.setup() + + vfs.make_path("etc/selinux") + vfs.make_path("etc/rc.d") + diskapi._inject_key_into_fs("mysshkey", vfs) + + self.assertTrue("/etc/rc.d/rc.local" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/etc/rc.d/rc.local"], + {'isdir': False, + 'content': "Hello World#!/bin/sh\n# Added by " + + "Nova to ensure injected ssh keys " + + "have the right context\nrestorecon " + + "-RF root/.ssh 2>/dev/null || :\n", + 'gid': 100, + 'uid': 100, + 'mode': 0700}) + + self.assertTrue("/root/.ssh" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/root/.ssh"], + {'isdir': True, 'gid': 0, 'uid': 0, 'mode': 0700}) + self.assertTrue("/root/.ssh/authorized_keys" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/root/.ssh/authorized_keys"], + {'isdir': False, + 'content': "Hello World\n# The following ssh " + + "key was injected by Nova\nmysshkey\n", + 'gid': 100, + 'uid': 100, + 'mode': 0700}) + + vfs.teardown() + + def test_inject_net(self): + + vfs = vfsguestfs.VFSGuestFS("/some/file", "qcow2") + vfs.setup() + + diskapi._inject_net_into_fs("mynetconfig", vfs) + + self.assertTrue("/etc/network/interfaces" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/etc/network/interfaces"], + {'content': 'mynetconfig', + 'gid': 100, + 'isdir': False, + 'mode': 0700, + 'uid': 100}) + vfs.teardown() + + def test_inject_metadata(self): + vfs = vfsguestfs.VFSGuestFS("/some/file", "qcow2") + vfs.setup() + + diskapi._inject_metadata_into_fs([{"key": "foo", + "value": "bar"}, + {"key": "eek", + "value": "wizz"}], vfs) + + self.assertTrue("/meta.js" in vfs.handle.files) + self.assertEquals(vfs.handle.files["/meta.js"], + {'content': '{"foo": "bar", ' + + '"eek": "wizz"}', + 'gid': 100, + 'isdir': False, + 'mode': 0700, + 'uid': 100}) + vfs.teardown() + + def test_inject_admin_password(self): + vfs = vfsguestfs.VFSGuestFS("/some/file", "qcow2") + vfs.setup() + + def fake_salt(): + return "1234567890abcdef" + + self.stubs.Set(diskapi, '_generate_salt', fake_salt) + + vfs.handle.write("/etc/shadow", + "root:$1$12345678$xxxxx:14917:0:99999:7:::\n" + + "bin:*:14495:0:99999:7:::\n" + + "daemon:*:14495:0:99999:7:::\n") + + vfs.handle.write("/etc/passwd", + "root:x:0:0:root:/root:/bin/bash\n" + + "bin:x:1:1:bin:/bin:/sbin/nologin\n" + + "daemon:x:2:2:daemon:/sbin:/sbin/nologin\n") + + diskapi._inject_admin_password_into_fs("123456", vfs) + + self.assertEquals(vfs.handle.files["/etc/passwd"], + {'content': "root:x:0:0:root:/root:/bin/bash\n" + + "bin:x:1:1:bin:/bin:/sbin/nologin\n" + + "daemon:x:2:2:daemon:/sbin:" + + "/sbin/nologin\n", + 'gid': 100, + 'isdir': False, + 'mode': 0700, + 'uid': 100}) + self.assertEquals(vfs.handle.files["/etc/shadow"], + {'content': "root:$1$12345678$a4ge4d5iJ5vw" + + "vbFS88TEN0:14917:0:99999:7:::\n" + + "bin:*:14495:0:99999:7:::\n" + + "daemon:*:14495:0:99999:7:::\n", + 'gid': 100, + 'isdir': False, + 'mode': 0700, + 'uid': 100}) + vfs.teardown() diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index 443bd7b05..33773e59c 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -40,6 +40,7 @@ from nova import utils from nova.virt.disk.mount import guestfs from nova.virt.disk.mount import loop from nova.virt.disk.mount import nbd +from nova.virt.disk.vfs import api as vfs from nova.virt import images @@ -292,15 +293,20 @@ def inject_data(image, If partition is not specified it mounts the image as a single partition. """ - img = _DiskImage(image=image, partition=partition, use_cow=use_cow) - if img.mount(): - try: - inject_data_into_fs(img.mount_dir, - key, net, metadata, admin_password, files) - finally: - img.umount() - else: - raise exception.NovaException(img.errors) + LOG.debug(_("Inject data image=%(image)s key=%(key)s net=%(net)s " + "metadata=%(metadata)s admin_password=ha-ha-not-telling-you " + "files=%(files)s partition=%(partition)s use_cow=%(use_cow)s") + % locals()) + fmt = "raw" + if use_cow: + fmt = "qcow2" + fs = vfs.VFS.instance_for_image(image, fmt, partition) + fs.setup() + try: + inject_data_into_fs(fs, + key, net, metadata, admin_password, files) + finally: + fs.teardown() def setup_container(image, container_dir, use_cow=False): @@ -349,58 +355,32 @@ def inject_data_into_fs(fs, key, net, metadata, admin_password, files): _inject_file_into_fs(fs, path, contents) -def _join_and_check_path_within_fs(fs, *args): - '''os.path.join() with safety check for injected file paths. - - Join the supplied path components and make sure that the - resulting path we are injecting into is within the - mounted guest fs. Trying to be clever and specifying a - path with '..' in it will hit this safeguard. - ''' - absolute_path, _err = utils.execute('readlink', '-nm', - os.path.join(fs, *args), - run_as_root=True) - if not absolute_path.startswith(os.path.realpath(fs) + '/'): - raise exception.Invalid(_('injected file path not valid')) - return absolute_path - - def _inject_file_into_fs(fs, path, contents, append=False): - absolute_path = _join_and_check_path_within_fs(fs, path.lstrip('/')) - - parent_dir = os.path.dirname(absolute_path) - utils.execute('mkdir', '-p', parent_dir, run_as_root=True) - - args = [] + LOG.debug(_("Inject file fs=%(fs)s path=%(path)s append=%(append)s") % + locals()) if append: - args.append('-a') - args.append(absolute_path) - - kwargs = dict(process_input=contents, run_as_root=True) - - utils.execute('tee', *args, **kwargs) + fs.append_file(path, contents) + else: + fs.replace_file(path, contents) def _inject_metadata_into_fs(metadata, fs): + LOG.debug(_("Inject metadata fs=%(fs)s metadata=%(metadata)s") % + locals()) metadata = dict([(m['key'], m['value']) for m in metadata]) _inject_file_into_fs(fs, 'meta.js', jsonutils.dumps(metadata)) -def _setup_selinux_for_keys(fs): +def _setup_selinux_for_keys(fs, sshdir): """Get selinux guests to ensure correct context on injected keys.""" - se_cfg = _join_and_check_path_within_fs(fs, 'etc', 'selinux') - se_cfg, _err = utils.trycmd('readlink', '-e', se_cfg, run_as_root=True) - if not se_cfg: + if not fs.has_file(os.path.join("etc", "selinux")): return - rclocal = _join_and_check_path_within_fs(fs, 'etc', 'rc.local') + rclocal = os.path.join('etc', 'rc.local') + rc_d = os.path.join('etc', 'rc.d') - # Support systemd based systems - rc_d = _join_and_check_path_within_fs(fs, 'etc', 'rc.d') - rclocal_e, _err = utils.trycmd('readlink', '-e', rclocal, run_as_root=True) - rc_d_e, _err = utils.trycmd('readlink', '-e', rc_d, run_as_root=True) - if not rclocal_e and rc_d_e: + if not fs.has_file(rclocal) and fs.has_file(rc_d): rclocal = os.path.join(rc_d, 'rc.local') # Note some systems end rc.local with "exit 0" @@ -409,12 +389,11 @@ def _setup_selinux_for_keys(fs): restorecon = [ '#!/bin/sh\n', '# Added by Nova to ensure injected ssh keys have the right context\n', - 'restorecon -RF /root/.ssh/ 2>/dev/null || :\n', + 'restorecon -RF %s 2>/dev/null || :\n' % sshdir, ] - rclocal_rel = os.path.relpath(rclocal, fs) - _inject_file_into_fs(fs, rclocal_rel, ''.join(restorecon), append=True) - utils.execute('chmod', 'a+x', rclocal, run_as_root=True) + _inject_file_into_fs(fs, rclocal, ''.join(restorecon), append=True) + fs.set_permissions(rclocal, 0700) def _inject_key_into_fs(key, fs): @@ -423,12 +402,15 @@ def _inject_key_into_fs(key, fs): key is an ssh key string. fs is the path to the base of the filesystem into which to inject the key. """ - sshdir = _join_and_check_path_within_fs(fs, 'root', '.ssh') - utils.execute('mkdir', '-p', sshdir, run_as_root=True) - utils.execute('chown', 'root', sshdir, run_as_root=True) - utils.execute('chmod', '700', sshdir, run_as_root=True) - keyfile = os.path.join('root', '.ssh', 'authorized_keys') + LOG.debug(_("Inject key fs=%(fs)s key=%(key)s") % + locals()) + sshdir = os.path.join('root', '.ssh') + fs.make_path(sshdir) + fs.set_ownership(sshdir, "root", "root") + fs.set_permissions(sshdir, 0700) + + keyfile = os.path.join(sshdir, 'authorized_keys') key_data = ''.join([ '\n', @@ -440,7 +422,7 @@ def _inject_key_into_fs(key, fs): _inject_file_into_fs(fs, keyfile, key_data, append=True) - _setup_selinux_for_keys(fs) + _setup_selinux_for_keys(fs, sshdir) def _inject_net_into_fs(net, fs): @@ -448,10 +430,13 @@ def _inject_net_into_fs(net, fs): net is the contents of /etc/network/interfaces. """ - netdir = _join_and_check_path_within_fs(fs, 'etc', 'network') - utils.execute('mkdir', '-p', netdir, run_as_root=True) - utils.execute('chown', 'root:root', netdir, run_as_root=True) - utils.execute('chmod', 755, netdir, run_as_root=True) + + LOG.debug(_("Inject key fs=%(fs)s net=%(net)s") % + locals()) + netdir = os.path.join('etc', 'network') + fs.make_path(netdir) + fs.set_ownership(netdir, "root", "root") + fs.set_permissions(netdir, 0744) netfile = os.path.join('etc', 'network', 'interfaces') _inject_file_into_fs(fs, netfile, net) @@ -472,6 +457,9 @@ def _inject_admin_password_into_fs(admin_passwd, fs): # files from the instance filesystem to local files, make any # necessary changes, and then copy them back. + LOG.debug(_("Inject admin password fs=%(fs)s " + "admin_passwd=ha-ha-not-telling-you") % + locals()) admin_user = 'root' fd, tmp_passwd = tempfile.mkstemp() @@ -479,19 +467,27 @@ def _inject_admin_password_into_fs(admin_passwd, fs): fd, tmp_shadow = tempfile.mkstemp() os.close(fd) - passwd_path = _join_and_check_path_within_fs(fs, 'etc', 'passwd') - shadow_path = _join_and_check_path_within_fs(fs, 'etc', 'shadow') + passwd_path = os.path.join('etc', 'passwd') + shadow_path = os.path.join('etc', 'shadow') + + passwd_data = fs.read_file(passwd_path) + shadow_data = fs.read_file(shadow_path) + + new_shadow_data = _set_passwd(admin_user, admin_passwd, + passwd_data, shadow_data) + + fs.replace_file(shadow_path, new_shadow_data) - utils.execute('cp', passwd_path, tmp_passwd, run_as_root=True) - utils.execute('cp', shadow_path, tmp_shadow, run_as_root=True) - _set_passwd(admin_user, admin_passwd, tmp_passwd, tmp_shadow) - utils.execute('cp', tmp_passwd, passwd_path, run_as_root=True) - os.unlink(tmp_passwd) - utils.execute('cp', tmp_shadow, shadow_path, run_as_root=True) - os.unlink(tmp_shadow) +def _generate_salt(): + salt_set = ('abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789./') + salt = 16 * ' ' + return ''.join([random.choice(salt_set) for c in salt]) -def _set_passwd(username, admin_passwd, passwd_file, shadow_file): + +def _set_passwd(username, admin_passwd, passwd_data, shadow_data): """set the password for username to admin_passwd The passwd_file is not modified. The shadow_file is updated. @@ -508,14 +504,10 @@ def _set_passwd(username, admin_passwd, passwd_file, shadow_file): if os.name == 'nt': raise exception.NovaException(_('Not implemented on Windows')) - salt_set = ('abcdefghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '0123456789./') # encryption algo - id pairs for crypt() algos = {'SHA-512': '$6$', 'SHA-256': '$5$', 'MD5': '$1$', 'DES': ''} - salt = 16 * ' ' - salt = ''.join([random.choice(salt_set) for c in salt]) + salt = _generate_salt() # crypt() depends on the underlying libc, and may not support all # forms of hash. We try md5 first. If we get only 13 characters back, @@ -528,39 +520,34 @@ def _set_passwd(username, admin_passwd, passwd_file, shadow_file): if len(encrypted_passwd) == 13: encrypted_passwd = crypt.crypt(admin_passwd, algos['DES'] + salt) - try: - p_file = open(passwd_file, 'rb') - s_file = open(shadow_file, 'rb') - - # username MUST exist in passwd file or it's an error - found = False - for entry in p_file: - split_entry = entry.split(':') - if split_entry[0] == username: - found = True - break - if not found: - msg = _('User %(username)s not found in password file.') - raise exception.NovaException(msg % username) - - # update password in the shadow file.It's an error if the - # the user doesn't exist. - new_shadow = list() - found = False - for entry in s_file: - split_entry = entry.split(':') - if split_entry[0] == username: - split_entry[1] = encrypted_passwd - found = True - new_entry = ':'.join(split_entry) - new_shadow.append(new_entry) - s_file.close() - if not found: - msg = _('User %(username)s not found in shadow file.') - raise exception.NovaException(msg % username) - s_file = open(shadow_file, 'wb') - for entry in new_shadow: - s_file.write(entry) - finally: - p_file.close() - s_file.close() + p_file = passwd_data.split("\n") + s_file = shadow_data.split("\n") + + # username MUST exist in passwd file or it's an error + found = False + for entry in p_file: + split_entry = entry.split(':') + if split_entry[0] == username: + found = True + break + if not found: + msg = _('User %(username)s not found in password file.') + raise exception.NovaException(msg % username) + + # update password in the shadow file.It's an error if the + # the user doesn't exist. + new_shadow = list() + found = False + for entry in s_file: + split_entry = entry.split(':') + if split_entry[0] == username: + split_entry[1] = encrypted_passwd + found = True + new_entry = ':'.join(split_entry) + new_shadow.append(new_entry) + + if not found: + msg = _('User %(username)s not found in shadow file.') + raise exception.NovaException(msg % username) + + return "\n".join(new_shadow) diff --git a/nova/virt/disk/vfs/api.py b/nova/virt/disk/vfs/api.py index 7d7768bb2..5a3f748e7 100644 --- a/nova/virt/disk/vfs/api.py +++ b/nova/virt/disk/vfs/api.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +from nova.openstack.common import importutils from nova.openstack.common import log as logging LOG = logging.getLogger(__name__) @@ -21,6 +22,30 @@ LOG = logging.getLogger(__name__) class VFS(object): + @staticmethod + def instance_for_image(imgfile, imgfmt, partition): + LOG.debug(_("Instance for image imgfile=%(imgfile)s " + "imgfmt=%(imgfmt)s partition=%(partition)s") + % locals()) + hasGuestfs = False + try: + LOG.debug(_("Trying to import guestfs")) + importutils.import_module("guestfs") + hasGuestfs = True + except Exception: + pass + + if hasGuestfs: + LOG.debug(_("Using primary VFSGuestFS")) + return importutils.import_object( + "nova.virt.disk.vfs.guestfs.VFSGuestFS", + imgfile, imgfmt, partition) + else: + LOG.debug(_("Falling back to VFSLocalFS")) + return importutils.import_object( + "nova.virt.disk.vfs.localfs.VFSLocalFS", + imgfile, imgfmt, partition) + """ The VFS class defines an interface for manipulating files within a virtual disk image filesystem. This allows file injection code diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index a9d11ac49..7dcbd36fd 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -44,6 +44,7 @@ from nova.openstack.common import excutils from nova.openstack.common import log as logging from nova import utils from nova.virt.disk import api as disk +from nova.virt.disk.vfs import localfs as vfsimpl from nova.virt import driver from nova.virt.xenapi import agent from nova.virt.xenapi import volume_utils @@ -2106,11 +2107,14 @@ def _mounted_processing(device, key, net, metadata): try: # This try block ensures that the umount occurs if not agent.find_guest_agent(tmpdir): + vfs = vfsimpl.VFSLocalFS(imgfile=None, + imgfmt=None, + imgdir=tmpdir) LOG.info(_('Manipulating interface files directly')) # for xenapi, we don't 'inject' admin_password here, # it's handled at instance startup time, nor do we # support injecting arbitrary files here. - disk.inject_data_into_fs(tmpdir, + disk.inject_data_into_fs(vfs, key, net, metadata, None, None) finally: utils.execute('umount', dev_path, run_as_root=True) |
