02月22, 2017

neutron中metadata相关代码分析(上)

metadata做为openstack中的一个功能组件,可以结合cloud-init,在创建虚拟机后可以执行一些自定义的功能,比如自动创建账号等。这样就极大的增加的创建后自定义配置的灵活性,我们在使用过程当中,偶尔会有metadata失灵的情况,而且不是每次必现,所以借助一次现场,详细分析了neutron中metadata的代码流程,看是否能复现此问题。

现象

alt

在虚拟机内部调用169.254.169.154可看到返回500错误。

分析过程

首先我们通过借助代码抛出的异常开始定位到class MetadataProxyHandler(object)的call入口开始

016-01-21 02:35:04.032 28142 ERROR neutron.agent.metadata.agent [-] Unexpected error.
2016-01-21 02:35:04.032 28142 TRACE neutron.agent.metadata.agent Traceback (most recent call last):
2016-01-21 02:35:04.032 28142 TRACE neutron.agent.metadata.agent   File "/usr/lib/python2.7/site-packages/neutron/agent/metadata/agent.py", line 109, in __call__
2016-01-21 02:35:04.032 28142 TRACE neutron.agent.metadata.agent     instance_id, tenant_id = self._get_instance_and_tenant_id(req)

其核心代码 是instance_id, tenant_id = self._get_instance_and_tenant_id(req) ,接收curl请求,返回正确的instance id和租户id,然后metadata-agent拿着这个信息去请求nova-api获取metadata。

@webob.dec.wsgify(RequestClass=webob.Request)
  def __call__(self, req):
      try:
          LOG.debug("Request: %s", req)
          instance_id, tenant_id = self._get_instance_and_tenant_id(req)
          LOG.debug("wbp instance_id: %s, tenant_id:%s", instance_id, tenant_id)
          if instance_id:
              return self._proxy_request(instance_id, tenant_id, req)
          else:
              return webob.exc.HTTPNotFound()
      except Exception:
          LOG.exception(_LE("Unexpected error."))
          msg = _('An unknown error has occurred. '
                  'Please try your request again.')
          return webob.exc.HTTPInternalServerError(explanation=unicode(msg))

alt

这里我们在vm内部做一个curl 169.254.169.254的主动请求, 返回的结果instanceid是8dbab89c-db0e-4c2f-b337-038500b5f578,tenant_id = 176e6938dadd45dcaa847d8242778321。

alt

在一个curl请求过来,然后获取到该vm的instance id,和租户id,然后就执行 self._proxy_request(instance_id, tenant_id, req) ,去和nova-api要metata。

以上是一个很正常的没有报异常的metadata请求。

在我们的异常环境中,第一步都过不去,因为第一步都不能返回正确的instance id和tenant id。所以我们需要集中看看这一步内部究竟在干嘛,我们trace一下self._get_instance_and_tenant_id(req)函数

