Work with Multiple Hosts

This guide collects log filenames from several SSH hosts. It then adds a concurrency limit, handles an unavailable host, and reuses open connections. Replace the example hostnames with machines in your SSH configuration. The same scheduling pattern applies to other Transports; choose the appropriate connection factory for each target.

Collect Results with a Concurrency Limit

Use a semaphore to limit how many hosts are connected at once. Here at most two connections are active, including their startup and cleanup:

import asyncio
from rmote.protocol import Protocol
from rmote.tools import FileSystem

HOSTS = ["web1", "web2", "web3"]


async def list_logs(host, limit):
    async with limit:
        async with await Protocol.from_ssh(host) as remote:
            files = await remote(FileSystem.glob, "/var/log", "*.log")
            return sorted(files)


async def inventory():
    limit = asyncio.Semaphore(2)
    results = await asyncio.gather(*[list_logs(host, limit) for host in HOSTS])
    assert len(results) == len(HOSTS)
    for host, files in zip(HOSTS, results):
        assert all(path.startswith("/var/log/") and path.endswith(".log") for path in files)
        print(f"{host}: {len(files)} log files")


asyncio.run(inventory())

gather returns results in the order of its input calls, even when hosts finish in another order. Each connection closes after its operation completes. An empty list means there were no matching files visible to that SSH user.

The semaphore limits active connections, but gather still schedules one task per input host. For a large inventory, feed hosts to a fixed number of workers through an asyncio.Queue instead of scheduling the whole inventory at once.

Keep Results When One Host Fails

By default, gather propagates the first exception to its caller. Set return_exceptions=True to wait for all calls and inspect each result. Append this example to the script above:

async def inventory_with_errors():
    hosts = ["web1", "unreachable.example", "web3"]
    limit = asyncio.Semaphore(2)
    results = await asyncio.gather(
        *[list_logs(host, limit) for host in hosts],
        return_exceptions=True,
    )
    assert len(results) == len(hosts)
    assert isinstance(results[0], list) and isinstance(results[2], list)
    assert isinstance(results[1], Exception)
    for host, result in zip(hosts, results):
        if isinstance(result, BaseException):
            print(f"{host}: failed: {result}")
        else:
            print(f"{host}: {len(result)} log files")


asyncio.run(inventory_with_errors())

unreachable.example intentionally represents an unavailable host. Replace it with a known unreachable target when trying this failure case. The assertions expect the other two hosts to work.

return_exceptions=True changes how errors are reported; it does not make operations succeed or retry them. Cancellation of the enclosing gather is still cancellation, not a normal per-host error result.

Use a Synchronous Client

In a synchronous program, use a thread pool. Each worker owns its connection; max_workers bounds the number of simultaneous connections:

from concurrent.futures import ThreadPoolExecutor
from rmote.sync import Connection
from rmote.tools import FileSystem

HOSTS = ["web1", "web2", "web3"]


def list_logs(host):
    with Connection.from_ssh(host, rpc_timeout=10.0) as remote:
        return host, sorted(remote(FileSystem.glob, "/var/log", "*.log"))


with ThreadPoolExecutor(max_workers=2) as pool:
    results = list(pool.map(list_logs, HOSTS))

assert [host for host, files in results] == HOSTS
for host, files in results:
    assert all(path.startswith("/var/log/") and path.endswith(".log") for path in files)
    print(f"{host}: {len(files)} log files")

Catch expected exceptions inside a worker if you need one result per host after a failure. The connection context still closes when a worker raises.

An open Connection can also serve several caller threads. Keep it open until those callers finish. Each connection owns a background loop thread; there is no shared runtime. See Synchronous Connection for deadlines and cleanup.