Iro# Ironic-部署物理机流程代码分析

部署物理机跟部署虚拟机的概念在nova来看是一样,都是nova通过创建虚拟机的方式来触发,只是底层nova-scheduler和nova-compute的驱动不一样。虚拟机的底层驱动采用的libvirt的虚拟化技术,而物理机是采用Ironic技术,ironic可以看成一组Hypervisor API的集合,其功能与libvirt类似。

部署流程

Bare Metal Deployment Steps 此图是Liberty版的官方裸机部署过程图,部署过程描述如下:

  1. 通过Nova API给出一个启动实例的请求,Nova API将此消息发送到消息队列Message Queue。
  2. Nova Schedule从消息队列拿到消息,并应用过滤条件确定目标物理节点,并指定计算节点。Nova Schedule会使用flavor的信息来匹配目标物理节点,比如‘cpu_arch’、‘baremetal:deploy_kernel_id’、‘baremetal:deploy_ramdisk_id’。
  3. Nova Computer从消息队列获取消息,调用驱动的spawn()函数,执行以下操作:
    1. 调用ironic API从Ironic Database获取目标物理节点
    2. 配置用于启动的相关信息,比如镜像id、虚拟网络接口(VIF)、驱动信息
    3. 调用ironic API 设置ironic node的provision_state为ACTIVE,调用set_provision_state触发部署动作,然后等待部署完成。
  4. Ironic API接受到set_provision_state请求,发送RPC请求调用Ironic Conductor的do_node_deploy
  5. Ironic conductor执行do_node_deploy,

从Glance下载镜像到本地磁盘。其中,pxe为前缀的驱动包含deploy ramdisk和user instance images;而agent为前缀的驱动只下载deploy ramdisk镜像,用户实例镜像作为一个对象存储的临时链接被给出。

  1. 配置虚拟接口VIFs,同时 Neutron API更新DHCP端口用于支持PXE/TFTP选项。
  2. Nova的ironic驱动通过Ironic API向Ironic conductor发出一个部署请求。
  3. PXE驱动准备TFTP bootloader。
  4. IPMI驱动发出从网络启动节点命令并且打开电源。
  5. 启动deploy ramdisk进行部署。部署完成后Ironic Conductor将pxe配置修改为server模式,并通知ramdisk中的agent部署成功。
  6. IPMI驱动重启bare metal node。
  7. 更新bare metal 节点状态信息,节点实例变成可用状态。

废话不多说,下面我们从liberty版的代码入手分析。

部署流程代码分析

配置

在/etc/nova/nova.conf中修改manager和driver

[DEFAULT]  
scheduler_host_manager = nova.scheduler.ironic_host_manager.IronicHostManager  
compute_driver = nova.virt.ironic.driver.IronicDriver  
compute_manager = ironic.nova.compute.manager.ClusteredComputeManager  
[ironic]  
admin_username = ironic  
admin_password = unset  
admin_url = http://127.0.0.1:35357/v2.0  
admin_tenant_name = service

compute_manager的代码实现是在ironic项目里面。

启动流程

第一步, nova-api接收到nova boot的请求,通过消息队列到达nova-scheduler

第二步, nova-scheduler收到请求后,在scheduler_host_manager里面处理。nova-scheduler会使用flavor里面的额外属性extra_specs,像cpu_arch,baremetal:deploy_kernel_id,baremetal:deploy_ramdisk_id等过滤条件找到相匹配的物理节点,然后发送RPC消息到nova-computer。

