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:
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):
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¶
- Systemd Services — Create services on remote hosts
- Examples — Complete deployment examples