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>
This commit is contained in:
Daniel P. Berrange 2012-11-13 11:21:09 +00:00
parent 74e38f1bae
commit 72918415f2
5 changed files with 290 additions and 157 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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')
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)
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)
def _set_passwd(username, admin_passwd, passwd_file, shadow_file):
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_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')
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)
# 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()
# 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)

View File

@ -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

View File

@ -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)