Skip to content

Remote Execution

fscm can execute configurations on remote hosts via SSH, powered by mitogen for efficient Python context sharing.

Installation

Remote execution requires the optional remote dependencies:

pip install "fscm[remote]"

Basic Usage

from fscm.remote import Host, SSH, Sudo, executor

# Define a remote host
host = Host(
    name="webserver",
    connection=SSH(hostname="10.0.0.5", username="ubuntu"),
    become=Sudo()
)

# Execute fscm code remotely
with executor(host) as exe:
    exe.fscm.s.pkgs_install("nginx")
    exe.fscm.file("/var/www/index.html", "<h1>Hello!</h1>")
    exe.fscm.run("systemctl restart nginx", sudo=True)

Connection Types

SSH

The most common connection type:

from fscm.remote import SSH

# Basic SSH
conn = SSH(hostname="10.0.0.5", username="ubuntu")

# With custom port
conn = SSH(hostname="10.0.0.5", username="ubuntu", port=2222)

# With SSH key
conn = SSH(
    hostname="10.0.0.5",
    username="ubuntu",
    identity_file="/path/to/key.pem"
)

# With jump host (bastion)
conn = SSH(
    hostname="10.0.0.5",
    username="ubuntu",
    ssh_args=["-J", "bastion.example.com"]
)

Local

Execute locally (useful for testing):

from fscm.remote import Local

conn = Local()

Privilege Escalation

Sudo

from fscm.remote import Sudo

# Basic sudo
become = Sudo()

# With password
become = Sudo(password="secret")

# As different user
become = Sudo(username="postgres")

Su

from fscm.remote import Su

# Switch to root
become = Su(password="root_password")

# Switch to specific user
become = Su(username="postgres", password="pg_password")

The Host Object

from fscm.remote import Host, SSH, Sudo

host = Host(
    name="webserver",              # Friendly name
    connection=SSH(                # How to connect
        hostname="10.0.0.5",
        username="ubuntu"
    ),
    become=Sudo(),                 # How to get root
    secrets={"db_pass": "secret"}, # Host-specific secrets
    tags=["web", "production"]     # Optional tags
)

Host Properties

Property Type Description
name str Friendly identifier
connection SSH/Local Connection specification
become Sudo/Su Privilege escalation
secrets dict Host-specific secrets
tags list Categorization tags

The Executor

The executor() context manager sets up the remote connection:

from fscm.remote import executor

with executor(host, dry_run=False) as exe:
    # Access remote fscm
    exe.fscm.file("/etc/config", "content")

    # Access remote system
    exe.fscm.s.pkgs_install("nginx")

    # Run commands
    exe.fscm.run("systemctl restart nginx")

Executor Parameters

Parameter Type Default Description
host Host required Target host
dry_run bool False Simulate operations
sudo_password str None Password for sudo

Working with Multiple Hosts

from fscm.remote import Host, SSH, Sudo, executor

hosts = [
    Host(name="web1", connection=SSH(hostname="10.0.0.1", username="ubuntu"), become=Sudo()),
    Host(name="web2", connection=SSH(hostname="10.0.0.2", username="ubuntu"), become=Sudo()),
    Host(name="web3", connection=SSH(hostname="10.0.0.3", username="ubuntu"), become=Sudo()),
]

def configure_webserver(exe):
    """Configure a single webserver."""
    exe.fscm.s.pkgs_install("nginx")
    exe.fscm.file("/var/www/index.html", "<h1>Hello!</h1>")

# Configure all hosts
for host in hosts:
    print(f"Configuring {host.name}...")
    with executor(host) as exe:
        configure_webserver(exe)

RemoteCliApp

For deployment scripts, use RemoteCliApp:

#!/usr/bin/env python3
from fscm.remote_cli import RemoteCliApp

app = RemoteCliApp(
    description="Deploy my application",
    hosts_path="hosts.yaml",
    secrets_path="secrets/"
)

