调试 nova rescue –password 参数过程记录

本文以 Ubuntu16.04+Devstack Queens 版本 all-in-one 环境为基础

代码逻辑

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

在执行 nova rescue 命令进入 rescue 模式时,首先会调用 RescueController 中的 _rescue 方法,其会从执行的命令请求中判断是否指定了要注入的密码,如果有就保存并传递至下一方法,没有指定就随机生成一个密码并传递,同时会判断是否指定了 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
    # 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 {}

然后,现在代码运行进入了 nova/nova/compute/api.py 中的 rescue 方法,会先进行一系列对 instace 的判断等操作,最后又会调用 nova/nova/compute/rpcapi.py 中的 rescue_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
    # 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)

进入 nova/nova/compute/rpcapi.pyrescue_instance 方法,该方法会把传递过来的参数进行整理,整合成 dict 通过 cctxt.cast 方法异步调用 nova/nova/compute/manager.py 中的 rescue_instance 方法。

 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)

调用 _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
 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)

同样再一次调用同一文件里的 _create_and_inject_local_root 方法。

 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)

同样,再调用 _inject_data 方法。

 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)

接下来又再调用了 nova/nova/virt/disk/api.py 中的 inject_data 方法。

 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=<SANITIZED> "
              "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()

再调用 inject_data_into_fs 方法,循环对需要注入的元素调用对应的注入方法。

 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

接下来又会调用 _inject_admin_password_into_fs 方法来进行密码的注入。

 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=<SANITIZED>", {'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)

依次调用 read_file 方法,读取目标实例镜像中的 /etc/passwd/etc/shadow 文件内容,读取后又再将内容传递至 _set_passwd 方法进行 root 用户密码的注入替换。

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)

调用 _set_passwd 方法,完成对 root 用户的密码替换,替换完成后将密码返回至调用方。

 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)

整个密码注入的流程顺序大致就是上面文字描述所述以及所列出的代码顺序,上面所列出的代码和剪短描述对于一些细节上的东西并没有太过关注,主要只关注了密码注入相关流程,所以可能显得不是特别全面,流程也不是很流程,有待进一步完善相关描述。不过,最重要的注入流程应该都已经提到并且跟随代码过了一遍了,应该还是比较容易理解的。

调试过程

虽然上面的代码逻辑看着还是挺简单的,但是在调试 –password 参数为何不生效的原因时,还是很费了一番功夫。一开始以为是代码可能有什么逻辑上的错误,但是等真正找到具体原因时才发现代码并没有任何的问题,是自己的配置和使用方式出了问题。

配置不生效

首先就是配置不生效的问题,照着网上的说明,对 /etc/nova/nova.conf 文件进行了配置,加入了 inject_password = True 但是在重启了相应服务后,配置并没有生效,在查找了下相关服务后发现, DevStack 搭建的 all-in-one 环境 compute 服务对应修改的配置文件应该是 /etc/nova/nova-cpu.conf ,修改对配置文件后,该配置项终于生效了,但是又发现了新的问题。

缺少 inject_partition 配置项

完成了 inject_password = True 的配置后,发现还是不能完成密码注入,于是使用 PDB 一步一步进行调试后发现,在代码执行到下面的代码块时,其他两项都是 True ,唯独 CONF.libvirt.inject_partition 的值为-2,导致最后 need_inject 为 False,这样在后面的判断中,就不能进入 _inject_data 方法,也就不能实现密码的注入了。

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)

查询 OpenStack 相关配置文档,发现要想启用密码注入(其实更好的叫法好像是 file injection)需要在配置文件中配置 inject_partition = -1 ,因为 inject_password 这个配置项其实也是依赖于它的,如果不配置 inject_partition ,就算配置了 inject_password 也没用。

缺少模块

本以为完成上述配置后应该就能直接进行密码的注入了,直接尝试,然后发现并不行。于是又使用 PDB 进行单步调试,看看问题出在哪儿,经过漫长的追踪,在下面的代码块的位置会抛出异常,提示无法加载模块。查看了一下具体报错信息,检索到原来是因为没有 python-libguestfslibguestfs-tools 两个包,程序无法加载对应的模块完成 fs 的初始化,也就无法再进行后续的密码注入操作了。

 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

所以使用 sudo apt install python-libguestfs libguestfs-tools 安装缺失的两个包即可解决此报错问题。

权限问题

解决了模块缺失的问题,新的问题又来了,继续使用 PDB 调试时发现又抛出了新的异常,而且直接说明了是权限的问题,让我们去修改 /boot/vmlinuz* 文件的权限为 0644。那么我们就执行 sudo chmod 0644 /boot/vmlinuz* 将相关文件的权限修改为 0644,修改完后,应该就能解决这个问题。

参考链接