Le blog technique

Toutes les astuces #tech des collaborateurs de PI Services.

#openblogPI

Retrouvez les articles à la une

Sécuriser votre PC : Guide étape par étape pour la mise à jour des certificats Secure Boot (2023) – Partie 1

Introduction :

Bienvenue dans la première partie de notre série consacrée à la mise à jour des certificats Secure Boot sur les PC Windows. Avec l’expiration imminente des anciens certificats (2011) prévue entre juin et octobre 2026, il est crucial pour les entreprises et les utilisateurs avancés de préparer leurs systèmes pour garantir un démarrage sécurisé et la compatibilité avec les composants signés.

Dans cette série, nous allons vous guider à travers le processus complet de préparation et de déploiement des certificats Secure Boot 2023, en commençant par l’inventaire des machines et la vérification des prérequis. Les articles suivants couvriront les différentes méthodes de mise à jour et de déploiement à grande échelle.

Ce premier article se concentre sur la préparation et l’inventaire des systèmes, avec un script PowerShell pratique fourni par Microsoft pour collecter toutes les informations nécessaires avant le déploiement.

Contexte :

Microsoft distribue de nouveaux certificats Secure Boot (version 2023) car les anciens (2011) arriveront à expiration entre juin et octobre 2026. Sans cette mise à jour, les PC Windows avec Secure Boot risquent de ne plus recevoir de correctifs de sécurité pour le démarrage ou de refuser des composants de démarrage signés.

Terminologie :

  • KEK (Key Enrollment Key) : clé utilisée pour signer les mises à jour de la base de données Secure Boot.
  • CA (Certificate Authority) : autorité de certification.
  • DB (Secure Boot Signature Database) : base de données contenant les certificats valides pour Secure Boot.
  • DBX (Secure Boot Revoked Signature Database) : base de données contenant les certificats révoqués.
Certificat arrivant à expirationDate d’expirationNouveau certificatEmplacementRôle / Utilisation
Microsoft Corporation KEK CA 2011Juin 2026Microsoft Corporation KEK 2K CA 2023Stocké dans le KEKSigne les mises à jour de la DB et de la DBX
Microsoft Windows Production PCA 2011Octobre 2026Windows UEFI CA 2023Stocké dans la DBUtilisé pour signer le chargeur de démarrage Windows
Microsoft UEFI CA 2011*Juin 2026Microsoft UEFI CA 2023Stocké dans la DBSigne les chargeurs de démarrage tiers et les applications EFI
Microsoft UEFI CA 2011*Juin 2026Microsoft Option ROM UEFI CA 2023Stocké dans la DBSigne les Option ROM tierces

Étape 1 : Préparation et inventaire des systèmes

Avant de déployer les certificats Secure Boot 2023, il est essentiel d’identifier les caractéristiques matérielles et logicielles des PC de votre environnement. Cette étape permet de vérifier la compatibilité des appareils et de préparer le déploiement en toute sécurité.

Les éléments à inventorier incluent notamment :

  • Le fabricant du système
  • La version et la date du BIOS/UEFI
  • L’architecture du système d’exploitation
  • L’état du Secure Boot

Microsoft met à disposition un script PowerShell permettant d’automatiser cette collecte d’informations. Ce script facilite l’inventaire des machines et la vérification des prérequis nécessaires avant l’application des mises à jour de certificats Secure Boot.

Script d’inventaire Secure Boot (PowerShell mode administrateur)

Ce script PowerShell collecte automatiquement les informations système, l’état Secure Boot, et les registres liés à la mise à jour des certificats Secure Boot. Il doit être exécuté en mode administrateur.

#Sample_Secure_Boot_Inventory_Data_Collection_script

# 1. HostName
# PS Version: All | Admin: No | System Requirements: None
try {
    $hostname = $env:COMPUTERNAME
    if ([string]::IsNullOrEmpty($hostname)) {
        Write-Warning "Hostname could not be determined"
        $hostname = "Unknown"
    }
    Write-Host "Hostname: $hostname"
} catch {
    Write-Warning "Error retrieving hostname: $_"
    $hostname = "Error"
    Write-Host "Hostname: $hostname"
}

# 2. CollectionTime
# PS Version: All | Admin: No | System Requirements: None
try {
    $collectionTime = Get-Date
    if ($null -eq $collectionTime) {
        Write-Warning "Could not retrieve current date/time"
        $collectionTime = "Unknown"
    }
    Write-Host "Collection Time: $collectionTime"
} catch {
    Write-Warning "Error retrieving date/time: $_"
    $collectionTime = "Error"
    Write-Host "Collection Time: $collectionTime"
}

# Registry: Secure Boot Main Key (3 values)

# 3. SecureBootEnabled
# PS Version: 3.0+ | Admin: May be required | System Requirements: UEFI/Secure Boot capable system
try {
    $secureBootEnabled = Confirm-SecureBootUEFI -ErrorAction Stop
    Write-Host "Secure Boot Enabled: $secureBootEnabled"
} catch {
    Write-Warning "Unable to determine Secure Boot status via cmdlet: $_"
    # Try registry fallback
    try {
        $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State" -Name UEFISecureBootEnabled -ErrorAction Stop
        $secureBootEnabled = [bool]$regValue.UEFISecureBootEnabled
        Write-Host "Secure Boot Enabled: $secureBootEnabled"
    } catch {
        Write-Warning "Unable to determine Secure Boot status via registry. System may not support UEFI/Secure Boot."
        $secureBootEnabled = $null
        Write-Host "Secure Boot Enabled: Not Available"
    }
}

