理解裸機部署過程ironic


部署物理機跟部署虛擬機的概念在nova來看是一樣,都是nova通過創建虛擬機的方式來觸發,只是底層nova-scheduler和nova-compute的驅動不一樣。虛擬機的底層驅動采用的libvirt的虛擬化技術,而物理機是采用Ironic技術,ironic可以看成一組Hypervisor API的集合,其功能與libvirt類似。

操作系統安裝過程

Linux系統啟動過程

  • bootloader(引導程序,常見的有GRUB、LILO)
  • kernel(內核)
  • ramdisk(虛擬內存盤)
  • initrd/initramfs (初始化內存磁盤鏡像)

下面我們分別介紹每個概念:

  • 引導加載程序是系統加電后運行的第一段軟件代碼。PC機中的引導加載程序由BIOS(其本質就是一段固件程序)和位於硬盤MBR(主引導記錄,通常位於第一塊硬盤的第一個扇區)中的OS BootLoader(比如,LILO和GRUB等)一起組成。BIOS在完成硬件檢測和資源分配后,硬盤MBR中的BootLoader讀到系統的RAM中,然后控制權交給OS BootLoader。
  • bootloader負責將kernel和ramdisk從硬盤讀到內存中,然后跳轉到內核的入口去運行。
  • kernel是Linux的內核,包含最基本的程序。
  • ramdisk是一種基於內存的虛擬文件系統,就好像你又有一個硬盤,你可以對它上面的文件添加修改刪除等等操作。但是一掉電,就什么也沒有了,無法保存。一般驅動程序放在這里面。
  • initrd是boot loader initialized RAM disk, 顧名思義,是在系統初始化引導時候用的ramdisk。也就是由啟動加載器所初始化的RamDisk設備,它的作用是完善內核的模塊機制,讓內核的初始化流程更具彈性;內核以及initrd,都由 bootloader在機子啟動后被加載至內存的指定位置,主要功能為按需加載模塊以及按需改變根文件系統。initramfs與initrd功能類似,是initrd的改進版本,改進了initrd大小不可變等等缺點。

boot procedure

Linux boot process

為什么需要initrd?

在早期的Linux系統中,一般就只有軟盤或者硬盤被用來作為Linux的根文件系統,因此很容易把這些設備的驅動程序集成到內核中。但是現在根文件系統 可能保存在各種存儲設備上,包括SCSI, SATA, U盤等等。因此把這些設備驅動程序全部編譯到內核中顯得不太方便,違背了“內核”的精神。在Linux內核模塊自動加載機制中可以利用udevd可以實現內核模塊的自動加載,因此我們希望根文件系統的設備驅動程序也能夠實現自動加載。但是這里有一個矛盾,udevd是一個可執行文件,在根文件系統被掛載前,是不可能執行udevd的,但是如果udevd沒有啟動,那就無法自動加載根根據系統設備的驅動程序,同時也無法在/dev目錄下建立相應的設備節點。

為了解決這個矛盾,於是出現了initrd(boot loader initialized RAM disk)。initrd是一個被壓縮過的小型根目錄,這個目錄中包含了啟動階段中必須的驅動模塊,可執行文件和啟動腳本。包括上面提到的udevd,當系統啟動的時候,bootload會把內核和initrd文件讀到內存中,然后把initrd的起始地址告訴內核。內核在運行過程中會解壓initrd,然后把initrd掛載為根目錄,然后執行根目錄中的/initrc腳本,可以在這個腳本中運行initrd中的udevd,讓它來自動加載設備驅動程序以及 在/dev目錄下建立必要的設備節點。在udevd自動加載磁盤驅動程序之后,就可以mount真正的根目錄,並切換到這個根目錄中。

Linux啟動一定要用initrd么?

如果把需要的功能全都編譯到內核中(非模塊方式),只需要一個內核文件即可。initrd 能夠減小啟動內核的體積並增加靈活性,如果你的內核以模塊方式支持某種文件系統(例如ext3, UFS),而啟動階段的驅動模塊放在這些文件系統上,內核是無法讀取文件系統的,從而只能通過initrd的虛擬文件系統來裝載這些模塊。這里有些人會問: 既然內核此時不能讀取文件系統,那內核的文件是怎么裝入內存中的呢?答案很簡單,Grub是file-system sensitive的,能夠識別常見的文件系統。通用安裝流程如下:

  1. 開機啟動,BIOS完成硬件檢測和資源分配,選擇操作系統的啟動(安裝)模式(此時,內存是空白的)
  2. 然后根據相關的安裝模式,尋找操作系統的引導程序(bootloader)(不同的模式,對應不同的引導程序當然也對應着不同的引導程序存在的位置)
  3. 引導程序加載文件系統初始化(initrd)程序和內核初始鏡像(vmlinuz),完成操作系統安裝前的初始化
  4. 操作系統開始安裝相關的系統和應用程序。

