Pyinfra/Implementazione deploy VM linea per linea: differenze tra le versioni

Da GazziNet.
Vai alla navigazione Vai alla ricerca
(Aggiunta documentazione tecnica dettagliata del deploy VM)
 
(Aggiunta documentazione tecnica dettagliata del deploy VM)
 
(Una versione intermedia di uno stesso utente non è mostrata)
Riga 29: Riga 29:


=== Lettura riga per riga ===
=== Lettura riga per riga ===
<syntaxhighlight lang="python">
<pre>
#!/usr/bin/env python3
#!/usr/bin/env python3
import os
import os
Riga 36: Riga 36:


from pyinfra.operations import server
from pyinfra.operations import server
</syntaxhighlight>
</pre>


Spiegazione:
Spiegazione:
Riga 43: Riga 43:
* import dell'operation <code>server.shell</code>
* import dell'operation <code>server.shell</code>


<syntaxhighlight lang="python">
<pre>
BASE_DIR = Path(__file__).resolve().parents[1]
BASE_DIR = Path(__file__).resolve().parents[1]
python_bin = os.environ.get("PYTHON_BIN", str(BASE_DIR / "venv" / "bin" / "python"))
python_bin = os.environ.get("PYTHON_BIN", str(BASE_DIR / "venv" / "bin" / "python"))
helper = BASE_DIR / "lib" / "proxmox_ssh.py"
helper = BASE_DIR / "lib" / "proxmox_ssh.py"
cmd = f"{shlex.quote(python_bin)} {shlex.quote(str(helper))}"
cmd = f"{shlex.quote(python_bin)} {shlex.quote(str(helper))}"
</syntaxhighlight>
</pre>


Spiegazione:
Spiegazione:
Riga 56: Riga 56:
* costruisce un comando shell sicuro con quoting
* costruisce un comando shell sicuro con quoting


<syntaxhighlight lang="python">
<pre>
server.shell(
server.shell(
     name=f"Create Proxmox VM {os.environ.get('VM_NAME', '')}",
     name=f"Create Proxmox VM {os.environ.get('VM_NAME', '')}",
     commands=[cmd],
     commands=[cmd],
)
)
</syntaxhighlight>
</pre>


Spiegazione:
Spiegazione:
Riga 173: Riga 173:


Comando usato:
Comando usato:
<syntaxhighlight lang="bash">
<pre>
pyinfra @local deploys/create_vm.py -y
pyinfra @local deploys/create_vm.py -y
</syntaxhighlight>
</pre>


=== Blocco 7: interfaccia HTML ===
=== Blocco 7: interfaccia HTML ===
Riga 202: Riga 202:


Funzioni principali:
Funzioni principali:
* <code>syncNetworkFields()</code>
* <code>syncNetworkFields()</code>: disabilita i campi IP/gateway/CIDR quando la rete e DHCP
  disabilita i campi IP/gateway/CIDR quando la rete e DHCP
* <code>renderJobs()</code>: mostra i job recenti con stato, timing e output
* <code>renderJobs()</code>
* <code>loadVlans()</code>: chiama <code>/api/network/vlans</code>
  mostra i job recenti con stato, timing e output
* <code>loadBridges()</code>: chiama <code>/api/network/bridges</code>
* <code>loadVlans()</code>
* <code>loadFreeVmid()</code>: chiama <code>/api/proxmox/free-vmid?start=100</code>
  chiama <code>/api/network/vlans</code>
* <code>validateNetwork()</code>: chiama <code>/api/network/validate</code> prima del submit
* <code>loadBridges()</code>
* <code>loadJobs()</code>: ricarica i job recenti
  chiama <code>/api/network/bridges</code>
* submit handler della form: costruisce il payload JSON e lo invia a <code>/api/jobs/create-vm</code>
* <code>loadFreeVmid()</code>
  chiama <code>/api/proxmox/free-vmid?start=100</code>
* <code>validateNetwork()</code>
  chiama <code>/api/network/validate</code> prima del submit
* <code>loadJobs()</code>
  ricarica i job recenti
* submit handler della form
  costruisce il payload JSON e lo invia a <code>/api/jobs/create-vm</code>


Traduzione pratica:
Traduzione pratica:
Riga 281: Riga 273:


Funzioni:
Funzioni:
* <code>wait_for_guest_agent()</code>
* <code>wait_for_guest_agent()</code>: esegue poll su <code>qm agent VMID ping</code>
  esegue poll su <code>qm agent VMID ping</code>
* <code>guest_exec()</code>: esegue un comando dentro la VM via guest agent e aspetta l'exit status
* <code>guest_exec()</code>
  esegue un comando dentro la VM via guest agent e aspetta l'exit status