第三步,nova-computer拿到消息调用指定的driver的spawn方法进行部署,即调用nova.virt.ironic.driver.IronicDriver.spawn(), 该方法做了什么操作呢?我们来对代码进行分析(下面的代码只保留了主要的调用)。

 def spawn(self, context, instance, image_meta, injected_files,  
             admin_password, network_info=None, block_device_info=None):  

        #获取镜像信息
        image_meta = objects.ImageMeta.from_dict(image_meta)

        ......

        #调用ironic的node.get方法查询node的详细信息,锁定物理机,获取该物理机的套餐信息
        node = self.ironicclient.call("node.get", node_uuid)
        flavor = instance.flavor

        #将套餐里面的baremetal:deploy_kernel_id和baremetal:deploy_ramdisk_id信息
        #更新到driver_info,将image_source、root_gb、swap_mb、ephemeral_gb、
        #ephemeral_format、preserve_ephemeral信息更新到instance_info中,
        #然后将driver_info和instance_info更新到ironic的node节点对应的属性上。
        self._add_driver_fields(node, instance, image_meta, flavor)

        .......

        # 验证是否可以部署,只有当deply和power都准备好了才能部署
        validate_chk = self.ironicclient.call("node.validate", node_uuid)
        .....

        # 准备部署
        try:
            #将节点的虚拟网络接口和物理网络接口连接起来并调用ironic API进行更新,以便neutron可以连接
            self._plug_vifs(node, instance, network_info)
            self._start_firewall(instance, network_info)
        except Exception:
            ....

        # 配置驱动
        onfigdrive_value = self._generate_configdrive(
                instance, node, network_info, extra_md=extra_md,
                files=injected_files)


        # 触发部署
        try:
            #调用ironic API,设置provision_state的状态ACTIVE
            self.ironicclient.call("node.set_provision_state", node_uuid,
                                   ironic_states.ACTIVE,
                                   configdrive=configdrive_value)
        except Exception as e:
            ....

        #等待node provision_state为ATCTIVE
        timer = loopingcall.FixedIntervalLoopingCall(self._wait_for_active,
                                                     self.ironicclient,
                                                     instance)
        try:
            timer.start(interval=CONF.ironic.api_retry_interval).wait()
        except Exception:
              ...

nova-compute的spawn的步骤包括:

  1. 获取节点
  2. 配置网络信息
  3. 配置驱动信息
  4. 触发部署,设置ironic的provision_state为ACTIVE
  5. 然后等待ironic的node provision_state为ACTIVE就结束了。

第四步 ironic-api接收到了provision_state的设置请求,然后返回202的异步请求,那我们下来看下ironic在做什么?

首先,设置ironic node的provision_stat为ACTIVE相当于发了一个POST请求:PUT /v1/nodes/(node_uuid)/states/provision。那根据openstack的wsgi的框架,注册了app为ironic.api.app.VersionSelectorApplication的类为ironic的消息处理接口,那PUT /v1/nodes/(node_uuid)/states/provision的消息处理就在ironic.api.controllers.v1.node.NodeStatesController的provision方法。

@expose.expose(None, types.uuid_or_name, wtypes.text,
                   wtypes.text, status_code=http_client.ACCEPTED)
    def provision(self, node_ident, target, configdrive=None):
        ....

        if target == ir_states.ACTIVE:
            #RPC调用do_node_deploy方法
            pecan.request.rpcapi.do_node_deploy(pecan.request.context,
                                                rpc_node.uuid, False,
                                                configdrive, topic)
       ...

然后RPC调用的ironic.condutor.manager.ConductorManager.do_node_deploy方法,在方法中会先检查电源和部署信息,其中部署信息检查指定的节点的属性是否包含驱动的要求,包括检查boot、镜像大小是否大于内存大小、解析根设备。检查完之后调用ironic.condutor.manager.do_node_deploy方法

def do_node_deploy(task, conductor_id, configdrive=None):
    """Prepare the environment and deploy a node."""
    node = task.node
    ...

    try:
        try:
            if configdrive:
                _store_configdrive(node, configdrive)
        except exception.SwiftOperationError as e:
            with excutils.save_and_reraise_exception():
                handle_failure(
                    e, task,
                    _LE('Error while uploading the configdrive for '
                        '%(node)s to Swift'),
                    _('Failed to upload the configdrive to Swift. '
                      'Error: %s'))

        try:
            #调用驱动的部署模块的prepare方法,不同驱动的动作不一样
            #1. pxe_* 驱动使用的是iscsi_deploy.ISCSIDeploy.prepare,
            #然后调用pxe.PXEBoot.prepare_instance()进行cache images、 update DHCP、
            #switch pxe_config、set_boot_device等操作
            #cache images 是从glance上取镜像缓存到condutor本地,
            #update DHCP指定bootfile文件地址为condutor
            #switch pxe_config将deploy mode设置成service mode
            #set_boot_device设置节点pxe启动
            #2. agent_*  因为AgentDploy从硬盘启动,所以这里什么也不做
            task.driver.deploy.prepare(task)
        except Exception as e:
            ...

        try:
            #调用驱动的deploy方法,不同驱动动作不一样
            #1. pxe_* 驱动调用iscsi_deploy.ISCSIDeploy.deploy()
            #进行拉取instance image,创建keystone token,为next boot 更新DHCP 端口
            #然后重启物理机
            #2. agent_* 驱动什么事也不干,直接重启
            new_state = task.driver.deploy.deploy(task)
        except Exception as e:
            ...

        # NOTE(deva): Some drivers may return states.DEPLOYWAIT
        #             eg. if they are waiting for a callback
        if new_state == states.DEPLOYDONE:
            task.process_event('done')
        elif new_state == states.DEPLOYWAIT:
            task.process_event('wait')

    finally:
        node.save()