PXE部署過程

PXE協議分為client和server兩端,PXE client在網卡的ROM中,當計算機啟動時,BIOS把PXE client調入內存執行,並顯示出命令菜單,經用戶選擇后,PXE client將放置在遠端的操作系統通過網絡下載到本地運行。

安裝流程如下:

  1. 客戶機從自己的PXE網卡啟動,向本網絡中的DHCP服務器索取IP,並搜尋引導文件的位置
  2. DHCP服務器返回分給客戶機IP以及NBP(Network Bootstrap Program )文件的放置位置(該文件一般是放在一台TFTP服務器上)
  3. 客戶機向本網絡中的TFTP服務器索取NBP
  4. 客戶機取得NBP后之執行該文件
  5. 根據NBP的執行結果,通過TFTP服務器加載內核和文件系統
  6. 安裝操作系統

PXE過程圖

流程小結:

客戶端廣播dhcp請求——服務器相應請求,建立鏈接——由dhcp和tftp配置得到ip還有引導程序所在地點——客戶端下載引導程序並開始運行——引導程序讀取系統鏡像-安裝操作系統

相關文件位置與內容:

  • dhcp配置文件/etc/dhcpd/dhcp.conf——ip管理與引導程序名稱
  • tftp配置文件/etc/xinetd.d/tftp——tftp根目錄,和上面的引導程序名稱組成完整路徑
  • 引導程序讀取的配置文件/tftpboot/pxelinux.cfg/default——啟動內核其他

參考資料: PXE網絡安裝操作系統過程

Ironic部署過程

部署流程

Bare Metal Deployment Steps此圖是Liberty版的官方裸機部署過程圖,部署過程描述如下:

  1. 部署物理機的請求通過 Nova API 進入Nova;
  2. Nova Scheduler 根據請求參數中的信息(指定的鏡像和硬件模板等)選擇合適的物理節點;
  3. Nova 創建一個 spawn 任務,並調用 Ironic API 部署物理節點,Ironic 將此次任務中所需要的硬件資源保留,並更新數據庫;
  4. Ironic 與 OpenStack 的其他服務交互,從 Glance 服務獲取部署物理節點所需的鏡像資源,並調用 Neutron 服務為物理機創建網路端口;
  5. Ironic 開始部署物理節點,PXE driver 准備 tftp bootloader,IPMI driver 設置物理機啟動模式並將機器上電;
  6. 物理機啟動后,通過 DHCP 獲得 Ironic Conductor 的地址並嘗試通過 tftp 協議從 Conductor 獲取鏡像,Conductor 將部署鏡像部署到物理節點上后,通過 iSCSI 協議將物理節點的硬盤暴露出來,隨后寫入用戶鏡像,成功部署用戶鏡像后,物理節點的部署就完成了。

下面我們通過代碼來分析Ironic的部署流程。

Ironic給物理機部署系統詳解

配置

在/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_ramdisk()准備部署進行和環境,包括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_* 生成鏡像swift_tmp_url加入節點的instance_info中 #然后調用pxe.PXEBoot.prepare_ramdisk()准備部署鏡像和環境 task.driver.deploy.prepare(task) except Exception as e: ... try: #調用驅動的deploy方法,不同驅動動作不一樣 #1. pxe_* 驅動調用iscsi_deploy.ISCSIDeploy.deploy() #進行拉取用戶鏡像,然后重啟物理機 #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和agent兩種部署方式分別來說明。

情況一、使用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設備。
# 安裝bootloader function install_bootloader { #此處省略很多 ... } #向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數據庫,以便后續管理。

情況二、使用agent驅動部署

在部署階段的prepare階段與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 /{api_version}/drivers/{driver}/vendor_passthru/lookup請求。 Condutor API在接受到lookup請求后調用指定驅動的lookup函數處理,返回節點UUID。

IPA收到UUID后調用Ironic-API發送Heartbeat請求(/{api_version}/nodes/{uuid}/vendor_passthru/heartbeat),Ironic-API把消息路由給節點的驅動heartbeat函數處理。Ironic-Condutor周期執行該函數,每隔一段時間執行該函數檢查IPA部署是否完成,如果完成則進入之后的動作.目前agent*驅動使用的是ironic.drivers.modouls.agent.AgentVendorInterface類實現的接口,代碼如下。

