声明:
本博客欢迎转载,但请保留原作者信息!
作者:姜飞
团队:华为杭州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上电。