聲明:
本博客歡迎轉載,但請保留原作者信息!
作者:姜飛
團隊:華爲杭州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上電。