# 4. HighConfidenceOptOut
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot" -Name HighConfidenceOptOut -ErrorAction Stop
    $highConfidenceOptOut = $regValue.HighConfidenceOptOut
    Write-Host "High Confidence Opt Out: $highConfidenceOptOut"
} catch {
    Write-Warning "HighConfidenceOptOut registry key not found or inaccessible"
    $highConfidenceOptOut = $null
    Write-Host "High Confidence Opt Out: Not Available"
}

# 5. AvailableUpdates
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot" -Name AvailableUpdates -ErrorAction Stop
    $availableUpdates = $regValue.AvailableUpdates
    if ($null -ne $availableUpdates) {
        # Convert to hexadecimal format
        $availableUpdatesHex = "0x{0:X}" -f $availableUpdates
        Write-Host "Available Updates: $availableUpdatesHex"
    } else {
        Write-Host "Available Updates: Not Available"
    }
} catch {
    Write-Warning "AvailableUpdates registry key not found or inaccessible"
    $availableUpdates = $null
    Write-Host "Available Updates: Not Available"
}

# Registry: Servicing Key (3 values)

# 6. UEFICA2023Status
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing" -Name UEFICA2023Status -ErrorAction Stop
    $uefica2023Status = $regValue.UEFICA2023Status
    Write-Host "UEFI CA 2023 Status: $uefica2023Status"
} catch {
    Write-Warning "UEFICA2023Status registry key not found or inaccessible"
    $uefica2023Status = $null
    Write-Host "UEFI CA 2023 Status: Not Available"
}

# 7. UEFICA2023Capable
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing" -Name WindowsUEFICA2023Capable -ErrorAction Stop
    $uefica2023Capable = $regValue.WindowsUEFICA2023Capable
    Write-Host "UEFI CA 2023 Capable: $uefica2023Capable"
} catch {
    Write-Warning "UEFICA2023Capable registry key not found or inaccessible"
    $uefica2023Capable = $null
    Write-Host "UEFI CA 2023 Capable: Not Available"
}

# 8. UEFICA2023Error
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing" -Name UEFICA2023Error -ErrorAction Stop
    $uefica2023Error = $regValue.UEFICA2023Error
    Write-Host "UEFI CA 2023 Error: $uefica2023Error"
} catch {
    Write-Warning "UEFICA2023Error registry key not found or inaccessible"
    $uefica2023Error = $null
    Write-Host "UEFI CA 2023 Error: Not Available"
}

# Registry: Device Attributes (7 values)

# 9. OEMManufacturerName
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name OEMManufacturerName -ErrorAction Stop
    $oemManufacturerName = $regValue.OEMManufacturerName
    if ([string]::IsNullOrEmpty($oemManufacturerName)) {
        Write-Warning "OEMManufacturerName is empty"
        $oemManufacturerName = "Unknown"
    }
    Write-Host "OEM Manufacturer Name: $oemManufacturerName"
} catch {
    Write-Warning "OEMManufacturerName registry key not found or inaccessible"
    $oemManufacturerName = $null
    Write-Host "OEM Manufacturer Name: Not Available"
}

# 10. OEMModelSystemFamily
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name OEMModelSystemFamily -ErrorAction Stop
    $oemModelSystemFamily = $regValue.OEMModelSystemFamily
    if ([string]::IsNullOrEmpty($oemModelSystemFamily)) {
        Write-Warning "OEMModelSystemFamily is empty"
        $oemModelSystemFamily = "Unknown"
    }
    Write-Host "OEM Model System Family: $oemModelSystemFamily"
} catch {
    Write-Warning "OEMModelSystemFamily registry key not found or inaccessible"
    $oemModelSystemFamily = $null
    Write-Host "OEM Model System Family: Not Available"
}

# 11. OEMModelNumber
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name OEMModelNumber -ErrorAction Stop
    $oemModelNumber = $regValue.OEMModelNumber
    if ([string]::IsNullOrEmpty($oemModelNumber)) {
        Write-Warning "OEMModelNumber is empty"
        $oemModelNumber = "Unknown"
    }
    Write-Host "OEM Model Number: $oemModelNumber"
} catch {
    Write-Warning "OEMModelNumber registry key not found or inaccessible"
    $oemModelNumber = $null
    Write-Host "OEM Model Number: Not Available"
}

# 12. FirmwareVersion
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name FirmwareVersion -ErrorAction Stop
    $firmwareVersion = $regValue.FirmwareVersion
    if ([string]::IsNullOrEmpty($firmwareVersion)) {
        Write-Warning "FirmwareVersion is empty"
        $firmwareVersion = "Unknown"
    }
    Write-Host "Firmware Version: $firmwareVersion"
} catch {
    Write-Warning "FirmwareVersion registry key not found or inaccessible"
    $firmwareVersion = $null
    Write-Host "Firmware Version: Not Available"
}

# 13. FirmwareReleaseDate
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name FirmwareReleaseDate -ErrorAction Stop
    $firmwareReleaseDate = $regValue.FirmwareReleaseDate
    if ([string]::IsNullOrEmpty($firmwareReleaseDate)) {
        Write-Warning "FirmwareReleaseDate is empty"
        $firmwareReleaseDate = "Unknown"
    }
    Write-Host "Firmware Release Date: $firmwareReleaseDate"
} catch {
    Write-Warning "FirmwareReleaseDate registry key not found or inaccessible"
    $firmwareReleaseDate = $null
    Write-Host "Firmware Release Date: Not Available"
}

