在某次关于发布系统的项目中,需要调用ansible来发布任务,其中一段代码是初始化ansible的连接,并传入一个source(目标机器)的值,代码段如下:

from .ansible_api import AnsibleClient
sources = '192.168.1.218'
client = AnsibleClient(sources) ...

  完成后发现一直报错,如下所示错误信息:

[WARNING]: Unable to parse python/devops/192.168.1.218 as an inventory source

  看字面意思描述,是说无法解析python/devops/192.168.1.218为一个inventory源。inventory是ansible专用的一个用来存储目标机器的文件。通过报错信息大概能够猜出,当我想要以IP地址的方式传入目标机器时候,ansible却把IP当做inventory文件来解析了。

  先抛出结论,正确的传值方式,是在IP地址后增加一个 ',' ,让sources = '192.168.1.218,' ,再传入。或者传入两个以上的IP,如'192.168.1.218,192.168.1.219' 。

  但是传入一个IP是我的一个正常应用场景,那么通过源码分析,我们来看看为何要出现以上结果。

  实例化的AnsibleClient是在ansible_api文件中,有一个AnsibleClient类,其中有初始化方法__init__(self, source),代码如下:

class AnsibleClient:
def __init__(self, source):
self.source = source
self.loader = DataLoader()
self.inventory = InventoryManager(loader=self.loader, sources=self.source)
self.variable_manager = VariableManager(loader=self.loader, inventory=self.inventory)
self.passwords = dict(vault_pass='secret')
self.callback = None

  在以上代码中,可以看到在实例化AnsibleClient连接时,之前的192.168.1.218会由source参数传进来,并且最终再传入InventoryManager

  接着再查看InventoryManager的源码,源码所在文件位置为路径ansible/inventory/manager.py中,代码片段如下所示:

class InventoryManager(object):
def __init__(self, loader, sources=None):
if sources is None:
self._sources = []
elif isinstance(sources, string_types):
self._sources = [sources]
else:
self._sources = sources
self.parse_sources(cache=True)

  通过以上代码,会发现在InventoryManager实例化时候,如果sources有值传入,那么sources会赋值给self._sources,并继续走self.parse_sources(cache=True)这段,通过利用print('报错信息在此!!!')断点大法,确实如此。

  接着查看parse_sources方法,代码片段如下:

    def parse_sources(self, cache=False):
''' iterate over inventory sources and parse each one to populate it''' parsed = False
# allow for multiple inventory parsing
for source in self._sources: if source:
if ',' not in source:
source = unfrackpath(source, follow=False)
parse = self.parse_source(source, cache=cache)
if parse and not parsed:
parsed = True

  注意里面有一句if ',' not in source:,此时我们会走到这一分支,并进入unfrackpath这一步,接着查看这一方法的源代码,如下所示:

def unfrackpath(path, follow=True, basedir=None):
b_basedir = to_bytes(basedir, errors='surrogate_or_strict', nonstring='passthru') if b_basedir is None:
b_basedir = to_bytes(os.getcwd(), errors='surrogate_or_strict')
elif os.path.isfile(b_basedir):
b_basedir = os.path.dirname(b_basedir) b_final_path = os.path.expanduser(os.path.expandvars(to_bytes(path, errors='surrogate_or_strict'))) if not os.path.isabs(b_final_path):
b_final_path = os.path.join(b_basedir, b_final_path) if follow:
b_final_path = os.path.realpath(b_final_path) return to_text(os.path.normpath(b_final_path), errors='surrogate_or_strict')

  这个方法是用来解析文件路径的,通过这个方法,让我们的'192.168.1.218',最终变成了'python/devops/192.168.1.218',此时,我们得到了报错信息提示源是谁了。但是我们传入的是192.168.1.218,不想被解析为文件,所以回到parse_sources方法,将if ',' not in source变为if ',' and '.' not in source:,跳过if分支,再次运行程序,报错信息变成了下面这样:

[WARNING]: Unable to parse 192.168.1.218 as an inventory source

  成功了一半了,但是为何还会出现报错信息,得接着查

  回到parse_sources方法接着往下,source通过跳过unfrackpathparse的if分支后,被传入self.parse_source(source, cache=cache)方法,该方法代码如下:

