Skip to content

jamesob/fscm

Repository files navigation

fscm

Configuration management in Python. No YAML. No DSL. Just code.

pip install fscm

Why

Ansible is a mass of YAML and Jinja2 that pretends to be declarative but isn't. fscm is Python. You get loops, functions, conditionals, debugging, type hints, and everything else you already know.

Dependencies are kept minimal because this stuff is security critical.

Quick look

import fscm
from fscm import s, file, run, mkdir, template

# Install packages (auto-detects apt/pacman/brew)
s.pkgs_install("nginx", "certbot")

# Write a config file
file("/etc/nginx/sites-available/myapp", template("nginx.conf.j2", domain="example.com"))

# Run commands
run("ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/")
run("systemctl reload nginx", sudo=True)

# See what changed
for c in fscm.CHANGELIST:
    print(c)

Examples

Lineinfile

from fscm import lineinfile

# Ensure sshd config is secure
lineinfile("/etc/ssh/sshd_config", line="PermitRootLogin no", regexp=r"^#?PermitRootLogin")
lineinfile("/etc/ssh/sshd_config", line="PasswordAuthentication no", regexp=r"^#?PasswordAuthentication")

# Add a host entry
lineinfile("/etc/hosts", line="10.0.0.5 db.internal", regexp=r"db\.internal$")

Systemd services

from fscm.modules import systemd

systemd.simple_service(
    name="myapp",
    exec_start="/opt/myapp/venv/bin/gunicorn -w 4 -b unix:/run/myapp.sock app:app",
    user="www-data",
    working_dir="/opt/myapp",
    env_file="/etc/myapp/env"
)

Remote execution

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

host = Host(
    name="web1",
    connection=SSH(hostname="10.0.0.5", username="deploy"),
    become=Sudo()
)

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

Docker containers

from fscm.modules import docker
from fscm import run

if not docker.is_container_up("redis"):
    run("docker run -d --name redis -p 6379:6379 redis:alpine")

# Or use docker-compose with systemd
from fscm.modules import systemd
systemd.docker_compose_service("mystack", "/opt/mystack/docker-compose.yml")

WireGuard VPN

from fscm.modules.wireguard import Server, Peer, server

srv = Server(address="10.100.0.1/24", listen_port=51820, private_key=srv_key, public_key=srv_pub)
peers = [
    Peer(name="laptop", address="10.100.0.2/32", private_key=p1_key, public_key=p1_pub),
    Peer(name="phone", address="10.100.0.3/32", private_key=p2_key, public_key=p2_pub),
]
server(srv, peers, endpoint="vpn.example.com:51820")

TLS certificates

from fscm.modules import pki

ca_key, ca_cert = pki.create_ca("Internal CA", "My Org")
svc_key, svc_cert = pki.create_cert_for_service(ca_key, ca_cert, ["api.internal", "api"], "My Org")

Dry run

import fscm

fscm.settings.dry_run = True  # nothing actually happens
fscm.s.pkgs_install("nginx")
fscm.file("/etc/nginx/nginx.conf", "...")
print(fscm.CHANGELIST)  # see what would have changed

Putting it together

#!/usr/bin/env python3
"""Deploy a Flask app with nginx and gunicorn."""

import fscm
from fscm import s, file, run, mkdir, template
from fscm.modules import systemd
from fscm.thirdparty.clii import App

cli = App(description="Deploy Flask app")

APP = "myapp"
APP_DIR = f"/opt/{APP}"

@cli.main
def main(dry_run: bool = False):
    """dry_run: Preview changes without applying them"""
    fscm.settings.dry_run = dry_run

    # Packages
    s.pkgs_install("nginx", "python3", "python3-venv")

    # App directory
    mkdir(APP_DIR)
    run(f"python3 -m venv {APP_DIR}/venv")
    run(f"{APP_DIR}/venv/bin/pip install flask gunicorn")

    # Gunicorn service
    systemd.simple_service(
        name=APP,
        exec_start=f"{APP_DIR}/venv/bin/gunicorn -w 4 -b unix:/run/{APP}.sock app:app",
        user="www-data",
        working_dir=APP_DIR
    )

    # Nginx
    file(f"/etc/nginx/sites-available/{APP}", template("nginx.conf.j2", app=APP))
    run(f"ln -sf /etc/nginx/sites-available/{APP} /etc/nginx/sites-enabled/")
    run("nginx -t && systemctl reload nginx", sudo=True)

    print(f"\n{len(fscm.CHANGELIST)} changes made")

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

Goals

  • Python-native configuration management
  • Change tracking and dry-run by default
  • Remote execution via SSH (powered by mitogen)
  • Mainstream Unix only: Debian, Arch, macOS

Non-goals

  • Windows
  • Exotic distros
  • Being a provisioning tool
  • Being an orchestrator

Install

pip install fscm                    # core
pip install "fscm[remote]"          # + SSH execution
pip install "fscm[pki]"             # + certificate generation
pip install "fscm[remote,pki]"      # everything

Docs

https://jameso.be/fscm

License

MIT

About

f_ simple configuration management

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages