Tripleo之nova-compute 和Ironic的代碼深入分析(三)

聲明:

本博客歡迎轉載,但請保留原作者信息!

作者:姜飛

團隊:華爲杭州OpenStack團隊


上文說到,nova boot在nova-compute的spawn操作,設置了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方法。

    @wsme_pecan.wsexpose(None, types.uuid, wtypes.text, status_code=202)
    def provision(self, node_uuid, target):
        
        rpc_node = objects.Node.get_by_uuid(pecan.request.context, node_uuid)
        topic = pecan.request.rpcapi.get_topic_for(rpc_node)
		
        #進行狀態判斷,如果狀態相同,則返回400,表示狀態已經是目標狀態了。
        #如果狀態在ACTIVE或者REBUILD 或者狀態在刪除狀態,返回409,表示相沖突,#要麼在部署,要麼在刪除
        if target == rpc_node.provision_state:
            msg = (_("Node %(node)s is already in the '%(state)s' state.") %
                   {'node': rpc_node['uuid'], 'state': target})
            raise wsme.exc.ClientSideError(msg, status_code=400)

        if target in (ir_states.ACTIVE, ir_states.REBUILD):
            processing = rpc_node.target_provision_state is not None
        elif target == ir_states.DELETED:
            processing = (rpc_node.target_provision_state is not None and
                        rpc_node.provision_state != ir_states.DEPLOYWAIT)
        else:
            raise exception.InvalidStateRequested(state=target, node=node_uuid)

        if processing:
            msg = (_('Node %s is already being provisioned or decommissioned.')
                   % rpc_node.uuid)
            raise wsme.exc.ClientSideError(msg, status_code=409)  # Conflict

        # Note that there is a race condition. The node state(s) could change
        # by the time the RPC call is made and the TaskManager manager gets a
        # lock.
	#發送人rpc消息給ironic-conductor,告訴他是要進行開始部署物理節點還是將物理節點取消部署,然後返回消息給外部調用。
        if target in (ir_states.ACTIVE, ir_states.REBUILD):
            rebuild = (target == ir_states.REBUILD)
            pecan.request.rpcapi.do_node_deploy(
                    pecan.request.context, node_uuid, rebuild, topic)
        elif target == ir_states.DELETED:
            pecan.request.rpcapi.do_node_tear_down(
                    pecan.request.context, node_uuid, topic)
        # Set the HTTP Location Header
        url_args = '/'.join([node_uuid, 'states'])
        pecan.response.location = link.build_url('nodes', url_args)
那我們知道ironic-api發送了一個do_node_deploy的rpc消息給ironic-conductor的話,肯定是在ironic-conductor的manager類裏面處理。那在ironic.conductor.manager. ConductorManager處理的,在這裏找到do_node_deploy方法。

    def do_node_deploy(self, context, node_id, rebuild=False):
        LOG.debug("RPC do_node_deploy called for node %s." % node_id)
        with task_manager.acquire(context, node_id, shared=False) as task:
            node = task.node
            …
            try:
                task.driver.deploy.validate(task)
            except (exception.InvalidParameterValue,
                    exception.MissingParameterValue) as e:
                raise exception.InstanceDeployFailure(_(
                    "RPC do_node_deploy failed to validate deploy info. "
                    "Error: %(msg)s") % {'msg': e})
			…
            task.set_spawn_error_hook(self._provisioning_error_handler,
                                      node, previous_prov_state,
                                      previous_tgt_provision_state)
            task.spawn_after(self._spawn_worker, self._do_node_deploy, task)

注意task_manager.acquire這裏會跟_sync_power_states() 競爭鎖資源,當前會存在獲取鎖資源失敗的場景,需要用戶重新進行該操作。後面ironic這裏會增加重試或者其他額外同步的操作。

只要在node的provision_stat爲ACTIVE\ERROR\DEPLOYFAIL 3種狀態的一種纔可以rebuild,否則會返回InstanceDeployFailure。

如果node的provision_stat不爲NOSTATE而且不是rebuild也會返回InstanceDeployFailure。

如果node 在維護狀態,返回NodeInMaintenance。

這裏要說下task.driver.deploy.validate(task)這個如何解釋,tas就是task_manager.TaskManager的一個對象,這個對象在初始化的時候將self.driver初始化了。

        try:
            if not self.shared:
                reserve_node()
            else:
                self.node = objects.Node.get(context, node_id)
            self.ports = objects.Port.list_by_node_id(context, self.node.id)
            self.driver = driver_factory.get_driver(driver_name or
                                                    self.node.driver)
        except Exception:
            with excutils.save_and_reraise_exception():
                self.release_resources()