# 14. OSArchitecture
# PS Version: All | Admin: No | System Requirements: None
try {
    $osArchitecture = $env:PROCESSOR_ARCHITECTURE
    if ([string]::IsNullOrEmpty($osArchitecture)) {
        # Try registry fallback
        $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name OSArchitecture -ErrorAction Stop
        $osArchitecture = $regValue.OSArchitecture
    }
    if ([string]::IsNullOrEmpty($osArchitecture)) {
        Write-Warning "OSArchitecture could not be determined"
        $osArchitecture = "Unknown"
    }
    Write-Host "OS Architecture: $osArchitecture"
} catch {
    Write-Warning "Error retrieving OSArchitecture: $_"
    $osArchitecture = "Unknown"
    Write-Host "OS Architecture: $osArchitecture"
}

# 15. CanAttemptUpdateAfter (FILETIME)
# PS Version: All | Admin: May be required | System Requirements: None
try {
    $regValue = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\Servicing\DeviceAttributes" -Name CanAttemptUpdateAfter -ErrorAction Stop
    $canAttemptUpdateAfter = $regValue.CanAttemptUpdateAfter
    # Convert FILETIME to DateTime if it's a valid number
    if ($null -ne $canAttemptUpdateAfter -and $canAttemptUpdateAfter -is [long]) {
        try {
            $canAttemptUpdateAfter = [DateTime]::FromFileTime($canAttemptUpdateAfter)
        } catch {
            Write-Warning "Could not convert CanAttemptUpdateAfter FILETIME to DateTime"
        }
    }
    Write-Host "Can Attempt Update After: $canAttemptUpdateAfter"
} catch {
    Write-Warning "CanAttemptUpdateAfter registry key not found or inaccessible"
    $canAttemptUpdateAfter = $null
    Write-Host "Can Attempt Update After: Not Available"
}

# Event Logs: System Log (5 values)

# 16-20. Event Log queries
# PS Version: 3.0+ | Admin: May be required for System log | System Requirements: None
try {
    $allEventIds = @(1801, 1808)
    $events = @(Get-WinEvent -FilterHashtable @{LogName='System'; ID=$allEventIds} -MaxEvents 20 -ErrorAction Stop)

    if ($events.Count -eq 0) {
        Write-Warning "No Secure Boot events (1801/1808) found in System log"
        $latestEventId = $null
        $bucketId = $null
        $confidence = $null
        $event1801Count = 0
        $event1808Count = 0
        Write-Host "Latest Event ID: Not Available"
        Write-Host "Bucket ID: Not Available"
        Write-Host "Confidence: Not Available"
        Write-Host "Event 1801 Count: 0"
        Write-Host "Event 1808 Count: 0"
    } else {
        # 16. LatestEventId
        $latestEvent = $events | Sort-Object TimeCreated -Descending | Select-Object -First 1
        if ($null -eq $latestEvent) {
            Write-Warning "Could not determine latest event"
            $latestEventId = $null
            Write-Host "Latest Event ID: Not Available"
        } else {
            $latestEventId = $latestEvent.Id
            Write-Host "Latest Event ID: $latestEventId"
        }

        # 17. BucketID - Extracted from Event 1801/1808
        if ($null -ne $latestEvent -and $null -ne $latestEvent.Message) {
            if ($latestEvent.Message -match 'BucketId:\s*(.+)') {
                $bucketId = $matches[1].Trim()
                Write-Host "Bucket ID: $bucketId"
            } else {
                Write-Warning "BucketId not found in event message"
                $bucketId = $null
                Write-Host "Bucket ID: Not Found in Event"
            }
        } else {
            Write-Warning "Latest event or message is null, cannot extract BucketId"
            $bucketId = $null
            Write-Host "Bucket ID: Not Available"
        }

        # 18. Confidence - Extracted from Event 1801/1808
        if ($null -ne $latestEvent -and $null -ne $latestEvent.Message) {
            if ($latestEvent.Message -match 'BucketConfidenceLevel:\s*(.+)') {
                $confidence = $matches[1].Trim()
                Write-Host "Confidence: $confidence"
            } else {
                Write-Warning "Confidence level not found in event message"
                $confidence = $null
                Write-Host "Confidence: Not Found in Event"
            }
        } else {
            Write-Warning "Latest event or message is null, cannot extract Confidence"
            $confidence = $null
            Write-Host "Confidence: Not Available"
        }

        # 19. Event1801Count
        $event1801Array = @($events | Where-Object {$_.Id -eq 1801})
        $event1801Count = $event1801Array.Count
        Write-Host "Event 1801 Count: $event1801Count"

        # 20. Event1808Count
        $event1808Array = @($events | Where-Object {$_.Id -eq 1808})
        $event1808Count = $event1808Array.Count
        Write-Host "Event 1808 Count: $event1808Count"
    }
} catch {
    Write-Warning "Error retrieving event logs. May require administrator privileges: $_"
    $latestEventId = $null
    $bucketId = $null
    $confidence = $null
    $event1801Count = 0
    $event1808Count = 0
    Write-Host "Latest Event ID: Error"
    Write-Host "Bucket ID: Error"
    Write-Host "Confidence: Error"
    Write-Host "Event 1801 Count: 0"
    Write-Host "Event 1808 Count: 0"
}

# WMI/CIM Queries (4 values)

