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

来源:互联网 发布:网络宠物用品店加盟 编辑:程序博客网 时间:2024/05/20 00:17

声明:

本博客欢迎转载,但请保留原作者信息!

作者:姜飞

团队:华为杭州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上电。


0 0
原创粉丝点击