def parse_source(self, source, cache=False):
''' Generate or update inventory for the source provided ''' parsed = False
display.debug(u'Examining possible inventory source: %s' % source) # use binary for path functions
b_source = to_bytes(source) # process directories as a collection of inventories
if os.path.isdir(b_source):
display.debug(u'Searching for inventory files in directory: %s' % source)
for i in sorted(os.listdir(b_source)): display.debug(u'Considering %s' % i)
# Skip hidden files and stuff we explicitly ignore
if IGNORED.search(i):
continue # recursively deal with directory entries
fullpath = to_text(os.path.join(b_source, i), errors='surrogate_or_strict')
parsed_this_one = self.parse_source(fullpath, cache=cache)
display.debug(u'parsed %s as %s' % (fullpath, parsed_this_one))
if not parsed:
parsed = parsed_this_one
else:
# left with strings or files, let plugins figure it out # set so new hosts can use for inventory_file/dir vars
self._inventory.current_source = source # try source with each plugin
failures = []
for plugin in self._fetch_inventory_plugins(): plugin_name = to_text(getattr(plugin, '_load_name', getattr(plugin, '_original_path', '')))
display.debug(u'Attempting to use plugin %s (%s)' % (plugin_name, plugin._original_path)) # initialize and figure out if plugin wants to attempt parsing this file
try:
plugin_wants = bool(plugin.verify_file(source))
print(plugin_wants,source)
except Exception:
plugin_wants = False if plugin_wants:
try:
# FIXME in case plugin fails 1/2 way we have partial inventory
plugin.parse(self._inventory, self._loader, source, cache=cache)
try:
plugin.update_cache_if_changed()
except AttributeError:
# some plugins might not implement caching
pass
parsed = True
display.vvv('Parsed %s inventory source with %s plugin' % (source, plugin_name))
break
except AnsibleParserError as e:
display.debug('%s was not parsable by %s' % (source, plugin_name))
tb = ''.join(traceback.format_tb(sys.exc_info()[2]))
failures.append({'src': source, 'plugin': plugin_name, 'exc': e, 'tb': tb})
except Exception as e:
display.debug('%s failed while attempting to parse %s' % (plugin_name, source))
tb = ''.join(traceback.format_tb(sys.exc_info()[2]))
failures.append({'src': source, 'plugin': plugin_name, 'exc': AnsibleError(e), 'tb': tb})
else:
display.vvv("%s declined parsing %s as it did not pass its verify_file() method" % (plugin_name, source))
else:
if not parsed and failures:
# only if no plugin processed files should we show errors.
for fail in failures:
display.warning(u'\n* Failed to parse %s with %s plugin: %s' % (to_text(fail['src']), fail['plugin'], to_text(fail['exc'])))
if 'tb' in fail:
display.vvv(to_text(fail['tb']))
if C.INVENTORY_ANY_UNPARSED_IS_FAILED:
raise AnsibleError(u'Completely failed to parse inventory source %s' % (source))
if not parsed:
if source != '/etc/ansible/hosts' or os.path.exists(source):
# only warn if NOT using the default and if using it, only if the file is present
display.warning("Unable to parse %s as an inventory source" % source) # clear up, jic
self._inventory.current_source = None return parsed

  该方法比较长,但是可以看到里面的报错信息出处,在这里"Unable to parse %s as an inventory source",是因为if not parsed条件进来的,那么parsed为何False,还得看这段代码动作。

  首先最开始parsed被置为了False,然后通过print大法得知if os.path.isdir(b_source)分支没有进入,走的是后面else分支。通过代码各种变量命名规则,大概能够猜到这里面是在做inventory plugin的判断。

  如果parsed被置为了False,那么一定会报错,为了避免就一定会在这段代码里parsed会有置为True的地方,那么plugin_wants = bool(plugin.verify_file(source))这一句就至关重要。

  通过plugin.verify_file(source)接收到参数source(192.168.1.218)后,parsed被置为了False,通过方法字面意思知道verify_file是校验source是否为文件的。我们查看verify_file的源代码,文件位置为ansible/plugins/invenroy/host_list.py,代码如下:

def verify_file(self, host_list):

        valid = False
b_path = to_bytes(host_list, errors='surrogate_or_strict')
if not os.path.exists(b_path) and ',' in host_list:
valid = True
return valid

  果然,如果想让返回结果为True的话,需要满足if条件,即文件不存在,并且 ',' 存在。

  这里将if not os.path.exists(b_path) and ',' in host_list:这一句修改为if not os.path.exists(b_path):,运行程序,报错没了。

  再回头捋一遍整个源码思路,当我们将IP地址传入ansible连接初始化时,会校验该IP地址是否为文件,如果是文件,那么就解析文件路径,最终看该文件内是否有目标机器。

  不为文件,就校验inventory plugin的各种插件(默认开启5个,其中包括host_list)。如果该IP符合插件所需特征,就由插件来解析IP地址,如果不符合,最终抛出报错信息Unable to parse %s as an inventory source。

  要想解析这个问题,有两个思路:

  第一个,最简单的就是传入IP地址为一个的时候,末尾加上 ','

  第二个,修改源码两个地方,一是跳过unfrackpath分支,二是让host_list.py的verify_file方法为真。上文均有过修改代码的描述。

  

  好了,整个分析结束。我猜,ansible的意思是,既然初始化ansible连接需要传入host的list,那么单个IP地址不能称为list,如果想成为list,就得加个 ',' ,哈哈

