# 调试 nova rescue --password 参数过程记录

## 代码逻辑

nova rescue 进行密码注入的整个代码逻辑还是十分简洁明了的，在使用命令时加上 --password 参数便可以进行密码的注入。

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46   # nova/nova/api/openstack/compute/rescue.py @wsgi.expected_errors((400, 404, 409, 501)) @wsgi.action('rescue') @validation.schema(rescue.rescue) def _rescue(self, req, id, body): """Rescue an instance.""" context = req.environ["nova.context"] # 从请求中获取要注入的密码，如果没有指定密码就使用 utils.generate_password() 方法随机生成一个 if body['rescue'] and 'adminPass' in body['rescue']: password = body['rescue']['adminPass'] else: password = utils.generate_password() instance = common.get_instance(self.compute_api, context, id) context.can(rescue_policies.BASE_POLICY_NAME, target={'user_id': instance.user_id, 'project_id': instance.project_id}) rescue_image_ref = None if body['rescue']: # 尝试从请求中获取 rescue 要使用的镜像，此处要是没获取到也没关系，后面会使用启动实例的默认镜像 rescue_image_ref = body['rescue'].get('rescue_image_ref') try: # 调用 nova/nova/compute/api.py 中的 rescue 方法 self.compute_api.rescue(context, instance, rescue_password=password, rescue_image_ref=rescue_image_ref) except exception.InstanceUnknownCell as e: raise exc.HTTPNotFound(explanation=e.format_message()) except exception.InstanceIsLocked as e: raise exc.HTTPConflict(explanation=e.format_message()) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, 'rescue', id) except exception.InvalidVolume as volume_error: raise exc.HTTPConflict(explanation=volume_error.format_message()) except exception.InstanceNotRescuable as non_rescuable: raise exc.HTTPBadRequest( explanation=non_rescuable.format_message()) if CONF.api.enable_instance_password: # 返回注入的密码 return {'adminPass': password} else: return {} 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28   # nova/nova/compute/api.py @check_instance_lock @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.ERROR]) def rescue(self, context, instance, rescue_password=None, rescue_image_ref=None, clean_shutdown=True): """Rescue the given instance.""" bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( context, instance.uuid) for bdm in bdms: if bdm.volume_id: vol = self.volume_api.get(context, bdm.volume_id) self.volume_api.check_attached(context, vol) if compute_utils.is_volume_backed_instance(context, instance, bdms): reason = _("Cannot rescue a volume-backed instance") raise exception.InstanceNotRescuable(instance_id=instance.uuid, reason=reason) instance.task_state = task_states.RESCUING instance.save(expected_task_state=[None]) self._record_action_start(context, instance, instance_actions.RESCUE) # 调用 nova/nova/compute/rpcapi.py 的 rescue_instance 方法 self.compute_rpcapi.rescue_instance(context, instance=instance, rescue_password=rescue_password, rescue_image_ref=rescue_image_ref, clean_shutdown=clean_shutdown) 

  1 2 3 4 5 6 7 8 9 10 11 12 13   # nova/nova/compute/rpcapi.py def rescue_instance(self, ctxt, instance, rescue_password, rescue_image_ref=None, clean_shutdown=True): version = self._ver(ctxt, '4.0') msg_args = {'rescue_password': rescue_password, 'clean_shutdown': clean_shutdown, 'rescue_image_ref': rescue_image_ref, 'instance': instance, } cctxt = self.router.client(ctxt).prepare( server=_compute_host(None, instance), version=version) # 异步调用 nova/nova/compute/manager.py 中的 rescue_instance 方法 cctxt.cast(ctxt, 'rescue_instance', **msg_args) 

