4.3 Inspector 源码分析

ironic-inspector 使用 flask 框架来编写的。flask 是一个轻量级 的 python web 框架, 详细资料可以查看: http://flask.pocoo.org/

Ironic 处理阶段

我们首先通过 cli 来触发 ironic inspector 流程。

ironic node-set-provision-state <node_uuid> manage
ironic node-set-provision-state <node_uuid> inspect

我们知道 openstack 组件之前的调用都是通过 restful 接口来完成的, 而组件内部是通过 rpc 调用来完成的。

上面的 cli 会发送 PUT 请求到 /v1/nodes/{node_ident}/provision , ironic-api 收到这个请求后会根据 body 的 target 字段做处理:

# ironic/api/controllers/v1/node.py
def provision():
...
    elif target == ir_state.VERBS['inspect']:
        pecan.request.rpcapi.inspect_hardware()

然后 ironic 通过 rpc 调用 inspect_hardware 方法。然后通过发送 http 请求 到 ironic-inspector。inspect 的具体实现是跟 driver 有关,在 driver.inspect.inspect_hardware 中。

# ironic/drivers/modules/inspector.py
def _start_inspection(node_uuid, context):
    try:
        _call_inspector(client.introspect, node_uuid, context)
        ...

# ironic_inspector_client/client.py
def introspect(uuid, base_url=None, auth_token=None,
               new_ipmi_password=None, new_ipmi_username=None,
               api_version=DEFAULT_API_VERSION, session=None, **kwargs):
    c = v1.ClientV1(api_version=api_version, auth_token=auth_token,
                    inspector_url=base_url, session=session, **kwargs)
    return c.introspect(uuid, new_ipmi_username=new_ipmi_username,
                        new_ipmi_password=new_ipmi_password)

# ironic_inspector_client/v1.py
def introspect(self, uuid, new_ipmi_password=None, new_ipmi_username=None):
    ...
    params = {'new_ipmi_username': new_ipmi_username,
              'new_ipmi_password': new_ipmi_password}
    self.request('post', '/introspection/%s' % uuid, params=params)

通过上面的代码,我们可以看到 ironic 发送了 post 请求到 /introspection/<uuid> 。 下面流程就到了 inspector 了。

Inspector处理阶段

ironic-inspector 的 restful api 实现在 main.py 中,我们首先根据 url 找到对应的函数:

# main.py
@app.route('/v1/introspection/<uuid>')
@convert_exceptions
def api_introspection(uuid):
    ...
    introspect.introspect(uuid,
                          new_ipmi_credentials=new_ipmi_credentails,
                          token=flask.request.headers.get('X-Auth-Token'))
    return '', 202

再看看introspect的具体实现:

# introspect.py
def introspect():
node_info = node_cache.add_node(node.uuid,
                                bmc_address=bmc_address,
                                ironic=ironic)
future = utils.executor().submit(_background_introspect, ironic, node_info)
...

introspect函数先是更新了ipmi信息,然后在inspector的node表里添加一条记录, 另外在attributes表里添加bmc_address信息。最终后台调用 _background_introspect做主机发现。

# introspect.py
def _background_introspect_locked(ironic, node_info):
    try:
        ironic.node.set_boot_device(node_info.uuid, 'pxe',
                                    persistent=False)
    try:
        ironic.node.set_power_state(node_info.uuid, 'reboot')

我们在已经配置好了裸机,并在/tftpboot/pxelinux.cfg/default设置了如下信息:

default introspect
label introspect
kernel ironic-agent.vmlinuz
append initrd=ironic-agent.initramfs ipa-inspection-callback-url=http://192.168.2.2:5050/v1/continue ipa-inspection-collectors=default,logs systemd.journald.forward_to_console=no
ipappend 3

IPA 阶段

裸机从小系统启动之后,会启动ironic-python-agent服务。 该服务会收集裸机的硬件信息,并发送到 ipa-inspection-callback-url指定的url。

Inspector主机上报阶段

先看看inspector怎么处理ipa上报的数据:

# main.py
@app.route('/v1/continue', methods=[post])
@convert_exceptions
def api_continue():
    data = flask.request.get_json(force=True)
    return flask.jsonify(process.process(data))
# process.py
def process(introspection_data):
    """Process data from the ramdisk.

    This function heavily relies on the hooks to do the actual data processing.
    """
    hooks = plugins_base.processing_hooks_manager()
    failures = []
    for hook_ext in hooks:
        try:
            hook_ext.obj.before_processing(introspection_data)
        except utils.Error as exc:
            ...
    # 根据ipmi_address和macs获取inpsector node
    node_info = _finde_node_info(introspection_data, failures)
    try:
        node = node_info.node()
        ...

    try:
        return _process_node(node, introspection_data, node_info)

我们可以看到这里数据是交由process出来,而process函数又调用各种钩子来出来ipa数据。 接着根据ipmi_address查找对应的inspector node,再根据获取到的uuid来得到 ironic node,交由_process_node()函数处理。

# process.py
def _process_node(node, introspection_data, node_info):
    ir_utils.check_provision_state(node)
    node_info.create_ports(introspection_data.get('macs') or ())
    _run_post_hooks(node_inof, introspection_data)

    if CONF.processing.store_data == 'switf':
        stored_data = {k: v for k, v in introspection_data.items()
                       if k not in _STORAGE_EXCLUDE_KEYS}
        swift_object_name = switf.store.store_introspection_data(stored_data,
                                                                 node_info.uuid)
    ironic = ir_utils.get_client()
    firewall.update_filters(ironic)

    node_info.invalidate_cache()
    rules.apply(node_info, introspection_data)
    ...
    utils.executor().submit(_finish, ironic, node_info, introspection_data)

def _finish(ironic, node_info, introspection_data):
    try:
        ironic.node.set_power_state(node_info.uuid, 'off')
    node_info.finished()

我们可以看到,如果配置了store_data=swift,inspector会把ipa上报的数据 存储到swift中。最后的 node_info.finished()是删除inspector数据库中已 完成的数据。