Uso concreto nel progetto:
Uso concreto nel progetto:
Riga 330: Riga 320:


Prima di creare la VM, il file esegue:
Prima di creare la VM, il file esegue:
<syntaxhighlight lang="bash">
<pre>
qm config <VMID>
qm config <VMID>
</syntaxhighlight>
</pre>


Se il comando funziona, il VMID esiste gia e il job viene fermato.
Se il comando funziona, il VMID esiste gia e il job viene fermato.

Versione attuale delle 18:57, 29 mar 2026

Pyinfra/Implementazione deploy VM linea per linea

Questa pagina documenta l'implementazione del deploy automatico di macchine virtuali pubblicata su bot.gazzi.net/pyinfra/.

Obiettivo della pagina:

  • far capire il flusso a una persona umana
  • spiegare il ruolo di ogni file
  • descrivere il codice in ordine logico
  • collegare il comportamento osservabile alle righe principali dell'implementazione

Non vengono riportati segreti runtime.

Vista d'insieme

I file che contano sono tre:

  • app.py: web UI, login LDAP, API, job queue
  • deploys/create_vm.py: entry point pyinfra
  • lib/proxmox_ssh.py: logica concreta di creazione VM su Proxmox

Sequenza completa:

  1. l'utente invia la form
  2. app.py valida il payload e crea il job
  3. il job esegue pyinfra @local deploys/create_vm.py -y
  4. create_vm.py richiama lib/proxmox_ssh.py
  5. proxmox_ssh.py esegue i comandi qm, gestisce cloud-init e cleanup

File 1: deploys/create_vm.py

Questo file e volutamente piccolo. Il suo scopo non e contenere logica complessa, ma fare da ponte tra pyinfra e l'helper specializzato.

Lettura riga per riga

#!/usr/bin/env python3
import os
import shlex
from pathlib import Path

from pyinfra.operations import server

Spiegazione:

  • shebang Python
  • import minimi
  • import dell'operation server.shell
BASE_DIR = Path(__file__).resolve().parents[1]
python_bin = os.environ.get("PYTHON_BIN", str(BASE_DIR / "venv" / "bin" / "python"))
helper = BASE_DIR / "lib" / "proxmox_ssh.py"
cmd = f"{shlex.quote(python_bin)} {shlex.quote(str(helper))}"

Spiegazione:

  • calcola la base del progetto
  • legge il Python del virtualenv da variabile ambiente oppure usa il default
  • individua l'helper vero da eseguire
  • costruisce un comando shell sicuro con quoting
server.shell(
    name=f"Create Proxmox VM {os.environ.get('VM_NAME', '')}",
    commands=[cmd],
)

Spiegazione:

  • pyinfra vede una singola operation
  • il nome dell'operation include il nome VM per leggibilita
  • l'operation esegue localmente il comando helper

Idea pratica:

  • pyinfra orchestra
  • l'helper specializzato realizza la logica

File 2: app.py

Questo file ha quattro responsabilita:

  • leggere la configurazione
  • fare autenticazione LDAP
  • servire UI e API
  • creare e monitorare i job

= Blocco 1: costanti e caricamento environment

Righe utili: 16-64

Che cosa succede:

  • viene definita la base del progetto
  • viene caricato /etc/pyinfra-webui/pyinfra-webui.env
  • viene inizializzata la directory stato
  • vengono lette le variabili principali di app, LDAP e Proxmox
  • Flask viene configurato con secret key e cookie di sessione
  • vengono preparate strutture dati e lock per i job

Perche e importante:

  • tutta la logica runtime dipende da questo blocco
  • separa configurazione e codice
  • permette di non versionare i segreti

= Blocco 2: autenticazione e URL dietro proxy

Righe utili: 67-140

Funzioni:

  • now_iso(): timestamp uniforme dei job
  • require_token(): token opzionale per API
  • forwarded_prefix() e url_with_prefix(): supporto a /pyinfra dietro reverse proxy
  • is_authenticated() e require_login(): controllo sessione
  • validate_username(): sanifica lo username LDAP
  • ldap_authenticate(): bind LDAP con template DN

Traduzione umana:

  • l'app sa stare dietro Nginx senza rompere i path
  • l'utente deve autenticarsi via LDAP
  • eventuali API possono richiedere un token aggiuntivo se configurato

= Blocco 3: lettura dati da Proxmox

Righe utili: 121-189

Qui trovi:

  • proxmox_run(): apre una connessione SSH a Proxmox per eseguire comandi
  • discover_vlans(): parsifica l'output di bridge vlan show
  • discover_bridges(): legge i bridge con ip -o link show type bridge
  • discover_used_vmids(): legge i VMID esistenti con qm list
  • first_free_vmid(): cerca il primo VMID libero da un valore iniziale

