声明:
本博客欢迎转载,但请保留原作者信息!
作者:姜飞
团队:华为杭州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的接口验证镜像信息是否正确。
{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、ramdisk,deploy_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上电。