001_记一次ansible api二次开发遇到的小问题
在某次关于发布系统的项目中,需要调用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二次开发遇到的小问题的更多相关文章
- Civil 3D API二次开发学习指南
Civil 3D构建于AutoCAD 和 Map 3D之上,在学习Civil 3D API二次开发之前,您至少需要了解AutoCAD API的二次开发,你可以参考AutoCAD .NET API二次开 ...
- (50)zabbix API二次开发使用与介绍
zabbix API开发库 zabbix API请求和响应都是json,并且还提供了各种语法的lib库,http://zabbix.org/wiki/Docs/api/libraries,包含php. ...
- 美客分销商城-接力购源码系统,全开源代码可进行二次开发,微信小程序分销商城
1. 准备服务器.域名(SSL证书).认证的微信小程序.微信支付商户号 2. 系统功能简介 三.演示案例,微信扫码查看 四.后台管理系统 五. 全套开源源码,进行二次开发 六.本系统完美运营,全套代码 ...
- Autodesk View and Data API二次开发学习指南
什么是View and Data API? 使用View and Data API,你可以轻松的在网页上显示大型三维模型或者二维图纸而不需要安装任何插件.通过View and Data API,你可以 ...
- Kettle api 二次开发之 日志的保存
使用kettle做数据抽取的时候可以使用图形化的工具配置日志保存参数,选择数据库连接,输入日志表名称, 点击sql 执行对应的sql创建日志表即可. 点击保存之后,日志配置会保存在trans或者job ...
- Python调用ansible API系列(四)动态生成hosts文件
方法一:通过最原始的操作文件的方式 #!/usr/bin/env python # -*- coding: utf-8 -*- """ 通过操作文件形式动态生成ansib ...
- Navisworks API 简单二次开发 (自定义工具条)
在Navisworks软件运行的时候界面右侧有个工具条.比较方便.但是在二次开发的时候我不知道在Api那里调用.如果有网友知道请告诉我.谢谢. 我用就自己设置一个工具.界面比较丑!没有美工. 代码: ...
- 用JSON-server模拟REST API(二) 动态数据
用JSON-server模拟REST API(二) 动态数据 上一篇演示了如何安装并运行 json server , 在这里将使用第三方库让模拟的数据更加丰满和实用. 目录: 使用动态数据 为什么选择 ...
- Express4.x API (二):Request (译)
写在前面 最近学习express想要系统的过一遍API,www.expressjs.com是express英文官网(进入www.epxressjs.com.cn发现也是只有前几句话是中文呀~~),所以 ...
随机推荐
- ReadWriteLock锁的应用
对于 Lock 锁来说,如果要实现 "一写多读" 的并发状态(即允许同时读,不允许同时写),需要对 "写操作" 加锁,对 "读操作" 不作要 ...
- PHP使用array_filter查找二维数组中符合字段和字段值的数据集合
1.方法: /** * 获取符合字段和字段值的数组集合 * @param array $data 待过滤数组 * @param string $field 要查找的字段 * @param $value ...
- Scala 面向对象(十):特质(接口) 三
1 在特质中重写抽象方法特例 提出问题,看段代码 trait Operate5 { def insert(id : Int) } trait File5 extends Operate5 { def ...
- 数据可视化基础专题(十五):pyecharts 基础(二)flask 框架整合
Flask 前后端分离 Step 1: 新建一个 Flask 项目 $ mkdir pyecharts-flask-demo $ cd pyecharts-flask-demo $ mkdir tem ...
- Flask 基础组件(十):中间件
from flask import Flask, flash, redirect, render_template, request app = Flask(__name__) app.secret_ ...
- redis(四):Redis 键(key)
Redis 键命令用于管理 redis 的键. 语法 Redis 键命令的基本语法如下: redis 127.0.0.1:6379> COMMAND KEY_NAME 实例 redis 127. ...
- SpringBoot2 整合JTA组件,多数据源事务管理
本文源码:GitHub·点这里 || GitEE·点这里 一.JTA组件简介 1.JTA基本概念 JTA即Java-Transaction-API,JTA允许应用程序执行分布式事务处理,即在两个或多个 ...
- Tips1:考虑用静态工厂方法代替构造器
用静态工厂方法来代替构造器为外界提供对象 描述: 静态工厂方法代替构造器来给外界提供对象,创建对象依然是由构造器来完成的 创建对象和提供对象: 创建对象的方式: 构造器 提供对象来哦方式: 构造器 类 ...
- P4554 小明的游戏 (洛谷) 双端队列BFS
最近没有更新博客,全是因为英语,英语太难了QWQ 洛谷春令营的作业我也不会(我是弱鸡),随机跳了2个题,难度不高,还是讲讲吧,学学新算法也好(可以拿来水博客) 第一题就是这个小明的游戏 小明最近喜欢玩 ...
- C++语法小记---类型检测
类型检测 C++使用typeid关键字进行类型检查 不同的编译器使用typeid返回的类型名称不严格一致,需要特别注意 也可以使用虚函数,返回各自的类型名 如果typeid的操作数不是类类型(类指针也 ...