rescue_instance 方法中，会对目标 instance 进行关机，状态切换等操作，同时会再调用 nova/nova/virt/libvirt/driver.py 中的 rescue 方法。

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60   # nova/nova/compute/manager.py @wrap_exception() @reverts_task_state @wrap_instance_event(prefix='compute') @wrap_instance_fault def rescue_instance(self, context, instance, rescue_password, rescue_image_ref, clean_shutdown): context = context.elevated() LOG.info('Rescuing', instance=instance) # 再次对要注入的密码进行判断，如果为空就重新随机生成一个 admin_password = (rescue_password if rescue_password else utils.generate_password()) network_info = self.network_api.get_instance_nw_info(context, instance) rescue_image_meta = self._get_rescue_image(context, instance, rescue_image_ref) extra_usage_info = {'rescue_image_name': self._get_image_name(rescue_image_meta)} self._notify_about_instance_usage(context, instance, "rescue.start", extra_usage_info=extra_usage_info, network_info=network_info) compute_utils.notify_about_instance_rescue_action( context, instance, self.host, rescue_image_ref, action=fields.NotificationAction.RESCUE, phase=fields.NotificationPhase.START) try: self._power_off_instance(context, instance, clean_shutdown) # 进入 nova/nova/virt/libvirt/driver.py 的 rescue 方法 self.driver.rescue(context, instance, network_info, rescue_image_meta, admin_password) except Exception as e: LOG.exception("Error trying to Rescue Instance", instance=instance) self._set_instance_obj_error_state(context, instance) raise exception.InstanceNotRescuable( instance_id=instance.uuid, reason=_("Driver Error: %s") % e) compute_utils.notify_usage_exists(self.notifier, context, instance, current_period=True) instance.vm_state = vm_states.RESCUED instance.task_state = None instance.power_state = self._get_power_state(context, instance) instance.launched_at = timeutils.utcnow() instance.save(expected_task_state=task_states.RESCUING) self._notify_about_instance_usage(context, instance, "rescue.end", extra_usage_info=extra_usage_info, network_info=network_info) compute_utils.notify_about_instance_rescue_action( context, instance, self.host, rescue_image_ref, action=fields.NotificationAction.RESCUE, phase=fields.NotificationPhase.END) 