至此,ironic-conductor的动作完成,等待物理机进行上电。

值得说明的是,task是task_manager.TaskManager的一个对象,这个对象在初始化的时候将self.driver初始化了 self.driver = driver_factory.get_driver(driver_name or self.node.driver) driver_name是传入的参数,默认为空;这个self.node.driver指物理机使用的驱动,不同物理机使用的驱动可能不同,这是在注册物理机时指定的。

第五步

在上一步中已经设置好了启动方式和相关网络信和给机器上电了,那么下一步就是机器启动,进行部署了。

PXE

我们知道安装操作系统的通用流程是:首先,bios启动,选择操作系统的启动(安装)模式(此时,内存是空白的),然后根据相关的安装模式,寻找操作系统的引导程序(不同的模式,对应不同的引导程序当然也对应着不同的引导程序存在的位置),引导程序加载文件系统初始化程序(initrd)和内核初始镜像(vmlinuz),完成操作系统安装前的初始化;接着,操作系统开始安装相关的系统和应用程序。

PXE启动方式的过程为:

  1. 物理机上电后,BIOS把PXE client调入内存执行,客户端广播DHCP请求
  2. DHCP服务器(neutron)给客户机分配IP并给定bootstrap文件的放置位置
  3. 客户机向本网络中的TFTP服务器索取bootstrap文件
  4. 客户机取得bootstrap文件后之执行该文件
  5. 根据bootstrap的执行结果,通过TFTP服务器(conductor)加载内核和文件系统
  6. 在内存中启动安装

启动后运行init启动脚本,那么init启动脚本是什么样子的。

首先,我们需要知道当前创建deploy-ironic的镜像,使用的diskimage-build命令,参考diskimage-builder/elements/deploy-ironic这个元素,最重要的是init.d/80-deploy-ironic这个脚本,这个脚本主要其实就是做以下几个步骤:

  1. 找到磁盘,以该磁盘启动iSCSI设备
  2. Tftp获取到ironic准备的token文件
  3. 调用ironic的api接(POST v1/nodes/{node-id}/vendor_passthru/pass_deploy_info)
  4. 启动iSCSI设备, 开启socket端口 10000等待通知PXE结束
  5. 结束口停止iSCSI设备。


function install_bootloader {

    # We need to run partprobe to ensure all partitions are visible
    partprobe $target_disk

    # root partition is always the last partition of the disk
    readonly root_part=$(ls $target_disk* | tr " " "\n" | tail -n1)
    readonly root_part_mount=/mnt/rootfs

    mkdir -p $root_part_mount
    mkdir -p $root_part_mount/dev
    mkdir -p $root_part_mount/sys
    mkdir -p $root_part_mount/proc

    mount $root_part $root_part_mount 2>&1
    if [ $? != "0" ]; then
        echo "Failed to mount root partition $root_part on $root_part_mount"
        return 1
    fi


    mount -o bind /dev $root_part_mount/dev
    mount -o bind /sys $root_part_mount/sys
    mount -o bind /proc $root_part_mount/proc

    # If boot mode is uefi, then mount the system partition in /boot/efi.
    # Grub expects the efi system partition to be mounted here.
    if [ "$IRONIC_BOOT_MODE" = "uefi" ]; then

        # efi system partition is labelled as "efi-part" by Ironic.
        # lsblk output looks like this:
        #   NAME="sda1" LABEL="efi-part"
        readonly efi_system_part=$(lsblk -Pio NAME,LABEL $target_disk | \
                                   awk -F'"' '/"efi-part"/{print $2}')
        readonly efi_system_part_dev_file="/dev/$efi_system_part"
        readonly efi_system_part_mount="$root_part_mount/boot/efi"
        mkdir -p $efi_system_part_mount
        mount $efi_system_part_dev_file $efi_system_part_mount 2>&1
        if [ $? != "0" ]; then
            echo "Failed to mount efi system partition \
                  $efi_system_part_dev_file on $efi_system_part_mount"
            return 1
        fi
    fi

    # TODO(lucasagomes): Add extlinux as a fallback
    # Find grub version
    V=
    if [ -x $root_part_mount/usr/sbin/grub2-install ]; then
        V=2
    fi

    # Install grub
    ret=1
    if chroot $root_part_mount /bin/bash -c "/usr/sbin/grub$V-install ${target_disk}"; then
        echo "Generating the grub configuration file"

        # tell GRUB2 to preload its "lvm" module to gain LVM booting on direct-attached disks
        if [ "$V" = "2" ]; then
            echo "GRUB_PRELOAD_MODULES=lvm" >> $root_part_mount/etc/default/grub
        fi

        chroot $root_part_mount /bin/bash -c "/usr/sbin/grub$V-mkconfig -o /boot/grub$V/grub.cfg"
        ret=$?
    fi

    # If we had mounted efi system partition, umount it.
    if [ "$IRONIC_BOOT_MODE" = "uefi" ]; then
        umount $efi_system_part_mount
    fi

    umount $root_part_mount/dev
    umount $root_part_mount/sys
    umount $root_part_mount/proc
    umount $root_part_mount

    if [ $ret != "0" ]; then
        echo "Installing grub bootloader failed"
    fi
    return $ret
}