Perche serve:

  • la UI non usa valori statici
  • l'utente puo interrogare Proxmox prima del submit

= Blocco 4: validazione input

Righe utili: 192-263

Qui l'app controlla:

  • range di interi
  • formato nome VM
  • formato utente Linux
  • lunghezza minima password
  • modalita rete
  • formato bridge
  • VLAN opzionale
  • IP, gateway e CIDR se la rete e statica

Risultato:

  • l'input viene normalizzato in spec
  • i segreti vengono separati in secrets

Questa separazione e importante:

  • spec puo essere esposto nel job
  • secrets va trattato con piu cautela

= Blocco 5: costruzione environment job

Righe utili: 266-290

build_job_env() converte il dizionario del job in variabili ambiente da passare al runner.

Questo blocco e il ponte tra:

  • input raccolto dalla UI
  • variabili ambiente lette da proxmox_ssh.py

Esempi:

  • spec["vmid"] diventa VM_ID
  • spec["bridge"] diventa VM_BRIDGE
  • la password admin diventa VM_ADMIN_PASSWORD

= Blocco 6: job queue e logging

Righe utili: 293-347

Componenti:

  • append_job(): inserisce il job nelle strutture in memoria
  • write_job_log(): salva il log su file in APP_STATE_DIR
  • run_job(): lancia pyinfra in subprocess
  • create_job(): impedisce job concorrenti e avvia il thread

Punto chiave:

  • active_job_lock fa da semaforo per evitare due creazioni VM contemporanee

Comando usato:

pyinfra @local deploys/create_vm.py -y

Blocco 7: interfaccia HTML

Righe utili: 350-560

La parte HTML/CSS contiene:

  • layout pagina
  • campi del form
  • sezione job recenti

Campi operativi importanti:

  • VMID
  • nome VM
  • utente sudo
  • password sudo
  • CPU, RAM, disco
  • rete, bridge, VLAN
  • gateway, CIDR, IP statico
  • checkbox avvio automatico

Nota:

  • le righe di stile puro servono a presentazione e leggibilita, non alla logica di deploy

Blocco 8: JavaScript della UI

Righe utili: 563-726

Funzioni principali:

  • syncNetworkFields(): disabilita i campi IP/gateway/CIDR quando la rete e DHCP
  • renderJobs(): mostra i job recenti con stato, timing e output
  • loadVlans(): chiama /api/network/vlans
  • loadBridges(): chiama /api/network/bridges
  • loadFreeVmid(): chiama /api/proxmox/free-vmid?start=100
  • validateNetwork(): chiama /api/network/validate prima del submit
  • loadJobs(): ricarica i job recenti
  • submit handler della form: costruisce il payload JSON e lo invia a /api/jobs/create-vm

Traduzione pratica:

  • il browser non parla direttamente con Proxmox
  • parla solo con le API dell'app
  • la UI ferma l'utente prima se bridge/VLAN non sono validi

Blocco 9: login e API backend

Righe utili: 733-938

Route principali:

  • /api/health
  • /login
  • /logout
  • /api/jobs
  • /api/network/vlans
  • /api/network/bridges
  • /api/network/validate
  • /api/proxmox/free-vmid
  • /api/jobs/create-vm

Punto critico:

  • /api/jobs/create-vm fa il passaggio da richiesta web a job reale

Il flusso della route finale e:

  1. richiede login
  2. opzionalmente richiede token
  3. legge il JSON
  4. valida il payload
  5. crea il job
  6. restituisce 202 Accepted

File 3: lib/proxmox_ssh.py

Questo file e il cuore del provisioning.

= Blocco 1: lettura environment

Righe utili: 11-29

Funzioni:

  • env_required()
  • env_int()
  • env_bool()

Servono a:

  • leggere parametri obbligatori
  • convertire interi
  • interpretare flag booleani

= Blocco 2: classe ProxmoxSSH

Righe utili: 32-75

Questa classe:

  • apre la sessione SSH
  • esegue comandi
  • stampa output e errori nel log del job
  • scrive file remoti via SFTP

Perche e utile:

  • centralizza tutte le interazioni con Proxmox

= Blocco 3: attesa guest agent ed esecuzione comandi guest

Righe utili: 77-116

Funzioni:

  • wait_for_guest_agent(): esegue poll su qm agent VMID ping
  • guest_exec(): esegue un comando dentro la VM via guest agent e aspetta l'exit status

Uso concreto nel progetto:

  • attendere che cloud-init abbia davvero finito prima di fare cleanup