rescue 方法中又会对相关信息做一次整理，再调用同一文件中的 _create_image 方法。

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50   # nova/nova/virt/libvirt/driver.py def rescue(self, context, instance, network_info, image_meta, rescue_password): """Loads a VM using rescue images. A rescue is normally performed when something goes wrong with the primary images and data needs to be corrected/recovered. Rescuing should not edit or over-ride the original image, only allow for data recovery. """ instance_dir = libvirt_utils.get_instance_path(instance) unrescue_xml = self._get_existing_domain_xml(instance, network_info) unrescue_xml_path = os.path.join(instance_dir, 'unrescue.xml') libvirt_utils.write_to_file(unrescue_xml_path, unrescue_xml) rescue_image_id = None if image_meta.obj_attr_is_set("id"): rescue_image_id = image_meta.id rescue_images = { 'image_id': (rescue_image_id or CONF.libvirt.rescue_image_id or instance.image_ref), 'kernel_id': (CONF.libvirt.rescue_kernel_id or instance.kernel_id), 'ramdisk_id': (CONF.libvirt.rescue_ramdisk_id or instance.ramdisk_id), } disk_info = blockinfo.get_disk_info(CONF.libvirt.virt_type, instance, image_meta, rescue=True) injection_info = InjectionInfo(network_info=network_info, admin_pass=rescue_password, files=None) gen_confdrive = functools.partial(self._create_configdrive, context, instance, injection_info, rescue=True) # NOTE(sbauza): Since rescue recreates the guest XML, we need to # remember the existing mdevs for reusing them. mdevs = self._get_all_assigned_mediated_devices(instance) mdevs = list(mdevs.keys()) self._create_image(context, instance, disk_info['mapping'], injection_info=injection_info, suffix='.rescue', disk_images=rescue_images) xml = self._get_guest_xml(context, instance, network_info, disk_info, image_meta, rescue=rescue_images, mdevs=mdevs) self._destroy(instance) self._create_domain(xml, post_xml_callback=gen_confdrive) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137   # nova/nova/virt/libvirt/driver.py def _create_image(self, context, instance, disk_mapping, injection_info=None, suffix='', disk_images=None, block_device_info=None, fallback_from_host=None, ignore_bdi_for_swap=False): booted_from_volume = self._is_booted_from_volume(block_device_info) def image(fname, image_type=CONF.libvirt.images_type): return self.image_backend.by_name(instance, fname + suffix, image_type) def raw(fname): return image(fname, image_type='raw') # ensure directories exist and are writable fileutils.ensure_tree(libvirt_utils.get_instance_path(instance)) LOG.info('Creating image', instance=instance) inst_type = instance.get_flavor() swap_mb = 0 if 'disk.swap' in disk_mapping: mapping = disk_mapping['disk.swap'] if ignore_bdi_for_swap: # This is a workaround to support legacy swap resizing, # which does not touch swap size specified in bdm, # but works with flavor specified size only. # In this case we follow the legacy logic and ignore block # device info completely. # NOTE(ft): This workaround must be removed when a correct # implementation of resize operation changing sizes in bdms is # developed. Also at that stage we probably may get rid of # the direct usage of flavor swap size here, # leaving the work with bdm only. swap_mb = inst_type['swap'] else: swap = driver.block_device_info_get_swap(block_device_info) if driver.swap_is_usable(swap): swap_mb = swap['swap_size'] elif (inst_type['swap'] > 0 and not block_device.volume_in_mapping( mapping['dev'], block_device_info)): swap_mb = inst_type['swap'] if swap_mb > 0: if (CONF.libvirt.virt_type == "parallels" and instance.vm_mode == fields.VMMode.EXE): msg = _("Swap disk is not supported " "for Virtuozzo container") raise exception.Invalid(msg) if not disk_images: disk_images = {'image_id': instance.image_ref, 'kernel_id': instance.kernel_id, 'ramdisk_id': instance.ramdisk_id} if disk_images['kernel_id']: fname = imagecache.get_cache_fname(disk_images['kernel_id']) raw('kernel').cache(fetch_func=libvirt_utils.fetch_raw_image, context=context, filename=fname, image_id=disk_images['kernel_id']) if disk_images['ramdisk_id']: fname = imagecache.get_cache_fname(disk_images['ramdisk_id']) raw('ramdisk').cache(fetch_func=libvirt_utils.fetch_raw_image, context=context, filename=fname, image_id=disk_images['ramdisk_id']) if CONF.libvirt.virt_type == 'uml': # PONDERING(mikal): can I assume that root is UID zero in every # OS? Probably not. uid = pwd.getpwnam('root').pw_uid nova.privsep.path.chown(image('disk').path, uid=uid) self._create_and_inject_local_root(context, instance, booted_from_volume, suffix, disk_images, injection_info, fallback_from_host) # Lookup the filesystem type if required os_type_with_default = disk_api.get_fs_type_for_os_type( instance.os_type) # Generate a file extension based on the file system # type and the mkfs commands configured if any file_extension = disk_api.get_file_extension_for_os_type( os_type_with_default) vm_mode = fields.VMMode.get_from_instance(instance) ephemeral_gb = instance.flavor.ephemeral_gb if 'disk.local' in disk_mapping: disk_image = image('disk.local') fn = functools.partial(self._create_ephemeral, fs_label='ephemeral0', os_type=instance.os_type, is_block_dev=disk_image.is_block_dev, vm_mode=vm_mode) fname = "ephemeral_%s_%s" % (ephemeral_gb, file_extension) size = ephemeral_gb * units.Gi disk_image.cache(fetch_func=fn, context=context, filename=fname, size=size, ephemeral_size=ephemeral_gb) for idx, eph in enumerate(driver.block_device_info_get_ephemerals( block_device_info)): disk_image = image(blockinfo.get_eph_disk(idx)) specified_fs = eph.get('guest_format') if specified_fs and not self.is_supported_fs_format(specified_fs): msg = _("%s format is not supported") % specified_fs raise exception.InvalidBDMFormat(details=msg) fn = functools.partial(self._create_ephemeral, fs_label='ephemeral%d' % idx, os_type=instance.os_type, is_block_dev=disk_image.is_block_dev, vm_mode=vm_mode) size = eph['size'] * units.Gi fname = "ephemeral_%s_%s" % (eph['size'], file_extension) disk_image.cache(fetch_func=fn, context=context, filename=fname, size=size, ephemeral_size=eph['size'], specified_fs=specified_fs) if swap_mb > 0: size = swap_mb * units.Mi image('disk.swap').cache(fetch_func=self._create_swap, context=context, filename="swap_%s" % swap_mb, size=size, swap_mb=swap_mb) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43   # nova/nova/virt/libvirt/driver.py def _create_and_inject_local_root(self, context, instance, booted_from_volume, suffix, disk_images, injection_info, fallback_from_host): # File injection only if needed need_inject = (not configdrive.required_by(instance) and injection_info is not None and CONF.libvirt.inject_partition != -2) # NOTE(ndipanov): Even if disk_mapping was passed in, which # currently happens only on rescue - we still don't want to # create a base image. if not booted_from_volume: root_fname = imagecache.get_cache_fname(disk_images['image_id']) size = instance.flavor.root_gb * units.Gi if size == 0 or suffix == '.rescue': size = None backend = self.image_backend.by_name(instance, 'disk' + suffix, CONF.libvirt.images_type) if instance.task_state == task_states.RESIZE_FINISH: backend.create_snap(libvirt_utils.RESIZE_SNAPSHOT_NAME) if backend.SUPPORTS_CLONE: def clone_fallback_to_fetch(*args, **kwargs): try: backend.clone(context, disk_images['image_id']) except exception.ImageUnacceptable: libvirt_utils.fetch_image(*args, **kwargs) fetch_func = clone_fallback_to_fetch else: fetch_func = libvirt_utils.fetch_image self._try_fetch_image_cache(backend, fetch_func, context, root_fname, disk_images['image_id'], instance, size, fallback_from_host) # 需要注入就调用_inject_data 方法 if need_inject: self._inject_data(backend, instance, injection_info) elif need_inject: LOG.warning('File injection into a boot from volume ' 'instance is not supported', instance=instance) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57   # nova/nova/virt/libvirt/driver.py def _inject_data(self, disk, instance, injection_info): """Injects data in a disk image Helper used for injecting data in a disk image file system. :param disk: The disk we're injecting into (an Image object) :param instance: The instance we're injecting into :param injection_info: Injection info """ # Handles the partition need to be used. LOG.debug('Checking root disk injection %s', str(injection_info), instance=instance) target_partition = None if not instance.kernel_id: target_partition = CONF.libvirt.inject_partition if target_partition == 0: target_partition = None if CONF.libvirt.virt_type == 'lxc': target_partition = None # Handles the key injection. if CONF.libvirt.inject_key and instance.get('key_data'): key = str(instance.key_data) else: key = None # Handles the admin password injection. if not CONF.libvirt.inject_password: admin_pass = None else: admin_pass = injection_info.admin_pass # Handles the network injection. net = netutils.get_injected_network_template( injection_info.network_info, libvirt_virt_type=CONF.libvirt.virt_type) # Handles the metadata injection metadata = instance.get('metadata') if any((key, net, metadata, admin_pass, injection_info.files)): LOG.debug('Injecting %s', str(injection_info), instance=instance) img_id = instance.image_ref try: disk_api.inject_data(disk.get_model(self._conn), key, net, metadata, admin_pass, injection_info.files, partition=target_partition, mandatory=('files',)) except Exception as e: with excutils.save_and_reraise_exception(): LOG.error('Error injecting data into image ' '%(img_id)s (%(e)s)', {'img_id': img_id, 'e': e}, instance=instance) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49  # nova/nova/virt/disk/api.py def inject_data(image, key=None, net=None, metadata=None, admin_password=None, files=None, partition=None, mandatory=()): """Inject the specified items into a disk image. :param image: instance of nova.virt.image.model.Image :param key: the SSH public key to inject :param net: the network configuration to inject :param metadata: the user metadata to inject :param admin_password: the root password to set :param files: the files to copy into the image :param partition: the partition number to access :param mandatory: the list of parameters which must not fail to inject If an item name is not specified in the MANDATORY iterable, then a warning is logged on failure to inject that item, rather than raising an exception. it will mount the image as a fully partitioned disk and attempt to inject into the specified partition number. If PARTITION is not specified the image is mounted as a single partition. Returns True if all requested operations completed without issue. Raises an exception if a mandatory item can't be injected. """ items = {'image': image, 'key': key, 'net': net, 'metadata': metadata, 'files': files, 'partition': partition} LOG.debug("Inject data image=%(image)s key=%(key)s net=%(net)s " "metadata=%(metadata)s admin_password= " "files=%(files)s partition=%(partition)s", items) try: fs = vfs.VFS.instance_for_image(image, 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 = items[inject] if inject_val: raise LOG.warning('Ignoring error injecting data into image %(image)s ' '(%(e)s)', {'image': image, 'e': e}) return False try: return inject_data_into_fs(fs, key, net, metadata, admin_password, files, mandatory) finally: fs.teardown() 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35  # nova/nova/virt/disk/api.py def inject_data_into_fs(fs, key, net, metadata, admin_password, files, mandatory=()): """Injects data into a filesystem already mounted by the caller. Virt connections can call this directly if they mount their fs in a different way to inject_data. If an item name is not specified in the MANDATORY iterable, then a warning is logged on failure to inject that item, rather than raising an exception. Returns True if all requested operations completed without issue. Raises an exception if a mandatory item can't be injected. """ items = {'key': key, 'net': net, 'metadata': metadata, 'admin_password': admin_password, 'files': files} functions = { 'key': _inject_key_into_fs, 'net': _inject_net_into_fs, 'metadata': _inject_metadata_into_fs, 'admin_password': _inject_admin_password_into_fs, 'files': _inject_files_into_fs, } status = True for inject, inject_val in items.items(): if inject_val: try: inject_func = functions[inject] inject_func(inject_val, fs) except Exception as e: if inject in mandatory: raise LOG.warning('Ignoring error injecting %(inject)s into ' 'image (%(e)s)', {'inject': inject, 'e': e}) status = False return status 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32  # nova/nova/virt/disk/api.py def _inject_admin_password_into_fs(admin_passwd, fs): """Set the root password to admin_passwd admin_password is a root password fs is the path to the base of the filesystem into which to inject the key. This method modifies the instance filesystem directly, and does not require a guest agent running in the instance. """ # The approach used here is to copy the password and shadow # 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=", {'fs': fs}) admin_user = 'root' passwd_path = os.path.join('etc', 'passwd') shadow_path = os.path.join('etc', 'shadow') # 读取 passwd 文件和 shadow 文件内容 passwd_data = fs.read_file(passwd_path) shadow_data = fs.read_file(shadow_path) # 更新 shadow 文件中的 root 密码 new_shadow_data = _set_passwd(admin_user, admin_passwd, passwd_data, shadow_data) # 将更新了 root 密码的 shadow 文件内容写入/tec/shadow 文件完成密码注入 fs.replace_file(shadow_path, new_shadow_data) 

 1 2 3 4 5   # nova/nova/virt/disk/vfs/guestfs.py def read_file(self, path): LOG.debug("Read file path=%s", path) path = self._canonicalize_path(path) return self.handle.read_file(path) 

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70  # nova/nova/virt/disk/api.py 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. if the username is not found in both files, an exception is raised. :param username: the username :param admin_passwd: the admin password :param passwd_data: path to the passwd file :param shadow_data: path to the shadow password file :returns: nothing :raises: exception.NovaException(), IOError() """ # 检测平台，不支持 Windows if os.name == 'nt': raise exception.NovaException(_('Not implemented on Windows')) # encryption algo - id pairs for crypt() algos = {'SHA-512': '$6$', 'SHA-256': '$5$', 'MD5': '$1$', 'DES': ''} # 生成盐 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, # then the underlying crypt() didn't understand the '$n$salt' magic, # so we fall back to DES. # md5 is the default because it's widely supported. Although the # local crypt() might support stronger SHA, the target instance # might not. # 加密密码 encrypted_passwd = crypt.crypt(admin_passwd, algos['MD5'] + salt) if len(encrypted_passwd) == 13: # 判断加密后的密码位数，等于 13 就重新使用 DES 加密方式进行加密 encrypted_passwd = crypt.crypt(admin_passwd, algos['DES'] + salt) # 以"\n"切分，生成 list p_file = passwd_data.split("\n") s_file = shadow_data.split("\n") # username MUST exist in passwd file or it's an error for entry in p_file: split_entry = entry.split(':') if split_entry[0] == username: break else: 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 # user doesn't exist. new_shadow = list() found = False for entry in s_file: split_entry = entry.split(':') # 判断 root 是否存在，存在就使用上面生成的新密码替换原有密码 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) 

fs.replace_file(shadow_path, new_shadow_data) 这一行代码调用 replace_file 方法完成了将替换 root 用户密码后的文件内容写入到目标实例镜像中的 /etc/shadow 文件中，至此整个密码注入基本宣告完成，剩下的就是等待一层一层的函数调用栈返回至最上层完成这个 rescue 过程。

 1 2 3 4 5   # nova/nova/virt/disk/vfs/guestfs.py def replace_file(self, path, content): LOG.debug("Replace file path=%s", path) path = self._canonicalize_path(path) self.handle.write(path, content) 

## 调试过程

### 缺少 inject_partition 配置项

 1 2 3 4 5 6  need_inject = (not configdrive.required_by(instance) and injection_info is not None and CONF.libvirt.inject_partition != -2) …… if need_inject: self._inject_data(backend, instance, injection_info) 

### 缺少模块

  1 2 3 4 5 6 7 8 9 10 11 12 13   try: fs = vfs.VFS.instance_for_image(image, 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 = items[inject] if inject_val: raise LOG.warning('Ignoring error injecting data into image %(image)s ' '(%(e)s)', {'image': image, 'e': e}) return False