如何在多台远程计算机上运行和停止带参数的脚本

如何在多台远程计算机上运行和停止带参数的脚本

10年前,有一个问题:在许多服务器上通过 SSH 自动运行命令

我基本上有相同的任务,但我需要使用(可能不同的)参数运行命令/脚本,并且需要停止长时间运行的任务。另外,我更喜欢现代监视器(例如至少 Web UI 或弹性输出),这样我就可以监视哪些脚本正在运行和/或完成以及结果如何。最终最好让他们排队,或者给予一些时间限制。另外,我无法将我的公钥添加到所有计算机,但我可以(让某人)在那里安装一些软件。

这主要用于人工智能训练过程,但也适用于升级框架并最终发送(下载)新的脚本和数据。

在上面的链接中,大家建议使用 Ansible,我认为自动化方式是现代方式,但是还有其他方式吗?

一位朋友还建议了 CI/CD(gitlab 操作),但这似乎有点太多了,而且用于代码测试等其他目的。我还得到了 AutoML 的提示,但这是一个完整的 AI 框架,我不需要它,因为我还需要运行带有各种参数的多个命令/脚本。

答案1

将参数放入字典中。

  • 例如,让我们从默认开始
shell> cat group_vars/all/scripts.yml
scripts:
  default:
    script: /root/bin/default.sh
    params: p1 p2 p3
    timeout: 30
    retries: 10
    delay: 3
    log: /tmp/ansible_script.log

给定控制器上的脚本

shell> tree files
files
├── default.sh
├── script_A.sh
├── script_B.sh
└── script_C.sh
shell> cat files/default.sh 
#!/bin/sh
echo $1 $2 $3
echo finished > /tmp/ansible_script.log
exit 0

下面的剧本

shell> cat pb.yml
- hosts: all

  vars:
    my_script: "{{ scripts[inventory_hostname]|d(scripts['default']) }}"
    _script: "{{ my_script.script|d(scripts.default.script) }}"
    _params: "{{ my_script.params|d(scripts.default.params) }}"
    _timeout: "{{ my_script.timeout|d(scripts.default.timeout) }}"
    _retries: "{{ my_script.retries|d(scripts.default.retries) }}"
    _delay: "{{ my_script.delay|d(scripts.default.delay) }}"
    _log: "{{ my_script.log|d(scripts.default.log) }}"

  tasks:
    - debug:
        msg: |-
          _script: {{ _script }}
          _params: {{ _params }}
          _timeout: {{ _timeout }}
          _retries: {{ _retries }}
          _delay: {{ _delay }}
          _log: {{ _log }}
      when: debug|d(false)|bool

    - name: Copy script
      block:
        - file:
            state: directory
            path: "{{ _script|dirname }}"
            mode: 0750
        - copy:
            src: "{{ _script|basename }}"
            dest: "{{ _script }}"
            mode: 0550
      when: copy_script|d(false)|bool

    - name: Run script
      block:
        - command:
            cmd: "{{ _script }} {{ _params }}"
          async: "{{ _timeout }}"
          poll: 0
          register: cmd_async
        - debug:
            var: cmd_async.ansible_job_id
          when: debug|d(false)|bool

    - name: Read log until finished
      block:
        - command:
            cmd: "cat {{ _log }}"
          register: cmd_log
          until: cmd_log.stdout == 'finished'
          retries: "{{ _retries }}"
          delay:  "{{ _delay }}"
        - debug:
            var: cmd_log.stdout
          when: debug|d(false)|bool
      when: read_log_fin|d(false)|bool

    - name: Check async script
      block:
        - async_status:
            jid: "{{ cmd_async.ansible_job_id }}"
          register: job_result
          until: job_result.finished
          retries: "{{ _retries }}"
          delay: "{{ _delay }}"
        - debug:
            msg: >-
              {{ job_result.start }}
              {{ job_result.end }}
              rc: {{ job_result.rc}}
          when: debug|d(false)|bool

给出

shell> ansible-playbook pb.yml -e debug=true -e copy_script=true -e read_log_fin=true

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