001_记一次ansible api二次开发遇到的小问题的更多相关文章

  1. Civil 3D API二次开发学习指南

    Civil 3D构建于AutoCAD 和 Map 3D之上,在学习Civil 3D API二次开发之前,您至少需要了解AutoCAD API的二次开发,你可以参考AutoCAD .NET API二次开 ...

  2. (50)zabbix API二次开发使用与介绍

    zabbix API开发库 zabbix API请求和响应都是json,并且还提供了各种语法的lib库,http://zabbix.org/wiki/Docs/api/libraries,包含php. ...

  3. 美客分销商城-接力购源码系统,全开源代码可进行二次开发,微信小程序分销商城

    1. 准备服务器.域名(SSL证书).认证的微信小程序.微信支付商户号 2. 系统功能简介 三.演示案例,微信扫码查看 四.后台管理系统 五. 全套开源源码,进行二次开发 六.本系统完美运营,全套代码 ...

  4. Autodesk View and Data API二次开发学习指南

    什么是View and Data API? 使用View and Data API,你可以轻松的在网页上显示大型三维模型或者二维图纸而不需要安装任何插件.通过View and Data API,你可以 ...

  5. Kettle api 二次开发之 日志的保存

    使用kettle做数据抽取的时候可以使用图形化的工具配置日志保存参数,选择数据库连接,输入日志表名称, 点击sql 执行对应的sql创建日志表即可. 点击保存之后,日志配置会保存在trans或者job ...

  6. Python调用ansible API系列(四)动态生成hosts文件

    方法一:通过最原始的操作文件的方式 #!/usr/bin/env python # -*- coding: utf-8 -*- """ 通过操作文件形式动态生成ansib ...

  7. Navisworks API 简单二次开发 (自定义工具条)

    在Navisworks软件运行的时候界面右侧有个工具条.比较方便.但是在二次开发的时候我不知道在Api那里调用.如果有网友知道请告诉我.谢谢. 我用就自己设置一个工具.界面比较丑!没有美工. 代码: ...

  8. 用JSON-server模拟REST API(二) 动态数据

    用JSON-server模拟REST API(二) 动态数据 上一篇演示了如何安装并运行 json server , 在这里将使用第三方库让模拟的数据更加丰满和实用. 目录: 使用动态数据 为什么选择 ...

  9. Express4.x API (二):Request (译)

    写在前面 最近学习express想要系统的过一遍API,www.expressjs.com是express英文官网(进入www.epxressjs.com.cn发现也是只有前几句话是中文呀~~),所以 ...

随机推荐

  1. DVWA学习记录 PartⅦ

    SQL Injection 1. 题目 SQL Injection,即SQL注入,是指攻击者通过注入恶意的SQL命令,破坏SQL查询语句的结构,从而达到执行恶意SQL语句的目的. 2. Low a. ...

  2. CMDB03 /今日未采集的资产、资产入库、资产变更记录、资产采集

    CMDB03 /今日未采集的资产.资产入库.资产变更记录.资产采集 目录 CMDB03 /今日未采集的资产.资产入库.资产变更记录.资产采集 1. 获取今日未采集的服务器 2. server资产入库以 ...

  3. How to install nginx in Ubuntu

    The steps for installing the nginx on Ubuntu below. 1.install the packages first. apt-get install gc ...

  4. [apue] Linux / Windows 系统上只能建立不超过 PATH_MAX / MAX_PATH 长度的路径吗?

    问题的提出 在处理文件系统路径的时候,我们一般会先开辟一块内存区,用来接收路径.或者拼接好路径传递给系统调用.这是因为路径在各个系统上都有最大长度限制,在 Windows 上这个值是 MAX_PATH ...

  5. pyenv虚拟环境安装

    安装过程 配置yum源 # curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo ...

  6. Ant Design Pro 学习笔记:数据流向

    在讲这个问题之前,有一个问题应当讲一下: Ant Design Pro / umi / dva 是什么关系? 首先是 umi / dva 的关系. umi 是一个基于路由的 react 开发框架. d ...

  7. 技能实际操作:如何为Centos7 配置静态路由?

    如图: 业务地址:192.168.10.0/24 ---- 192.168.20.0/24 管理地址:172.168.10.0/24 --- 172.168.20.0/24 需求:每台主机配置两张网卡 ...

  8. [日常摘要] -- ThreadLocal篇

    简介 ThreadLocal,即线程变量,是一个以ThreadLocal对象为键.任意对象为值的存储结构.这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个 ...

  9. [redis] -- 缓存雪崩和缓存穿透、缓存击穿问题解决方案篇

    缓存雪崩 缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉 解决方案 事前:尽量保证整个redis集群的高可用性,发现机器宕机尽快补上.选择合适的内存淘 ...

  10. presto 转换静态catlog为动态catlog

    近年来,基于hadoop的sql框架层出不穷,presto也是其中的一员.从2012年发展至今,依然保持年轻的活力(版本迭代依然很快),presto的相关介绍,我们就不赘述了,相信看官多对presto ...