@base.passthru(['POST']) def heartbeat(self, task, **kwargs): """Method for agent to periodically check in. The agent should be sending its agent_url (so Ironic can talk back) as a kwarg. kwargs should have the following format:: { 'agent_url': 'http://AGENT_HOST:AGENT_PORT' } AGENT_PORT defaults to 9999. """ node = task.node driver_internal_info = node.driver_internal_info LOG.debug( 'Heartbeat from %(node)s, last heartbeat at %(heartbeat)s.', {'node': node.uuid, 'heartbeat': driver_internal_info.get('agent_last_heartbeat')}) driver_internal_info['agent_last_heartbeat'] = int(_time()) try: driver_internal_info['agent_url'] = kwargs['agent_url'] except KeyError: raise exception.MissingParameterValue(_('For heartbeat operation, ' '"agent_url" must be ' 'specified.')) node.driver_internal_info = driver_internal_info node.save() # Async call backs don't set error state on their own # TODO(jimrollenhagen) improve error messages here msg = _('Failed checking if deploy is done.') try: if node.maintenance: # this shouldn't happen often, but skip the rest if it does. LOG.debug('Heartbeat from node %(node)s in maintenance mode; ' 'not taking any action.', {'node': node.uuid}) return elif (node.provision_state == states.DEPLOYWAIT and not self.deploy_has_started(task)): msg = _('Node failed to get image for deploy.') self.continue_deploy(task, **kwargs) #調用continue_deploy函數,下載鏡像 elif (node.provision_state == states.DEPLOYWAIT and self.deploy_is_done(task)): #查看IPA執行下載鏡像是否結束 msg = _('Node failed to move to active state.') self.reboot_to_instance(task, **kwargs) #如果鏡像已經下載完成,即部署完成,設置從disk啟動,重啟進入用戶系統, elif (node.provision_state == states.DEPLOYWAIT and self.deploy_has_started(task)): node.touch_provisioning() #更新數據庫,將節點的設置為alive # TODO(lucasagomes): CLEANING here for backwards compat # with previous code, otherwise nodes in CLEANING when this # is deployed would fail. Should be removed once the Mitaka # release starts. elif node.provision_state in (states.CLEANWAIT, states.CLEANING): node.touch_provisioning() if not node.clean_step: LOG.debug('Node %s just booted to start cleaning.', node.uuid) msg = _('Node failed to start the next cleaning step.') manager.set_node_cleaning_steps(task) self._notify_conductor_resume_clean(task) else: msg = _('Node failed to check cleaning progress.') self.continue_cleaning(task, **kwargs) except Exception as e: err_info = {'node': node.uuid, 'msg': msg, 'e': e} last_error = _('Asynchronous exception for node %(node)s: ' '%(msg)s exception: %(e)s') % err_info LOG.exception(last_error) if node.provision_state in (states.CLEANING, states.CLEANWAIT): manager.cleaning_error_handler(task, last_error) elif node.provision_state in (states.DEPLOYING, states.DEPLOYWAIT): deploy_utils.set_failed_state(task, last_error) 

根據上面bearthead函數,首先根據當前節點的狀態node.provision_state==DEPLOYWAIT,調用continue_deploy()函數進行部署.

@task_manager.require_exclusive_lock def continue_deploy(self, task, **kwargs): task.process_event('resume') node = task.node image_source = node.instance_info.get('image_source') LOG.debug('Continuing deploy for node %(node)s with image %(img)s', {'node': node.uuid, 'img': image_source}) image_info = { 'id': image_source.split('/')[-1], 'urls': [node.instance_info['image_url']], 'checksum': node.instance_info['image_checksum'], # NOTE(comstud): Older versions of ironic do not set # 'disk_format' nor 'container_format', so we use .get() # to maintain backwards compatibility in case code was # upgraded in the middle of a build request. 'disk_format': node.instance_info.get('image_disk_format'), 'container_format': node.instance_info.get( 'image_container_format') } #通知IPA下載swift上的鏡像,並寫入本地磁盤 # Tell the client to download and write the image with the given args self._client.prepare_image(node, image_info) task.process_event('wait') 

Condutor然后依次調用:

  1. deploy_is_done()檢查IPA執行下載鏡像是否結束,
  2. 如果鏡像已經下載完成,即部署完成,設置從disk啟動,重啟進入用戶系統reboot_to_instance()
  3. 然后調用node.touch_provisioning() 更新數據庫,將節點的設置為alive

至此,使用agent方式進行部署操作系統的過程到處結束。下面我們用兩張圖來回顧一下部署過程:

  1. 使用pxe_* 為前綴的驅動的部署過程

pxe_deploy_process

  1. 使用agent_* 為前綴的驅動的部署過程

agent_deploy_process

下圖是Liberty版的狀態裝換圖

Ironic’s State Machine


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM