Создание модуля для ansible на python. Копирование ansible’ом по FTP.

  DevOps

Представим что нам нужно что-то скопировать ансиблом на сервер, а SSH нет. Ситуация абсурдная, но все же. Пошарившишь на гитхабе, не нашёл ничего достаточно простого и в тоже время работающего, поэтому решил сделать все своими силами.
План такой:
— понять как сделать свой модуль
— понять как копировать Питоном по FTP.

Что касается пункта 2 нашего списка, все просто. Есть дефолтная библиотека на этот случай. Её и будем использовать. А вот с модулем ансибла чуть сложнее, но дока тоже есть.

Хочется чтобы данные отправлялись на сервер вот такой таской:

    - name: Upload file
      local_action:
        module: ftp
        host: IP
        user: username
        password: password
        src: file.txt
        dest: file.txt
        command: put
      changed_when: false

Для этого примем параметры как указанно в доке:

    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type='str', required=True),
            port=dict(type='int', default=21),
            user=dict(type='str', required=True),
            password=dict(type='str', required=True),
            src=dict(type='str'),
            dest=dict(type='str'),
            command=dict(type='str', choices=["get", "put"], required=True)
        ),
        supports_check_mode=True
    )

Тут я думаю все ясно. Если в command указанно get, то скачиваем файл, put — загружаем на сервер.

Далее напишем часть, которая будет отвечать за подключение по FTP и загрузку/скачивание файла.

ftp = FTP()
ftp.connect(module.params["host"], module.params["port"])
ftp.login(module.params["user"], module.params["password"])

if module.params["command"] == "put":
    with open(module.params["src"], 'rb') as file:
        out = ftp.storbinary("STOR " + module.params["dest"], file)
elif module.params["command"] == "get":
    with open(module.params["dest"], 'wb') as file:
        out = ftp.retrbinary('RETR ' + module.params["src"], file.write)

if "226" in out:
    result = dict(res=out, changed=True, failed=False)
else:
    result = dict(res=out, changed=False, failed=True)

Итого имеем такой модуль:

from ftplib import FTP
from ansible.module_utils.basic import AnsibleModule


def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(type='str', required=True),
            port=dict(type='int', default=21),
            user=dict(type='str', required=True),
            password=dict(type='str', required=True),
            src=dict(type='str'),
            dest=dict(type='str'),
            command=dict(type='str', choices=["get", "put"], required=True)
        ),
        supports_check_mode=True
    )

    ftp = FTP()
    ftp.connect(module.params["host"], module.params["port"])
    ftp.login(module.params["user"], module.params["password"])

    if module.params["command"] == "put":
        with open(module.params["src"], 'rb') as file:
            out = ftp.storbinary("STOR " + module.params["dest"], file)
    elif module.params["command"] == "get":
        with open(module.params["dest"], 'wb') as file:
            out = ftp.retrbinary('RETR ' + module.params["src"], file.write)

    if "226" in out:
        result = dict(res=out, changed=True, failed=False)
    else:
        result = dict(res=out, changed=False, failed=True)

    module.exit_json(**result)


if __name__ == '__main__':
    main()

Этот файл я назову ftp.py и положу в папку library.

Делаю инвентарь и плейбук
ftp.yml:

---
- hosts: all
  gather_facts: no
  connection: local

  tasks:
    - name: Upload file
      local_action:
        module: ftp
        host: 77.222.61.40
        user: ***_test
        password: ***
        src: file.txt
        dest: file.txt
        command: put
      register: out

    - debug:
        msg: "{{ out }}"

Сразу добавлю дебаг, чтобы видеть ошибки, если будут.
Инвентарь любой, так как хост определяется в самом модуле.
inventory.ini:

localhost

Итого получили:

 tree
.
├── file.txt
├── ftp.yml
├── inventory.ini
└── library
    └── ftp.py

Ну и запускаем.

ansible-playbook -i inventory.ini ftp.yml

PLAY [all] *************************************************************************************************************

TASK [Upload file] *****************************************************************************************************
[WARNING]: Module did not set no_log for password
ok: [localhost]

TASK [debug] ***********************************************************************************************************
ok: [localhost] => {
    "msg": {
        "changed": false,
        "failed": false,
        "res": "226-File successfully transferred\n226 0.061 seconds (measured here), 361.21 bytes per second",
        "warnings": [
            "Module did not set no_log for password"
        ]
    }
}

PLAY RECAP *************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

На мой взгляд все ок. Проверим на сервере, что файл file.txt появился и готово.
Итого: мы узнали как работает библиотека python FTP и написали модуль, который может загрузить файлик на сервер не используя SSH.

На гитхабе это лежит тут: https://github.com/ATPstealer/Scripts/tree/master/ansible_ftp