diff --git a/README.md b/README.md index 17de27e..25c3461 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Les mots de passe (MariaDB, Redis, admin de l'UI) sont générés au premier lan ## Compte Proxmox dédié -Le clonage et le pilotage des VM passent par l'API avec l'utilisateur `bwpoc@pve` et un jeton, limité à un pool `bwpoc`. Seule la création du modèle (import d'une image disque par chemin, réservé à `root@pam`) se fait en SSH root, une fois. +Le clonage et le pilotage des VM passent par l'API avec l'utilisateur `bwpoc@pve` et un jeton, limité à un pool `bwpoc`. Par défaut, le PoC clone un modèle cloud-init Debian 13 existant (`pve_template_vmid`, 9001 sur lancelot : VLAN 200, agent QEMU), donc aucun SSH root n'est nécessaire. Avec `pve_template_create: true`, le modèle 9100 est fabriqué en SSH root (import d'une image disque par chemin, réservé à `root@pam`), une fois. Privilèges nécessaires, regroupés dans un rôle `BWPoC` : @@ -71,7 +71,7 @@ Et les ACL, au plus juste : | Chemin | Rôle | Pourquoi | |---|---|---| -| `/pool/bwpoc` | `BWPoC` | les VM du PoC et le modèle 9100, placé dans le pool | +| `/pool/bwpoc` | `BWPoC` | les VM du PoC (et le modèle 9100 s'il est fabriqué par le PoC) | | `/storage/zfs` | `PVEDatastoreUser` | allouer les disques des clones | | `/sdn/zones/localnetwork/vmbr0/200` | `PVESDNUser` | brancher les VM sur le VLAN 200 (et seulement lui) | | `/nodes/lancelot` | `PVEAuditor` | lecture du nœud par le module Ansible | @@ -112,7 +112,7 @@ make deploy # = ansible-playbook playbooks/site.yml | Étape | Playbook | Ce qu'elle fait | |---|---|---| -| 1 | `00-proxmox.yml` | en root (une fois) : télécharge l'image Debian 13 genericcloud, y ajoute l'agent QEMU, crée le modèle cloud-init (VMID 9100) et le pool `bwpoc`. Puis par l'API avec `bwpoc@pve` : clone les VM dans le pool, cloud-init en DHCP + clé SSH, agrandit les disques, démarre, lit l'IP obtenue par l'agent QEMU et l'écrit dans `inventory/host_vars//dhcp.yml` | +| 1 | `00-proxmox.yml` | si `pve_template_create: true`, en root (une fois) : fabrique le modèle 9100 depuis l'image Debian 13 genericcloud. Puis par l'API : crée le pool `bwpoc`, clone le modèle (9001 par défaut), cloud-init en DHCP + clé SSH, agrandit les disques, démarre, régénère le machine-id par l'agent QEMU (sinon tous les clones obtiennent la même IP DHCP), lit l'IP obtenue par l'agent QEMU et l'écrit dans `inventory/host_vars//dhcp.yml` | | 2 | `10-base.yml` | paquets de base, agent QEMU, résolution locale des noms du PoC vers le WAF, Docker CE depuis le dépôt officiel | | 3 | `20-apps.yml` | stack des applis, filtrage `DOCKER-USER` (mode split) | | 4 | `30-bunkerweb.yml` | stack BunkerWeb, attente qu'`api` réponde au travers du WAF, écriture de `credentials/ACCES.txt` | @@ -233,8 +233,8 @@ La SQLi de Juice Shop renvoie alors la liste complète des produits (200) au lie ```bash make waf # réapplique la conf BunkerWeb après modification de bunkerweb.yml -make destroy # supprime les VM en root sur le nœud (confirmation demandée), modèle conservé -ansible-playbook playbooks/99-destroy.yml -e destroy_template=true # supprime aussi le modèle +make destroy # supprime les VM par l'API (confirmation demandée), modèle conservé +ansible-playbook playbooks/99-destroy.yml -e destroy_template=true # supprime aussi le modèle, s'il a été fabriqué par le PoC ``` Sur `bw-waf`, la stack vit dans `/opt/bwpoc/bunkerweb` (`docker compose ps`, `logs`, `pull`...). Sur `bw-apps`, dans `/opt/bwpoc/apps`. diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml index 8227877..546b834 100644 --- a/inventory/group_vars/all.yml +++ b/inventory/group_vars/all.yml @@ -25,14 +25,19 @@ pve_api_token_id: ansible pve_api_token_secret: "{{ lookup('ansible.builtin.env', 'PVE_TOKEN_SECRET') }}" pve_api_password: "{{ lookup('ansible.builtin.env', 'PROXMOX_PASSWORD') }}" pve_validate_certs: false # certificat auto-signé du nœud +# Modèle cloud-init Debian 13 AVEC qemu-guest-agent installé et machine-id vierge. +# Sur lancelot : 9100 "debian13-bwpoc", dérivé de 9001 (agent ajouté, cloud-init clean). +# pve_template_create: true pour le fabriquer depuis l'image officielle (SSH root sur le nœud). +pve_template_create: false pve_template_vmid: 9100 -pve_template_name: debian13-cloud +pve_template_name: debian13-bwpoc pve_image_url: https://cloud.debian.org/images/cloud/trixie/latest/debian-13-genericcloud-amd64.qcow2 pve_image_path: /var/lib/vz/template/debian-13-genericcloud-amd64.qcow2 # --- Accès aux VM ----------------------------------------------------- poc_vm_user: debian -poc_ssh_pubkey_file: "~/.ssh/id_ed25519.pub" # clé injectée par cloud-init +# Clé injectée par cloud-init : la première trouvée. +poc_ssh_pubkey_file: "{{ lookup('ansible.builtin.first_found', ['~/.ssh/id_ed25519.pub', '~/.ssh/id_rsa.pub'] | map('expanduser') | list) }}" # --- Versions (épinglées) -------------------------------------------- bunkerweb_version: "1.6.17" diff --git a/inventory/hosts.yml b/inventory/hosts.yml index 24054ed..975e49e 100644 --- a/inventory/hosts.yml +++ b/inventory/hosts.yml @@ -36,4 +36,4 @@ all: vm_id: 9103 vm_cores: 1 vm_memory: 1024 - vm_disk_gb: 10 + vm_disk_gb: 20 # pas plus petit que le disque du modèle (20 Go) diff --git a/playbooks/00-proxmox.yml b/playbooks/00-proxmox.yml index 22b64d9..400d586 100644 --- a/playbooks/00-proxmox.yml +++ b/playbooks/00-proxmox.yml @@ -5,7 +5,8 @@ hosts: proxmox gather_facts: false roles: - - proxmox_template + - role: proxmox_template + when: pve_template_create | bool # 2) Clonage des VM par l'API, avec le compte dédié bwpoc@pve (jeton API). # Authentification : jeton (PVE_TOKEN_SECRET) ou mot de passe (PROXMOX_CLAUDE / PROXMOX_PASSWORD). diff --git a/playbooks/99-destroy.yml b/playbooks/99-destroy.yml index 03ff49e..aa47823 100644 --- a/playbooks/99-destroy.yml +++ b/playbooks/99-destroy.yml @@ -2,7 +2,19 @@ # Supprime les VM du PoC (le modèle est conservé, sauf -e destroy_template=true). - name: Détruire les VM du PoC hosts: proxmox + connection: local gather_facts: false + vars: + ansible_become: false + ansible_python_interpreter: "{{ ansible_playbook_python }}" + module_defaults: + group/community.proxmox.proxmox: + api_host: "{{ ansible_host }}" + api_user: "{{ pve_api_user }}" + api_token_id: "{{ pve_api_token_id if pve_api_token_secret | length > 0 else omit }}" + api_token_secret: "{{ pve_api_token_secret if pve_api_token_secret | length > 0 else omit }}" + api_password: "{{ pve_api_password if pve_api_token_secret | length == 0 else omit }}" + validate_certs: "{{ pve_validate_certs }}" vars_prompt: - name: confirm prompt: "Taper 'detruire' pour supprimer les VM {{ groups['poc_vms'] | unique | join(', ') }}" @@ -12,30 +24,26 @@ ansible.builtin.meta: end_play when: confirm != 'detruire' - - name: Arrêter et supprimer les VM - ansible.builtin.shell: | - if qm status {{ hostvars[item].vm_id }} >/dev/null 2>&1; then - qm stop {{ hostvars[item].vm_id }} --skiplock 1 || true - qm destroy {{ hostvars[item].vm_id }} --purge 1 --destroy-unreferenced-disks 1 - echo destroyed - fi - register: destroy - changed_when: "'destroyed' in destroy.stdout" + - name: Arrêter et supprimer les VM (API) + community.proxmox.proxmox_kvm: + node: "{{ pve_node }}" + vmid: "{{ hostvars[item].vm_id }}" + state: absent + force: true + timeout: 300 loop: "{{ groups['poc_vms'] | unique }}" - - name: Supprimer le modèle - ansible.builtin.shell: | - if qm status {{ pve_template_vmid }} >/dev/null 2>&1; then - qm destroy {{ pve_template_vmid }} --purge 1 && echo destroyed - fi - register: tpl_destroy - changed_when: "'destroyed' in tpl_destroy.stdout" - when: destroy_template | default(false) | bool + - name: Supprimer le modèle (seulement s'il a été créé par le PoC) + community.proxmox.proxmox_kvm: + node: "{{ pve_node }}" + vmid: "{{ pve_template_vmid }}" + state: absent + when: + - destroy_template | default(false) | bool + - pve_template_create | bool - name: Oublier les IP DHCP mémorisées ansible.builtin.file: path: "{{ inventory_dir }}/host_vars/{{ item }}/dhcp.yml" state: absent loop: "{{ groups['poc_vms'] | unique }}" - delegate_to: localhost - become: false diff --git a/roles/proxmox_vm/tasks/machine_id.yml b/roles/proxmox_vm/tasks/machine_id.yml new file mode 100644 index 0000000..a3313fe --- /dev/null +++ b/roles/proxmox_vm/tasks/machine_id.yml @@ -0,0 +1,48 @@ +--- +# Exécution dans la VM par l'agent QEMU (API /agent/exec), sans SSH. +- name: "{{ vm_name }} : authentification à l'API" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:8006/api2/json/access/ticket" + method: POST + body_format: form-urlencoded + body: + username: "{{ pve_api_user }}" + password: "{{ pve_api_password }}" + validate_certs: "{{ pve_validate_certs }}" + register: pve_ticket + no_log: true + when: pve_api_token_secret | length == 0 + +- name: "{{ vm_name }} : en-têtes d'API" + ansible.builtin.set_fact: + pve_headers: >- + {{ {'Authorization': 'PVEAPIToken=' ~ pve_api_user ~ '!' ~ pve_api_token_id ~ '=' ~ pve_api_token_secret} + if pve_api_token_secret | length > 0 else + {'Cookie': 'PVEAuthCookie=' ~ pve_ticket.json.data.ticket, + 'CSRFPreventionToken': pve_ticket.json.data.CSRFPreventionToken} }} + no_log: true + +- name: "{{ vm_name }} : attente de l'agent QEMU" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:8006/api2/json/nodes/{{ pve_node }}/qemu/{{ vm.vm_id }}/agent/ping" + method: POST + headers: "{{ pve_headers }}" + validate_certs: "{{ pve_validate_certs }}" + register: agent_ping + until: agent_ping.status == 200 + retries: 60 # premier démarrage cloud-init : jusqu'à 5 min + delay: 5 + +- name: "{{ vm_name }} : régénérer le machine-id puis redémarrer" + ansible.builtin.uri: + url: "https://{{ ansible_host }}:8006/api2/json/nodes/{{ pve_node }}/qemu/{{ vm.vm_id }}/agent/exec" + method: POST + headers: "{{ pve_headers }}" + body_format: json + body: + command: ["sh", "-c", "test -f /etc/bwpoc-machine-id || { rm -f /etc/machine-id /var/lib/dbus/machine-id && systemd-machine-id-setup && touch /etc/bwpoc-machine-id && systemctl reboot; }"] + validate_certs: "{{ pve_validate_certs }}" + +- name: "{{ vm_name }} : laisser le redémarrage commencer" + ansible.builtin.pause: + seconds: 20 diff --git a/roles/proxmox_vm/tasks/main.yml b/roles/proxmox_vm/tasks/main.yml index 3e7d259..00d1e03 100644 --- a/roles/proxmox_vm/tasks/main.yml +++ b/roles/proxmox_vm/tasks/main.yml @@ -7,6 +7,11 @@ fail_msg: "Exportez PVE_TOKEN_SECRET (jeton {{ pve_api_user }}!{{ pve_api_token_id }}) ou PROXMOX_CLAUDE et PROXMOX_PASSWORD" quiet: true +- name: Créer le pool {{ pve_pool }} + community.proxmox.proxmox_pool: + poolid: "{{ pve_pool }}" + comment: PoC BunkerWeb + - name: Créer le répertoire des host_vars par VM ansible.builtin.file: path: "{{ inventory_dir }}/host_vars/{{ item }}" diff --git a/roles/proxmox_vm/tasks/vm.yml b/roles/proxmox_vm/tasks/vm.yml index 486cdfa..85e2627 100644 --- a/roles/proxmox_vm/tasks/vm.yml +++ b/roles/proxmox_vm/tasks/vm.yml @@ -1,7 +1,12 @@ --- - name: "{{ vm_name }} : lecture des paramètres" ansible.builtin.set_fact: - vm: "{{ hostvars[vm_name] }}" + # Uniquement les champs utiles : copier tout hostvars[vm] échoue avec ansible-core 2.21 + vm: + vm_id: "{{ hostvars[vm_name].vm_id }}" + vm_cores: "{{ hostvars[vm_name].vm_cores }}" + vm_memory: "{{ hostvars[vm_name].vm_memory }}" + vm_disk_gb: "{{ hostvars[vm_name].vm_disk_gb }}" - name: "{{ vm_name }} : la VM existe-t-elle ?" community.proxmox.proxmox_vm_info: @@ -54,6 +59,11 @@ state: started timeout: 120 +# Un clone hérite du machine-id du modèle : systemd-networkd en dérive son identifiant +# DHCP, et toutes les VM recevraient la même IP. On le régénère une fois (marqueur /etc/bwpoc-machine-id). +- name: "{{ vm_name }} : nouveau machine-id (identifiant DHCP propre)" + ansible.builtin.include_tasks: machine_id.yml + - name: "{{ vm_name }} : adresse DHCP remontée par l'agent QEMU" community.proxmox.proxmox_vm_info: node: "{{ pve_node }}"