driver_factory.get_driver會從driver_factory.DriverFactory中獲取該節點node的driver,主要就是利用stevedore這個第三方python庫,從entry_points 讀出ironic.drivers,這個就ironic當前所支持的driver,都在這裏了,後續要擴展的話,也可以在setup.cfg文件中ironic.drivers段中增加你所擴展的driver。

def _init_extension_manager(cls):
    cls._extension_manager = \
                dispatch.NameDispatchExtensionManager(
                        'ironic.drivers',
                        _check_func,
                        invoke_on_load=True,
                        on_load_failure_callback=_catch_driver_not_found)

比如我當前使用的driver是pxe_ssh,那麼我們可以看到ironic.drivers中pxe_ssh = ironic.drivers.pxe:PXEAndSSHDriver。 那麼task.driver就是ironic.drivers.pxe:PXEAndSSHDriver類的對象了,可以看到類初始化後,task.drive.deploy 就是pxe.PXEDeploy()的對象。而validate方法肯定就是pxe.PXEDeploy()裏面的方法了。

class PXEAndSSHDriver(base.BaseDriver):
    def __init__(self):
        self.power = ssh.SSHPower()
        self.deploy = pxe.PXEDeploy()
        self.management = ssh.SSHManagement()
        self.vendor = pxe.VendorPassthru()

class PXEDeploy(base.DeployInterface):
    def validate(self, task):
        # Check the boot_mode capability parameter value.
        #查看node的properties是否有capabilities信息,如果有的話,則查看boot_mode #的是否是'bios', 'uefi',如果不是,則失敗
        driver_utils.validate_boot_mode_capability(task.node)
		
        #是否開啓了IPXE,當前一般都使用PXE,IPXE能夠支持HTTP\iSCSI,PXE只能TFPT        
        d_info = _parse_deploy_info(task.node)
        iscsi_deploy.validate(task)
        props = ['kernel_id', 'ramdisk_id']
        iscsi_deploy.validate_glance_image_properties(task.context, d_info, props)

task.drive.deploy.validate在ironic. drivers.modules.pxe.PXEDeploy的validate方法,_parse_deploy_info主要就是解析instance_info和driver_info,查看pxe_deploy_kernel和pxe_deploy_ramdisk是否爲空,不爲空的話,調用glance的接口驗證鏡像信息是否正確。


node節點的instance_info信息參考如下:

{u'ramdisk': u'24fee7c8-d7ab-42a8-93fc-595668143344', u'kernel': u'5bfedd13-b041-4186-8524-040274eb7f70', u'root_gb': u'10', u'image_source': u'59c72602-42f6-4f8f-b420-bf7abc0380dd', u'ephemeral_format': u'ext4', u'ephemeral_gb': u'30', u'deploy_key': u'9ATNBQX1M8O9UQTXM8IEHJKX8J6QYV5H', u'swap_mb': u'0'}

task.driver.deploy.validate(task)執行完後,保存當前provision_state和目標provision_state,設置provision_state爲DEPLOYDONE,開始部署物理單板。設置部署失敗的鉤子函數task.set_spawn_error_hook。

我們來重點關注下,task.spawn_after(self._spawn_worker,self._do_node_deploy, task)
    def spawn_after(self, _spawn_method, *args, **kwargs):
        """Call this to spawn a thread to complete the task."""
        self._spawn_method = _spawn_method
        self._spawn_args = args
        self._spawn_kwargs = kwargs

在離開with task_manager.acquire(context, node_id, shared=False) as task的作用域之外,task_manager定義__exit__方法,那麼在離開這個對象的時候,會調用到該類的__exit__方法,這個方法會調用self._spawn_method方法,也就是self._spawn_worker,這個方法其實只是創建了一個綠色線程eventlet,來調用self._do_node_deploy函數,真正調用的函數是self._do_node_deploy這個方法,這裏會判斷綠色線程池有沒有滿,如果滿的話,會拋異常NoFreeConductorWorker。_do_node_deploy方法最主要的函數調用就是task.driver.deploy.prepare(task)這部分,實現的功能就是將PXE和TFTP的環境準備好。

    def prepare(self, task):
	pxe_info = _get_image_info(task.node, task.context)
        pxe_options = _build_pxe_config_options(task.node, pxe_info,
                                                task.context)
        pxe_utils.create_pxe_config(task, pxe_options,
                                    pxe_config_template)
        _cache_ramdisk_kernel(task.context, task.node, pxe_info)

pxe_options的內容在CONF.pxe.pxe_config_template需要用該node的信息替換,後面幾部分是iSCSI用到的信息。

