感谢朋友支持本博客,欢迎共同探讨交流,由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn
上一篇博客中我们介绍了文件注入的相关内容,相关代码的结构,以及文件注入过程中支持的镜像文件挂载方式。现在,我们具体来看代码中文件注入的具体实现,由于在之前的虚拟机建立代码分析过程中涉及到了此部分的内容,所以我们只是来看看大致的流程,以便更好的掌握上一篇博文中的内容,就不对代码的每一行实现进行详细的解析。
在虚拟机建立的过程中,在虚拟机启动之前,可以对实例镜像进行相关文件和元数据的注入操作,具体的代码如下:
def _create_image(self, context, instance, libvirt_xml,
disk_mapping, suffix='',
disk_images=None, network_info=None,
block_device_info=None, files=None, admin_pass=None):
......
"""
1.调用类Image下的方法cache,来实现对各种类型磁盘镜像的建立部分;
"""
......
"""
2.驱动配置部分;
"""
......
"""
3.文件注入部分;
"""
elif CONF.libvirt_inject_partition != -2:
# 要注入文件的目标分区号;
target_partition = None
if not instance['kernel_id']:
# 如果不是kernel_id镜像;
target_partition = CONF.libvirt_inject_partition
if target_partition == 0:
target_partition = None
# 如果虚拟机实例类型为lxc,则目标分区号设置为None;
if CONF.libvirt_type == 'lxc':
target_partition = None
# 如果定义了开机时注入ssh公钥,而且实例中具有'key_data'数据,则获取这个'key_data'数据;
# libvirt_inject_key:这个参数定义了在开机时,是否注入ssh公钥;
# 参数的默认值为True;
if CONF.libvirt_inject_key and instance['key_data']:
key = str(instance['key_data'])
else:
key = None
# get_injected_network_template:根据给定的网络信息返回一个渲染好的网络模板;
net = netutils.get_injected_network_template(network_info)
# 获取虚拟机实例的元数据metadata;
metadata = instance.get('metadata')
# libvirt_inject_password:这个参数定义了在开机时,是否注入管理员密码;
# 参数的默认值为False;
if not CONF.libvirt_inject_password:
admin_pass = None
# 如果key, net, metadata, admin_pass, files有一项不为none,就执行下面的代码;
if any((key, net, metadata, admin_pass, files)):
# If we're not using config_drive, inject into root fs
injection_path = image('disk').path
img_id = instance['image_ref']
for inj in ('key', 'net', 'metadata', 'admin_pass', 'files'):
if locals()[inj]:
LOG.info(_('Injecting %(inj)s into image '
'%(img_id)s'), locals(), instance=instance)
# inject_data:注入指定的项目到指定的磁盘镜像;
# injection_path:要注入磁盘镜像的存储路径;
# key, net, metadata, admin_pass, files:要注入的项目;
# partition=target_partition:要注入磁盘的分区号;
# use_cow=CONF.use_cow_images:这个参数定义了是否使用cow格式的镜像文件,默认值为True;
try:
disk.inject_data(injection_path,
key, net, metadata, admin_pass, files,
partition=target_partition,
use_cow=CONF.use_cow_images,
mandatory=('files',))
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_('Error injecting data into image '
'%(img_id)s (%(e)s)') % locals(),
instance=instance)
# chown:为admin_passwd设置超级用户密码;
if CONF.libvirt_type == 'uml':
libvirt_utils.chown(image('disk').path, 'root')
我们关注的是这里的语句:
disk.inject_data(injection_path,
key, net, metadata, admin_pass, files,
partition=target_partition,
use_cow=CONF.use_cow_images,
mandatory=('files',))
实现了注入指定的项目到指定的磁盘镜像文件;
具体来看方法disk.inject_data的实现:
/nova/virt/disk/api.py
def inject_data(image, key=None, net=None, metadata=None, admin_password=None,
files=None, partition=None, use_cow=False, mandatory=()):
"""
注入指定的项目到磁盘镜像image;
# image:要注入磁盘镜像的存储路径;
# key, net, metadata, admin_pass, files:要注入的项目;
# partition:要注入磁盘的分区号;
# use_cow:这个参数定义了是否使用cow格式的镜像文件,默认值为True;
"""
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"
try:
fs = vfs.VFS.instance_for_image(image, fmt, partition)
fs.setup()
except Exception as e:
# If a mandatory item is passed to this function,
# then reraise the exception to indicate the error.
for inject in mandatory:
inject_val = locals()[inject]
if inject_val:
raise
LOG.warn(_('Ignoring error injecting data into image '
'(%(e)s)') % locals())
return False
try:
return inject_data_into_fs(fs, key, net, metadata,
admin_password, files, mandatory)
finally:
fs.teardown()
首先来看语句fs = vfs.VFS.instance_for_image(image, fmt, partition),方法instance_for_image实现了尝试导入guestfs模块;如果guestfs模块导入成功,则继而导入类nova.virt.disk.vfs.guestfs.VFSGuestFS;如果guestfs模块导入不成功,则继而导入类nova.virt.disk.vfs.localfs.VFSLocalFS;从而实现了选择不同的文件系统类型(本地文件系统或来宾文件系统)对磁盘镜像的操作实现类;具体来看方法instance_for_image的代码实现:
def instance_for_image(imgfile, imgfmt, partition):
"""
尝试导入guestfs模块;
如果guestfs模块导入成功,则继而导入类nova.virt.disk.vfs.guestfs.VFSGuestFS;
如果guestfs模块导入不成功,则继而导入类nova.virt.disk.vfs.localfs.VFSLocalFS;
"""
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)
回到方法inject_data中,来看接下来的语句:
fs.setup()
语句根据上面选择不同的磁盘镜像文件处理类,调用相应类下的setup方法,实现挂载设备到具体的挂载点上。
所以根据前面选择不同的磁盘镜像文件处理类,我们接下来分两种情况对代码进行解析:
1.如果guestfs模块导入成功,则继而导入类nova.virt.disk.vfs.guestfs.VFSGuestFS
来看方法setup的实现代码:
/nova/virt/disk/vfs/guestfs.py
def setup(self):
LOG.debug(_("Setting up appliance for %(imgfile)s %(imgfmt)s") %
{'imgfile': self.imgfile, 'imgfmt': self.imgfmt})
self.handle = tpool.Proxy(guestfs.GuestFS())
try:
self.handle.add_drive_opts(self.imgfile, format=self.imgfmt)
if self.handle.get_attach_method() == 'libvirt':
libvirt_url = 'libvirt:' + libvirt_driver.LibvirtDriver.uri()
self.handle.set_attach_method(libvirt_url)
self.handle.launch()
self.setup_os()
self.handle.aug_init("/", 0)
except RuntimeError, e:
# dereference object and implicitly close()
self.handle = None
raise exception.NovaException(
_("Error mounting %(imgfile)s with libguestfs (%(e)s)") %
{'imgfile': self.imgfile, 'e': e})
except Exception:
self.handle = None
raise
这个方法实现的就是把设备挂载在指定的挂载点上。具体的实现过程中,主要以代理的方式调用了类GuestFS下面的一些方法。类GuestFS实现的就是来宾文件系统的操作方式。
重点来看语句self.setup_os(),来看方法setup_os:
def setup_os(self):
if self.partition == -1:
self.setup_os_inspect()
else:
self.setup_os_static()
这里说明一下partition的取值定义,如果值为-2说明磁盘不能分区,如果值为-1说明只针对libguestfs来进行磁盘操作,如果值为0说明磁盘没有进行分区,如果值大于0说明磁盘进行了分区,具体数值就代表了要注入文件的目标磁盘的分区号。先来看方法setup_os_inspect,说明此时只针对libguestfs来进行磁盘操作:
def setup_os_inspect(self):
LOG.debug(_("Inspecting guest OS image %s"), self.imgfile)
# 这里返回的是["/dev/guestvgf/lv_root"],即roots = ["/dev/guestvgf/lv_root"];
roots = self.handle.inspect_os()
if len(roots) == 0:
raise exception.NovaException(_("No operating system found in %s")
% self.imgfile)
if len(roots) != 1:
LOG.debug(_("Multi-boot OS %(roots)s") % {'roots': str(roots)})
raise exception.NovaException(
_("Multi-boot operating system found in %s") %
self.imgfile)
self.setup_os_root(roots[0])
def setup_os_root(self, root):
LOG.debug(_("Inspecting guest OS root filesystem %s"), root)
# 这里返回的是[["/", "/dev/mapper/guestvgf-lv_root"],["/boot", "/dev/vda1"]];
# 即mounts = [["/", "/dev/mapper/guestvgf-lv_root"],["/boot", "/dev/vda1"]];
mounts = self.handle.inspect_get_mountpoints(root)
if len(mounts) == 0:
raise exception.NovaException(
_("No mount points found in %(root)s of %(imgfile)s") %
{'root': root, 'imgfile': self.imgfile})
mounts.sort(key=lambda mount: mount[1])
for mount in mounts:
LOG.debug(_("Mounting %(dev)s at %(dir)s") %
{'dev': mount[1], 'dir': mount[0]})
self.handle.mount_options("", mount[1], mount[0])
def mount_options(self, options, device, mntpoint):
self.mounts.append((options, device, mntpoint))
可以见到,这里实现的就是确定["/", "/dev/mapper/guestvgf-lv_root"]和["/boot", "/dev/vda1"]中的设备信息和挂载点信息,分别是设备"/dev/mapper/guestvgf-lv_root"对应挂载点"/",设备"/dev/vda1"对应挂载点"/boot"。当然这是只针对libguestfs来进行磁盘操作的情况下。
下面我们再会到方法setup_os中,来看方法setup_os_static,这是在指定了具体的文件注入目标磁盘分区的情况之下的。
def setup_os_static(self):
LOG.debug(_("Mount guest OS image %(imgfile)s partition %(part)s"),
{'imgfile': self.imgfile, 'part': str(self.partition)})
if self.partition:
self.handle.mount_options("", "/dev/sda%d" % self.partition, "/")
else:
self.handle.mount_options("", "/dev/sda", "/")
def mount_options(self, options, device, mntpoint):
self.mounts.append((options, device, mntpoint))
最后也是确定了具体的设备信息和挂载点信息。
2.如果guestfs模块导入不成功,则继而导入类nova.virt.disk.vfs.localfs.VFSLocalFS
来看方法setup的实现代码:
def setup(self):
self.imgdir = tempfile.mkdtemp(prefix="openstack-vfs-localfs")
try:
if self.imgfmt == "raw":
LOG.debug(_("Using LoopMount"))
mount = loop.LoopMount(self.imgfile,
self.imgdir,
self.partition)
else:
LOG.debug(_("Using NbdMount"))
mount = nbd.NbdMount(self.imgfile,
self.imgdir,
self.partition)
if not mount.do_mount():
raise exception.NovaException(mount.error)
self.mount = mount
except Exception, e:
LOG.debug(_("Failed to mount image %(ex)s)") %
{'ex': str(e)})
self.teardown()
raise e
可见这里实现了根据镜像文件的格式来选择所应用的磁盘挂载方式;具体来说就是如果磁盘镜像格式为raw,则采用loop挂载方式,获取类LoopMount的初始化对象;如果磁盘镜像格式不为raw,则采用nbd挂载方式,获取类NbdMount的初始化对象;然后调用相应的do_mount方法,来实现磁盘的挂载。
所以我们也分两种情况来进行分析:
2.1 如果实例镜像格式为raw
具体来看方法do_mount的实现(方法do_mount只在挂载类基类Mount中有一个实现,但是其中的具体实现分情况调用了类LoopMount和类NbdMount中的方法):
/nova/virt/disk/mount/api.py
def do_mount(self):
"""
Call the get, map and mnt operations.
调用get、map和mnt等操作实现设备的挂载操作;
"""
status = False
try:
status = self.get_dev() and self.map_dev() and self.mnt_dev()
finally:
if not status:
LOG.debug(_("Fail to mount, tearing back down"))
self.do_teardown()
return status
首先是方法get_dev获取设备信息:
/nova/virt/disk/mount/loop.py
def get_dev(self):
return self._get_dev_retry_helper()
/nova/virt/disk/mount/api.py
def _get_dev_retry_helper(self):
start_time = time.time()
device = self._inner_get_dev()
while not device:
LOG.info(_('Device allocation failed. Will retry in 2 seconds.'))
time.sleep(2)
if time.time() - start_time > MAX_DEVICE_WAIT:
LOG.warn(_('Device allocation failed after repeated retries.'))
return False
device = self._inner_get_dev()
return True
/nova/virt/disk/mount/loop.py
def _inner_get_dev(self):
out, err = utils.trycmd('losetup', '--find', '--show', self.image,
run_as_root=True)
if err:
self.error = _('Could not attach image to loopback: %s') % err
LOG.info(_('Loop mount error: %s'), self.error)
self.linked = False
self.device = None
return False
self.device = out.strip()
LOG.debug(_("Got loop device %s"), self.device)
self.linked = True
return True
接下来是方法map_dev实现了映射设备的分区到文件系统的命名空间;
/nova/virt/disk/mount/api.py
def map_dev(self):
"""
映射设备的分区到文件系统的命名空间;
"""
assert(os.path.exists(self.device))
LOG.debug(_("Map dev %s"), self.device)
automapped_path = '/dev/%sp%s' % (os.path.basename(self.device),
self.partition)
if self.partition == -1:
self.error = _('partition search unsupported with %s') % self.mode
elif self.partition and not os.path.exists(automapped_path):
map_path = '/dev/mapper/%sp%s' % (os.path.basename(self.device),
self.partition)
assert(not os.path.exists(map_path))
# Note kpartx can output warnings to stderr and succeed
# Also it can output failures to stderr and "succeed"
# So we just go on the existence of the mapped device
_out, err = utils.trycmd('kpartx', '-a', self.device,
run_as_root=True, discard_warnings=True)
# Note kpartx does nothing when presented with a raw image,
# so given we only use it when we expect a partitioned image, fail
if not os.path.exists(map_path):
if not err:
err = _('partition %s not found') % self.partition
self.error = _('Failed to map partitions: %s') % err
else:
self.mapped_device = map_path
self.mapped = True
elif self.partition and os.path.exists(automapped_path):
# Note auto mapping can be enabled with the 'max_part' option
# to the nbd or loop kernel modules. Beware of possible races
# in the partition scanning for _loop_ devices though
# (details in bug 1024586), which are currently uncatered for.
self.mapped_device = automapped_path
self.mapped = True
self.automapped = True
else:
self.mapped_device = self.device
self.mapped = True
return self.mapped
最后是方法mnt_dev,实现了挂载设备到文件系统;
/nova/virt/disk/mount/api.py
def mnt_dev(self):
"""
挂载设备到文件系统;
"""
LOG.debug(_("Mount %(dev)s on %(dir)s") %
{'dev': self.mapped_device, 'dir': self.mount_dir})
_out, err = utils.trycmd('mount', self.mapped_device, self.mount_dir,
discard_warnings=True, run_as_root=True)
if err:
self.error = _('Failed to mount filesystem: %s') % err
LOG.debug(self.error)
return False
self.mounted = True
return True
在回到方法do_mount中,如果挂载设备不成功,则需要释放占用的资源,则需调用方法do_teardown来实现,来看代码:
/nova/virt/disk/mount/api.py
def do_teardown(self):
"""
调用umnt、unmap和unget操作实现相关资源的卸载及清理工作;
"""
if self.mounted:
self.unmnt_dev()
if self.mapped:
self.unmap_dev()
if self.linked:
self.unget_dev()
这里具体调用了三个方法来实现相关资源的释放,具体不再展开。
2.2 如果实例镜像格式不为raw
则需要调用类NbdMount中的方法来实现方法do_mount:
/nova/virt/disk/mount/api.py
def do_mount(self):
"""
Call the get, map and mnt operations.
调用get、map和mnt等操作实现设备的挂载操作;
"""
status = False
try:
status = self.get_dev() and self.map_dev() and self.mnt_dev()
finally:
if not status:
LOG.debug(_("Fail to mount, tearing back down"))
self.do_teardown()
return status
具体的看了一下,在类LoopMount和类NbdMount中,对应的分别实现的方法只有get_dev和unget_dev,其余的方法都是共用的在挂载操作基类Mount中所实现的。所以这里对实力镜像格式不为raw的情况下的do_mount方法的实现,即设备挂载的实现不再进行详细的解析,步骤跟2.1中所叙述大致相同,只是个别方法的实现不同而已(也就是方法get_dev和unget_dev);
至此,方法setup解析完成,也就是方法inject_data中的语句fs.setup解析完成,我们分别对本地文件系统和来宾文件系统两种不同情况下的设备挂载的实现进行了解析。
下面我们回到方法inject_data中,来看语句:
return inject_data_into_fs(fs, key, net, metadata, admin_password, files, mandatory)
这条语句调用方法inject_data_into_fs实现注入文件到已挂载的文件系统中。具体来看方法的代码实现:
def inject_data_into_fs(fs, key, net, metadata, admin_password, files,
mandatory=()):
"""
注入指定数据到已经挂载的文件系统;
"""
status = True
for inject in ('key', 'net', 'metadata', 'admin_password', 'files'):
inject_val = locals()[inject]
inject_func = globals()['_inject_%s_into_fs' % inject]
if inject_val:
try:
inject_func(inject_val, fs)
except Exception as e:
if inject in mandatory:
raise
LOG.warn(_('Ignoring error injecting %(inject)s into image '
'(%(e)s)') % locals())
status = False
return status
这里采用字符串匹配的方式,分不同的情况调用不同的方法,来实现不同类型文件的注入,调用的具体方法有:
_inject_files_into_fs
_inject_metadata_into_fs
_inject_key_into_fs
_inject_net_into_fs
_inject_admin_password_into_fs
在这个几方法实现的过程中,除了方法_inject_admin_password_into_fs,其余方法都最终调用了方法_inject_file_into_fs来实现注入文件到指定的文件系统,我们来看方法_inject_file_into_fs的代码:
def _inject_file_into_fs(fs, path, contents, append=False):
"""
注入指定的文件到指定的文件系统;
"""
LOG.debug(_("Inject file fs=%(fs)s path=%(path)s append=%(append)s") %
locals())
# 追加contents到path指定文件的结尾;
if append:
fs.append_file(path, contents)
# 用contents替换path指定文件的内容;
else:
fs.replace_file(path, contents)
可见这里分两种情况对文件的注入形式进行不同的操作,选择是替换掉原有的内容,还是追加新的内容到要注入位置的尾部(因为要注入的可能是文件、元数据、key和网络信息等,有的需要进行替换操作,有的则需要添加到要注入位置的尾部)。
而且方法append_file和方法replace_file都根据导入的类nova.virt.disk.vfs.guestfs.VFSGuestFS和nova.virt.disk.vfs.localfs.VFSLocalFS的不同,分别有两种具体的实现:
/nova/virt/disk/vfs/guestfs.py
def append_file(self, path, content):
LOG.debug(_("Append file path=%(path)s") % locals())
path = self._canonicalize_path(path)
self.handle.write_append(path, content)
/nova/virt/disk/vfs/localfs.py
def append_file(self, path, content):
LOG.debug(_("Append file path=%(path)s") % locals())
canonpath = self._canonical_path(path)
args = ["-a", canonpath]
kwargs = dict(process_input=content, run_as_root=True)
utils.execute('tee', *args, **kwargs)
/nova/virt/disk/vfs/guestfs.py
def replace_file(self, path, content):
LOG.debug(_("Replace file path=%(path)s") % locals())
path = self._canonicalize_path(path)
self.handle.write(path, content)
/nova/virt/disk/vfs/localfs.py
def replace_file(self, path, content):
LOG.debug(_("Replace file path=%(path)s") % locals())
canonpath = self._canonical_path(path)
args = [canonpath]
kwargs = dict(process_input=content, run_as_root=True)
utils.execute('tee', *args, **kwargs)
而在方法_inject_admin_password_into_fs中,最后直接调用了方法replace_file来实现对要注入的新的密码的系统的密码的替换操作。
至此,我们完成了对文件注入过程的代码的解析工作。
由于时间有限,理解肯定有不正确或不到位的地方,还希望大家批评指正。