#向Ironic Condutor发送消息,开启socket端口10000等待通知PXE结束
function do_vendor_passthru_and_wait {

    local data=$1
    local vendor_passthru_name=$2

    eval curl -i -X POST \
         "$TOKEN_HEADER" \
         "-H 'Accept: application/json'" \
         "-H 'Content-Type: application/json'" \
         -d "$data" \
         "$IRONIC_API_URL/v1/nodes/$DEPLOYMENT_ID/vendor_passthru/$vendor_passthru_name"

    echo "Waiting for notice of complete"
    nc -l -p 10000
}


readonly IRONIC_API_URL=$(get_kernel_parameter ironic_api_url)
readonly IRONIC_BOOT_OPTION=$(get_kernel_parameter boot_option)
readonly IRONIC_BOOT_MODE=$(get_kernel_parameter boot_mode)
readonly ROOT_DEVICE=$(get_kernel_parameter root_device)

if [ -z "$ISCSI_TARGET_IQN" ]; then
  err_msg "iscsi_target_iqn is not defined"
  troubleshoot
fi

#获取当前linux的本地硬盘  
target_disk=
if [[ $ROOT_DEVICE ]]; then
    target_disk="$(get_root_device)"
else
    t=0
    while ! target_disk=$(find_disk "$DISK"); do   
      if [ $t -eq 60 ]; then
        break
      fi
      t=$(($t + 1))
      sleep 1
    done
fi

if [ -z "$target_disk" ]; then
  err_msg "Could not find disk to use."
  troubleshoot
fi

#将找到的本地磁盘作为iSCSI磁盘启动,暴露给Ironic Condutor
echo "start iSCSI target on $target_disk"
start_iscsi_target "$ISCSI_TARGET_IQN" "$target_disk" ALL   
if [ $? -ne 0 ]; then
  err_msg "Failed to start iscsi target."
  troubleshoot
fi

#获取到相关的token文件,从tftp服务器上获取,token文件在ironic在prepare阶段就生成好的。  
if [ "$BOOT_METHOD" = "$VMEDIA_BOOT_TAG" ]; then
  TOKEN_FILE="$VMEDIA_DIR/token"
  if [ -f "$TOKEN_FILE" ]; then
    TOKEN_HEADER="-H 'X-Auth-Token: $(cat $TOKEN_FILE)'"
  else TOKEN_HEADER=""
  fi
else
  TOKEN_FILE=token-$DEPLOYMENT_ID

  # Allow multiple versions of the tftp client
  if tftp -r $TOKEN_FILE -g $BOOT_SERVER || tftp $BOOT_SERVER -c get $TOKEN_FILE; then
      TOKEN_HEADER="-H 'X-Auth-Token: $(cat $TOKEN_FILE)'"
  else
      TOKEN_HEADER=""
  fi
fi