TASK [debug] *********************************************************************************
ok: [test_11] => 
  msg: |-
    _script: /root/bin/default.sh
    _params: p1 p2 p3
    _timeout: 30
    _retries: 10
    _delay: 3
    _log: /tmp/ansible_script.log
ok: [test_12] => 
  msg: |-
    _script: /root/bin/default.sh
    _params: p1 p2 p3
    _timeout: 30
    _retries: 10
    _delay: 3
    _log: /tmp/ansible_script.log
ok: [test_13] => 
  msg: |-
    _script: /root/bin/default.sh
    _params: p1 p2 p3
    _timeout: 30
    _retries: 10
    _delay: 3
    _log: /tmp/ansible_script.log

TASK [file] **********************************************************************************
ok: [test_13]
ok: [test_12]
ok: [test_11]

TASK [copy] **********************************************************************************
ok: [test_12]
ok: [test_11]
ok: [test_13]

TASK [command] *******************************************************************************
changed: [test_12]
changed: [test_11]
changed: [test_13]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  cmd_async.ansible_job_id: '754707567219.90860'
ok: [test_12] => 
  cmd_async.ansible_job_id: '148176661548.90862'
ok: [test_13] => 
  cmd_async.ansible_job_id: '688240445475.90861'

TASK [command] *******************************************************************************
changed: [test_13]
changed: [test_11]
changed: [test_12]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  cmd_log.stdout: finished
ok: [test_12] => 
  cmd_log.stdout: finished
ok: [test_13] => 
  cmd_log.stdout: finished

TASK [async_status] **************************************************************************
changed: [test_12]
changed: [test_13]
changed: [test_11]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  msg: '2022-08-01 16:02:50.287027 2022-08-01 16:02:50.320177 rc: 0'
ok: [test_12] => 
  msg: '2022-08-01 16:02:49.770331 2022-08-01 16:02:49.801347 rc: 0'
ok: [test_13] => 
  msg: '2022-08-01 16:02:50.189800 2022-08-01 16:02:50.343773 rc: 0'

PLAY RECAP ***********************************************************************************
test_11: ok=9    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_12: ok=9    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
test_13: ok=9    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

  • 迭代期间无法显示中间日志。回调插件在迭代后将所有结果一起显示。如果你想观察中间日志,你必须走出 Ansible。例如,获取日志文件
    - name: Fetch log until finished
      fetch:
        dest: /tmp/ansible/
        src: "{{ _log }}"
      until: lookup('file', my_logfile) == 'finished'
      retries: "{{ _retries }}"
      delay:  "{{ _delay }}"
      vars:
        my_logfile: "/tmp/ansible/{{ inventory_hostname}}/tmp/ansible_script.log"
      when: fetch_log_fin|d(false)|bool

这将在控制器上创建定期更新的文件

shell> tree /tmp/ansible/
/tmp/ansible/
├── test_11
│   └── tmp
│       └── ansible_script.log
├── test_12
│   └── tmp
│       └── ansible_script.log
└── test_13
    └── tmp
        └── ansible_script.log

在控制器上显示文件。例如,使用手表

shell> watch cat /tmp/ansible/test_11/tmp/ansible_script.log

为了测试它,下面的脚本以 $2 间隔向日志写入 $1 次

shell> cat files/script_A.sh
#!/bin/sh
for i in $(seq 1 $1); do
    echo step $i  > /tmp/ansible_script.log
    sleep $2
done
echo finished > /tmp/ansible_script.log
exit 0

更新字典让楼主测试_11运行脚本

shell> cat group_vars/all/scripts.yml
scripts:
  default:
    script: /root/bin/default.sh
    params: p1 p2 p3
    timeout: 30
    retries: 10
    delay: 3
    log: /tmp/ansible_script.log
  test_11:
    script: /root/bin/script_A.sh
    params: 7 3

该剧本有删节。 (再次运行 playbook 之前请删除获取的文件。否则,任务将在最后一个文件上跳过。)

