Ansible 开发Action插件之【le_copy】

用于构造action 类


from ansible.plugins.action import ActionBase

class ActionModule(ActionBase):
    def run(self, tmp=None, task_vars=None):
    
        result = super(ActionModule, self).run(tmp, task_vars)
        
        return result

类名需为ActionModule,需从ActionBase继承。类中必须要有run方法。

本次实现的action需求


  • 将本地文件拷贝到远程主机

创建action


在playbook目录下新建action_plugins目录,在action_plugins目录下新建le_copy.py文件。

[root@master ansible]# cat action_plugins/le_copy.py
# coding=utf-8

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import json
import os
import stat
import tempfile

from ansible.constants import mk_boolean as boolean
from ansible.errors import AnsibleError, AnsibleFileNotFound
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.plugins.action import ActionBase
from ansible.utils.hashing import checksum


class ActionModule(ActionBase):

    def run(self, tmp=None, task_vars=None):
        ''' handler for file transfer operations '''
        if task_vars is None:
            task_vars = dict()
        # 执行父类的run方法
        result = super(ActionModule, self).run(tmp, task_vars)

        if result.get('skipped'):
            return result

        # 获取参数
        source  = self._task.args.get('src', None)
        dest    = self._task.args.get('dest', None)
        force   = boolean(self._task.args.get('force', 'yes'))
        remote_src = boolean(self._task.args.get('remote_src', False))

        # 判定参数
        result['failed'] = True
        if source is None or dest is None:
            result['msg'] = "src and dest are required"
        elif source is not None and source.endswith("/"):
            result['msg'] = "src must be a file"
        else:
            del result['failed']

        if result.get('failed'):
            return result

        # 如果copy动作在远端执行,直接返回
        if remote_src:
            result.update(self._execute_module(task_vars=task_vars))
            return result

        # 找到source的路径地址
        try:
            source = self._find_needle('files', source)
        except AnsibleError as e:
            result['failed'] = True
            result['msg'] = to_text(e)
            return result

        # 判断是否是目录,如果是跳出返回
        if os.path.isdir(to_bytes(source, errors='surrogate_or_strict')):
            result['failed'] = True
            result['msg'] = to_text(u'不支持目录')
            return result

        changed = False
        module_return = dict(changed=False)

        # 创建临时目录
        if tmp is None or "-tmp-" not in tmp:
            tmp = self._make_tmp_path()

        #  获取本地文件,不存在抛出异常
        try:
            source_full = self._loader.get_real_file(source)
            source_rel = os.path.basename(source)
        except AnsibleFileNotFound as e:
            result['failed'] = True
            result['msg'] = "could not find src=%s, %s" % (source_full, e)
            self._remove_tmp_path(tmp)
            return result


        # 获取远程文件信息
        if self._connection._shell.path_has_trailing_slash(dest):
            dest_file = self._connection._shell.join_path(dest, source_rel)
        else:
            dest_file = self._connection._shell.join_path(dest)

        dest_status = self._execute_remote_stat(dest_file, all_vars=task_vars, follow=False, tmp=tmp, checksum=force)

        # 如果是目录,则返回
        if dest_status['exists'] and dest_status['isdir']:
           self._remove_tmp_path(tmp)
           result['failed'] = True
           result['msg'] = "can not use content with a dir as dest"
           return result

        # 如果存在,但force为false。则返回
        if dest_status['exists'] and not force:
          return result

        # 定义拷贝到远程的文件路径
        tmp_src = self._connection._shell.join_path(tmp, 'source')

        # 传送文件

        remote_path = None
        remote_path = self._transfer_file(source_full, tmp_src)

        # 确保我们的文件具有执行权限
        if remote_path:
            self._fixup_perms2((tmp, remote_path))

        # 运行remote_copy 模块
        new_module_args = self._task.args.copy()
        new_module_args.update(
            dict(
                src=tmp_src,
                dest=dest,
                original_basename=source_rel,
            )
        )

        module_return = self._execute_module(module_name='le_copy',
            module_args=new_module_args, task_vars=task_vars,
            tmp=tmp)

        # 判断运行结果
        if module_return.get('failed'):
            result.update(module_return)
            return result
        if module_return.get('changed'):
            changed = True

        if module_return:
           result.update(module_return)
        else:
           result.update(dict(dest=dest, src=source, changed=changed))

        # 清理临时文件
        self._remove_tmp_path(tmp)
        
        # 返回结果
        return result

action只能在本地执行,所以目标服务器上的操作是要交给modules来做。下面modules是用来把action传送的文件移动到目标位置。

创建module


在playbook目录下新建library目录,在library目录下新建le_copy.py文件。

注:module和action plugin的名称要一致。

[root@master ansible]# cat  library/le_copy.py
#!/usr/bin/python
# coding: utf-8