# 21. OSVersion
# PS Version: 3.0+ (use Get-WmiObject for 2.0) | Admin: No | System Requirements: None
try {
    $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
    if ($null -eq $osInfo -or [string]::IsNullOrEmpty($osInfo.Version)) {
        Write-Warning "Could not retrieve OS version"
        $osVersion = "Unknown"
    } else {
        $osVersion = $osInfo.Version
    }
    Write-Host "OS Version: $osVersion"
} catch {
    Write-Warning "Error retrieving OS version: $_"
    $osVersion = "Unknown"
    Write-Host "OS Version: $osVersion"
}

# 22. LastBootTime
# PS Version: 3.0+ (use Get-WmiObject for 2.0) | Admin: No | System Requirements: None
try {
    $osInfo = Get-CimInstance Win32_OperatingSystem -ErrorAction Stop
    if ($null -eq $osInfo -or $null -eq $osInfo.LastBootUpTime) {
        Write-Warning "Could not retrieve last boot time"
        $lastBootTime = $null
        Write-Host "Last Boot Time: Not Available"
    } else {
        $lastBootTime = $osInfo.LastBootUpTime
        Write-Host "Last Boot Time: $lastBootTime"
    }
} catch {
    Write-Warning "Error retrieving last boot time: $_"
    $lastBootTime = $null
    Write-Host "Last Boot Time: Not Available"
}

# 23. BaseBoardManufacturer
# PS Version: 3.0+ (use Get-WmiObject for 2.0) | Admin: No | System Requirements: None
try {
    $baseBoard = Get-CimInstance Win32_BaseBoard -ErrorAction Stop
    if ($null -eq $baseBoard -or [string]::IsNullOrEmpty($baseBoard.Manufacturer)) {
        Write-Warning "Could not retrieve baseboard manufacturer"
        $baseBoardManufacturer = "Unknown"
    } else {
        $baseBoardManufacturer = $baseBoard.Manufacturer
    }
    Write-Host "Baseboard Manufacturer: $baseBoardManufacturer"
} catch {
    Write-Warning "Error retrieving baseboard manufacturer: $_"
    $baseBoardManufacturer = "Unknown"
    Write-Host "Baseboard Manufacturer: $baseBoardManufacturer"
}

# 24. BaseBoardProduct
# PS Version: 3.0+ (use Get-WmiObject for 2.0) | Admin: No | System Requirements: None
try {
    $baseBoard = Get-CimInstance Win32_BaseBoard -ErrorAction Stop
    if ($null -eq $baseBoard -or [string]::IsNullOrEmpty($baseBoard.Product)) {
        Write-Warning "Could not retrieve baseboard product"
        $baseBoardProduct = "Unknown"
    } else {
        $baseBoardProduct = $baseBoard.Product
    }
    Write-Host "Baseboard Product: $baseBoardProduct"
} catch {
    Write-Warning "Error retrieving baseboard product: $_"
    $baseBoardProduct = "Unknown"
    Write-Host "Baseboard Product: $baseBoardProduct"}

Compréhension des événements et clés

Event ID : 1801 = Cet événement est une erreur qui indique que les certificats mis à jour n’ont pas été appliqués sur l’appareil. Il fournit des détails spécifiques à l’appareil, y compris ses attributs, ce qui permet de déterminer quels appareils nécessitent encore la mise à jour.
Event ID : 1808 = Cet événement est informatif et indique que l’appareil dispose des nouveaux certificats Secure Boot appliqués au firmware.