pxe_options = {
        'deployment_aki_path': deploy_kernel,
        'deployment_ari_path': deploy_ramdisk,
        'aki_path': kernel,
        'ari_path': ramdisk,
        'pxe_append_params': CONF.pxe.pxe_append_params,
        'tftp_server': CONF.pxe.tftp_server,
        'deployment_id': node['uuid'],
        'deployment_key': deploy_key,
        'iscsi_target_iqn': "iqn-%s" % node.uuid,
        'ironic_api_url': ironic_api,
        'disk': CONF.pxe.disk_devices,
    }

使用CONF.pxe.pxe_config_template定義的模板,將裏面的內容用pxe_options修改變成config文#件(/tftpboot/f02e3aae-762e-4a4e-afc2-64afb092cdc1), 將/tftpboot/pxelinux.cfg/01-00-7b-6f-39-fe-28 這個mac地址就是node的port的mac地址,把它做軟鏈接成/tftpboot/f02e3aae-762e-4a4e-afc2-64afb092cdc1/config

從glance將內核和根文件系統的image文件下載下來,會先保存到#/tftp/master_images中做緩存,然後會將鏡像放到#/tftpboot/f02e3aae-762e-4a4e-afc2-64afb092cdc1 目錄下,該目錄下總共有5個文件config、deploy_kernel、deploy_ramdisk、kernel、ramdiskdeploy_kernel、deploy_ramdisk就是driver_info中的pxe_deploy_kernel和pxe_deploy_ramdisk,其他2個是instance_info的ramdisk和kernel,其實就是undercloud的內核和根文件系統,而image_source則是undercloud的磁盤文件系統。

Prepare完成後就要開始Deploy了,deploy開始就檢測image_source的鏡像,判斷該鏡像的大小要小於等於套餐的root-gb的大小, dhcp_factory.DHCPFactory獲取到ironic的dhcp_provider爲neutron的DHCP服務器(ironic.dhcp.neutron:NeutronDHCPApi),然後調用update_dhcp會調用一個port的update_port的操作,將物理單板的port相關信息(MAC 地址、網口等)進行更新。

 

然後設置物理單板的啓動順序,讓單板reboot進行PXE,狀態修改爲DEPLOYWAIT,這步完成後,會通過VendorPassthru._continue_deploy().進行部署物理單板的後續操作。


    def deploy(self, task):
        iscsi_deploy.cache_instance_image(task.context, task.node)
        iscsi_deploy.check_image_size(task)

        _create_token_file(task)
        dhcp_opts = pxe_utils.dhcp_options_for_instance(task)
        provider = dhcp_factory.DHCPFactory()
        provider.update_dhcp(task, dhcp_opts)

        try:
            manager_utils.node_set_boot_device(task, 'pxe', persistent=True)
        except exception.IPMIFailure:
            if driver_utils.get_node_capability(task.node,
                                                'boot_mode') == 'uefi':
                LOG.warning(_LW("ipmitool is unable to set boot device while "
                                "the node is in UEFI boot mode."
                                "Please set the boot device manually."))
            else:
                raise

        manager_utils.node_power_action(task, states.REBOOT)

        return states.DEPLOYWAIT

單板的driver_info的ssh_virt_type 是virsh,那涉及單板的重啓、上下電等操作就可以通過virsh命令來操作,因爲我用的是PXE+SSH,所以用的是virsh的命令來控制單板的上下電、重啓等相關操作,設置單板的啓動順序就是使用virsh set_boot_device,而單板重啓就是使用virsh reset。當然使用driver不同,那相關的命令也會不同。

    virsh_cmds = {
           'base_cmd': 'LC_ALL=C /usr/bin/virsh',
           'start_cmd': 'start {_NodeName_}',
           'stop_cmd': 'destroy {_NodeName_}',
           'reboot_cmd': 'reset {_NodeName_}',
           'list_all': "list --all | tail -n +2 | awk -F\" \"'{print $2}'",
           'list_running': ("list --all|grep running | "
                "awk -v qc='\"'-F\" \" '{print qc$2qc}'"),
           'get_node_macs': ("dumpxml {_NodeName_} | "
                "awk -F \"'\"'/mac address/{print $2}'| tr -d ':'"),
           'set_boot_device': ("EDITOR=\"sed -i '/<boot\(dev\|order\)=*\>/d;"
                "/<\/os>/i\<bootdev=\\\"{_BootDevice_}\\\"/>'\" "
                "{_BaseCmd_} edit{_NodeName_}"),
           'get_boot_device': ("{_BaseCmd_} dumpxml {_NodeName_} | "
                "awk '/boot dev=/ { gsub(\".*dev=\" Q, \"\" ); "
                "gsub( Q \".*\",\"\" ); print; }' "
                "Q=\"'\"RS=\"[<>]\" | "
                "head -1"),
       }

到此ironic-conductor的動作完成,等待物理單板進行PXE上電。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章