[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Déploiement de VM SUR un hôte Proxmox VE, piloté à distance par SSH.
|
|
|
|
|
|
|
|
|
|
Différence de nature avec `script/qemu/deploy_qemu.py` : là-bas, l'hyperviseur
|
|
|
|
|
est la machine qui exécute le script. Ici, il est AILLEURS — « on n'exécute pas
|
|
|
|
|
dessus ». Tout ce que ce module produit part donc sur l'hôte choisi, et rien
|
|
|
|
|
n'exige de privilège local.
|
|
|
|
|
|
|
|
|
|
Pourquoi SSH et `qm` plutôt que l'API REST : l'API demande un jeton ou un
|
|
|
|
|
ticket à créer et à renouveler, quand `qm` est la voie que tout administrateur
|
|
|
|
|
Proxmox connaît, et que le dépôt sait déjà gérer des accès SSH (~/.ssh/config,
|
|
|
|
|
ProxyJump, clés). Les commandes restent lisibles dans le journal, donc
|
|
|
|
|
rejouables à la main — c'est ce qui a permis de diagnostiquer chaque panne de
|
|
|
|
|
ce module.
|
|
|
|
|
|
|
|
|
|
Découpage voulu : TOUT ce qui construit une commande ou lit une sortie est une
|
|
|
|
|
fonction PURE, vérifiable sans hôte Proxmox. Seul `run()` parle au réseau.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
import shlex
|
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
# Réglages par défaut d'une VM Proxmox. Chacun a sa raison :
|
|
|
|
|
#
|
|
|
|
|
# - virtio-scsi-single : le contrôleur que Proxmox recommande depuis PVE 7, et
|
|
|
|
|
# le seul qui donne l'iothread par disque.
|
|
|
|
|
# - agent enabled=1 : sans l'agent invité, « qm guest cmd » ne rend aucune
|
|
|
|
|
# adresse IP et le menu ne peut pas dire où joindre la VM.
|
|
|
|
|
# - serial0 socket + vga serial0 : c'est ce qui rend « qm terminal » utilisable.
|
|
|
|
|
# Une console graphique seule obligerait à passer par l'interface web.
|
|
|
|
|
# - ostype l26 : Linux 2.6+, ce qui règle les horloges et les pilotes.
|
|
|
|
|
DEFAULT_BRIDGE = "vmbr0"
|
|
|
|
|
DEFAULT_STORAGE = "" # vide = on choisit d'après « pvesm status »
|
|
|
|
|
IMAGE_DIR = "/var/lib/vz/template/iso"
|
|
|
|
|
VMID_MIN = 100
|
|
|
|
|
|
|
|
|
|
# Stockages qui savent héberger un disque de VM. « pvesm status » liste aussi
|
|
|
|
|
# des stockages de sauvegarde ou d'ISO, où un disque ne peut PAS aller : les
|
|
|
|
|
# proposer produirait un « qm set » refusé après le téléchargement de l'image.
|
|
|
|
|
DISK_CONTENT = ("images", "rootdir")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ssh_argv(host: dict, remote: str, tty: bool = False) -> list:
|
|
|
|
|
"""Commande ssh complète pour exécuter `remote` sur l'hôte Proxmox.
|
|
|
|
|
|
|
|
|
|
`host` : {"target": "root@10.0.0.5", "jump": "rebond", "port": "22"} —
|
|
|
|
|
« target » suffit quand l'alias vient de ~/.ssh/config, qui porte déjà
|
|
|
|
|
l'utilisateur, le port et le ProxyJump.
|
|
|
|
|
"""
|
|
|
|
|
argv = ["ssh"]
|
|
|
|
|
if not tty:
|
|
|
|
|
argv += ["-o", "BatchMode=yes"]
|
|
|
|
|
argv += ["-o", "ConnectTimeout=10"]
|
|
|
|
|
if host.get("port"):
|
|
|
|
|
argv += ["-p", str(host["port"])]
|
|
|
|
|
if host.get("jump"):
|
|
|
|
|
argv += ["-J", host["jump"]]
|
|
|
|
|
if tty:
|
|
|
|
|
argv.append("-t")
|
|
|
|
|
argv += [host["target"], remote]
|
|
|
|
|
return argv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wrap_privilege(remote: str, prefix: str) -> str:
|
|
|
|
|
"""Enveloppe la commande pour qu'elle tourne en root, si nécessaire.
|
|
|
|
|
|
|
|
|
|
« sudo sh -c '<tout>' » et non « sudo <tout> » : les commandes de ce module
|
|
|
|
|
sont des SUITES (« mkdir && if … fi », une boucle for, une redirection).
|
|
|
|
|
Préfixer par sudo n'élèverait que le premier mot, et la redirection
|
|
|
|
|
resterait celle du shell non privilégié — donc « permission denied » sur
|
|
|
|
|
/root ou /boot/efi.
|
|
|
|
|
"""
|
|
|
|
|
if not prefix:
|
|
|
|
|
return remote
|
|
|
|
|
return "sudo sh -c " + shlex.quote(remote)
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
# Ce que ssh écrit de lui-même, et qui n'est pas la réponse de l'hôte. Retiré
|
|
|
|
|
# à la source : un avertissement laissé dans la sortie a été pris pour un nom
|
|
|
|
|
# de pont par `parse_bridges`, et « (ED25519) » s'est retrouvé dans un
|
|
|
|
|
# « qm create » enrobé de « sudo sh -c » — d'où le « sh: 1: Syntax error:
|
|
|
|
|
# "(" unexpected » rapporté. Filtrer chez chaque lecteur aurait laissé le
|
|
|
|
|
# suivant retomber dans le piège.
|
|
|
|
|
_BRUIT_SSH = (
|
|
|
|
|
"Warning: Permanently added",
|
|
|
|
|
"Pseudo-terminal will not be allocated",
|
|
|
|
|
"Connection to ",
|
|
|
|
|
"Shared connection to ",
|
|
|
|
|
"Killed by signal",
|
|
|
|
|
"mesg: ttyname failed",
|
|
|
|
|
"stdin: is not a tty",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def strip_ssh_noise(text: str) -> str:
|
|
|
|
|
"""La sortie de l'hôte, débarrassée de ce que ssh y a ajouté.
|
|
|
|
|
|
|
|
|
|
Ce sont des lignes de ssh lui-même (clé d'hôte enregistrée, pseudo-terminal
|
|
|
|
|
refusé, connexion fermée) : elles n'apprennent rien sur la commande et
|
|
|
|
|
n'ont donc rien à faire dans ce qu'on analyse ou affiche.
|
|
|
|
|
"""
|
|
|
|
|
gardees = [
|
|
|
|
|
ligne
|
|
|
|
|
for ligne in (text or "").splitlines()
|
|
|
|
|
if not ligne.strip().startswith(_BRUIT_SSH)
|
|
|
|
|
]
|
|
|
|
|
return "\n".join(gardees) + ("\n" if gardees else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Les lignes d'AVANCEMENT : « transferred 1.2 GiB of 3.0 GiB (40%) » répété
|
|
|
|
|
# cent fois par « qm set --import-from », les points de wget. Elles ne disent
|
|
|
|
|
# qu'une chose, et la dernière la dit aussi bien.
|
|
|
|
|
_RE_PROGRES = re.compile(
|
|
|
|
|
r"^\s*(transferred\s+[\d.]+|\d+K\s+\.|.*\.{10}.*\d+%)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def collapse_progress(text: str) -> str:
|
|
|
|
|
"""Ne garde que la DERNIÈRE ligne de chaque salve d'avancement.
|
|
|
|
|
|
|
|
|
|
Le journal du premier essai réel faisait 136 lignes, dont cent
|
|
|
|
|
« transferred … » : l'erreur utile se lisait au chausse-pied. Un
|
|
|
|
|
avancement compte pendant qu'il défile, pas dans un fichier qu'on relit.
|
|
|
|
|
"""
|
|
|
|
|
sortie, salve = [], 0
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
if _RE_PROGRES.match(ligne):
|
|
|
|
|
salve += 1
|
|
|
|
|
continue
|
|
|
|
|
if salve:
|
|
|
|
|
sortie.append(f" … {salve} lignes d'avancement …")
|
|
|
|
|
salve = 0
|
|
|
|
|
sortie.append(ligne)
|
|
|
|
|
if salve:
|
|
|
|
|
sortie.append(f" … {salve} lignes d'avancement …")
|
|
|
|
|
return "\n".join(sortie)
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
def run(host: dict, remote: str, timeout: int = 120) -> tuple:
|
|
|
|
|
"""(code, sortie) de `remote` exécuté sur l'hôte. Ne lève jamais.
|
|
|
|
|
|
|
|
|
|
`host["sudo"]` non vide -> la commande passe par sudo : « qm » exige les
|
|
|
|
|
privilèges, et l'accès offert par une VM du parc est celui d'`erplibre`.
|
|
|
|
|
"""
|
|
|
|
|
remote = wrap_privilege(remote, host.get("sudo") or "")
|
|
|
|
|
try:
|
|
|
|
|
res = subprocess.run(
|
|
|
|
|
ssh_argv(host, remote),
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=timeout,
|
|
|
|
|
)
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
return 255, "timeout"
|
|
|
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
|
|
|
return 255, str(exc)
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
return res.returncode, strip_ssh_noise(
|
|
|
|
|
(res.stdout or "") + (res.stderr or "")
|
|
|
|
|
)
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Lecture des sorties de l'hôte — fonctions pures
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
def parse_pveversion(text: str) -> str:
|
|
|
|
|
"""« pve-manager/9.2.11/f6997e69 (running kernel: 7.0.14-12-pve) » -> 9.2.11.
|
|
|
|
|
|
|
|
|
|
Sert de PREUVE que l'hôte est bien un Proxmox : une adresse saisie à la
|
|
|
|
|
main peut être n'importe quoi, et la première commande `qm` échouerait
|
|
|
|
|
alors sur un message qui ne dit pas pourquoi.
|
|
|
|
|
"""
|
|
|
|
|
m = re.search(r"pve-manager/(\d[\w.]*)", text or "")
|
|
|
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
# D'abord le fichier de systemd-resolved, qui porte les serveurs RÉELS :
|
|
|
|
|
# /etc/resolv.conf n'y renvoie qu'un stub sur 127.0.0.53, inutilisable pour un
|
|
|
|
|
# invité. On tente les deux, dans cet ordre.
|
|
|
|
|
RESOLV_CMD = (
|
|
|
|
|
"cat /run/systemd/resolve/resolv.conf 2>/dev/null || cat /etc/resolv.conf"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_nameservers(text: str) -> list:
|
|
|
|
|
"""Résolveurs UTILISABLES PAR UN INVITÉ, tirés d'un resolv.conf.
|
|
|
|
|
|
|
|
|
|
Les adresses de boucle sont écartées : « nameserver 127.0.0.53 » est le
|
|
|
|
|
stub de systemd-resolved, qui n'existe que sur l'hôte. Une VM qui le
|
|
|
|
|
reçoit n'a pas de DNS — mesuré, la VM d'essai ne résolvait rien alors que
|
|
|
|
|
le NAT marchait, et « apt update » aurait échoué sans rien expliquer.
|
|
|
|
|
"""
|
|
|
|
|
serveurs = []
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
parts = ligne.split()
|
|
|
|
|
if len(parts) >= 2 and parts[0] == "nameserver":
|
|
|
|
|
adresse = parts[1].strip()
|
|
|
|
|
if adresse.startswith("127.") or adresse in ("::1", "localhost"):
|
|
|
|
|
continue
|
|
|
|
|
if adresse not in serveurs:
|
|
|
|
|
serveurs.append(adresse)
|
|
|
|
|
return serveurs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_kernel(text: str) -> str:
|
|
|
|
|
"""Noyau ANNONCÉ par pveversion, ou ''.
|
|
|
|
|
|
|
|
|
|
« pve-manager/9.2.11/abc (running kernel: 6.12.95+deb13-cloud-amd64) » ->
|
|
|
|
|
« 6.12.95+deb13-cloud-amd64 ». Ce n'est pas un détail : tant que l'hôte
|
|
|
|
|
tourne le noyau de la distribution, il n'a ni le module bridge ni la table
|
|
|
|
|
NAT, donc pas de pont et pas de VM.
|
|
|
|
|
"""
|
|
|
|
|
trouve = re.search(r"running kernel:\s*([^)\s]+)", text or "")
|
|
|
|
|
return trouve.group(1) if trouve else ""
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : le pont NAT s'écrivait avant de savoir si le NAT existe
« Table does not exist » : six lignes d'iptables et « code de retour 1 »,
après avoir déjà posé la strophe dans /etc/network/interfaces. Rien dans ce
bruit ne dit qu'il faut redémarrer.
L'hôte tournait le noyau cloud de Debian, qui est dépouillé de tout netfilter
— aucun module NAT, ni legacy ni nft. Et le cas n'a rien d'exotique : c'est
notre propre install_proxmox.sh qui le produit. Il pose le noyau Proxmox sans
redémarrer, à raison — lancé par ssh, un reboot couperait la session et ferait
passer l'installation pour un échec. Une Proxmox imbriquée fraîchement
installée est donc TOUJOURS dans cet état.
L'avertissement sur le noyau existait déjà, mais à la CONFIRMATION de l'hôte,
et l'hôte est ensuite mémorisé : on revient des jours plus tard créer un pont,
et plus personne ne rappelle rien. Le garde va donc là où la conséquence
tombe, et AVANT toute écriture. Il interroge la table NAT elle-même et non le
NOM du noyau — « -pve » est un indice, pas une preuve — puis nomme le noyau
en cours, celui qui est posé, et la commande qui règle l'affaire.
Le sommaire de déploiement le dit désormais aussi, tant qu'on lit encore
l'écran plutôt qu'au bout d'un journal d'une heure.
--- EN ---
"Table does not exist": six lines of iptables and "exit code 1", after the
stanza had already been written into /etc/network/interfaces. Nothing in that
noise says a reboot is needed.
The host was running Debian's cloud kernel, stripped of all netfilter — no NAT
module, legacy or nft. And the case is not exotic: our own install_proxmox.sh
produces it. It installs the Proxmox kernel without rebooting, rightly — run
over ssh, a reboot would cut the session and make the install look failed. A
freshly installed nested Proxmox is therefore ALWAYS in this state.
The kernel warning already existed, but at host CONFIRMATION, and the host is
then remembered: you come back days later to create a bridge and nothing
reminds you. So the guard moves to where the consequence lands, and BEFORE any
write. It asks the NAT table itself rather than the kernel's NAME — "-pve" is
a hint, not a proof — then names the running kernel, the installed one, and
the command that settles it.
The deployment summary now says it too, while the screen is still being read
rather than at the end of an hour-long log.
Assisted-by: Claude Opus 5
2026-08-25 02:16:54 -04:00
|
|
|
# Ce qu'il faut savoir AVANT d'écrire un pont NAT, en un aller-retour.
|
|
|
|
|
#
|
|
|
|
|
# Le noyau seul ne suffit pas à juger : « -pve » dans son nom est un indice,
|
|
|
|
|
# pas une preuve, et l'inverse non plus — c'est la table NAT elle-même qu'on
|
|
|
|
|
# interroge. « iptables -t nat -S » échoue avec « Table does not exist » quand
|
|
|
|
|
# aucun module netfilter n'est chargeable, et réussit sinon.
|
|
|
|
|
NAT_CHECK_CMD = (
|
|
|
|
|
"uname -r; echo '---ERPLIBRE-NAT---'; "
|
|
|
|
|
"iptables -t nat -S >/dev/null 2>&1 && echo NAT-OK || echo NAT-KO; "
|
|
|
|
|
"echo '---ERPLIBRE-PVE-KERNEL---'; "
|
|
|
|
|
"ls -1 /lib/modules 2>/dev/null | grep -- -pve | sort -V | tail -1"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_nat_check(text: str) -> dict:
|
|
|
|
|
"""{"kernel": …, "nat": bool, "pve_kernel": …} depuis NAT_CHECK_CMD.
|
|
|
|
|
|
|
|
|
|
`nat` à False sans `pve_kernel` veut dire que l'installation Proxmox n'est
|
|
|
|
|
pas allée au bout ; avec, qu'elle attend un redémarrage."""
|
|
|
|
|
brut = strip_ssh_noise(text or "")
|
|
|
|
|
parts = brut.split("---ERPLIBRE-NAT---")
|
|
|
|
|
kernel = parts[0].strip().splitlines()
|
|
|
|
|
reste = parts[1] if len(parts) > 1 else ""
|
|
|
|
|
suite = reste.split("---ERPLIBRE-PVE-KERNEL---")
|
|
|
|
|
pve_kernel = suite[1].strip().splitlines() if len(suite) > 1 else []
|
|
|
|
|
return {
|
|
|
|
|
"kernel": kernel[-1].strip() if kernel else "",
|
|
|
|
|
"nat": "NAT-OK" in (suite[0] if suite else ""),
|
|
|
|
|
"pve_kernel": pve_kernel[-1].strip() if pve_kernel else "",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
def parse_qm_list(text: str) -> list:
|
|
|
|
|
"""Sortie de « qm list » -> [{vmid, name, status, mem, disk}].
|
|
|
|
|
|
|
|
|
|
L'en-tête et les lignes vides sont écartés. Les colonnes sont séparées par
|
|
|
|
|
des espaces, mais un NOM peut en contenir : on découpe donc par la
|
|
|
|
|
GAUCHE (vmid) et par la DROITE (status, mem, bootdisk, pid), et ce qui
|
|
|
|
|
reste au milieu est le nom.
|
|
|
|
|
"""
|
|
|
|
|
out = []
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
parts = ligne.split()
|
|
|
|
|
if len(parts) < 6 or not parts[0].isdigit():
|
|
|
|
|
continue
|
|
|
|
|
vmid = parts[0]
|
|
|
|
|
pid = parts[-1]
|
|
|
|
|
bootdisk = parts[-2]
|
|
|
|
|
mem = parts[-3]
|
|
|
|
|
status = parts[-4]
|
|
|
|
|
nom = " ".join(parts[1:-4])
|
|
|
|
|
out.append(
|
|
|
|
|
{
|
|
|
|
|
"vmid": int(vmid),
|
|
|
|
|
"name": nom,
|
|
|
|
|
"status": status,
|
|
|
|
|
"mem": mem,
|
|
|
|
|
"disk": bootdisk,
|
|
|
|
|
"pid": pid,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_storages(text: str) -> list:
|
|
|
|
|
"""Sortie de « pvesm status --content images » -> [{name, type, avail}]."""
|
|
|
|
|
out = []
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
parts = ligne.split()
|
|
|
|
|
if len(parts) < 6 or parts[0] == "Name":
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
avail = int(parts[5])
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
out.append(
|
|
|
|
|
{
|
|
|
|
|
"name": parts[0],
|
|
|
|
|
"type": parts[1],
|
|
|
|
|
"actif": parts[2] == "active",
|
|
|
|
|
"avail": avail * 1024, # pvesm compte en Kio
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
# « 2: vmbr0: <BROADCAST,MULTICAST,UP> mtu 1500 … » — l'index, le nom, les
|
|
|
|
|
# drapeaux. Exiger cette forme, et pas « quelque chose avant deux-points » :
|
|
|
|
|
# n'importe quelle ligne de bruit devenait sinon un nom de pont.
|
|
|
|
|
_RE_LIEN = re.compile(r"^\s*\d+:\s*([A-Za-z0-9][A-Za-z0-9._@-]*):\s*<")
|
|
|
|
|
|
|
|
|
|
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
def parse_bridges(text: str) -> list:
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
"""Sortie de « ip -o link show type bridge » -> ['vmbr0', …].
|
|
|
|
|
|
|
|
|
|
Rien d'autre ne passe : un avertissement de ssh a déjà été pris pour un
|
|
|
|
|
pont, et son « (ED25519) » a fait échouer le « qm create » qui suivait sur
|
|
|
|
|
une erreur de syntaxe shell incompréhensible.
|
|
|
|
|
"""
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
ponts = []
|
|
|
|
|
for ligne in (text or "").splitlines():
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
trouve = _RE_LIEN.match(ligne)
|
|
|
|
|
if trouve:
|
|
|
|
|
ponts.append(trouve.group(1).split("@")[0])
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
return ponts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_guest_ips(text: str) -> list:
|
|
|
|
|
"""Adresses IPv4 rendues par « qm guest cmd <id> network-get-interfaces ».
|
|
|
|
|
|
|
|
|
|
L'agent invité répond du JSON. Les adresses de bouclage sont écartées : la
|
|
|
|
|
question posée est « où joindre cette VM », et 127.0.0.1 n'y répond pas.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
data = json.loads(text or "")
|
|
|
|
|
except (ValueError, TypeError):
|
|
|
|
|
return []
|
|
|
|
|
ips = []
|
|
|
|
|
for iface in data if isinstance(data, list) else []:
|
|
|
|
|
for addr in iface.get("ip-addresses") or []:
|
|
|
|
|
ip = addr.get("ip-address") or ""
|
|
|
|
|
if addr.get("ip-address-type") == "ipv4" and not ip.startswith(
|
|
|
|
|
"127."
|
|
|
|
|
):
|
|
|
|
|
ips.append(ip)
|
|
|
|
|
return ips
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mac_from_config(text: str) -> str:
|
|
|
|
|
"""MAC de net0 dans « qm config <id> ».
|
|
|
|
|
|
|
|
|
|
C'est le seul lien entre une VM Proxmox et son adresse IP quand l'agent
|
|
|
|
|
invité n'est pas là : l'image cloud Debian ne l'embarque PAS, et Proxmox ne
|
|
|
|
|
distribue pas les baux lui-même — il ne peut donc pas répondre.
|
|
|
|
|
"""
|
|
|
|
|
m = re.search(
|
|
|
|
|
r"^net0:.*?([0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5})",
|
|
|
|
|
text or "",
|
|
|
|
|
re.M,
|
|
|
|
|
)
|
|
|
|
|
return m.group(1).lower() if m else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ip_from_neigh(text: str, mac: str) -> str:
|
|
|
|
|
"""Adresse vue par le voisinage de l'hôte (« ip neigh »), pour cette MAC.
|
|
|
|
|
|
|
|
|
|
Marche dès que la VM a émis un paquet — un bail DHCP suffit. C'est le
|
|
|
|
|
repli quand l'agent invité manque, et il ne demande rien à l'invité.
|
|
|
|
|
"""
|
|
|
|
|
if not mac:
|
|
|
|
|
return ""
|
|
|
|
|
cible = mac.lower()
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
if cible in ligne.lower():
|
|
|
|
|
parts = ligne.split()
|
|
|
|
|
if parts and re.match(r"^\d+\.\d+\.\d+\.\d+$", parts[0]):
|
|
|
|
|
return parts[0]
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_vmid(existing, mini: int = VMID_MIN) -> int:
|
|
|
|
|
"""Premier VMID libre à partir de `mini`.
|
|
|
|
|
|
|
|
|
|
Proxmox refuse un VMID déjà pris, et le message (« CT/VM 100 already
|
|
|
|
|
exists ») arrive APRÈS le téléchargement de l'image : on choisit donc
|
|
|
|
|
avant, d'après ce que l'hôte déclare.
|
|
|
|
|
"""
|
|
|
|
|
pris = {int(v["vmid"]) for v in existing or () if str(v["vmid"]).isdigit()}
|
|
|
|
|
vmid = max(mini, VMID_MIN)
|
|
|
|
|
while vmid in pris:
|
|
|
|
|
vmid += 1
|
|
|
|
|
return vmid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pick_storage(storages, voulu: str = "") -> str:
|
|
|
|
|
"""Stockage où poser le disque : celui demandé, sinon le plus libre.
|
|
|
|
|
|
|
|
|
|
Aucun repli sur un nom devinné (« local-lvm » n'existe pas partout) : sans
|
|
|
|
|
stockage utilisable, on rend une chaîne vide et l'appelant le dit.
|
|
|
|
|
"""
|
|
|
|
|
utiles = [s for s in storages or () if s.get("actif")]
|
|
|
|
|
if voulu:
|
|
|
|
|
return voulu if any(s["name"] == voulu for s in utiles) else ""
|
|
|
|
|
if not utiles:
|
|
|
|
|
return ""
|
|
|
|
|
return max(utiles, key=lambda s: s.get("avail") or 0)["name"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pick_bridge(bridges, voulu: str = "") -> str:
|
|
|
|
|
"""Pont réseau : celui demandé, sinon vmbr0, sinon le premier déclaré."""
|
|
|
|
|
ponts = list(bridges or ())
|
|
|
|
|
if voulu:
|
|
|
|
|
return voulu if voulu in ponts else ""
|
|
|
|
|
if DEFAULT_BRIDGE in ponts:
|
|
|
|
|
return DEFAULT_BRIDGE
|
|
|
|
|
return ponts[0] if ponts else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Construction des commandes — fonctions pures
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Réseau interne proposé quand l'hôte n'a AUCUN pont. Choisi pour être sûr :
|
|
|
|
|
# un pont sans port physique ne peut pas couper l'accès SSH à l'hôte, alors
|
|
|
|
|
# qu'ajouter « bridge-ports enp1s0 » déplace l'adresse et coupe la session en
|
|
|
|
|
# cours — sur une machine distante, c'est un aller sans retour.
|
|
|
|
|
INTERNAL_BRIDGE = "vmbr0"
|
|
|
|
|
INTERNAL_CIDR = "10.10.10.1/24"
|
|
|
|
|
|
[FIX] proxmox : le pont interne prenait l'adresse de sa propre passerelle
Un Proxmox dans un Proxmox hérite du réseau interne de son parent : la VM
vivait en 10.10.10.152, passerelle 10.10.10.1. Le pont interne, lui, avait son
adresse CODÉE EN DUR à 10.10.10.1/24. Lui demander de la poser sur son propre
pont, c'est prendre l'adresse de sa passerelle et rendre tout le /24 local. La
machine s'isole au milieu de la commande qui la configure : « ifup » n'a jamais
rendu la main, la VM ne répondait plus ni en ssh ni en ping.
Le réseau est donc CHOISI, d'après ce que l'hôte connaît déjà — ses adresses et
ses routes, car une route sans adresse locale suffit à créer le conflit, et la
route par défaut en est l'exemple exact. Le chevauchement se calcule sur les
réseaux et non sur les trois premiers octets : « 10.0.0.0/8 » écarte alors bien
tous les candidats en 10.x. Plus aucun libre ? On le dit, plutôt que d'en
écraser un — écraser, ici, c'est couper la seule voie d'accès.
Le repli « ifreload -a » s'en va aussi. Il rechargeait TOUTES les interfaces, y
compris celle qui porte la session, et sur une image cloud l'interface
principale est décrite ailleurs — ifupdown2 la descend sans la remonter. Le
repli monte maintenant le pont à la main, sans toucher à rien d'autre ; la
strophe le rend persistant. La règle de masquerading se teste avant de
s'ajouter, donc une reprise n'empile rien.
Le test d'origine interdisait « 2>/dev/null » sur toute la ligne pour que
l'erreur d'ifup reste lisible. L'intention est gardée, portée sur l'appel à
ifup seul : le repli, lui, sonde légitimement.
--- EN ---
Proxmox inside Proxmox inherits its parent's internal network: the VM lived at
10.10.10.152, gateway 10.10.10.1. The internal bridge had its address HARDCODED
to 10.10.10.1/24. Asking it to put that on its own bridge takes its gateway's
address and makes the whole /24 local. The machine isolates itself in the
middle of the command configuring it: "ifup" never returned, the VM answered
neither ssh nor ping.
The subnet is now CHOSEN from what the host already knows — its addresses and
its routes, since a route with no local address is enough to collide, and the
default route is exactly that case. Overlap is computed on networks rather than
on the first three octets, so "10.0.0.0/8" correctly rules out every 10.x
candidate. None left? We say so rather than overwrite one — overwriting here
means cutting the only way in.
The "ifreload -a" fallback goes too. It reloaded ALL interfaces, including the
one carrying the session, and on a cloud image the main interface is described
elsewhere — ifupdown2 takes it down without bringing it back. The fallback now
raises the bridge by hand, touching nothing else; the stanza makes it
persistent. The masquerade rule is checked before being added, so a retry piles
nothing up.
The original test banned "2>/dev/null" across the whole line so ifup's error
stayed readable. That intent is kept, narrowed to the ifup call itself: the
fallback legitimately probes.
Assisted-by: Claude Opus 5
(cherry picked from commit 57991b186cf891b0db6b7228fb626c4c1af317cd)
2026-08-25 03:46:57 -04:00
|
|
|
# Le réseau interne ne peut PAS être une constante : un Proxmox dans un
|
|
|
|
|
# Proxmox hérite du réseau interne de son parent, et 10.10.10.1 y est
|
|
|
|
|
# l'adresse de sa propre PASSERELLE. La poser sur son pont rend tout le /24
|
|
|
|
|
# local — la passerelle devient injoignable et la machine s'isole
|
|
|
|
|
# instantanément, au milieu de la commande qui la configure. Vécu : « ifup »
|
|
|
|
|
# n'a jamais rendu la main et la VM ne répondait plus, ni en ssh ni en ping.
|
|
|
|
|
#
|
|
|
|
|
# On choisit donc un /24 que l'hôte ne connaît pas encore. La liste va du plus
|
|
|
|
|
# attendu au plus improbable : un parc imbriqué descend d'un cran par étage.
|
|
|
|
|
INTERNAL_CANDIDATES = (
|
|
|
|
|
"10.10.10.1/24",
|
|
|
|
|
"10.10.20.1/24",
|
|
|
|
|
"10.10.30.1/24",
|
|
|
|
|
"10.10.40.1/24",
|
|
|
|
|
"10.20.10.1/24",
|
|
|
|
|
"10.30.10.1/24",
|
|
|
|
|
"172.31.10.1/24",
|
|
|
|
|
"192.168.210.1/24",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Tout ce que l'hôte sait déjà d'IPv4 : ses adresses ET ses routes. Les deux,
|
|
|
|
|
# parce qu'une route sans adresse locale suffit à créer le conflit — la route
|
|
|
|
|
# par défaut « via 10.10.10.1 » en est l'exemple exact.
|
|
|
|
|
USED_NETS_CMD = "ip -o -4 addr show; ip -4 route show"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_used_nets(text: str) -> set:
|
|
|
|
|
"""Réseaux IPv4 lus dans la sortie de USED_NETS_CMD.
|
|
|
|
|
|
|
|
|
|
Une adresse nue compte pour un /32 : c'est honnête, et le
|
|
|
|
|
chevauchement avec un /24 candidat se calcule pareil. Un préfixe plus
|
|
|
|
|
large qu'un /24 — « 10.0.0.0/8 » — écarte donc bien tous nos candidats
|
|
|
|
|
en 10.x, ce qu'un test sur les trois premiers octets aurait raté."""
|
|
|
|
|
import ipaddress
|
|
|
|
|
|
|
|
|
|
nets = set()
|
|
|
|
|
motif = r"\b(\d{1,3}(?:\.\d{1,3}){3})(?:/(\d{1,2}))?\b"
|
|
|
|
|
for adresse, prefixe in re.findall(motif, text or ""):
|
|
|
|
|
try:
|
|
|
|
|
nets.add(
|
|
|
|
|
ipaddress.ip_network(
|
|
|
|
|
f"{adresse}/{prefixe or 32}", strict=False
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
return nets
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pick_internal_cidr(text: str, candidats=INTERNAL_CANDIDATES) -> str:
|
|
|
|
|
"""Le premier candidat qui ne chevauche RIEN de ce que l'hôte connaît.
|
|
|
|
|
|
|
|
|
|
Chaîne vide quand tous sont pris : le dire, plutôt que d'en écraser un.
|
|
|
|
|
Écraser, ici, c'est couper la seule voie d'accès à la machine."""
|
|
|
|
|
import ipaddress
|
|
|
|
|
|
|
|
|
|
utilises = parse_used_nets(text)
|
|
|
|
|
for candidat in candidats:
|
|
|
|
|
reseau = ipaddress.ip_network(candidat, strict=False)
|
|
|
|
|
if not any(reseau.overlaps(u) for u in utilises):
|
|
|
|
|
return candidat
|
|
|
|
|
return ""
|
|
|
|
|
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
|
|
|
|
|
def parse_bridge_config(text: str) -> dict:
|
|
|
|
|
"""/etc/network/interfaces -> {pont: {ports, address}}.
|
|
|
|
|
|
|
|
|
|
Sert à savoir si un pont donne sur le LAN (il a des ports) ou s'il est
|
|
|
|
|
interne (« bridge-ports none ») : les VM du premier prennent leur adresse
|
|
|
|
|
en DHCP, celles du second n'en auraient aucune et doivent recevoir une
|
|
|
|
|
adresse fixe.
|
|
|
|
|
"""
|
|
|
|
|
ponts = {}
|
|
|
|
|
courant = ""
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
nu = ligne.strip()
|
|
|
|
|
m = re.match(r"^iface\s+(\S+)\s", nu)
|
|
|
|
|
if m:
|
|
|
|
|
courant = m.group(1)
|
|
|
|
|
continue
|
|
|
|
|
if not courant:
|
|
|
|
|
continue
|
|
|
|
|
if nu.startswith("bridge-ports") or nu.startswith("bridge_ports"):
|
|
|
|
|
ports = nu.split(None, 1)[1].strip() if " " in nu else ""
|
|
|
|
|
ponts.setdefault(courant, {})["ports"] = (
|
|
|
|
|
"" if ports in ("none", "") else ports
|
|
|
|
|
)
|
|
|
|
|
elif nu.startswith("address"):
|
|
|
|
|
ponts.setdefault(courant, {})["address"] = nu.split()[1]
|
|
|
|
|
return ponts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bridge_setup_cmds(
|
|
|
|
|
nom: str = INTERNAL_BRIDGE,
|
|
|
|
|
cidr: str = INTERNAL_CIDR,
|
|
|
|
|
uplink: str = "",
|
|
|
|
|
) -> list:
|
|
|
|
|
"""Crée un pont INTERNE, et le masque derrière l'uplink si demandé.
|
|
|
|
|
|
|
|
|
|
« bridge-ports none » : aucune interface physique n'est touchée, donc
|
|
|
|
|
l'accès à l'hôte survit. Les lignes post-up/post-down de masquerading sont
|
|
|
|
|
celles que documente Proxmox pour un hôte à une seule adresse routée : sans
|
|
|
|
|
elles les VM se parlent entre elles mais ne sortent pas.
|
|
|
|
|
"""
|
|
|
|
|
reseau = cidr.rsplit(".", 1)[0] + ".0/" + cidr.split("/")[1]
|
|
|
|
|
bloc = [
|
|
|
|
|
"",
|
|
|
|
|
f"auto {nom}",
|
|
|
|
|
f"iface {nom} inet static",
|
|
|
|
|
f" address {cidr}",
|
|
|
|
|
" bridge-ports none",
|
|
|
|
|
" bridge-stp off",
|
|
|
|
|
" bridge-fd 0",
|
|
|
|
|
]
|
|
|
|
|
if uplink:
|
|
|
|
|
bloc += [
|
|
|
|
|
f" post-up iptables -t nat -A POSTROUTING -s '{reseau}'"
|
|
|
|
|
f" -o {uplink} -j MASQUERADE",
|
|
|
|
|
f" post-down iptables -t nat -D POSTROUTING -s '{reseau}'"
|
|
|
|
|
f" -o {uplink} -j MASQUERADE",
|
|
|
|
|
]
|
|
|
|
|
texte = "\n".join(bloc) + "\n"
|
|
|
|
|
cmds = [
|
|
|
|
|
# Idempotent : on n'ajoute la strophe que si le pont n'y est pas déjà.
|
|
|
|
|
f"grep -qE '^(auto|iface) {nom}( |$)' /etc/network/interfaces"
|
|
|
|
|
f" || printf '%s' {shlex.quote(texte)} >> /etc/network/interfaces",
|
|
|
|
|
]
|
|
|
|
|
if uplink:
|
|
|
|
|
cmds.append(
|
|
|
|
|
"printf 'net.ipv4.ip_forward=1\\n' >"
|
|
|
|
|
" /etc/sysctl.d/99-erplibre-nat.conf && sysctl -q -p"
|
|
|
|
|
" /etc/sysctl.d/99-erplibre-nat.conf"
|
|
|
|
|
)
|
|
|
|
|
# ifup plutôt qu'« ifreload -a » : recharger TOUTE la configuration d'un
|
|
|
|
|
# hôte distant peut emporter l'interface qui porte la session.
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
#
|
|
|
|
|
# « mkdir -p /run/network » d'abord : ifupdown2 y pose son verrou, et
|
|
|
|
|
# quand le répertoire manque il annonce « Another instance of this program
|
|
|
|
|
# is already running » — son lockFile() attrape aussi le fichier
|
|
|
|
|
# introuvable. Le message est un MENSONGE, et il a caché deux heures la
|
|
|
|
|
# vraie panne. Sur une Debian installée en image cloud, networking.service
|
|
|
|
|
# n'a jamais démarré, donc personne n'a créé le répertoire.
|
|
|
|
|
#
|
|
|
|
|
# Et l'erreur d'ifup n'est PAS masquée : « 2>/dev/null » cachait
|
|
|
|
|
# « operation failed with 'Operation not supported' » — le noyau cloud n'a
|
|
|
|
|
# pas le module bridge, et c'est ce qu'il fallait lire.
|
[FIX] proxmox : le pont interne prenait l'adresse de sa propre passerelle
Un Proxmox dans un Proxmox hérite du réseau interne de son parent : la VM
vivait en 10.10.10.152, passerelle 10.10.10.1. Le pont interne, lui, avait son
adresse CODÉE EN DUR à 10.10.10.1/24. Lui demander de la poser sur son propre
pont, c'est prendre l'adresse de sa passerelle et rendre tout le /24 local. La
machine s'isole au milieu de la commande qui la configure : « ifup » n'a jamais
rendu la main, la VM ne répondait plus ni en ssh ni en ping.
Le réseau est donc CHOISI, d'après ce que l'hôte connaît déjà — ses adresses et
ses routes, car une route sans adresse locale suffit à créer le conflit, et la
route par défaut en est l'exemple exact. Le chevauchement se calcule sur les
réseaux et non sur les trois premiers octets : « 10.0.0.0/8 » écarte alors bien
tous les candidats en 10.x. Plus aucun libre ? On le dit, plutôt que d'en
écraser un — écraser, ici, c'est couper la seule voie d'accès.
Le repli « ifreload -a » s'en va aussi. Il rechargeait TOUTES les interfaces, y
compris celle qui porte la session, et sur une image cloud l'interface
principale est décrite ailleurs — ifupdown2 la descend sans la remonter. Le
repli monte maintenant le pont à la main, sans toucher à rien d'autre ; la
strophe le rend persistant. La règle de masquerading se teste avant de
s'ajouter, donc une reprise n'empile rien.
Le test d'origine interdisait « 2>/dev/null » sur toute la ligne pour que
l'erreur d'ifup reste lisible. L'intention est gardée, portée sur l'appel à
ifup seul : le repli, lui, sonde légitimement.
--- EN ---
Proxmox inside Proxmox inherits its parent's internal network: the VM lived at
10.10.10.152, gateway 10.10.10.1. The internal bridge had its address HARDCODED
to 10.10.10.1/24. Asking it to put that on its own bridge takes its gateway's
address and makes the whole /24 local. The machine isolates itself in the
middle of the command configuring it: "ifup" never returned, the VM answered
neither ssh nor ping.
The subnet is now CHOSEN from what the host already knows — its addresses and
its routes, since a route with no local address is enough to collide, and the
default route is exactly that case. Overlap is computed on networks rather than
on the first three octets, so "10.0.0.0/8" correctly rules out every 10.x
candidate. None left? We say so rather than overwrite one — overwriting here
means cutting the only way in.
The "ifreload -a" fallback goes too. It reloaded ALL interfaces, including the
one carrying the session, and on a cloud image the main interface is described
elsewhere — ifupdown2 takes it down without bringing it back. The fallback now
raises the bridge by hand, touching nothing else; the stanza makes it
persistent. The masquerade rule is checked before being added, so a retry piles
nothing up.
The original test banned "2>/dev/null" across the whole line so ifup's error
stayed readable. That intent is kept, narrowed to the ifup call itself: the
fallback legitimately probes.
Assisted-by: Claude Opus 5
(cherry picked from commit 57991b186cf891b0db6b7228fb626c4c1af317cd)
2026-08-25 03:46:57 -04:00
|
|
|
# Et SURTOUT pas « ifreload -a » en repli : il recharge TOUTES les
|
|
|
|
|
# interfaces, y compris celle qui porte la session ssh, et sur une image
|
|
|
|
|
# cloud l'interface principale est décrite ailleurs (interfaces.d, ou
|
|
|
|
|
# netplan) — ifupdown2 la descend alors sans la remonter. Le repli est
|
|
|
|
|
# donc CHIRURGICAL : on monte le pont à la main, sans toucher à rien
|
|
|
|
|
# d'autre. La strophe, elle, le rend persistant au prochain démarrage.
|
|
|
|
|
manuel = [
|
|
|
|
|
f"ip link show {nom} >/dev/null 2>&1 || ip link add {nom} type bridge",
|
|
|
|
|
f"ip addr add {cidr} dev {nom} 2>/dev/null || true",
|
|
|
|
|
f"ip link set {nom} up",
|
|
|
|
|
]
|
|
|
|
|
if uplink:
|
|
|
|
|
regle = f"POSTROUTING -s {reseau} -o {uplink} -j MASQUERADE"
|
|
|
|
|
manuel.append(
|
|
|
|
|
f"iptables -t nat -C {regle} 2>/dev/null"
|
|
|
|
|
f" || iptables -t nat -A {regle}"
|
|
|
|
|
)
|
|
|
|
|
cmds.append(
|
|
|
|
|
f"mkdir -p /run/network; ifup {nom} || {{ " + "; ".join(manuel) + "; }"
|
|
|
|
|
)
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
return cmds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ipconfig_for(pont_info: dict, vmid: int) -> str:
|
|
|
|
|
"""« ip=dhcp » sur un pont qui donne sur le LAN, adresse FIXE sur un pont
|
|
|
|
|
interne — où aucun serveur DHCP ne répondrait.
|
|
|
|
|
|
|
|
|
|
L'adresse est dérivée du VMID : deux VM déployées à la suite ne peuvent pas
|
|
|
|
|
se retrouver avec la même, et le lien entre les deux reste lisible.
|
|
|
|
|
"""
|
|
|
|
|
info = pont_info or {}
|
|
|
|
|
adresse = info.get("address") or ""
|
|
|
|
|
if info.get("ports") or not adresse:
|
|
|
|
|
return "ip=dhcp"
|
|
|
|
|
base, _, masque = adresse.partition("/")
|
|
|
|
|
tronc = base.rsplit(".", 1)[0]
|
|
|
|
|
hote = 50 + (int(vmid) % 200)
|
|
|
|
|
return f"ip={tronc}.{hote}/{masque or '24'},gw={base}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ip_from_ipconfig(ipconfig: str) -> str:
|
|
|
|
|
"""Adresse fixe d'un « ip=10.10.10.150/24,gw=… », ou '' si c'est du DHCP.
|
|
|
|
|
|
|
|
|
|
Quand c'est NOUS qui avons attribué l'adresse, la chercher ensuite est
|
|
|
|
|
absurde : elle est connue avant que la VM ne démarre. La découverte (agent
|
|
|
|
|
invité, voisinage de l'hôte) ne sert qu'au DHCP.
|
|
|
|
|
"""
|
|
|
|
|
m = re.search(r"ip=(\d+\.\d+\.\d+\.\d+)", ipconfig or "")
|
|
|
|
|
return m.group(1) if m else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def image_fetch_cmd(url: str, nom: str, repertoire: str = IMAGE_DIR) -> str:
|
|
|
|
|
"""Télécharge l'image cloud SUR l'hôte Proxmox, une seule fois.
|
|
|
|
|
|
|
|
|
|
C'est là que le disque de la VM sera écrit : faire descendre l'image chez
|
|
|
|
|
soi pour la renvoyer ensuite doublerait le transfert. Le test de présence
|
|
|
|
|
évite de retélécharger 325 Mio à chaque VM.
|
|
|
|
|
"""
|
|
|
|
|
cible = f"{repertoire}/{nom}"
|
|
|
|
|
return (
|
|
|
|
|
f"mkdir -p {shlex.quote(repertoire)} && "
|
|
|
|
|
f"if [ -s {shlex.quote(cible)} ]; then "
|
|
|
|
|
f'echo "image déjà présente : {cible}"; else '
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
f"wget -nv -O {shlex.quote(cible)} {shlex.quote(url)}; "
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
f"fi"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_cmds(vmid: int, spec: dict) -> list:
|
|
|
|
|
"""Séquence complète de création d'une VM, dans l'ordre.
|
|
|
|
|
|
|
|
|
|
Une liste et non une seule commande : chaque étape est lisible dans le
|
|
|
|
|
journal, et un échec nomme celle qui a échoué. C'est le contraire d'un
|
|
|
|
|
« qm create » géant dont on ne sait pas quel morceau a cédé.
|
|
|
|
|
"""
|
|
|
|
|
nom = spec["name"]
|
|
|
|
|
stockage = spec["storage"]
|
|
|
|
|
image = f"{spec.get('image_dir', IMAGE_DIR)}/{spec['image']}"
|
|
|
|
|
cmds = [
|
|
|
|
|
# 1. La coquille : processeur, mémoire, réseau, contrôleur, agent.
|
|
|
|
|
"qm create {id} --name {nom} --memory {mem} --cores {cpu}"
|
|
|
|
|
" --cpu host --ostype l26 --scsihw virtio-scsi-single"
|
|
|
|
|
" --net0 virtio,bridge={pont} --agent enabled=1"
|
|
|
|
|
" --serial0 socket --vga serial0".format(
|
|
|
|
|
id=vmid,
|
|
|
|
|
nom=shlex.quote(nom),
|
|
|
|
|
mem=int(spec["memory"]),
|
|
|
|
|
cpu=int(spec["vcpus"]),
|
|
|
|
|
pont=spec["bridge"],
|
|
|
|
|
),
|
|
|
|
|
# 2. Le disque, importé DEPUIS l'image cloud. « import-from » (PVE 8+)
|
|
|
|
|
# remplace l'ancien « qm importdisk » en une seule étape et attache
|
|
|
|
|
# le disque du même coup.
|
|
|
|
|
f"qm set {vmid} --scsi0"
|
|
|
|
|
f" {stockage}:0,import-from={shlex.quote(image)},discard=on,ssd=1",
|
|
|
|
|
# 3. Le lecteur cloud-init, et l'ordre d'amorçage. Sans « boot order »,
|
|
|
|
|
# Proxmox laisse le disque importé hors de la liste et la VM démarre
|
|
|
|
|
# sur le réseau.
|
|
|
|
|
f"qm set {vmid} --ide2 {stockage}:cloudinit"
|
|
|
|
|
f" --boot order=scsi0 --bootdisk scsi0",
|
|
|
|
|
]
|
|
|
|
|
# 4. cloud-init : utilisateur, clé, réseau. La clé est un FICHIER sur
|
|
|
|
|
# l'hôte — « --sshkeys » n'accepte pas la clé en ligne.
|
|
|
|
|
ci = (
|
|
|
|
|
f"qm set {vmid} --ciuser {shlex.quote(spec.get('user') or 'erplibre')}"
|
|
|
|
|
)
|
|
|
|
|
if spec.get("sshkey_path"):
|
|
|
|
|
ci += f" --sshkeys {shlex.quote(spec['sshkey_path'])}"
|
|
|
|
|
if spec.get("password"):
|
|
|
|
|
ci += f" --cipassword {shlex.quote(spec['password'])}"
|
|
|
|
|
ci += f" --ipconfig0 {spec.get('ipconfig') or 'ip=dhcp'}"
|
[FIX] proxmox : sh: 1: Syntax error: "(" unexpected
Rapporté. La chaîne, en trois maillons : sur un hôte sans pont, « ip -o link
show type bridge » ne rend RIEN, la sortie ne contient donc que
l'avertissement de ssh sur la clé d'hôte — que le lecteur a pris pour un nom
de pont. « (ED25519) » s'est retrouvé dans « --net0 virtio,bridge=… », enrobé
de « sudo sh -c », et dash a répondu ce que l'utilisateur a lu. Le bruit de
ssh est maintenant retiré à la source, et un pont doit avoir la forme d'un
lien pour en être un.
Éprouvé sur l'hôte réel, VM créée puis détruite : le pont ne montait pas
(ifupdown2 accuse « another instance » quand /run/network manque — un
mensonge), le noyau Debian n'a ni module bridge ni table NAT, et une VM en
adresse fixe n'avait aucun résolveur. Le déploiement écrit désormais un
journal par VM sous ~/.erplibre/proxmox-deploy et en donne le chemin.
--- EN ---
Reported. The chain, in three links: on a host with no bridge, "ip -o link
show type bridge" returns NOTHING, so the output holds only ssh's host-key
warning — which the parser took for a bridge name. "(ED25519)" landed in
"--net0 virtio,bridge=…", wrapped in "sudo sh -c", and dash answered what the
user read. Ssh's noise is now stripped at the source, and a bridge must have
the shape of a link to be one.
Proven on the real host, VM created then destroyed: the bridge would not come
up (ifupdown2 claims "another instance" when /run/network is missing — a lie),
the Debian kernel has neither the bridge module nor the NAT table, and a
statically addressed VM had no resolver at all. Deployment now writes one log
per VM under ~/.erplibre/proxmox-deploy and prints its path.
Assisted-by: Claude Opus 5
2026-08-24 04:17:36 -04:00
|
|
|
# « --ipconfig0 » ne porte PAS le DNS : une VM en adresse fixe n'a alors
|
|
|
|
|
# aucun résolveur, et rien ne le dit. En DHCP le bail s'en charge.
|
|
|
|
|
serveurs = [s for s in (spec.get("nameservers") or ()) if s]
|
|
|
|
|
if serveurs and "dhcp" not in (spec.get("ipconfig") or "dhcp"):
|
|
|
|
|
ci += f" --nameserver {shlex.quote(' '.join(serveurs))}"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
cmds.append(ci)
|
|
|
|
|
# 5. La taille. L'image cloud fait 2 Gio : sans agrandissement, il ne reste
|
|
|
|
|
# rien pour installer quoi que ce soit.
|
|
|
|
|
if spec.get("disk"):
|
|
|
|
|
cmds.append(f"qm resize {vmid} scsi0 {spec['disk']}")
|
|
|
|
|
if spec.get("start", True):
|
|
|
|
|
cmds.append(f"qm start {vmid}")
|
|
|
|
|
return cmds
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def destroy_cmds(vmid: int, purge: bool = True) -> list:
|
|
|
|
|
"""Arrêt puis suppression. « --purge » retire aussi les disques et les
|
|
|
|
|
entrées de sauvegarde : sans lui, le stockage garde des volumes orphelins
|
|
|
|
|
que rien ne réclame plus."""
|
|
|
|
|
return [
|
|
|
|
|
f"qm stop {vmid} --skiplock 1 || true",
|
|
|
|
|
f"qm destroy {vmid} --purge {1 if purge else 0}"
|
|
|
|
|
" --destroy-unreferenced-disks 1",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resize_cmd(vmid: int, taille: str, disque: str = "scsi0") -> str:
|
|
|
|
|
"""« +10G » agrandit, « 40G » fixe. Proxmox REFUSE de rétrécir un disque —
|
|
|
|
|
le dire ici évite de croire à un bug de l'outil."""
|
|
|
|
|
return f"qm resize {vmid} {disque} {taille}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def status_cmd(vmid: int) -> str:
|
|
|
|
|
return f"qm status {vmid} --verbose"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def guest_ip_cmd(vmid: int) -> str:
|
|
|
|
|
return f"qm guest cmd {vmid} network-get-interfaces"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def console_cmd(vmid: int) -> str:
|
|
|
|
|
"""Console série. `qm terminal` demande serial0, que create_cmds pose."""
|
|
|
|
|
return f"qm terminal {vmid}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def orphan_disks_cmd() -> str:
|
|
|
|
|
"""Volumes de disque qui n'appartiennent à aucune VM déclarée.
|
|
|
|
|
|
|
|
|
|
Proxmox ne les efface pas tout seul : un « qm destroy » sans « --purge »,
|
|
|
|
|
ou une création interrompue, en laisse. On les LISTE, on n'efface rien
|
|
|
|
|
sans demander.
|
|
|
|
|
"""
|
|
|
|
|
return (
|
|
|
|
|
"for s in $(pvesm status --content images | awk 'NR>1 {print $1}'); "
|
|
|
|
|
'do pvesm list "$s" 2>/dev/null; done'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_orphans(text: str, vmids) -> list:
|
|
|
|
|
"""[(volid, taille)] des volumes dont le VMID n'existe plus."""
|
|
|
|
|
connus = {str(v) for v in vmids or ()}
|
|
|
|
|
out = []
|
|
|
|
|
for ligne in (text or "").splitlines():
|
|
|
|
|
parts = ligne.split()
|
|
|
|
|
if len(parts) < 5 or parts[0] == "Volid":
|
|
|
|
|
continue
|
|
|
|
|
volid, vmid = parts[0], parts[-1]
|
|
|
|
|
if vmid.isdigit() and vmid not in connus:
|
|
|
|
|
try:
|
|
|
|
|
taille = int(parts[3])
|
|
|
|
|
except ValueError:
|
|
|
|
|
taille = 0
|
|
|
|
|
out.append((volid, taille))
|
|
|
|
|
return out
|