AvailableUpdates (REG_DWORD (bitmask) : Paramètres : 

  • ou non défini : aucune mise à jour de la clé de démarrage sécurisé n’est effectuée.
  • 0x5944 : déployer tous les certificats nécessaires et effectuer une mise à jour vers le gestionnaire de démarrage PCA2023 signé

HighConfidenceOptOut (REG_DWORD) : Paramètres : 

    • ou la clé n’existe pas – Accepter
    • – Refuser

    UEFICA2023Status (REG_SZ) : Paramètres : 

    • NotStarted : la mise à jour n’a pas encore été exécutée. (Initialement, le status est NotStarted)
    • InProgress : la mise à jour est activement en cours.
    • Mise à jour : la mise à jour s’est terminée avec succès.

    UEFICA2023Error (REG_DWORD) : Paramètres : Cette valeur reste en cas de réussite. Si le processus de mise à jour rencontre une erreur, UEFICA2023Error est défini sur un code d’erreur différent de zéro.

    WindowsUEFICA2023Capable (REG_DWORD) : Paramètres : 

    • 0 – ou la clé n’existe pas – le certificat « Windows UEFI CA 2023 » n’est pas dans la base de données
    • 1 – Le certificat « Windows UEFI CA 2023 » se trouve dans la base de données
    • 2 – Le certificat « Windows UEFI CA 2023 » se trouve dans la base de données et le système démarre à partir du gestionnaire de démarrage signé 2023

    Conclusion

    Restez connectés pour nos prochains blogs qui expliqueront en détail le processus de déploiement des certificats avec différentes méthodes.

    Python – VMWare Vcenter – Script d’inventaire

    Le script suivant interroge un ou plusieurs Vcenter VMWare pour construire un inventaire et l’exporter en csv/json

    Assurez-vous d’avoir Python installé, puis installez la dépendance nécessaire :

    Bash

    pip install pyvmomi
    

    #!/usr/bin/env python3
    """
    ================================================================================
    Script d'inventaire VMware vSphere Multi-vCenter
    ================================================================================
    
    Description:
        Ce script se connecte à un ou plusieurs serveurs vCenter/ESXi pour récupérer
        l'inventaire complet des machines virtuelles et des clusters. Il peut exporter
        les données en JSON ou CSV. En cas d'échec de connexion à un vCenter, le script
        continue avec les autres vCenters sans interruption.
    
    Fonctionnalités:
        - Connexion à un ou plusieurs vCenter/ESXi avec authentification
        - Gestion de la tolérance aux pannes (continue si un vCenter est indisponible)
        - Récupération des informations des VMs (nom, UUID, CPU, RAM, état, etc.)
        - Récupération des informations des clusters
        - Export en JSON et/ou CSV avec identification du vCenter source
        - Support des variables d'environnement pour les credentials
        - Rapport de connexion détaillé (succès/échecs)
    
    Usage:
        
        # Un ou plusieurs vCenters avec même credentials
        python vsphere_inventory.py \
            --servers vcenter1.example.com vcenter2.example.com vcenter3.example.com \
            --user administrator@vsphere.local --password MonMotDePasse
        
        
        # Export en JSON et CSV
        python vsphere_inventory.py \
            --servers vcenter1.example.com vcenter2.example.com \
            --output-json all_vms.json --output-csv all_vms.csv
        
    
    Arguments:
        --servers            Un ou Liste de serveurs vCenter/ESXi (espace ou virgule)
        --user, -u           Nom d'utilisateur vSphere (commun à tous)
        --password, -p       Mot de passe (demandé si non fourni)
        --port               Port de connexion (défaut: 443)
        --output-json        Fichier de sortie JSON (défaut: vsphere_inventory.json)
        --output-csv         Fichier de sortie CSV (optionnel)
        --no-verify-ssl      Désactiver la vérification SSL (non recommandé)
        --continue-on-error  Continuer même si tous les vCenters échouent (défaut: True)
    
    
    
    Exemples d'utilisation:
        # Inventaire de plusieurs vCenters avec mêmes credentials
        python vsphere_inventory.py \
            --servers vcenter1.example.com vcenter2.example.com \
            --user admin@vsphere.local \
            --output-json multi_vcenter.json
        
        
        # Sans vérification SSL (environnement de test)
        python vsphere_inventory.py \
            --servers 192.168.1.10 192.168.1.11 \
            --user root \
            --no-verify-ssl
    
    Dépendances:
        - pyvmomi>=8.0.0.1
        
        Installation:
        pip install pyvmomi
    
    Notes:
        - La connexion utilise SSL par défaut (port 443)
        - Les mots de passe ne sont jamais affichés dans les logs
        - Le script gère automatiquement la déconnexion à la fin
        - Compatible avec vCenter 6.x, 7.x et 8.x
        - Si un vCenter est inaccessible, le script continue avec les autres
        - Les erreurs de connexion sont loguées mais n'interrompent pas le processus
        - Le résumé final indique le nombre de connexions réussies/échouées
    
    ================================================================================
    """
    
    from pyVim.connect import SmartConnect, Disconnect
    from pyVmomi import vim
    import ssl
    import atexit
    import getpass
    import argparse
    import os
    from datetime import datetime
    import csv
    import json
    
    
    def get_obj(content, vimtype, name=None):
        """
        Récupère un objet vSphere par type et optionnellement par nom
        
        Args:
            content: ServiceInstance content
            vimtype: Type d'objet vim (ex: vim.VirtualMachine)
            name: Nom optionnel de l'objet
            
        Returns:
            obj: L'objet trouvé ou None
        """
        obj = None
        container = content.viewManager.CreateContainerView(
            content.rootFolder, [vimtype], True)
        
        if name:
            for c in container.view:
                if c.name == name:
                    obj = c
                    break
        else:
            obj = container.view
        
        container.Destroy()
        return obj
    
    
    def get_vm_info(vm, vcenter_source=None):
        """
        Récupère les informations d'une VM
        
        Args:
            vm: Objet vim.VirtualMachine
            vcenter_source: Nom du vCenter source (optionnel)
            
        Returns:
            dict: Dictionnaire contenant les infos de la VM
        """
        summary = vm.summary
        config = vm.config
        guest = vm.guest
        runtime = vm.runtime
        
        # Récupérer le nom de l'hôte ESXi
        host_name = runtime.host.name if runtime.host else None
        
        # Récupérer le cluster
        cluster_name = None
        if runtime.host and runtime.host.parent:
            if isinstance(runtime.host.parent, vim.ClusterComputeResource):
                cluster_name = runtime.host.parent.name
        
        # Récupérer le datacenter
        datacenter_name = None
        obj = vm
        while obj:
            if isinstance(obj, vim.Datacenter):
                datacenter_name = obj.name
                break
            try:
                obj = obj.parent
            except:
                break
        
        vm_info = {
            'vcenter_source': vcenter_source,
            'name': vm.name,
            'uuid': config.uuid if config else None,
            'instance_uuid': config.instanceUuid if config else None,
            'power_state': summary.runtime.powerState,
            'num_cpu': config.hardware.numCPU if config else None,
            'memory_mb': config.hardware.memoryMB if config else None,
            'guest_os': config.guestFullName if config else None,
            'ip_address': guest.ipAddress if guest else None,
            'hostname': guest.hostName if guest else None,
            'tools_status': guest.toolsStatus if guest else None,
            'tools_version': guest.toolsVersion if guest else None,
            'host': host_name,
            'cluster': cluster_name,
            'datacenter': datacenter_name,
            'annotation': config.annotation if config else None,
            'num_disks': len(config.hardware.device) if config else 0,
            'num_nics': sum(1 for dev in config.hardware.device if isinstance(dev, vim.vm.device.VirtualEthernetCard)) if config else 0,
        }
        
        return vm_info
    
    
    def get_cluster_info(cluster, vcenter_source=None):
        """
        Récupère les informations d'un cluster
        
        Args:
            cluster: Objet vim.ClusterComputeResource
            vcenter_source: Nom du vCenter source (optionnel)
            
        Returns:
            dict: Dictionnaire contenant les infos du cluster
        """
        # Récupérer le datacenter
        datacenter_name = None
        obj = cluster
        while obj:
            if isinstance(obj, vim.Datacenter):
                datacenter_name = obj.name
                break
            try:
                obj = obj.parent
            except:
                break
        
        summary = cluster.summary
        
        cluster_info = {
            'vcenter_source': vcenter_source,
            'name': cluster.name,
            'datacenter': datacenter_name,
            'num_hosts': summary.numHosts,
            'num_effective_hosts': summary.numEffectiveHosts,
            'total_cpu_mhz': summary.totalCpu,
            'total_memory_mb': summary.totalMemory / (1024 * 1024),
            'num_cpu_cores': summary.numCpuCores,
            'num_cpu_threads': summary.numCpuThreads,
            'effective_cpu_mhz': summary.effectiveCpu,
            'effective_memory_mb': summary.effectiveMemory,
            'overall_status': summary.overallStatus,
        }
        
        return cluster_info
    
    
    def connect_to_vsphere(server, user, password, port=443, verify_ssl=True):
        """
        Se connecte à vSphere
        
        Args:
            server: Nom d'hôte ou IP du serveur vCenter/ESXi
            user: Nom d'utilisateur
            password: Mot de passe
            port: Port de connexion (défaut: 443)
            verify_ssl: Vérifier le certificat SSL (défaut: True)
            
        Returns:
            tuple: (ServiceInstance, error_message) - si erreur, si=None et message d'erreur
        """
        context = None
        if not verify_ssl:
            context = ssl._create_unverified_context()
        
        try:
            si = SmartConnect(
                host=server,
                user=user,
                pwd=password,
                port=int(port),
                sslContext=context
            )
            atexit.register(Disconnect, si)
            return si, None
        except Exception as e:
            return None, str(e)
    
    
    def get_vcenter_inventory(si, vcenter_name):
        """
        Récupère l'inventaire complet d'un vCenter
        
        Args:
            si: ServiceInstance
            vcenter_name: Nom du vCenter
            
        Returns:
            dict: Inventaire avec VMs et clusters
        """
        content = si.RetrieveContent()
        
        # Récupérer les VMs
        vm_list = get_obj(content, vim.VirtualMachine)
        vms_info = []
        for vm in vm_list:
            vm_info = get_vm_info(vm, vcenter_source=vcenter_name)
            vms_info.append(vm_info)
        
        # Récupérer les clusters
        cluster_list = get_obj(content, vim.ClusterComputeResource)
        clusters_info = []
        for cluster in cluster_list:
            cluster_info = get_cluster_info(cluster, vcenter_source=vcenter_name)
            clusters_info.append(cluster_info)
        
        return {
            'status': 'success',
            'vms': vms_info,
            'clusters': clusters_info,
            'summary': {
                'total_vms': len(vms_info),
                'total_clusters': len(clusters_info),
                'vms_powered_on': sum(1 for vm in vms_info if vm['power_state'] == 'poweredOn'),
                'vms_powered_off': sum(1 for vm in vms_info if vm['power_state'] == 'poweredOff'),
            }
        }
    
    
    def export_to_json(data, filename):
        """Exporte les données en JSON"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        print(f"✓ Données exportées vers {filename}")
    
    
    def export_to_csv(all_vms, filename):
        """Exporte toutes les VMs de tous les vCenters en CSV"""
        if not all_vms:
            print("Aucune VM à exporter")
            return
        
        fieldnames = all_vms[0].keys()
        
        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(all_vms)
        
        print(f"✓ {len(all_vms)} VMs exportées vers {filename}")
    
    def export_clusters_to_csv(all_clusters, filename):
        """Exporte tous les clusters de tous les vCenters en CSV"""
        if not all_clusters:
            print("Aucun cluster à exporter")
            return
        
        fieldnames = all_clusters[0].keys()
        
        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(all_clusters)
        
        print(f"✓ {len(all_clusters)} clusters exportés vers {filename}")
    
    
    def export_clusters_to_csv(all_clusters, filename):
        """Exporte tous les clusters de tous les vCenters en CSV"""
        if not all_clusters:
            print("Aucun cluster à exporter")
            return
        
        fieldnames = all_clusters[0].keys()
        
        with open(filename, 'w', newline='', encoding='utf-8') as f:
            writer = csv.DictWriter(f, fieldnames=fieldnames)
            writer.writeheader()
            writer.writerows(all_clusters)
        
        print(f"✓ {len(all_clusters)} clusters exportés vers {filename}")
    
    
    
    def main():
        parser = argparse.ArgumentParser(
            description='Récupérer l\'inventaire VMware vSphere Multi-vCenter (VMs et Clusters)',
            formatter_class=argparse.RawDescriptionHelpFormatter
            
        )
        
        parser.add_argument('--servers', nargs='+',
                            help='Un ou Liste de serveurs vCenter/ESXi')
        parser.add_argument('--user', '-u',
                            default=os.environ.get('VSPHERE_USER'),
                            help='Nom d\'utilisateur')
        parser.add_argument('--password', '-p',
                            default=os.environ.get('VSPHERE_PASSWORD'),
                            help='Mot de passe (sera demandé si non fourni)')
        parser.add_argument('--port',
                            default=os.environ.get('VSPHERE_PORT', 443),
                            help='Port de connexion (défaut: 443)')
        parser.add_argument('--output-json',
                            default='vsphere_inventory.json',
                            help='Fichier de sortie JSON (défaut: vsphere_inventory.json)')
        parser.add_argument('--output-csv',
                            help='Fichier de sortie CSV pour les VMs (optionnel)')
        parser.add_argument('--output-clusters-csv',
                            help='Fichier de sortie CSV pour les clusters (optionnel)')
        parser.add_argument('--no-verify-ssl',
                            action='store_true',
                            help='Désactiver la vérification SSL')
        parser.add_argument('--continue-on-error',
                            action='store_true',
                            default=True,
                            help='Continuer même si connexion échoue (défaut: True)')
        
        args = parser.parse_args()
        
        # Déterminer la liste des vCenters
        vcenter_configs = []
        
        # Vérifier si --servers est fourni
        if not args.servers:
            # Essayer la variable d'environnement
            servers_env = os.environ.get('VSPHERE_SERVERS', os.environ.get('VSPHERE_SERVER'))
            if servers_env:
                servers = [s.strip() for s in servers_env.split(',')]
                print(f"ℹ Utilisation de VSPHERE_SERVERS depuis l'environnement: {', '.join(servers)}")
            else:
                print("❌ ERREUR: Aucun serveur vCenter spécifié")
                print("\nVeuillez fournir au moins un serveur vCenter via:")
                print("  1. Option --servers : python vsphere_inventory.py --servers vcenter1.example.com")
                print("  2. Variable d'environnement VSPHERE_SERVERS : export VSPHERE_SERVERS=vcenter1.example.com")
                print("\nExemple complet:")
                print("  python vsphere_inventory.py --servers vc1.example.com vc2.example.com --user admin@vsphere.local")
                parser.print_help()
                return 1
        else:
            servers = []
            for server_arg in args.servers:
                # Permettre séparation par virgule ou espace
                servers.extend([s.strip() for s in server_arg.split(',')])
        
        # Demander credentials si nécessaire
        user = args.user
        if not user:
            user = input("Nom d'utilisateur vSphere: ")
        
        password = args.password
        if not password:
            password = getpass.getpass("Mot de passe: ")
        
        for server in servers:
            vcenter_configs.append({
                'server': server,
                'user': user,
                'password': password,
                'port': args.port,
                'verify_ssl': not args.no_verify_ssl
            })
        
        
        print("="*80)
        print("INVENTAIRE VMWARE VSPHERE MULTI-VCENTER")
        print("="*80)
        print(f"Nombre de vCenter(s): {len(vcenter_configs)}")
        print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print()
        
        # Collecter les inventaires
        results = {}
        all_vms = []
        all_clusters = []
        successful_connections = 0
        failed_connections = 0
        
        for i, config in enumerate(vcenter_configs, 1):
            server = config['server']
            print(f"[{i}/{len(vcenter_configs)}] Connexion à {server}...")
            
            si, error = connect_to_vsphere(
                server=config['server'],
                user=config['user'],
                password=config['password'],
                port=config.get('port', 443),
                verify_ssl=config.get('verify_ssl', True)
            )
            
            if si:
                print(f"  ✓ Connecté à {server}")
                successful_connections += 1
                
                try:
                    print(f"  → Récupération de l'inventaire...")
                    inventory = get_vcenter_inventory(si, server)
                    results[server] = inventory
                    
                    # Ajouter aux listes globales
                    all_vms.extend(inventory['vms'])
                    all_clusters.extend(inventory['clusters'])
                    
                    print(f"  ✓ {inventory['summary']['total_vms']} VMs, {inventory['summary']['total_clusters']} clusters")
                
                except Exception as e:
                    print(f"  ✗ Erreur lors de la récupération: {e}")
                    results[server] = {
                        'status': 'failed',
                        'error': f"Erreur lors de la récupération: {str(e)}"
                    }
                    failed_connections += 1
            else:
                print(f"  ✗ Échec de connexion: {error}")
                results[server] = {
                    'status': 'failed',
                    'error': error
                }
                failed_connections += 1
            
            print()
        
        # Vérifier si au moins une connexion a réussi
        if successful_connections == 0:
            print("✗ ERREUR: Aucune connexion réussie")
            if not args.continue_on_error:
                return 1
            print("  Mode continue-on-error activé, export des données disponibles...")
        
        # Préparer les données finales
        data = {
            'timestamp': datetime.now().isoformat(),
            'vcenters': results,
            'summary': {
                'total_vcenters': len(vcenter_configs),
                'successful_connections': successful_connections,
                'failed_connections': failed_connections,
                'total_vms': len(all_vms),
                'total_clusters': len(all_clusters),
                'vms_powered_on': sum(1 for vm in all_vms if vm.get('power_state') == 'poweredOn'),
                'vms_powered_off': sum(1 for vm in all_vms if vm.get('power_state') == 'poweredOff'),
            }
        }
        
        # Export
        print("="*80)
        print("EXPORT DES DONNÉES")
        print("="*80)
        
        export_to_json(data, args.output_json)
        
        if args.output_csv and all_vms:
            export_to_csv(all_vms, args.output_csv)
        
        if args.output_clusters_csv and all_clusters:
            export_clusters_to_csv(all_clusters, args.output_clusters_csv)
        
        if args.output_clusters_csv and all_clusters:
            export_clusters_to_csv(all_clusters, args.output_clusters_csv)
        
        # Résumé final
        print()
        print("="*80)
        print("RÉSUMÉ")
        print("="*80)
        print(f"vCenters interrogés: {len(vcenter_configs)}")
        print(f"  ✓ Connexions réussies: {successful_connections}")
        print(f"  ✗ Connexions échouées: {failed_connections}")
        print()
        print(f"VMs totales: {len(all_vms)}")
        print(f"  - Allumées: {data['summary']['vms_powered_on']}")
        print(f"  - Éteintes: {data['summary']['vms_powered_off']}")
        print(f"Clusters: {len(all_clusters)}")
        print()
        
        if failed_connections > 0:
            print("⚠ Serveurs en échec:")
            for server, result in results.items():
                if result['status'] == 'failed':
                    print(f"  - {server}: {result['error']}")
            print()
        
        print("✓ Terminé")
        
        return 0 if successful_connections > 0 else 1
    
    
    if __name__ == '__main__':
        exit(main())
    
    

    🐍 Interroger l’API de Rundeck avec Python : Un Guide Rapide

    Rundeck est une plateforme pour l’automatisation des opérations et la gestion des exécutions. Bien que vous puissiez tout faire via l’interface web, la vraie puissance vient de son API, qui vous permet d’intégrer Rundeck dans vos scripts et vos systèmes existants.

    Ce guide rapide vous montrera comment utiliser la bibliothèque standard de Python, requests, pour interagir avec l’API de Rundeck.


    Prérequis

    • Un serveur Rundeck en cours d’exécution.
    • Un Jeton d’API (API Token) créé dans les paramètres de votre profil utilisateur Rundeck.
    • Python et la bibliothèque requests installés (pip install requests).

    Étape 1 : Définir les Constantes de Connexion

    Nous allons commencer par définir l’URL de base de votre instance Rundeck et l’en-tête d’autorisation qui contiendra votre Jeton d’API

    import requests
    import json
    
    # Remplacez par l'URL de votre serveur Rundeck
    RUNDECK_URL = "http://votreserveur:4440" 
    
    # Remplacez par votre véritable Jeton d'API
    API_TOKEN = "votre_jeton_api_secret"
    
    HEADERS = {
        'X-Rundeck-Auth-Token': API_TOKEN,
        'Accept': 'application/json'  # Nous voulons une réponse au format JSON
    }
    

    Étape 2 : Interroger l’API

    L’une des requêtes les plus courantes est de lister tous les projets. L’API de Rundeck a généralement un chemin qui inclut la version d’API que vous ciblez (souvent api/40 ou plus).

    Pour lister les projets, le chemin d’accès est souvent /api/40/projects.

    Exemple 1 : Lister les Projets

    def lister_projets():
        # Chemin complet de l'API pour lister les projets
        endpoint = f"{RUNDECK_URL}/api/40/projects"
        
        print(f"-> Requête à : {endpoint}")
        
        try:
            # Envoi de la requête GET
            reponse = requests.get(endpoint, headers=HEADERS)
            reponse.raise_for_status()  # Lève une exception pour les codes d'erreur (4xx ou 5xx)
    
            # La réponse est une liste de projets au format JSON
            projets = reponse.json()
            
            print(f"\n✅ Nombre de projets trouvés : {len(projets)}")
            
            for projet in projets:
                print(f"- Nom : {projet['name']} (Description : {projet.get('description', 'N/A')})")
                
        except requests.exceptions.RequestException as e:
            print(f"❌ Erreur lors de la requête : {e}")
    
    lister_projets()
    

    Exemple 2 : Exécuter un Job

    Pour une opération plus complexe comme l’exécution d’un job, vous utilisez généralement une méthode POST et vous devez cibler l’ID du job.

    # Remplacez par l'ID de votre job
    JOB_ID = "1a2b3c4d-5e6f-7890-abcd-ef0123456789" 
    
    def executer_job():
        # Chemin de l'API pour l'exécution d'un job
        endpoint = f"{RUNDECK_URL}/api/40/job/{JOB_ID}/run"
        
        print(f"-> Exécution du job ID: {JOB_ID}")
        
        try:
            # Requête POST sans corps (par défaut, il n'y a pas d'options)
            reponse = requests.post(endpoint, headers=HEADERS)
            reponse.raise_for_status()
    
            resultat = reponse.json()
            
            # Le résultat contient l'ID de l'exécution
            execution_id = resultat['id'] 
            
            print(f"\n✅ Job lancé avec succès !")
            print(f"   ID d'exécution : {execution_id}")
            print(f"   URL de suivi : {resultat.get('permalink')}")
            
        except requests.exceptions.RequestException as e:
            print(f"❌ Erreur lors de l'exécution du job : {e}")
    
    # executer_job() # Décommentez pour tester l'exécution
    

    Conclusion

    En utilisant la simple bibliothèque requests, vous pouvez facilement automatiser et interagir avec votre environnement Rundeck en quelques lignes de Python. Que ce soit pour récupérer des métriques, déclencher des workflows ou gérer des ressources.