@app.cmd
def deploy(ctx):
    """Deploy the application."""
    ctx.fscm.s.pkgs_install("nginx", "python3")
    ctx.fscm.file("/var/www/app/config.py", app_config)

@app.cmd
def restart(ctx):
    """Restart services."""
    ctx.fscm.run("systemctl restart myapp", sudo=True)

if __name__ == "__main__":
    app.run()

Usage

# Deploy to all hosts
./deploy.py deploy

# Deploy to specific host
./deploy.py deploy --host webserver

# Dry run
./deploy.py deploy --dry-run

# With sudo password prompt
./deploy.py deploy --sudo-password

Hosts Configuration

Create hosts.yaml:

hosts:
  - name: webserver
    connection:
      type: ssh
      hostname: 10.0.0.5
      username: ubuntu
    become:
      type: sudo

  - name: database
    connection:
      type: ssh
      hostname: 10.0.0.6
      username: ubuntu
    become:
      type: sudo
    tags:
      - database
      - production

Transferring Files

Files are transferred automatically when using templates:

with executor(host) as exe:
    # Template is read locally, rendered, sent to remote
    config = fscm.template("nginx.conf.j2", domain="example.com")
    exe.fscm.file("/etc/nginx/nginx.conf", config)

For explicit file transfer:

from pathlib import Path

with executor(host) as exe:
    # Send local file content
    local_content = Path("local/config.txt").read_text()
    exe.fscm.file("/etc/remote/config.txt", local_content)

Secrets Management

from fscm.remote import Host, SSH, Sudo

# Secrets in host definition
host = Host(
    name="webserver",
    connection=SSH(hostname="10.0.0.5", username="ubuntu"),
    become=Sudo(),
    secrets={
        "db_password": "secret123",
        "api_key": "key456"
    }
)

with executor(host) as exe:
    # Access secrets
    db_pass = host.secrets["db_password"]
    config = f"DATABASE_PASSWORD={db_pass}\n"
    exe.fscm.file("/etc/myapp/config", config, mode="0600")

Using pass

Integrate with the pass password manager:

import fscm

# Load secrets from pass
secrets = fscm.get_secrets("myapp/production")

host = Host(
    name="webserver",
    connection=SSH(hostname="10.0.0.5", username="ubuntu"),
    become=Sudo(),
    secrets=secrets
)

Error Handling

from fscm.remote import executor
from fscm import CmdFailedException

try:
    with executor(host) as exe:
        exe.fscm.run("failing-command")
except CmdFailedException as e:
    print(f"Command failed on {host.name}: {e}")
except ConnectionError as e:
    print(f"Could not connect to {host.name}: {e}")

Example: Multi-Host Deployment

#!/usr/bin/env python3
"""Deploy a web application to multiple servers."""

from fscm.remote import Host, SSH, Sudo, executor
from fscm import template

HOSTS = [
    Host(name="web1", connection=SSH(hostname="10.0.0.1", username="deploy"), become=Sudo()),
    Host(name="web2", connection=SSH(hostname="10.0.0.2", username="deploy"), become=Sudo()),
]

def deploy_to_host(host: Host, version: str):
    """Deploy application to a single host."""
    with executor(host) as exe:
        # Install dependencies
        exe.fscm.s.pkgs_install("python3", "python3-pip", "nginx")

        # Create app directory
        exe.fscm.mkdir("/opt/myapp", mode="0755")

        # Deploy config
        config = template("config.py.j2", version=version, host=host.name)
        exe.fscm.file("/opt/myapp/config.py", config, mode="0644")

        # Deploy nginx config
        nginx = template("nginx.conf.j2", server_name=host.name)
        exe.fscm.file("/etc/nginx/sites-available/myapp", nginx)

        # Restart services
        exe.fscm.run("systemctl restart myapp nginx", sudo=True)

def main():
    version = "1.2.3"
    for host in HOSTS:
        print(f"Deploying to {host.name}...")
        deploy_to_host(host, version)
        print(f"  Done!")

if __name__ == "__main__":
    main()

Next Steps