#向Ironic请求部署镜像,POST node的/vendor_passthru/pass_deploy_info请求
echo "Requesting Ironic API to deploy image"
deploy_data="'{\"address\":\"$BOOT_IP_ADDRESS\",\"key\":\"$DEPLOYMENT_KEY\",\"iqn\":\"$ISCSI_TARGET_IQN\",\"error\":\"$FIRST_ERR_MSG\"}'"
do_vendor_passthru_and_wait "$deploy_data" "pass_deploy_info"

#部署镜像下载结束,停止iSCSI设备
echo "Stopping iSCSI target on $target_disk"
stop_iscsi_target

#如果是本地启动,安装bootloarder
# If localboot is set, install a bootloader
if [ "$IRONIC_BOOT_OPTION" = "local" ]; then
    echo "Installing bootloader"

    error_msg=$(install_bootloader)
    if [ $? -eq 0 ]; then
        status=SUCCEEDED
    else
        status=FAILED
    fi

    echo "Requesting Ironic API to complete the deploy"
 bootloader_install_data="'{\"address\":\"$BOOT_IP_ADDRESS\",\"status\":\"$status\",\"key\":\"$DEPLOYMENT_KEY\",\"error\":\"$error_msg\"}'"
    do_vendor_passthru_and_wait "$bootloader_install_data" "pass_bootloader_install_info"
fi

下面我们来看一下node的/vendor_passthru/pass_deploy_info都干了什么?Ironic-api在接受到请求后,是在ironic.api.controllers.v1.node.NodeVendorPassthruController._default()方法处理的,这个方法将调用的方法转发到ironic.condutor.manager.CondutorManager.vendor_passthro()去处理,进而调用相应task.driver.vendor.pass_deploy_info()去处理,这里不同驱动不一样,可以根据源码查看到,比如使用pxe_ipmptoos驱动, 则是转发给ironic.drivers.modules.iscsi_deploy.VendorPassthru.pass_deploy_info()处理,其代码是

@base.passthru(['POST'])
    @task_manager.require_exclusive_lock
    def pass_deploy_info(self, task, **kwargs):
        """Continues the deployment of baremetal node over iSCSI.

        This method continues the deployment of the baremetal node over iSCSI
        from where the deployment ramdisk has left off.

        :param task: a TaskManager instance containing the node to act on.
        :param kwargs: kwargs for performing iscsi deployment.
        :raises: InvalidState
        """
        node = task.node
        LOG.warning(_LW("The node %s is using the bash deploy ramdisk for "
                        "its deployment. This deploy ramdisk has been "
                        "deprecated. Please use the ironic-python-agent "
                        "(IPA) ramdisk instead."), node.uuid)
        task.process_event('resume')   #设置任务状态
        LOG.debug('Continuing the deployment on node %s', node.uuid)

        is_whole_disk_image = node.driver_internal_info['is_whole_disk_image']

        #继续部署的函数,连接到iSCSI设备,将用户镜像写到iSCSI设备上,退出删除iSCSI设备,
        #然后在Condutor上删除镜像文件
        uuid_dict_returned = continue_deploy(task, **kwargs)

        root_uuid_or_disk_id = uuid_dict_returned.get(
            'root uuid', uuid_dict_returned.get('disk identifier'))

        # save the node's root disk UUID so that another conductor could
        # rebuild the PXE config file. Due to a shortcoming in Nova objects,
        # we have to assign to node.driver_internal_info so the node knows it
        # has changed.
        driver_internal_info = node.driver_internal_info
        driver_internal_info['root_uuid_or_disk_id'] = root_uuid_or_disk_id
        node.driver_internal_info = driver_internal_info
        node.save()

        try:
            #再一次设置PXE引导,为准备进入用户系统做准备
            task.driver.boot.prepare_instance(task)

            if deploy_utils.get_boot_option(node) == "local":
                if not is_whole_disk_image:
                    LOG.debug('Installing the bootloader on node %s',
                              node.uuid)
                    deploy_utils.notify_ramdisk_to_proceed(kwargs['address'])
                    task.process_event('wait')
                    return

        except Exception as e:
            LOG.error(_LE('Deploy failed for instance %(instance)s. '
                          'Error: %(error)s'),
                      {'instance': node.instance_uuid, 'error': e})
            msg = _('Failed to continue iSCSI deployment.')
            deploy_utils.set_failed_state(task, msg)
        else:
            #结束部署,通知ramdisk重启,将物理机设置为ative
            finish_deploy(task, kwargs.get('address'))

