> For the complete documentation index, see [llms.txt](https://huataihuang.gitbook.io/cloud-atlas-draft/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://huataihuang.gitbook.io/cloud-atlas-draft/develop/shell/utilities/pssh.md).

# 并行执行ssh命令工具pssh

在维护集群时，常常需要在大量服务器上执行相同的命令，虽然可以自己写循环执行脚本，但是不仅麻烦而且执行效率不高。此时我们通常会使用pssh工具来并发执行SSH指令。

pssh是早期在Python 2上开发的工具，最早开源在google code上，后来代码库搬迁到github [robinbowes/pssh](https://github.com/robinbowes/pssh) 不过已经很久没有更新了(最后更新是2012年)，甚至在 [pssh · PyPI](https://pypi.org/project/pssh) 也没有说明这个软件包在Python 3上运行是存在兼容问题的。不过，Github上 [lilydjwg/pssh](https://github.com/lilydjwg/pssh) 提供了兼容Python 3的修正。

## 安装pssh

### Ubuntu/debain 安装pssh

在Ubuntu上， `pssh` 包安装之后，直接执行 `pssh` 命令会提示无法找到指令。实际上Ubuntu安装 `pssh` 软件包后实际的执行程序是采用了 `parallel-` 开头的命令，例如 `parallel-ssh` 和 `parallel-scp` 等。所以，为了方便使用，可以建立软链接:

```bash
cd /usr/bin
sudo ln -s parallel-ssh pssh
sudo ln -s parallel-scp pscp
sudo ln -s parallel-rsync prsync
sudo ln -s parallel-nuke pnuke
sudo ln -s parallel-slurp pslurp
```

> 以下在Ubuntu上实践都直接使用 `pssh` 和 `pscp` 等命令，如果你在Ubuntu上看不到这个指令（并且已经安装好 `pssh` 软件包)，可以尝试先通过软链接修正。

### arch linux 安装pssh

arch的软件包仓库没有提供pssh，但是可以通过 [pssh AUR package](https://aur.archlinux.org/packages/python-pssh/) 安装:

```
sudo pacman -S base-devel git
git clone https://aur.archlinux.org/python-pssh.git
cd python-pssh
makepkg -sri
```

### Fedora安装pssh

```
sudo dnf install pssh -y
```

### CentOS安装mpssh

CentOS可以使用EPEL安装pssh，但是现在(CentOS 8)只提供MPSSH(Mass Parallel Secure Shell)来并发执行SSH。

```bash
dnf --enablerepo=epel -y install mpssh
```

使用方法和pssh类似，但是没有提供 `-A` 参数，也就是只能使用密钥认证，无法使用密码认证。这个问题我主要通过复用ssh连接方式解决，即在 `~/.ssh/config` 中添加配置:

```bash
Host *
    ServerAliveInterval 60
    StrictHostKeyChecking no
    # 以下3行配置提供了ssh复用，即只需要登陆一次服务器，后续ssh登陆将基于第一次登陆的通道
    ControlMaster auto
    ControlPath ~/.ssh/%h-%p-%r
    ControlPersist yes
```

然后执行一次循环ssh登陆建立连接：

```bash
for i in `cat host`;do sshpass <password> ssh <username>@$i uptime;done
```

之后就可以通过 `mpssh` 并发执行ssh命令。

### OpenSUSE

```
sudo zypper install pssh
```

### 通过pip安装(推荐使用这个通用方法安装)

pssh实际上是一个python程序，所以可以通过 Python pip方式安装。通过pip安装可以用于Python 2环境通用，而且，通过Python virtualenv方式，可以自主在个人用户目录下安装，非常方便。

```bash
# 如果是RHEL/CentOS则使用以下yum安装命令
yum install python-pip
# 如果是Debian/Ubuntu则使用以下apt安装命令
apt install python-pip

# 通过pip安装pssh
pip install pssh
```

我在Python 3的virtualenv中通过pip安装了pssh之后，执行报错

```bash
Traceback (most recent call last):
  File "/Users/huatai/venv3/bin/pssh", line 26, in <module>
    from psshlib.cli import common_parser, common_defaults
  File "/Users/huatai/venv3/lib/python3.7/site-packages/psshlib/cli.py", line 9, in <module>
    import version
ModuleNotFoundError: No module named 'version'
```

参考 [pssh的安装和问题](https://blog.csdn.net/wjzholmes/article/details/102239639) 改为使用 Python 2的virtualenv环境就可以解决。

## 命令说明

| 命令     | 说明                   |
| ------ | -------------------- |
| pssh   | 并行在多个远程主机上执行ssh命令    |
| pscp   | 并行从多个主机上复制文件         |
| prsync | 并行从多个主机使用rsync同步文件   |
| pnuke  | 并行在多个主机上杀死进程         |
| pslurp | 并行在多个主机上复制文件到一个中心主机上 |

## 使用pssh指令

* 首先创建一个hosts文件，名字可以按需设置，例如，我要访问ceph集群，则创建 `ceph-hosts` 配置文件，内容如下

```
172.18.0.11
172.18.0.12
172.18.0.13
172.18.0.14
172.18.0.15
```

如果SSH端口不同，可以在主机ip后面加上端口号，例如 `172.18.0.11:2222` 表示该主机的SSH访问端口是 `2222`

* 常用参数

| 参数 | 说明                   |
| -- | -------------------- |
| -h | 主机名列表文件              |
| -l | 登陆用户名，例如 `-l root`   |
| -A | 提供统一的登陆密码            |
| -i | 交互模式，远程服务器的命令执行结果会输出 |

举例：

```
pssh -ih ceph-hosts -l root -A "uptime"
```

## 忽略服务器密钥

在批量处理主机时，如果需要每个服务器都确认服务器密钥是不现实的，这里就需要使用ssh的一个参数 `-O StrictHostKeyChecking=no` ，这个参数也可以传递给pssh

```
pssh -O StrictHostKeyChecking=no -ih hosts_ip -l huatai -A "uptime"
```

## 忽略错误密码

对于部分主机密码错误，我们希望直接跳过错误密码的节点，可以使用ssh的批处理模式 `BatchMode=yes` ，可以配置在用户的 `~/.ssh/config` 中，这样执行pssh命令可以直接忽略错误密码的节点。

## 终端tty

在pssh执行 `sudo` 命令时候，会出现报错：

```
...
[14] 14:45:00 [FAILURE] 192.168.1.11 Exited with error code 1
Stderr: sudo: no tty present and no askpass program specified
...
```

这个报错在ssh远程执行sudo命令时候也会遇到，原因是远程执行强制的基于screen的程序时，需要使用`-t`参数来分配一个tty，即使ssh没有本地tty。不过，我没有找到如何把这个参数传递给pssh的方法，所以遇到这个问题，我暂时使用循环方式使用ssh命令。举例：

```bash
for i in `cat host`;do ssh -t huatai@$i "echo PASSWORD | sudo -S cp /tmp/my_script.sh /usr/local/bin/my_script.sh";done
```

> 这里远程服务器sudo需要输入密码，采用了 [sudo工具](https://github.com/huataihuang/cloud-atlas-draft/tree/b24eb2b03d19c4625a5316f587138e0dfefd0179/develop/shell/utilities/sudo/README.md) 中通过管道向sudo传输密码的方法，此时 sudo 需要使用参数 `-S` 从 `stdin` 获取密码。

## 并发

pssh和pscp支持并发，但是在macOS上并发执行，例如 `-p 100` 则出现报错

```
    do_pscp(hosts, localargs, remote, opts)
  File "/usr/local/Cellar/pssh/2.3.1/libexec/bin/pscp", line 80, in do_pscp
    statuses = manager.run()
  File "/usr/local/Cellar/pssh/2.3.1/lib/python2.7/site-packages/psshlib/manager.py", line 75, in run
    self.update_tasks(writer)
  File "/usr/local/Cellar/pssh/2.3.1/lib/python2.7/site-packages/psshlib/manager.py", line 133, in update_tasks
    self._start_tasks_once(writer)
  File "/usr/local/Cellar/pssh/2.3.1/lib/python2.7/site-packages/psshlib/manager.py", line 146, in _start_tasks_once
    task.start(self.taskcount, self.iomap, writer, self.askpass_socket)
  File "/usr/local/Cellar/pssh/2.3.1/lib/python2.7/site-packages/psshlib/task.py", line 99, in start
    close_fds=False, preexec_fn=os.setsid, env=environ)
  File "/usr/local/Cellar/python@2/2.7.16/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 394, in __init__
    errread, errwrite)
  File "/usr/local/Cellar/python@2/2.7.16/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 938, in _execute_child
    self.pid = os.fork()
OSError: [Errno 35] Resource temporarily unavailable
```

我修订成 `-p 10` 能够缓解，但是多次运行脚本依然出现问题。参考 [OSError: \[Errno 35\] Resource temporarily unavailable](https://github.com/sprintly/Sprintly-GitHub/issues/37) 这个问题和macOS的默认文件句柄数量限制有关。解决方法是放宽限制:

```
sudo launchctl limit maxfiles 1000000 1000000
```

不过，需要重新登陆一次（或者关闭iterm终端，重新启动一次iterm）。但是，似乎并没有完全解决这个问题，退出iterm2，重新启动后再次执行脚本完成。似乎是资源释放问题。

## 使用ssh密钥登陆

对于使用SSH密钥的登陆方式，需要使用参数 `-x` 来使用扩展ssh参数指定密钥登陆，举例：

```
pssh  -i  -h list_of_hosts  \
-x "-oStrictHostKeyChecking=no  -i /home/xxx/.ssh/ec2.pem" 'uptime'
```

也可以在 `~/.ssh/config` 指定扩展参数，例如

```
Host *.eu-west-1.compute.amazonaws.com
    StrictHostKeyChecking no
    IdentityFile ~/.ssh/ec2.pem
```

> 不过，最好的方法还是采用 keychain 来解决密钥认证，实际上就不需要使用 `-x` 参数来扩展。

### 对于密码保护的密钥

对于密码保护的密钥，建议使用 keychain 来解决密码输入

```
sudo apt-get install keychain
keychain ~/.ssh/id_rsa
. ~/.keychain/$(uname -n)-sh
```

然后执行 pssh 指令就不再需要输入密钥保护密码了。

建议在 `~/.bashrc` 中添加以下内容，则每次终端登陆就只要输入一次密钥保护密码就可以

```
keychain --clear $HOME/.ssh/id_rsa
. $HOME/.keychain/$(uname -n)-sh
```

> 应该也可以使用 [SSH密钥认证：ssh-agent](https://github.com/huataihuang/cloud-atlas-draft/tree/b24eb2b03d19c4625a5316f587138e0dfefd0179/service/ssh/ssh_key/README.md) 来实现一次输入保护密码的密钥认证

## 参考

* [Pssh – Execute Commands on Multiple Remote Linux Servers Using Single Terminal](https://www.tecmint.com/execute-commands-on-multiple-linux-servers-using-pssh/)
* [parallel-ssh](http://manpages.ubuntu.com/manpages/precise/man1/parallel-ssh.1.html)
* [pssh-howto.md](https://gist.github.com/carlessanagustin/c5e70c8edfa8408547545e26b61ab783)
* [parallel-ssh with Passphrase Protected SSH Key](https://unix.stackexchange.com/questions/128974/parallel-ssh-with-passphrase-protected-ssh-key)
* [How to use multiple SSH connections on Linux with Pssh](https://www.addictivetips.com/ubuntu-linux-tips/use-multiple-ssh-connections-with-pssh/)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://huataihuang.gitbook.io/cloud-atlas-draft/develop/shell/utilities/pssh.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