= Blocco 4: helper di quoting

Righe utili: 119-124

  • q() usa shlex.quote per shell quoting
  • jq() usa json.dumps per serializzare stringhe nel cloud-config

Questo evita errori banali di quoting e parsing.

= Blocco 5: lettura parametri del job

Righe utili: 127-151

Il file legge dall'environment:

  • VMID, nome, utente, password
  • CPU, RAM, disco
  • rete, bridge, VLAN
  • IP, gateway, CIDR
  • flag avvio automatico
  • storage, immagine cloud-init e default runtime

Qui il job passa da "payload web" a "parametri concreti di provisioning".

= Blocco 6: costruzione rete

Righe utili: 153-164

Se la rete e statica:

  • richiede IP, gateway e CIDR
  • costruisce ipconfig0 nel formato Proxmox

Se la rete e DHCP:

  • usa ip=dhcp

Poi costruisce net0:

  • NIC virtio
  • bridge obbligatorio
  • firewall abilitato
  • eventuale tag VLAN

= Blocco 7: verifica collisione VMID

Righe utili: 166-169

Prima di creare la VM, il file esegue:

qm config <VMID>

Se il comando funziona, il VMID esiste gia e il job viene fermato.

= Blocco 8: generazione snippet cloud-init

Righe utili: 171-191

Questo e uno dei punti piu importanti:

  • crea un file pyinfra-user-<VMID>.yaml
  • lo salva in /var/lib/vz/snippets/
  • definisce l'utente richiesto
  • assegna gruppi adm e sudo
  • imposta shell bash
  • abilita sudo senza password
  • inserisce la password iniziale
  • abilita password auth SSH
  • disabilita root

Perche esiste:

  • il deploy deve creare un utente iniziale specificato dall'operatore

= Blocco 9: lista comandi qm

Righe utili: 193-224

Questo array contiene i passi di creazione macchina:

  • qm create con nome, BIOS, machine type, RAM, CPU, NIC e seriale
  • qm set --efidisk0
  • qm set --scsihw
  • qm set --scsi0 import-from=IMAGE
  • qm set --ide2 CLOUDINIT
  • qm set --boot order=scsi0
  • qm set --ciuser
  • qm set --ipconfig0
  • qm resize

Idea pratica:

  • la VM nasce come cloud image Proxmox correttamente preparata

= Blocco 10: password cloud-init, cicustom e avvio

Righe utili: 226-238

Passi:

  • se c'e una password, esegue qm set --cipassword
  • applica lo snippet custom con qm set --cicustom user=...
  • se esiste un default PROXMOX_CICUSTOM_USER, lo ignora e lo segnala a log
  • se richiesto, avvia la VM con qm start

= Blocco 11: attesa primo boot e cleanup

Righe utili: 240-251

Se la VM parte subito:

  • aspetta il guest agent
  • esegue dentro la VM cloud-init status --wait
  • rimuove il riferimento cicustom
  • cancella lo snippet da /var/lib/vz/snippets/

Se la VM non parte subito:

  • scrive un warning
  • lo snippet non puo essere eliminato immediatamente

Questo blocco esiste per ridurre il tempo in cui la password resta scritta nel file cloud-init sul nodo Proxmox.

= Blocco 12: messaggio finale

Riga utile: 253

Se tutto va bene, il log termina con il messaggio di creazione VM completata.

Esempio di flusso completo

Caso:

  • VMID 120
  • nome demo.gazzi.local
  • utente gazzinet
  • rete DHCP
  • bridge vmbr1

Flusso:

  1. l'utente compila la form
  2. JavaScript invia JSON a /api/jobs/create-vm
  3. parse_payload() valida tutti i campi
  4. create_job() apre il job
  5. run_job() lancia pyinfra
  6. create_vm.py richiama l'helper
  7. proxmox_ssh.py costruisce la VM
  8. se la VM parte subito, il sistema aspetta cloud-init e rimuove lo snippet

Cosa leggere se devi fare modifiche

Se devi cambiare la UI:

  • app.py blocchi HTML e JavaScript

Se devi cambiare validazioni:

  • app.py blocchi parse_payload() e API network

Se devi cambiare il provisioning Proxmox:

  • lib/proxmox_ssh.py

Se devi cambiare il modo in cui pyinfra lancia l'helper:

  • deploys/create_vm.py

Limiti di questa documentazione

Questa pagina e molto piu dettagliata del normale, ma continua a privilegiare:

  • chiarezza
  • leggibilita umana
  • raggruppamento per blocchi logici

Non descrive ogni singola riga di CSS decorativo, perche quella parte non governa il deploy automatico VM. La logica operativa invece e coperta integralmente.

Collegamenti