Scenario
This example is to collect login-able accounts in 4 groups of hosts. In a host-group, hosts are collected one by one. Every host-group has multiple hosts. Host-groups are grouped into 2 top-groups.
The main thread would spawn 2 threads for every top-group. Then the thread of a top-group, spawns threads for every host-groups.
Every host group is run in an individual thread. One more to say is that before starting our data collection , we have to ssh to a bridge host.

file: example.spy
import tabulate
import threading
# define top-groups and host-groups
hostgroups = [
[
['host11','hos12','host13','host14'],
['host21','hos22','host23','host24','joe@host25'],
],[
['host31','hos32','host33','host34'],
['host41','bill@hos42','host43'],
]]
username = 'jenifer'
# connect to bridge-host
$.connect(f'{username}@bridge-host')
$ hostname
print('connected to',$.stdout)
locker = threading.Lock()
def job(accounts,ret,pkey):
global job, locker
account = accounts.pop(0)
$.connect(account,pkey=pkey)
$cat /etc/passwd
regularAccounts = {}
nonShellKeywords = ('false','nologin','halt','shutdown','sync')
for line in $.stdout.split('\n'):
line = line.strip()
if not line: continue
elif line.startswith('#'): continue
cols = line.split(':')
shells = cols[-1].split('/')
nonShell = False
for keyword in nonShellKeywords:
if keyword in shells:
nonShell = True
break
if not nonShell: regularAccounts[cols[0]] = cols[-1]
locker.acquire()
ret[hostname] = regularAccounts
locker.release()
$.close()
if len(accounts):
job(accounts,ret,pkey)
# in a top-group, every host group has its own thread
def topgroupjob(hostgroup,ret,pkey):
groupthreads = []
for hosts in hostgroup:
accounts = [(username + x if x.find('@')==-1 else x ) for x in hosts]
t = $.thread(target=job,args=(accounts,ret,pkey))
groupthreads.append(t)
t.start()
for t in groupthreads:
t.join()
pkey = $.pkey(f'/home/{username}/.ssh/id_rsa')
ret = {}
threads = []
# dispatch top group to every thread
for topgroup in hostgroups:
t = $.thread(target=topgroupjob,args=(topgroup,ret,pkey))
threads.append(t)
t.start()
# wait for threads to complete
for t in threads:
t.join()
# output
hostnames = list(ret.keys())
hostnames.sort()
headers = ('usernmae','shell')
for hostname in hostnames:
usernames = list(ret[hostname].keys())
usernames.sort()
rows = []
for username in usernames:
rows.append((username,ret[hostname][username]))
print(hostname)
print('=' * 40)
print(tabulate.tabulate(rows,headers=headers))
print()
Execution
$sshscript blackip.spy
$sshscript blackip.spy --verbose
$sshscript blackip.spy --debug
$sshscript blackip.spy --verbose --debug
Last Updated: 2026-07-25 16:59:40
Table of contents
- SSHScript v3.1 Documentation
- Accounts (legacy URL)
- Checking the syncing status of two MySQL servers
- Getting Started with SSHScript
- Page not found
- Q: What can it do that Netmiko doesn’t do? (origin)
- SSHScript Documentation
- SSHScript Documents
- SSHScript v2.0 CLI(Draft)
- SSHScript v2.0 Commands Execution and Console
- SSHScript v2.0 Console of sudo and su
- SSHScript v2.0 Getting Started
- SSHScript v2.0 Interactive Commands and Foreground Commands
- SSHScript v2.0 Make Connections
- SSHScript v2.0 Recipes
- SSHScript v2.0 Reference Guide
- SSHScript v2.0 Stdout, Stderr and Exitcode
- SSHScript v2.0 Threading Support
- SSHScript v2.0 Upload and Download Files
- SSHScript v2.0.2 Release Notes
- SSHScript v3.0 Interactive and Foreground Commands
- SSHScript v3.0 Reference Guide
- SSHScript v3.0.0 Release Notes
- SSHScript v3.1
- SSHScript vs. Ansible
- 用 Python 作系統自動化