ANSIBLE_METADATA = {'metadata_version': '1.0',
                    'status': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = '''
---
module: le_copy
version_added: "2.3"
short_description: Copy a C(file) to  remote host
description:
     - The C(le_copy) module copies a file to remote host from a given source to a provided destination.
options:
  src:
    description:
      - Path to a file on the source file to remote host
    required: true
  dest:
    description:
      - Path to the destination on the remote host for the copy
    required: true
  force:
    description:
      - the default is C(yes), which will replace the remote file when contents
        are different than the source. If C(no), the file will only be transferred
        if the destination does not exist.
    required: false
    choices: [ "yes", "no" ]
    default: "yes"
  remote_src:
    description:
      - If False, it will search for src at originating/master machine, if True it will go to the remote/target machine for the src. Default is False.
      - Currently remote_src does not support recursive copying.
    choices: [ "True", "False" ]
    required: false
    default: "False"

author:
    - "Lework"
'''

EXAMPLES = '''
# Example from Ansible Playbooks
- name: copy a config C(file)
  le_copy:
    src: /etc/herp/derp.conf
    dest: /root/herp-derp.conf
'''

RETURN = '''
src:
    description: source file used for the copy
    returned: success
    type: string
    sample: "/path/to/file.name"
dest:
    description: destination of the copy
    returned: success
    type: string
    sample: "/path/to/destination.file"
checksum:
    description: sha1 checksum of the file after running copy
    returned: success
    type: string
    sample: "6e642bb8dd5c2e027bf21dd923337cbb4214f827"
gid:
    description: group id of the file, after execution
    returned: success
    type: int
    sample: 100
group:
    description: group of the file, after execution
    returned: success
    type: string
    sample: "httpd"
owner:
    description: owner of the file, after execution
    returned: success
    type: string
    sample: "httpd"
uid:
    description: owner id of the file, after execution
    returned: success
    type: int
    sample: 100
mode:
    description: permissions of the target, after execution
    returned: success
    type: string
    sample: "0644"
size:
    description: size of the target, after execution
    returned: success
    type: int
    sample: 1220
state:
    description: C(state) of the target, after execution
    returned: success
    type: string
    sample: "file"
'''

import os
import shutil


from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.pycompat24 import get_exception

def main():
    # 定义modules需要的参数
    module = AnsibleModule(
        argument_spec=dict(
            src=dict(required=True, type='path'),
            dest=dict(required=True, type='path'),
            force=dict(default=True, type='bool'),
            original_basename=dict(required=False),
            remote_src=dict(required=False, type='bool')
        ),
        supports_check_mode=True,
    )

    # 获取modules的参数
    src = module.params['src']
    dest = module.params['dest']
    b_src = to_bytes(src, errors='surrogate_or_strict')
    b_dest = to_bytes(dest, errors='surrogate_or_strict')
    force = module.params['force']
    remote_src = module.params['remote_src']
    original_basename = module.params.get('original_basename', None)

    # 判断参数是否合规
    if not os.path.exists(b_src):
        module.fail_json(msg="Source %s not found" % (src))
    if not os.access(b_src, os.R_OK):
        module.fail_json(msg="Source %s not readable" % (src))
    if os.path.isdir(b_src):
        module.fail_json(msg="Remote copy does not support recursive copy of directory: %s" % (src))

    # 获取文件的sha1
    checksum_src = module.sha1(src)
    checksum_dest = None

    changed = False

    # 确定dest文件路径
    if original_basename and dest.endswith(os.sep):
        dest = os.path.join(dest, original_basename)
        b_dest = to_bytes(dest, errors='surrogate_or_strict')

    # 判断目标文件是否存在
    if os.path.exists(b_dest):
        if not force:
            module.exit_json(msg="file already exists", src=src, dest=dest, changed=False)
        if os.access(b_dest, os.R_OK):
            checksum_dest = module.sha1(dest)
    # 目录不存在,退出执行
    elif not os.path.exists(os.path.dirname(b_dest)):
        try:
            os.stat(os.path.dirname(b_dest))
        except OSError:
            e = get_exception()
            if "permission denied" in to_native(e).lower():
                module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest)))
        module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest)))

    # 源文件与目标文件sha1值不一致时覆盖源文件
    if checksum_src != checksum_dest:
      if not module.check_mode:
        try:
            if remote_src:
                shutil.copy(b_src, b_dest)
            else:
                module.atomic_move(b_src, b_dest)
        except IOError:
            module.fail_json(msg="failed to copy: %s to %s" % (src, dest))
        changed = True

    else:
        changed = False
    
    # 返回值
    res_args = dict(
        dest=dest, src=src, checksum=checksum_src, changed=changed
    )

    module.exit_json(**res_args)


if __name__ == '__main__':
    main()

playbook


---
- hosts: node1
  gather_facts: false
  
  tasks:
   - name: copy file to remote
     le_copy: src=/tmp/foo dest=/tmp/test

   - name: unforce copy file to remote
     le_copy: src=/tmp/foo dest=/tmp/test force=false

   - name: copy file on remote
     le_copy: src=/tmp/foo dest=/tmp/123 remote_src=true

执行结果


image.png

github

https://github.com/kuailemy123/Ansible-dev

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容