Skip to content

Add optional pyinfra integration for idempotent server provisioning#8

Draft
Copilot wants to merge 2 commits intocopilot/add-cloud-resource-provisioningfrom
copilot/add-pyinfra-integration
Draft

Add optional pyinfra integration for idempotent server provisioning#8
Copilot wants to merge 2 commits intocopilot/add-cloud-resource-provisioningfrom
copilot/add-pyinfra-integration

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 25, 2026

Adds idempotent VPS provisioning via pyinfra as an optional layer over existing SSH/rsync operations. Running provision() or deploy() multiple times now only applies necessary changes instead of re-syncing everything.

Changes

  • fastops/infra.py (389 lines)

    • provision() - Full server setup with automatic fallback to SSH when pyinfra unavailable
    • deploy_infra() - Lightweight compose deployment with drift detection
    • install_docker() - Idempotent Docker installation with user group management
    • harden_server() - UFW, fail2ban, and sysctl security configuration
    • setup_caddy() - Caddy reverse proxy with auto-TLS using fastops.proxy.Caddyfile
    • setup_compose_stack() - Idempotent compose deployment with .env support
    • server_status() - Server health check (Docker, Caddy, containers, resources)
  • pyproject.toml

    • Added [project.optional-dependencies] with infra = ["pyinfra>=3.0"]
    • No breaking changes - pyinfra is opt-in via pip install fastops[infra]
  • fastops/__init__.py

    • Exported infra functions at top level

Usage

from fastops import provision, Compose

dc = Compose().svc('web', image='nginx', ports={80: 80})

# Idempotent provisioning - safe to run repeatedly
result = provision(
    host='192.0.2.1',
    docker=True,          # Install Docker if missing
    harden=True,          # Apply security hardening
    compose=dc,           # Deploy stack (only if changed)
    domain='app.com',     # Setup Caddy reverse proxy
    env={'SECRET': 'x'}
)
# Returns: {'method': 'pyinfra', 'status': 'provisioned', ...}

Implementation Notes

  • All pyinfra imports are lazy - module loads without pyinfra installed
  • _require_pyinfra() raises helpful ImportError with installation instructions
  • provision() and deploy_infra() automatically fall back to existing vps.deploy() when pyinfra unavailable
  • server_status() uses SSH only (no pyinfra dependency)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • dummy-host
    • Triggering command: /usr/bin/ssh ssh -o StrictHostKeyChecking=accept-new deploy@dummy-host systemctl is-active docker 2>/dev/null || echo stopped (dns block)
    • Triggering command: /usr/bin/ssh ssh -o StrictHostKeyChecking=accept-new deploy@dummy-host docker ps --format "{{.Names}}: {{.Status}}" 2>/dev/null || echo none (dns block)
    • Triggering command: /usr/bin/ssh ssh -o StrictHostKeyChecking=accept-new deploy@dummy-host systemctl is-active caddy 2>/dev/null || echo stopped (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

Overview

Add optional pyinfra integration for idempotent VPS/bare-metal provisioning. Currently fastops uses raw SSH/rsync (run_ssh, sync, deploy) which works but isn't idempotent — running deploy() twice rsyncs everything again, and there's no drift detection.

pyinfra adds idempotent state management: only changes what needs changing, supports dry-run mode, and handles multi-server deployments natively.

This should be an optional layer — don't force pyinfra as a dependency. It's available via pip install fastops[infra].

Branch off copilot/add-cloud-resource-provisioning.


File 1: fastops/infra.py

Module docstring

"""Idempotent server provisioning via pyinfra. Install: pip install fastops[infra]"""

__all__

['provision', 'deploy_infra', 'harden_server', 'install_docker', 'setup_caddy', 'setup_compose_stack', 'server_status']

Imports

import os, subprocess, shutil
from pathlib import Path

All pyinfra imports should be lazy (inside function bodies) with clear ImportError messages.

Helper: _require_pyinfra()

def _require_pyinfra():
    'Check pyinfra is installed, raise helpful error if not'
    try:
        import pyinfra
        return pyinfra
    except ImportError:
        raise ImportError(
            'pyinfra is required for idempotent provisioning.\n'
            'Install it: pip install fastops[infra]\n'
            'Or: pip install pyinfra>=3.0'
        )

Function: install_docker(user='deploy')

Idempotently install Docker on a remote server. Returns list of pyinfra operation results.

def install_docker(user='deploy'):
    'Idempotently install Docker Engine and add user to docker group'
    _require_pyinfra()
    from pyinfra.operations import apt, server, files
    
    results = []
    
    # Install prerequisites
    results.append(apt.packages(
        name='Install Docker prerequisites',
        packages=['ca-certificates', 'curl', 'gnupg', 'lsb-release'],
        update=True,
    ))
    
    # Add Docker GPG key
    results.append(server.shell(
        name='Add Docker GPG key',
        commands=['curl -fsSL https://get.docker.com | sh'],
    ))
    
    # Add user to docker group
    results.append(server.shell(
        name=f'Add {user} to docker group',
        commands=[f'usermod -aG docker {user}'],
    ))
    
    # Enable Docker service
    from pyinfra.operations import systemd
    results.append(systemd.service(
        name='Enable and start Docker',
        service='docker',
        running=True,
        enabled=True,
    ))
    
    return results

Function: harden_server(ssh_port=22, allowed_ports=None)

Idempotent server hardening — UFW, fail2ban, sysctl tweaks.

def harden_server(ssh_port=22, allowed_ports=None):
    'Idempotent server hardening: UFW, fail2ban, sysctl security tweaks'
    _require_pyinfra()
    from pyinfra.operations import apt, server, files
    
    allowed = allowed_ports or [22, 80, 443]
    results = []
    
    # Install security packages
    results.append(apt.packages(
        name='Install security packages',
        packages=['ufw', 'fail2ban', 'unattended-upgrades'],
        update=True,
    ))
    
    # Configure UFW
    results.append(server.shell(
        name='Configure UFW defaults',
        commands=[
            'ufw default deny incoming',
            'ufw default allow outgoing',
        ],
    ))
    
    for port in allowed:
        results.append(server.shell(
            name=f'Allow port {port}',
            commands=[f'ufw allow {port}'],
        ))
    
    results.append(server.shell(
        name='Enable UFW',
        commands=['echo "y" | ufw enable'],
    ))
    
    # Configure fail2ban
    jail_conf = '''[sshd]
enabled = true
port = {ssh_port}
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
'''.format(ssh_port=ssh_port)
    
    results.append(files.put(
        name='Configure fail2ban jail',
        src=None,  # Use content via StringIO
        dest='/etc/fail2ban/jail.local',
        contents=jail_conf,
    ))
    
    # Sysctl hardening
    sysctl_conf = '''# Hardening
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
kernel.sysrq = 0
'''
    results.append(files.put(
        name='Apply sysctl hardening',
        src=None,
        dest='/etc/sysctl.d/99-hardening.conf',
        contents=sysctl_conf,
    ))
    
    results.append(server.shell(
        name='Reload sysctl',
        commands=['sysctl --system'],
    ))
    
    return results

Function: setup_caddy(domain, app='app', port=5001, email=None)

Idempotently install and configure Caddy as reverse proxy.

def setup_caddy(domain, ap...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/Karthik777/fastops/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: Karthik777 <7102951+Karthik777@users.noreply.github.com>
Copilot AI changed the title [WIP] Add optional pyinfra integration for idempotent provisioning Add optional pyinfra integration for idempotent server provisioning Feb 25, 2026
Copilot AI requested a review from Karthik777 February 25, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants