Ansible源码分析之command模块
command模块是ansible中执行命令的模块,但不限于command,还有raw,shell,win_command等模块,只是使用场景不同。根据command的文档描述:
- C(command)模块采用命令名称,后跟由空格分隔的参数列表。
- 不会通过外壳程序处理命令,因此像C($ HOSTNAME)这样的变量和像C(“ *”),C(“ <”),C(“>”),C(“ | “),C(”;“)和C(”&“)无效。如果需要这些功能,请使用shell模块。
- 对于Windows目标,请改用win_command模块。
command模块的代码并不多,直接分析。
import datetime import glob import os import shlex from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native, to_bytes, to_text from ansible.module_utils.common.collections import is_iterable
从导入的模块来看,都是些常用的。其中 shlex 模块可能用的少一些。shkex 模块最常见的用法就是其中的split 函数,split 函数提供了和shell 处理命令行参数时一致的分隔方式。代码示例:
>>>shlex.split("python -u a.py -a A -b B -o test") ['python', '-u', 'a.py', '-a', 'A', '-b', 'B', '-o', 'test']
在shell 中,对于选项和对应的值之间可以有多个空格,而shlex.split 保持了和sell 一致的处理方式。
由于command模块只有check_command和main函数,我们就先看check_command的实现。
def check_command(module, commandline): arguments = {'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group', 'ln': 'state=link', 'mkdir': 'state=directory', 'rmdir': 'state=absent', 'rm': 'state=absent', 'touch': 'state=touch'} commands = {'curl': 'get_url or uri', 'wget': 'get_url or uri', 'svn': 'subversion', 'service': 'service', 'mount': 'mount', 'rpm': 'yum, dnf or zypper', 'yum': 'yum', 'apt-get': 'apt', 'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'replace, lineinfile or template', 'dnf': 'dnf', 'zypper': 'zypper'} become = ['sudo', 'su', 'pbrun', 'pfexec', 'runas', 'pmrun', 'machinectl'] if isinstance(commandline, list): command = commandline[0] else: command = commandline.split()[0] command = os.path.basename(command) disable_suffix = "If you need to use command because {mod} is insufficient you can add" \ " 'warn: false' to this command task or set 'command_warnings=False' in" \ " ansible.cfg to get rid of this message."
check_command接收两个参数,为模块和命令行。定义了arguments和commands两个字典,都是包含了常见的一些命令。
- arguments出现的命令都是可以用file模块以及对应的参数来代替的。
- commands中出现的命令,是ansible中对应的一个或多个模块来代替的。
- become这个列表都是不同系统中所用于提权的命令。
判断commandline的类型为列表,则取第一个元素作为命令。为字符串用split分割成列表后取第一个。因为传入的命令可能是带路径的,比如/usr/bin/whoami就会转化为whoami命令本身。
然后是提示信息,“如果由于{mod}不足而需要使用命令,可以在此命令任务中添加’warn:false’或在ansible.cfg中设置’command_warnings = False’来摆脱此消息。”
substitutions = {'mod': None, 'cmd': command} if command in arguments: msg = "Consider using the {mod} module with {subcmd} rather than running '{cmd}'. " + disable_suffix substitutions['mod'] = 'file' substitutions['subcmd'] = arguments[command] module.warn(msg.format(**substitutions)) if command in commands: msg = "Consider using the {mod} module rather than running '{cmd}'. " + disable_suffix substitutions['mod'] = commands[command] module.warn(msg.format(**substitutions)) if command in become: module.warn("Consider using 'become', 'become_method', and 'become_user' rather than running %s" % (command,))
这里就是判断命令是否在对应的字典/列表里,分别返回不同的提示信息。值得注意的是格式化字符串的使用。
Consider using the {mod} module rather than running ‘{cmd}
乍一看这个f-string不是在Python3.6及以后才有的语法吗,ansible可是用Python2开发的。其实在2中有字符串格式化format(**dict)的用法,而这里就是这样用的。
再看main函数的代码。
def main(): module = AnsibleModule( argument_spec=dict( _raw_params=dict(), _uses_shell=dict(type='bool', default=False), argv=dict(type='list', elements='str'), chdir=dict(type='path'), executable=dict(), creates=dict(type='path'), removes=dict(type='path'), warn=dict(type='bool', default=False, removed_in_version='2.14', removed_from_collection='ansible.builtin'), stdin=dict(required=False), stdin_add_newline=dict(type='bool', default=True), strip_empty_ends=dict(type='bool', default=True), ), supports_check_mode=True, ) shell = module.params['_uses_shell'] chdir = module.params['chdir'] executable = module.params['executable'] args = module.params['_raw_params'] argv = module.params['argv'] creates = module.params['creates'] removes = module.params['removes'] warn = module.params['warn'] stdin = module.params['stdin'] stdin_add_newline = module.params['stdin_add_newline'] strip = module.params['strip_empty_ends']
同样是实例化ansible模块对象,然后将各个参数的值赋值给变量。并且这里的注释提到:command模块是一个不带key = value args的ansible模块。因此,如果您要构建其他版本,请不要复制此版本!
不过这个写是给开发者看的。
if not shell and executable: module.warn("As of Ansible 2.4, the parameter 'executable' is no longer supported with the 'command' module. Not using '%s'." % executable) executable = None if (not args or args.strip() == '') and not argv: module.fail_json(rc=256, msg="no command given") if args and argv: module.fail_json(rc=256, msg="only command or argv can be given, not both") if not shell and args: args = shlex.split(args) args = args or argv if is_iterable(args, include_strings=False): args = [to_native(arg, errors='surrogate_or_strict', nonstring='simplerepr') for arg in args]
如果没有指定所使用的shell,但指定了executable。会提示command模块已经不支持executable选项了,算是兼容考虑吧。接着判断args或argv是否有值,两者冲突,只能选其一。保证所有参数必须是字符串,用is_iterable判断参数是否可迭代,并排除字符串(因为字符串也是可迭代的),最终将参数转换成列表。
if chdir: try: chdir = to_bytes(os.path.abspath(chdir), errors='surrogate_or_strict') except ValueError as e: module.fail_json(msg='Unable to use supplied chdir: %s' % to_text(e)) try: os.chdir(chdir) except (IOError, OSError) as e: module.fail_json(msg='Unable to change directory before execution: %s' % to_text(e)) if creates: if glob.glob(creates): module.exit_json( cmd=args, stdout="skipped, since %s exists" % creates, changed=False, rc=0 ) if removes: if not glob.glob(removes): module.exit_json( cmd=args, stdout="skipped, since %s does not exist" % removes, changed=False, rc=0 )
这里有三个判断,切换路径,创建/删除文件。其中的注释提到:如果该行包含creates = filename并且文件名已经存在,请不要运行该命令。这使得命令执行具有幂等性。幂等性的意思是无论多次执行,其结果都是一样的。
if warn: check_command(module, args) startd = datetime.datetime.now() if not module.check_mode: rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell, encoding=None, data=stdin, binary_data=(not stdin_add_newline)) elif creates or removes: rc = 0 out = err = b'Command would have run if not in check mode' else: module.exit_json(msg="skipped, running in check mode", skipped=True) endd = datetime.datetime.now() delta = endd - startd if strip: out = out.rstrip(b"\r\n") err = err.rstrip(b"\r\n") result = dict( cmd=args, stdout=out, stderr=err, rc=rc, start=str(startd), end=str(endd), delta=str(delta), changed=True, ) if rc != 0: module.fail_json(msg='non-zero return code', **result) module.exit_json(**result)
当warn为真就去检查命令,同时delta = endd – startd这种方式去记录命令的执行耗时。这里是用的 module.run_command 来执行的命令。而我们追踪run_command 的代码还是用的subprocess实现的。
try: if self._debug: self.log('Executing: ' + self._clean_args(args)) cmd = subprocess.Popen(args, **kwargs) if before_communicate_callback: before_communicate_callback(cmd)
所以这也能说明为什么command模块不会通过外壳程序处理命令的原因了,而是推荐使用shell模块。那么问题来了?既然shell模块可以完全代替command模块,为什么还要用command模块呢?
command模块的代码分析完了,可以说是非常简单的。主要就是对参数的一些处理,然后调用到底层的subprocess来完成命令执行。
赞赏微信赞赏支付宝赞赏
发表评论