接续上篇博文,我们继续来解析方法_create_image中的第二部分和第三部分,即驱动配置和文件注入部分的代码。
2.驱动配置部分代码解析
先来看方法_create_image中实现驱动配置部分的代码:
- 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):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ......
-
-
- if configdrive.required_by(instance):
- LOG.info(_('Using config drive'), instance=instance)
- extra_md = {}
- if admin_pass:
- extra_md['admin_pass'] = admin_pass
-
-
-
-
-
-
- inst_md = instance_metadata.InstanceMetadata(instance, content=files, extra_md=extra_md)
-
-
-
-
-
-
-
-
- with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb:
-
- configdrive_path = basepath(fname='disk.config')
- LOG.info(_('Creating config drive at %(path)s'),
- {'path': configdrive_path}, instance=instance)
-
-
- try:
- cdb.make_drive(configdrive_path)
- except exception.ProcessExecutionError, e:
- with excutils.save_and_reraise_exception():
- LOG.error(_('Creating config drive failed '
- 'with error: %s'),
- e, instance=instance)
首先经语句if configdrive.required_by(instance)判断,如果系统规定总是创建config drive,则进行后面的驱动配置操作。
接着调用语句inst_md = instance_metadata.InstanceMetadata(instance, content=files, extra_md=extra_md)实现类InstanceMetadata的初始化,获取虚拟机实例元数据操作类的实例化对象;
先来看看类InstanceMetadata的初始化方法:
- class InstanceMetadata():
-
-
-
-
- def __init__(self, instance, address=None, content=[], extra_md=None,
- conductor_api=None):
-
- self.instance = instance
-
- self.extra_md = extra_md
-
-
-
-
-
-
-
- if conductor_api:
- self.conductor_api = conductor_api
- else:
- self.conductor_api = conductor.API()
-
-
- ctxt = context.get_admin_context()
-
-
- capi = self.conductor_api
-
- self.availability_zone = ec2utils.get_availability_zone_by_host(instance['host'], capi)
-
- self.ip_info = ec2utils.get_ip_info_for_instance(ctxt, instance)
-
- self.security_groups = capi.security_group_get_by_instance(ctxt, instance)
-
- self.mappings = _format_instance_mapping(capi, ctxt, instance)
-
- if instance.get('user_data', None) is not None:
- self.userdata_raw = base64.b64decode(instance['user_data'])
- else:
- self.userdata_raw = None
-
- self.ec2_ids = capi.get_ec2_ids(ctxt, instance)
-
- self.address = address
-
-
- self.launch_metadata = {}
- for item in instance.get('metadata', []):
- self.launch_metadata[item['key']] = item['value']
-
- self.password = password.extract_password(instance)
-
- self.uuid = instance.get('uuid')
-
- self.content = {}
- self.files = []
-
-
-
- ctxt = context.get_admin_context()
-
-
- network_info = network.API().get_instance_nw_info(ctxt, instance, conductor_api=capi)
-
- self.network_config = None
-
- cfg = netutils.get_injected_network_template(network_info)
-
- if cfg:
- key = "%04i" % len(self.content)
- self.content[key] = cfg
- self.network_config = {"name": "network_config",
- 'content_path': "/%s/%s" % (CONTENT_DIR, key)}
-
- for (path, contents) in content:
- key = "%04i" % len(self.content)
- self.files.append({'path': path,
- 'content_path': "/%s/%s" % (CONTENT_DIR, key)})
- self.content[key] = contents
类的初始化方法完成了一些变量和参数的初始化过程,我们对其中比较重要的一些语句进行解析。
首先来看下面的语句:
if conductor_api:
self.conductor_api = conductor_api
else:
self.conductor_api = conductor.API()
由传入的参数知道conductor_api的值为none,所以直接执行self.conductor_api = conductor.API()。这条语句是对类API进行了初始化,并获取了实例化对象。具体来看代码:
- def API(*args, **kwargs):
-
-
-
-
-
-
-
-
- use_local = kwargs.pop('use_local', False)
- if oslo.config.cfg.CONF.conductor.use_local or use_local:
- api = conductor_api.LocalAPI
- else:
- api = conductor_api.API
- return api(*args, **kwargs)
就像注释中写的那样,这里根据配置参数“use_local”具体的选择初始化类conductor_api.LocalAPI或者是conductor_api.API,获取并返回类的实例对象。
其中,LocalAPI类是conductor API 的本地版本,这个类处理了本地数据库的更新等操作,而不是通过RPC;而API类则通过RPC处理了数据库的更新等操作。
这里需要简单解释一下nova conductor服务,在Grizzly版的Nova中,nova-conductor是在nova-compute之上的新的服务层,它使得nova-compute不再直接访问数据库。
我们回到类InstanceMetadata的初始化方法中,我们可以看到很多变量和参数的初始化赋值过程中,尤其是涉及到数据库查询的变量赋值中,都是通过nova-conductor这个服务层进行实现的,这个服务层实际上增强了代码的可扩展性和安全性。
我们再回到方法_create_image中,来看语句configdrive.ConfigDriveBuilder(instance_md=inst_md),这条鱼据实现的是对ConfigDriveBuilder这个构建配置驱动器的类进行实例的初始化,并获取类的初始化实例对象。具体来看这个类的初始化方法的代码:
- class ConfigDriveBuilder(object):
-
-
-
-
- def __init__(self, instance_md=None):
-
-
-
-
-
- self.imagefile = None
-
-
-
-
- self.tempdir = tempfile.mkdtemp(dir=CONF.config_drive_tempdir,
- prefix='cd_gen_')
-
-
-
-
-
- if instance_md is not None:
- self.add_instance_metadata(instance_md)
这个初始化过程中,最重要的语句就是self.add_instance_metadata(instance_md),它通过调用方法add_instance_metadata来实现把instance_md中的元数据写入到驱动配置的临时目录文件tempdir中。我们进一步来看方法add_instance_metadata的实现:
- def add_instance_metadata(self, instance_md):
-
-
-
-
-
-
-
-
- for (path, value) in instance_md.metadata_for_config_drive():
- self._add_file(path, value)
- LOG.debug(_('Added %(filepath)s to config drive'),
- {'filepath': path})
再来看方法metadata_for_config_drive:
- def metadata_for_config_drive(self):
-
-
-
-
-
-
-
-
-
-
- for version in VERSIONS + ["latest"]:
- if version in CONF.config_drive_skip_versions.split(' '):
- continue
-
-
- data = self.get_ec2_metadata(version)
- if 'user-data' in data:
- filepath = os.path.join('ec2', version, 'user-data')
- yield (filepath, data['user-data'])
- del data['user-data']
-
- try:
- del data['public-keys']['0']['_name']
- except KeyError:
- pass
-
- filepath = os.path.join('ec2', version, 'meta-data.json')
- yield (filepath, json.dumps(data['meta-data']))
-
- for version in OPENSTACK_VERSIONS + ["latest"]:
- path = 'openstack/%s/%s' % (version, MD_JSON_NAME)
- yield (path, self.lookup(path))
-
- path = 'openstack/%s/%s' % (version, UD_NAME)
- if self.userdata_raw is not None:
- yield (path, self.lookup(path))
-
- for (cid, content) in self.content.iteritems():
- yield ('%s/%s/%s' % ("openstack", CONTENT_DIR, cid), content)
以上的两个方法add_instance_metadata和metadata_for_config_drive最终实现了获取实例元数据,并根据版本version更新不同版本的EC2类型的实例元数据,并把实例元数据写入到配置驱动的临时存储文件当中。这里不再对这两个方法的具体实现进行代码解析,可以直接看我的代码注释即可。
至此,语句configdrive.ConfigDriveBuilder(instance_md=inst_md)解析完成。
我们再回到方法_create_image之中,继续看驱动配置的执行代码。之后最重要的一条语句就是cdb.make_drive(configdrive_path)。这条语句实现了根据配置参数选择,生成ISO格式或vfat格式的镜像文件,默认是ISO格式的。
我们来具体看方法make_drive的代码:
- def make_drive(self, path):
-
-
-
-
-
-
-
-
- if CONF.config_drive_format == 'iso9660':
- self._make_iso9660(path)
- elif CONF.config_drive_format == 'vfat':
- self._make_vfat(path)
- else:
- raise exception.ConfigDriveUnknownFormat(
- format=CONF.config_drive_format)
配置参数config_drive_format定义了配置驱动的格式,iso9660或者是vfat。由于参数的默认值为iso9660,所以这里默认调用方法_make_iso9660来生成ISO格式的镜像文件。进一步来看方法_make_iso9660的代码:
- def _make_iso9660(self, path):
-
-
-
- publisher = "%(product)s %(version)s" % {
- 'product': version.product_string(),
- 'version': version.version_string_with_package()
- }
-
- utils.execute(CONF.mkisofs_cmd,
- '-o', path,
- '-ldots',
- '-allow-lowercase',
- '-allow-multidot',
- '-l',
- '-publisher',
- publisher,
- '-quiet',
- '-J',
- '-r',
- '-V', 'config-2',
- self.tempdir,
- attempts=1,
- run_as_root=False)
可见这里通过配置参数CONF.mkisofs_cmd调用命令genisoimage,来实现ISO格式的镜像文件的建立。
至此,方法_create_image中的第二部分,驱动配置的实现解析完成。(但是理解上还不到位)
3.文件注入部分代码解析
我们先来看方法_create_image中实现文件注入部分的代码:
- 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):
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ......
-
-
- elif CONF.libvirt_inject_partition != -2:
-
- target_partition = None
- if not instance['kernel_id']:
-
- target_partition = CONF.libvirt_inject_partition
- if target_partition == 0:
- target_partition = None
-
- if CONF.libvirt_type == 'lxc':
- target_partition = None
-
-
-
-
- if CONF.libvirt_inject_key and instance['key_data']:
- key = str(instance['key_data'])
- else:
- key = None
-
-
- net = netutils.get_injected_network_template(network_info)
-
-
- metadata = instance.get('metadata')
-
-
-
- if not CONF.libvirt_inject_password:
- admin_pass = None
-
-
- if any((key, net, metadata, admin_pass, files)):
-
- 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)
-
-
-
-
-
- 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)
实际上,文件注入部分实现的就是把key(ssh公钥),net(渲染好的网络模板),metadata(虚拟机实例的元数据),admin_pass(用户密码)和files(传进来的已编码的文件)等注入到建立好的磁盘镜像之中。而实现这个过程的最重要的语句就是:
disk.inject_data(injection_path,
key, net, metadata, admin_pass, files,
partition=target_partition,
use_cow=CONF.use_cow_images,
mandatory=('files',))
我们来进一步看看方法inject_data的具体代码实现:
- def inject_data(image, key=None, net=None, metadata=None, admin_password=None,
- files=None, partition=None, use_cow=False, mandatory=()):
-
-
-
-
-
-
-
-
- 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:
- 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()
在这个方法中首先调用方法instance_for_image实现为挂载建立好的镜像准备磁盘,并调用方法setup实现磁盘的挂载。
再调用方法inject_data_into_fs实现相关文件的文件注入。我们来看方法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_key_into_fs、_inject_net_into_fs、_inject_metadata_into_fs、_inject_admin_password_into_fs和_inject_files_into_fs,具体实现key、net、metadata、admin_password和files等文件的注入过程。
我们以方法_inject_net_into_fs为例,来解析向磁盘镜像注入文件的实现过程。来看方法_inject_net_into_fs的代码实现:
- def _inject_net_into_fs(net, fs):
-
-
-
-
-
-
- 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)
这个方法实现了注入文件/etc/network/interfaces(即net)到文件系统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())
-
- if append:
- fs.append_file(path, contents)
-
- else:
- fs.replace_file(path, contents)
这个方法的实现比较好理解,直接看我的代码注释即可。
至此,方法_create_image中的第三部分,即文件注入已完全解析完成。
从而,方法_create_image也已经解析完成。
在下篇博文 OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(4)当中,我们将回到方法spawn中,继续对Nova虚拟机启动源码实现进行解析