def _get_instance_and_tenant_id(self, req):
    remote_address = req.headers.get('X-Forwarded-For')
    network_id = req.headers.get('X-Neutron-Network-ID')
    router_id = req.headers.get('X-Neutron-Router-ID')

    LOG.debug("wbp remote_address: %s, network_id:%s, router_id:%s", remote_address, network_id, router_id)

    ports = self._get_ports(remote_address, network_id, router_id)
    if len(ports) == 1:
        return ports[0]['d

这里我们先看一下curl请求以后,给该函数传参是那些,这里curl请求的头部传参,该vm的ip地址,网络id,router_id。这里可以看到在vm内部的curl请求的传参的ip地址,和该ip对应的网络id。信息和我们的环境中都是一致的。 alt

[root@w-openstack01 wangbaoping]# neutron net-list
+--------------------------------------+-----------------+------------------------------------------------------+
| id                                   | name            | subnets                                              |
+--------------------------------------+-----------------+------------------------------------------------------+
| 54e20e0d-e8e3-4b55-a418-556a06b5b887 | NET-xxxx | f7bdbe72-096a-4a8a-a3a1-a034a069b242 x.x.x.0/23 |

该ip对应的network id如上。routeid为空正常。 接下来代码走到,使用这三个参数去查找相应的port,然后返回正确的port。

ports = self._get_ports(remote_address, network_id, router_id)

alt

接下来我们再看看他内部是咋样通过ip和网络id来获取port的,这里我们需要分析一下_get_ports函数

def _get_ports(self, remote_address, network_id=None, router_id=None):

    LOG.debug("wbp remote_address: %s, network_id:%s, router_id:%s", remote_address, network_id, router_id)

    if network_id:
        networks = (network_id,)
    elif router_id:
        networks = self._get_router_networks(router_id)
    else:
        raise TypeError(_("Either one of parameter network_id or router_id"
                          " must be passed to _get_ports method."))

    LOG.debug("wbp remote_address: %s, networks:%s", remote_address, networks)
    return self._get_ports_for_remote_address(remote_address, networks)

该函数中,传参正确的参数,然后根据传参的network id,生成一个set,然后去执行_get_ports_for_remote_address(remote_address, networks)函数,去找port。

接下来就得看看这个函数得到传参以后咋样去找port的:

@utils.cache_method_results
def _get_ports_for_remote_address(self, remote_address, networks):
    ##省略注释 
    return self._get_ports_from_server(networks=networks,
                                       ip_address=remote_address)

该函数使用了装饰器,我们看看装饰器里面在做什么,首先被调用的是cache_method_results类中的call

class cache_method_results(object):
    def __call__(self, target_self, *args, **kwargs):
        LOG.debug("wbp target_self: %s, args:%s, kwargs:%s", target_self, str(args), str(kwargs))
        if not hasattr(target_self, '_cache'):
            raise NotImplementedError(
                "Instance of class %(module)s.%(class)s must contain _cache "
                "attribute" % {
                    'module': target_self.__module__,
                    'class': target_self.__class__.__name__})
        if not target_self._cache:
            if self._first_call:
                LOG.debug("Instance of class %(module)s.%(class)s doesn't "
                          "contain attribute _cache therefore results "
                          "cannot be cached for %(func_name)s.",
                          {'module': target_self.__module__,
                           'class': target_self.__class__.__name__,
                           'func_name': self.func.__name__})
                self._first_call = False
            return self.func(target_self, *args, **kwargs)
        return self._get_from_cache(target_self, *args, **kwargs)

该装饰器传参是什么?target_self应该是被装饰的函数名,args list是remote_address IP地址和网络id set组成的list。 alt

从log中看的出 target_self:

接下来判断一下neutron.agent.metadata.agent.MetadataProxyHandler是否有_cache属性。当然该类就是响应我们最原始curl请求的类。 然后我们这个类中看看,是否含有_cache属性

class MetadataProxyHandler(object):
    def __init__(self, conf):
        self.conf = conf
        self.auth_info = {}
        if self.conf.cache_url:
            self._cache = cache.get_cache(self.conf.cache_url)
        else:
            self._cache = False
        LOG.debug("wbp _cache: %s", self._cache)
        self.plugin_rpc = MetadataPluginAPI(topics.PLUGIN)
        self.context = context.get_admin_context_without_session()
        # Use RPC by default
        self.use_rpc = True

这里首先会判断我们有没有配置cache_url。有的话就返回cache object,没有的话直接返回false。 这里我们返回的是 说明系统默认有配置。 这里首先会判断我们有没有配置cache_url。有的话就返回cache object,没有的话直接返回false。 这里我们返回的是 说明系统默认有配置。 alt

然后我们看看系统究竟有没有默认配置 查看一下matata.ini,发现在最后一行有配置。

# URL to connect to the cache backend.
# default_ttl=0 parameter will cause cache entries to never expire.
# Otherwise default_ttl specifies time in seconds a cache entry is valid for.
# No cache is used in case no value is passed.
# cache_url = memory://?default_ttl=5

通过注释得知,如果default_ttl=0,则port永不失效,默认的配置是有效期为5s。

我们在细看一下/usr/lib/python2.7/site-packages/neutron/openstack/common/cache/_backends/memory.py 中的class MemoryBackend(backends.BaseCache类。

该类从名字上看就知道会对port的一些相关数据做一个内存缓存,这样的好处就是没必要每次拿port都要去和neutron-server交互,这就说明了我们的port数据大部分时候是可以从memory中读取的,只有从cache中读取不到,才需要用neutron client拿着认证信息去neutorn-server要port。

不走缓存的过程要先去keystone认证,所以我们尝试在memcache中删除vm的port信息,让代码去走认证拿port这条路,我们修改neutron的配置信息,先将缓存关闭。

然后再次请求会看到cache:false

alt

然后就会触发 if not target_self._cache,直接返回执行self.func(target_self, args, *kwargs)

if not target_self._cache:
    if self._first_call:
        LOG.debug("Instance of class %(module)s.%(class)s doesn't "
                  "contain attribute _cache therefore results "
                  "cannot be cached for %(func_name)s.",
                  {'module': target_self.__module__,
                   'class': target_self.__class__.__name__,
                   'func_name': self.func.__name__})
        self._first_call = False
    LOG.debug("wbp target_self._cache: %s, self._first_call:%s", target_self._cache, self._first_call)
    LOG.debug("wbp self.func:%s, target_self:%s", self.func, target_self)

    return self.func(target_self, *args, **kwargs)
return self._get_from_cache(target_self, *args, **kwargs)

alt

其实执行的就是我们被装饰的函数 _get_ports_for_remote_address(self, remote_address, networks)

def _get_ports_for_remote_address(self, remote_address, networks):

    LOG.debug("wbp2 go back remote_address:%s, networks:%s", remote_address, networks)
    return self._get_ports_from_server(networks=networks,
                                       ip_address=remote_address)

alt

然后执行_get_ports_from_server()函数

def _get_ports_from_server(self, router_id=None, ip_address=None,
                           networks=None):
    LOG.debug("wbp3")
    """Either get ports from server by RPC or fallback to neutron client"""
    filters = self._get_port_filters(router_id, ip_address, networks)
    LOG.debug("wbp4")
    LOG.debug("wbp ip_address: %s, networks:%s, use_rpc:%s, filters:%s", ip_address, str(networks), self.use_rpc, str(filters))
    if self.use_rpc:
        try:
            return self.plugin_rpc.get_ports(self.context, filters)
        except (oslo_messaging.MessagingException, AttributeError):
            # TODO(obondarev): remove fallback once RPC is proven
            # to work fine with metadata agent (K or L release at most)
            LOG.warning(_LW('Server does not support metadata RPC, '
                            'fallback to using neutron client'))
            self.use_rpc = False
    #LOG.debug("wbp ip_address: %s, networks:%s, use_rpc:%s, filters:%s", ip_address, str(networks), self.use_rpc, str(filters))
    #LOG.debug("wbp4")
    return self._get_ports_using_client(filters)

alt

这里由于默认使用rpc来获取port,会进去self.plugin_rpc.get_ports(self.context, filters)函数,还轮不到使用neutron_client来与neutron_server交互。所以就不会触发我们的neutron client与keystone的认证问题。

alt

这里我们要验证他走neutron_client去验证能否成功,我们尝试关闭掉使用rpc调用来获取port。

修改代码设置use_rpc=false

alt

def _get_ports_from_server(self, router_id=None, ip_address=None,
                           networks=None):
    LOG.debug("wbp3")
    """Either get ports from server by RPC or fallback to neutron client"""
    filters = self._get_port_filters(router_id, ip_address, networks)
    #LOG.debug("wbp ip_address: %s, networks:%s, use_rpc:%s, filters:%s", ip_address, str(networks), self.use_rpc, str(filters))
    if self.use_rpc:
        try:
            return self.plugin_rpc.get_ports(self.context, filters)
        except (oslo_messaging.MessagingException, AttributeError):
            # TODO(obondarev): remove fallback once RPC is proven
            # to work fine with metadata agent (K or L release at most)
            LOG.warning(_LW('Server does not support metadata RPC, '
                            'fallback to using neutron client'))
            self.use_rpc = False
    #LOG.debug("wbp ip_address: %s, networks:%s, use_rpc:%s, filters:%s", ip_address, str(networks), self.use_rpc, str(filters))
    #LOG.debug("wbp4")
    LOG.debug("wbp5 ip_address: %s, networks:%s, use_rpc:%s, filters:%s", ip_address, str(networks), self.use_rpc, str(filters))
    return self._get_ports_using_client(filters)

alt

代码会跳过plugin_rpc.get_ports, 直接通过neutron_client来获取port。至此我们关闭了前面的cache和rpc两条主要的获取port的路,只留了最后一条neutron-client认证获取port的路。

这里代码会走到self._get_ports_using_client(filters)。 我们看看该代码中执行了什么

def _get_ports_using_client(self, filters):
    # reformat filters for neutron client
    if 'device_id' in filters:
        filters['device_id'] = filters['device_id'][0]
    if 'fixed_ips' in filters:
        filters['fixed_ips'] = [
            'ip_address=%s' % filters['fixed_ips']['ip_address'][0]]
    client = self._get_neutron_client()
    ports = client.list_ports(**filters)
    self.auth_info = client.get_auth_info()
    return ports['ports']

这里的filter其实就是传参networkid和ip地址,过滤生成一个dict,_get_ports_using_client()函数中首先会获取neutron_client,然后通过neutron client的list_ports函数过滤出来合适的port。

这里client = self._get_neutron_client(),我们先看一下代码是怎么获取neutron_client的。

def _get_neutron_client(self):
    qclient = client.Client(
        username=self.conf.admin_user,
        password=self.conf.admin_password,
        tenant_name=self.conf.admin_tenant_name,
        auth_url=self.conf.auth_url,
        auth_strategy=self.conf.auth_strategy,
        region_name=self.conf.auth_region,
        token=self.auth_info.get('auth_token'),
        insecure=self.conf.auth_insecure,
        ca_cert=self.conf.auth_ca_cert,
        endpoint_url=self.auth_info.get('endpoint_url'),
        endpoint_type=self.conf.endpoint_type
    )
    return qclient

这里使用neutronclient库需要一些基本的认证参数,这里很明显有 auth_url,username,password,tenant_name等。这些参数都是从conf文件获取。

ok,这一步就走不过,因为要生成client首先要去keystone验证,我们看看是不是卡在验证上了。

我们看看/usr/lib/python2.7/site-packages/neutronclient/v2_0/client.py的Client类。 class Client(ClientBase): 该类继承自class ClientBase(object): alt

def __init__(self, **kwargs):
    _logger.debug("wbp8 kwargs:%s", str(kwargs))
    """Initialize a new client for the Neutron v2.0 API."""
    super(ClientBase, self).__init__()
    self.retries = kwargs.pop('retries', 0)
    self.raise_errors = kwargs.pop('raise_errors', True)
    self.httpclient = client.construct_http_client(**kwargs)
    self.version = '2.0'
    self.format = 'json'
    self.action_prefix = "/v%s" % (self.version)
    self.retry_interval = 1

这里我们的clent接收传参刚才从配置文件读取的那些认证参数dict,然后client.construct_http_client(**kwargs) 构造httpclient。

这里我们看一下前面代码给的认证传参:

alt

alt

这里在do_request之前得去认证和获取endpoint_url

ef authenticate_and_fetch_endpoint_url(self):
    if not self.auth_token:
        self.authenticate()
    elif not self.endpoint_url:
        self.endpoint_url = self._get_endpoint_url()

这里self.auth_token = token,但是我们前面的dict中token是none,所以执行认证函数:

def authenticate(self):
    if self.auth_strategy == 'keystone':
        self._authenticate_keystone()
    elif self.auth_strategy == 'noauth':
        self._authenticate_noauth()
    else:
        err_msg = _('Unknown auth strategy: %s') % self.auth_strategy
        raise exceptions.Unauthorized(message=err_msg)

这里我们的dict中auth_strategy 是keystone,所以执行_authenticate_keystone。 alt

因为这里的构造的token_url=传参+/tokens,而我们的传参是”http://xxxx:35357“ 也就是我们的配置文件中是这个值,这样就复现了我们遇到的metadata问题!

所以最终问题定位在我们的metadata.ini中的auth_url = “http://xxx:35357” ,我们尝试修改成auth_url=”http://xxx:35357/v2.0“ ,然后再次测试,发现metadata恢复正常。

alt

总结

metadta-agent拿port这个动作,有三条路。

  1. 系统默认配置了memorycache,从cache中拿
  2. 系统默认开启了use_rpc=true,可以通过rpc调度来获取port。
  3. 也是最后一条,通过neutron client走http先从keystone验证,然后get_ports来过滤获取port,最后一条必须保证参数配置正确,尤其url要写对。

通过rpc获取port的流程请看neutron中metadata相关代码分析(下)

本文链接:https://www.opsdev.cn/post/metadata-code-analyze.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。