shell> ansible-playbook pb.yml -e debug=true -e fetch_log_fin=true
...
TASK [Fetch log until finished] **************************************************************
ok: [test_12]
FAILED - RETRYING: [test_11]: Fetch log until finished (10 retries left).
ok: [test_13]
FAILED - RETRYING: [test_11]: Fetch log until finished (9 retries left).
FAILED - RETRYING: [test_11]: Fetch log until finished (8 retries left).
FAILED - RETRYING: [test_11]: Fetch log until finished (7 retries left).
FAILED - RETRYING: [test_11]: Fetch log until finished (6 retries left).
changed: [test_11]

TASK [async_status] **************************************************************************
changed: [test_13]
changed: [test_12]
changed: [test_11]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  msg: '2022-08-01 18:00:13.304133 2022-08-01 18:00:34.768385 rc: 0'
ok: [test_12] => 
  msg: '2022-08-01 18:00:13.413492 2022-08-01 18:00:13.480142 rc: 0'
ok: [test_13] => 
  msg: '2022-08-01 18:00:13.537767 2022-08-01 18:00:13.731926 rc: 0'

PLAY RECAP ***********************************************************************************
test_11: ok=6    changed=3    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0   
test_12: ok=6    changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0   
test_13: ok=6    changed=2    unreachable=0    failed=0    skipped=4    rescued=0    ignored=0

如果您阅读日志,输出将大致相同

shell> ansible-playbook pb.yml -e debug=true -e read_log_fin=true
...
TASK [Read log until finished] ***************************************************************
FAILED - RETRYING: [test_11]: Read log until finished (10 retries left).
changed: [test_12]
changed: [test_13]
FAILED - RETRYING: [test_11]: Read log until finished (9 retries left).
FAILED - RETRYING: [test_11]: Read log until finished (8 retries left).
FAILED - RETRYING: [test_11]: Read log until finished (7 retries left).
FAILED - RETRYING: [test_11]: Read log until finished (6 retries left).
changed: [test_11]

TASK [debug] *********************************************************************************
ok: [test_11] => 
  cmd_log.stdout: finished
ok: [test_12] => 
  cmd_log.stdout: finished
ok: [test_13] => 
  cmd_log.stdout: finished

完整剧本 pb.yml 的链接

答案2

你的问题似乎有两个部分:

  1. 如何停止/启动/管理在(大)服务器列表上运行的脚本/进程?
  2. 如何跟踪或监视同一服务器列表上长时间运行的脚本/进程的状态?

Ansible 和其他配置管理软件(Puppet、Chef、Saltstack 等)可能是问题#1 的良好候选者,但不提供对它们启动/停止的进程的跟踪/监视,特别是如果您正在设想进度图或警告脚本出错。 (IMO) 监测系统将更好地服务于此类状态监测和报告。

回到问题#1,Ansible 以其在多个服务器上运行命令/脚本的能力而闻名,无论是预先配置的服务器还是您在命令行中当场键入的“临时”命令/脚本。 Saltstack 具有类似的功能,尽管它可能无法像 Ansible 那样处理复杂的临时命令。其他配置管理包在目标服务器上执行命令的直接方法较少,并且可能无法轻松配置来解决您在此处描述的问题。

关于问题#2,选择监控系统需要考虑很多因素,例如是否已经存在用于服务器基础知识(CPU、内存、磁盘)的监控系统,以及如何轻松地扩展它来跟踪脚本/进程。此外,如果这些服务器专用于运行您的脚本/进程,或者也运行其他东西。这是一个足够大的主题,可能值得单独提出一个问题,其中包含您想要监视的内容、您希望如何查看状态以及您可能喜欢什么类型的警报的详细信息。

答案3

傀儡螺栓相对轻松地完成您所描述的内容。我们已将其用于我所描述的复杂工作流程,并在短时间内与 terraform(配置)和 serviceNow(CR 验证)等其他工具绑定在一起。您的用例可能不需要集成 - 但最好了解一下这些需求的变化。

OP 提到不要将公钥放在目标节点上……我不确定我是否完全理解,但从表面上接受这一点,可以使用用户名和密码通过 Bolt 进行身份验证。

CI 工具(就像你的同事建议的那样)将是一种通过网络查看所有内容的优雅方式。

相关内容