在continue_deploy函数中,先解析iscsi部署的信息,然后在进行分区、格式化、写入镜像到磁盘。 然后调用prepare_instance在设置一遍PXE环境,为进入系统做准备,我们知道在instance_info上设置了ramdisk、kernel、image_source 3个镜像,其实就是内核、根文件系统、磁盘镜像。这里就是设置了ramdisk和kernel,磁盘镜像上面已经写到磁盘中去了,调用switch_pxe_config方法将当前的操作系统的启动项设置为ramdisk和kernel作为引导程序。 最后向节点的10000发送一个‘done’通知节点关闭iSCSI设备,最后节点重启安装用户操作系统,至此部署结束。

在部署过程中,节点和驱动的信息都会被存入ironic数据库,以便后续管理。

Ironic-Python-Agent(IPA,agent)

我们知道当前ironic的deploy模块是通过打开一个iSCSI设备,ironic-conductro来OS的镜像文件写到iSCSI的设备 Ironic-Python-Agent认为deploy模块还可以直接访问硬件,提供以下功能:

  • 磁盘格式化
  • 磁盘分区
  • 安装OS( Bootloaders, OS)
  • 固件升级
  • raid配置 在Condutor端使用agent驱动,物理机端使用IPA,IPA通过暴露API给Condutor调用,则可完成相应功能。IPA启动时通过发送lookup()请求给Condutor获取UUID,相当于注册自己,并且每隔一段时间给Condutor发送心跳包进行连接。 下面我们来分析一下使用IPA来部署系统的过程。

使用iscsi部署流程: iscsi部署流程

使用IPA部署流程:

IPA部署流程

在部署阶段的前一部分与PXE一样,但是由于创建的ramdisk不一样所以部署方式则不一样,在PXE中,开机执行的是一段init脚本,而在Agent开机执行的是IPA。

机器上电后,ramdisk在内存中执行,然后启动IPA,入口为cmd.agent.run(),然后调用ironic-python-agent.agent.run(),其代码如下

def run(self):
        """Run the Ironic Python Agent."""
        # Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError
        # if there is an issue (uncaught, restart agent)
        self.started_at = _time()

        #加载hardware manager
        # Cached hw managers at runtime, not load time. See bug 1490008.
        hardware.load_managers()

        if not self.standalone:
            # Inspection should be started before call to lookup, otherwise
            # lookup will fail due to unknown MAC.
            uuid = inspector.inspect()

            #利用Ironic API给Condutor发送lookup()请求,用户获取UUID,相当于自发现
            content = self.api_client.lookup_node(
                hardware_info=hardware.dispatch_to_managers(
                                  'list_hardware_info'),
                timeout=self.lookup_timeout,
                starting_interval=self.lookup_interval,
                node_uuid=uuid)

            self.node = content['node']
            self.heartbeat_timeout = content['heartbeat_timeout']

        wsgi = simple_server.make_server(
            self.listen_address[0],
            self.listen_address[1],
            self.api,
            server_class=simple_server.WSGIServer)

        #发送心跳包
        if not self.standalone:
            # Don't start heartbeating until the server is listening          
            self.heartbeater.start()

        try:
            wsgi.serve_forever()
        except BaseException:
            self.log.exception('shutting down')
        #部署完成后停止心跳包
        if not self.standalone:
            self.heartbeater.stop()

其中self.api_client.lookup_node调用到ironic-python-api._do_lookup(),然后GET /v1/lookup请求。 Condutor API在接受到/v1/lookup请求在后再

驱动总结

驱动都有几个属性

  • 核心接口(core),最基本的功能,是其他服务的依赖,所有驱动都必须实现的,包括power,deploy
  • 标准接口(standard),实现通用功能,包括 management, console, boot, inspect, , raid.
  • 厂商接口(vendor),提供个性化功能,

驱动分类

  • pxe_ deploy 用的是iscsi_deploy.ISCSIDeploy(), boot用的是pxe.PXEBoot(), power根据后缀不同使用的不同
  • agent_ deploy用的是agent.AgentDeploy(), boot 用的是pxe.PXEBoot(), power根据后缀不同而不同
  • iscis_ deploy用的iscsi_deploy.ISCSIDeploy(),power根据后缀不同而不同

下面显示两个部署实例

  1. 使用pxe_* 为前缀的驱动的部署过程

pex_*

  1. 使用agent_* 为前缀的驱动的部署过程

angent_*


驱动

results matching ""

    No results matching ""