--- # Update bve (lab node) Proxmox LXC containers and VMs # Integrates features from community-scripts/ProxmoxVE update-lxcs script # Usage: ansible-playbook -i inventory/proxmox update-containers.yml - name: Update bve Lab LXC Containers and VMs hosts: bve gather_facts: no vars: candidate_containers: - 101 # chief-executive-officer - 103 # semaphore - 104 # cyberchef - 107 # shlink - 108 # paperclip candidate_vms: - 102 # mikrotik-routeros-chr - 106 # freepbx - 112 # dockploy - 200 # proxmox - 8000 # debian-13-cloud - 8001 # ubuntu-24.04-cloud - 8002 # docker-vm tasks: # ============================================================ # PHASE 1: Discover container/VM statuses and filter templates # ============================================================ - name: Check status and template status for candidate containers ansible.builtin.shell: cmd: | set -e CT_ID="{{ item }}" # Check if it's a template (skip templates) TEMPLATE_CHECK=$(sudo /usr/sbin/pct config "$CT_ID" 2>/dev/null | grep -i "template:" | head -1 || true) if echo "$TEMPLATE_CHECK" | grep -qi "template: 1"; then echo "TEMPLATE:$CT_ID" else # Check running status if sudo /usr/sbin/pct status "$CT_ID" 2>/dev/null | grep -q running; then echo "RUNNING:$CT_ID" elif sudo /usr/sbin/pct status "$CT_ID" 2>/dev/null | grep -q stopped; then echo "STOPPED:$CT_ID" else echo "MISSING:$CT_ID" fi fi loop: "{{ candidate_containers }}" register: container_statuses check_mode: no changed_when: false - name: Check status for candidate VMs ansible.builtin.shell: cmd: | set -e VM_ID="{{ item }}" if sudo /usr/sbin/qm status "$VM_ID" 2>/dev/null | grep -q running; then echo "RUNNING:$VM_ID" elif sudo /usr/sbin/qm status "$VM_ID" 2>/dev/null | grep -q stopped; then echo "STOPPED:$VM_ID" else echo "MISSING:$VM_ID" fi loop: "{{ candidate_vms }}" register: vm_statuses check_mode: no changed_when: false - name: Categorize containers by status ansible.builtin.set_fact: running_containers: "{{ container_statuses.results | map(attribute='stdout') | select('match', '^RUNNING:') | map('regex_replace', '^RUNNING:', '') | list }}" stopped_containers: "{{ container_statuses.results | map(attribute='stdout') | select('match', '^STOPPED:') | map('regex_replace', '^STOPPED:', '') | list }}" template_containers: "{{ container_statuses.results | map(attribute='stdout') | select('match', '^TEMPLATE:') | map('regex_replace', '^TEMPLATE:', '') | list }}" running_vms: "{{ vm_statuses.results | map(attribute='stdout') | select('match', '^RUNNING:') | map('regex_replace', '^RUNNING:', '') | list }}" stopped_vms: "{{ vm_statuses.results | map(attribute='stdout') | select('match', '^STOPPED:') | map('regex_replace', '^STOPPED:', '') | list }}" - name: Show templates being skipped ansible.builtin.debug: msg: "Skipping template containers: {{ template_containers | join(', ') }}" when: template_containers | length > 0 # ============================================================ # PHASE 2: Process running containers with OS detection # ============================================================ - name: Detect OS in running containers ansible.builtin.shell: cmd: | set -e CT_ID="{{ item }}" # Detect OS by checking /etc/os-release OS_INFO=$(sudo /usr/sbin/pct exec "$CT_ID" -- cat /etc/os-release 2>/dev/null || echo "") if echo "$OS_INFO" | grep -qi "alpine"; then echo "alpine" elif echo "$OS_INFO" | grep -qi "arch"; then echo "arch" elif echo "$OS_INFO" | grep -qi "debian"; then echo "debian" elif echo "$OS_INFO" | grep -qi "ubuntu"; then echo "ubuntu" elif echo "$OS_INFO" | grep -qi "devuan"; then echo "devuan" elif echo "$OS_INFO" | grep -qi "fedora"; then echo "fedora" elif echo "$OS_INFO" | grep -qi "rocky"; then echo "rocky" elif echo "$OS_INFO" | grep -qi "centos"; then echo "centos" elif echo "$OS_INFO" | grep -qi "alma"; then echo "alma" elif echo "$OS_INFO" | grep -qi "suse"; then echo "opensuse" else # Default to debian/ubuntu if detection fails echo "unknown" fi loop: "{{ running_containers }}" register: container_os_detection check_mode: no changed_when: false when: running_containers | length > 0 - name: Get disk usage for running containers ansible.builtin.shell: cmd: | set -e CT_ID="{{ item }}" sudo /usr/sbin/pct exec "$CT_ID" -- df -h / 2>/dev/null | tail -1 | awk '{print $5}' loop: "{{ running_containers }}" register: container_disk_usage check_mode: no changed_when: false when: running_containers | length > 0 - name: Update running containers with OS-specific package manager ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" OS_TYPE="{{ item.stdout }}" echo "=== Container $CT_ID ===" echo "OS Type: $OS_TYPE" # Get disk usage DISK_USAGE=$(sudo /usr/sbin/pct exec "$CT_ID" -- df -h / 2>/dev/null | tail -1 | awk '{print $5}' || echo "unknown") echo "Boot disk usage: $DISK_USAGE" echo "Starting update..." case "$OS_TYPE" in alpine) sudo /usr/sbin/pct exec "$CT_ID" -- sh -c 'apk update && apk upgrade -a' ;; arch) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'pacman -Syyu --noconfirm' ;; fedora|rocky|centos|alma) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'dnf -y update && dnf -y upgrade' ;; ubuntu|debian|devuan) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get -yq dist-upgrade' ;; opensuse) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'zypper ref && zypper --non-interactive dup' ;; *) echo "Unknown OS, trying apt..." sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get -yq dist-upgrade' ;; esac echo "Update complete for container $CT_ID" loop: "{{ container_os_detection.results | default([]) }}" loop_control: label: "{{ item.item }}" register: container_update_output when: running_containers | length > 0 - name: Check if reboot is required in containers ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" OS_TYPE="{{ item.stdout }}" REBOOT_NEEDED="no" # Check reboot-required file (Debian/Ubuntu style) if [ "$OS_TYPE" = "ubuntu" ] || [ "$OS_TYPE" = "debian" ] || [ "$OS_TYPE" = "devuan" ]; then if sudo /usr/sbin/pct exec "$CT_ID" -- test -f /var/run/reboot-required 2>/dev/null; then REBOOT_NEEDED="yes" fi fi # Check for needs-restarting (RHEL/Fedora style) if [ "$OS_TYPE" = "fedora" ] || [ "$OS_TYPE" = "rocky" ] || [ "$OS_TYPE" = "centos" ] || [ "$OS_TYPE" = "alma" ]; then if sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'which needs-restarting > /dev/null 2>&1 && needs-restarting -r' 2>/dev/null; then REBOOT_NEEDED="yes" fi fi echo "$CT_ID:$OS_TYPE:$REBOOT_NEEDED" loop: "{{ container_os_detection.results | default([]) }}" register: container_reboot_status check_mode: no changed_when: false when: running_containers | length > 0 - name: Run patchmon-agent report if present ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'if [ -x /usr/local/bin/patchmon-agent ]; then /usr/local/bin/patchmon-agent report; fi' 2>/dev/null || true loop: "{{ container_os_detection.results | default([]) }}" register: patchmon_results failed_when: false changed_when: false when: running_containers | length > 0 # ============================================================ # PHASE 3: Process running VMs (assumed Debian/Ubuntu with apt) # ============================================================ - name: Get disk usage for running VMs ansible.builtin.shell: cmd: | set -e VM_ID="{{ item }}" sudo /usr/sbin/qm guest exec "$VM_ID" -- bash -c "df -h / | tail -1 | awk '{print \$5}'" 2>/dev/null | jq -r '."out-data" // empty' | tr -d '\n' || echo "unknown" loop: "{{ running_vms }}" register: vm_disk_usage check_mode: no changed_when: false when: running_vms | length > 0 - name: Update running VMs ansible.builtin.shell: cmd: | set -e VM_ID="{{ item }}" echo "=== VM $VM_ID ===" # Get disk usage DISK_USAGE=$(sudo /usr/sbin/qm guest exec "$VM_ID" -- bash -c "df -h / | tail -1 | awk '{print \$5}'" 2>/dev/null | jq -r '."out-data" // empty' | tr -d '\n' || echo "unknown") echo "Boot disk usage: $DISK_USAGE" sudo /usr/sbin/qm guest exec "$VM_ID" -- bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get -yq dist-upgrade' echo "Update complete for VM $VM_ID" loop: "{{ running_vms }}" register: vm_update_output when: running_vms | length > 0 - name: Check if reboot is required in VMs ansible.builtin.shell: cmd: | set -e VM_ID="{{ item }}" if sudo /usr/sbin/qm guest exec "$VM_ID" -- test -f /var/run/reboot-required 2>/dev/null | grep -q '"exitcode": "OK"'; then echo "$VM_ID:yes" else echo "$VM_ID:no" fi loop: "{{ running_vms }}" register: vm_reboot_status check_mode: no changed_when: false when: running_vms | length > 0 - name: Run patchmon-agent report if present on VMs ansible.builtin.shell: cmd: | set -e VM_ID="{{ item }}" sudo /usr/sbin/qm guest exec "$VM_ID" -- bash -c 'if [ -x /usr/local/bin/patchmon-agent ]; then /usr/local/bin/patchmon-agent report; fi' 2>/dev/null || true loop: "{{ running_vms }}" register: vm_patchmon_results failed_when: false changed_when: false when: running_vms | length > 0 # ============================================================ # PHASE 4: Process stopped containers (start, update, stop) # ============================================================ - name: Start stopped containers for update ansible.builtin.shell: cmd: | set -e CT_ID="{{ item }}" echo "Starting container $CT_ID..." sudo /usr/sbin/pct start "$CT_ID" # Wait for container to be ready for i in {1..30}; do if sudo /usr/sbin/pct exec "$CT_ID" -- echo "ready" 2>/dev/null; then break fi sleep 2 done echo "Container $CT_ID is running" loop: "{{ stopped_containers }}" register: started_containers when: stopped_containers | length > 0 - name: Detect OS in newly started containers ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" OS_INFO=$(sudo /usr/sbin/pct exec "$CT_ID" -- cat /etc/os-release 2>/dev/null || echo "") if echo "$OS_INFO" | grep -qi "alpine"; then echo "alpine" elif echo "$OS_INFO" | grep -qi "arch"; then echo "arch" elif echo "$OS_INFO" | grep -qi "debian"; then echo "debian" elif echo "$OS_INFO" | grep -qi "ubuntu"; then echo "ubuntu" elif echo "$OS_INFO" | grep -qi "devuan"; then echo "devuan" elif echo "$OS_INFO" | grep -qi "fedora"; then echo "fedora" elif echo "$OS_INFO" | grep -qi "rocky"; then echo "rocky" elif echo "$OS_INFO" | grep -qi "centos"; then echo "centos" elif echo "$OS_INFO" | grep -qi "alma"; then echo "alma" elif echo "$OS_INFO" | grep -qi "suse"; then echo "opensuse" else echo "unknown" fi loop: "{{ started_containers.results | default([]) }}" register: stopped_container_os check_mode: no changed_when: false when: stopped_containers | length > 0 - name: Update stopped containers (now running) ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" OS_TYPE="{{ item.stdout }}" echo "=== Container $CT_ID (was stopped, now updating) ===" echo "OS Type: $OS_TYPE" case "$OS_TYPE" in alpine) sudo /usr/sbin/pct exec "$CT_ID" -- sh -c 'apk update && apk upgrade -a' ;; arch) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'pacman -Syyu --noconfirm' ;; fedora|rocky|centos|alma) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'dnf -y update && dnf -y upgrade' ;; ubuntu|debian|devuan) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get -yq dist-upgrade' ;; opensuse) sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'zypper ref && zypper --non-interactive dup' ;; *) echo "Unknown OS, trying apt..." sudo /usr/sbin/pct exec "$CT_ID" -- bash -c 'export DEBIAN_FRONTEND=noninteractive && apt-get update -qq && apt-get -yq dist-upgrade' ;; esac echo "Update complete for container $CT_ID" loop: "{{ stopped_container_os.results | default([]) }}" loop_control: label: "{{ item.item }}" register: stopped_container_update when: stopped_containers | length > 0 - name: Stop containers that were started for update ansible.builtin.shell: cmd: | set -e CT_ID="{{ item.item }}" echo "Stopping container $CT_ID..." sudo /usr/sbin/pct stop "$CT_ID" echo "Container $CT_ID stopped" loop: "{{ started_containers.results | default([]) }}" when: stopped_containers | length > 0 # ============================================================ # PHASE 5: Report summary # ============================================================ - name: Build update summary ansible.builtin.set_fact: summary_report: host: "bve" templates_skipped: "{{ template_containers }}" running_containers_updated: "{{ running_containers }}" running_vms_updated: "{{ running_vms }}" stopped_containers_updated: "{{ stopped_containers }}" containers_needing_reboot: "{{ container_reboot_status.results | default([]) | selectattr('stdout', 'search', ':yes') | map(attribute='stdout') | map('regex_replace', ':(.*):yes', '') | list }}" vms_needing_reboot: "{{ vm_reboot_status.results | default([]) | selectattr('stdout', 'search', ':yes') | map(attribute='stdout') | map('regex_replace', ':(.*):yes', '') | list }}" - name: Display update summary ansible.builtin.debug: msg: - "==========================================" - "BVE UPDATE SUMMARY" - "==========================================" - "Templates skipped: {{ summary_report.templates_skipped | default([]) | join(', ') | default('None') }}" - "Running containers updated: {{ summary_report.running_containers_updated | default([]) | join(', ') | default('None') }}" - "Running VMs updated: {{ summary_report.running_vms_updated | default([]) | join(', ') | default('None') }}" - "Stopped containers updated (and stopped): {{ summary_report.stopped_containers_updated | default([]) | join(', ') | default('None') }}" - "---" - "Containers requiring reboot: {{ summary_report.containers_needing_reboot | default([]) | join(', ') | default('None') }}" - "VMs requiring reboot: {{ summary_report.vms_needing_reboot | default([]) | join(', ') | default('None') }}" - "=========================================="