faitza.com
faitza.com
> blog_tech

Automatiser GLPI avec PowerShell et l'API REST

Calcul... powershell glpi rest-api itsm automation

Introduction

Faitza_GLPI.ps1 regroupe les fonctions PowerShell pour interagir avec GLPI via son API REST native (disponible depuis GLPI 9.1). L'objectif : supprimer les actions manuelles répétitives dans l'interface web — création de tickets depuis des alertes monitoring, ajout automatique de suivis, export de données pour des tableaux de bord, ou lookup d'utilisateurs pour l'assignation automatique.

L'API REST GLPI expose l'ensemble des itemtypes (Ticket, User, Computer, Software…) via des endpoints CRUD standard. Les fonctions du module se concentrent sur le périmètre helpdesk : tickets, suivis (ITILFollowup), et recherche d'utilisateurs.

Prérequis

Un user_token GLPI est nécessaire (Préférences utilisateur → API). L'accès à l'API REST doit être activé dans Administration → Configuration générale → API. L'URL de base ressemble à https://glpi.contoso.com/apirest.php/.

Authentification REST

L'authentification GLPI se fait en deux étapes : d'abord un appel à init_session avec le user_token, qui retourne un session_token. Ce token de session est ensuite passé dans chaque requête suivante. Il expire après inactivité (configurable dans GLPI, défaut : 1 heure).

function Get-Faitza_GLPI_Headers {
    <#
    .SYNOPSIS
        Authentifie à l'API REST GLPI et retourne les headers de session.
    .PARAMETER BaseUrl
        URL de base de l'API (ex: "https://glpi.contoso.com/apirest.php").
        Si absent, utilise $env:GLPI_BASE_URL.
    .PARAMETER UserToken
        Token utilisateur GLPI (Préférences → API → Régénérer).
        Si absent, utilise $env:GLPI_USER_TOKEN.
    .OUTPUTS
        Hashtable avec Session-Token et Content-Type prêts pour Invoke-RestMethod.
    #>
    [CmdletBinding()]
    param(
        [string]$BaseUrl   = $env:GLPI_BASE_URL,
        [string]$UserToken = $env:GLPI_USER_TOKEN
    )

    if (-not $BaseUrl)   { throw "GLPI_BASE_URL non défini." }
    if (-not $UserToken) { throw "GLPI_USER_TOKEN non défini." }

    $initUrl = "$BaseUrl/initSession"
    $initHeaders = @{
        Authorization  = "user_token $UserToken"
        "Content-Type" = "application/json"
    }

    try {
        $response = Invoke-RestMethod -Method Get -Uri $initUrl -Headers $initHeaders
    } catch {
        throw "Échec init_session GLPI : $($_.Exception.Message)"
    }

    Write-Verbose "Session GLPI ouverte : $($response.session_token)"

    return @{
        "Session-Token" = $response.session_token
        "Content-Type"  = "application/json"
        "App-Token"     = $env:GLPI_APP_TOKEN  # Optionnel, requis selon config GLPI
    }
}

# Fermer la session proprement en fin de script
function Close-Faitza_GLPI_Session {
    param(
        [hashtable]$Headers,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )
    Invoke-RestMethod -Method Get -Uri "$BaseUrl/killSession" -Headers $Headers | Out-Null
    Write-Verbose "Session GLPI fermée."
}

# Exemple d'utilisation avec try/finally
$glpiHeaders = Get-Faitza_GLPI_Headers
try {
    # ... appels API ...
} finally {
    Close-Faitza_GLPI_Session -Headers $glpiHeaders
}
App-Token vs User-Token

GLPI distingue deux niveaux d'authentification. Le user_token identifie l'utilisateur. L'App-Token (header App-Token) identifie l'application cliente — requis si l'option "Activer la vérification de l'App-Token" est cochée dans la configuration API. Sans lui, les appels retournent une erreur 401 peu explicite. Stockez les deux dans des variables d'environnement séparées.

Créer un ticket

New-Faitza_GLPI_Ticket crée un ticket d'incident ou de demande dans GLPI. Les champs principaux (titre, contenu, urgence, catégorie, technicien assigné) sont paramétrables. La fonction retourne l'ID du ticket créé pour un chaînage éventuel (ajout de suivi, pièce jointe…).

function New-Faitza_GLPI_Ticket {
    <#
    .SYNOPSIS
        Crée un ticket dans GLPI via l'API REST.
    .PARAMETER Headers
        Headers de session (issus de Get-Faitza_GLPI_Headers).
    .PARAMETER Titre
        Titre du ticket (name).
    .PARAMETER Contenu
        Description détaillée. Accepte du HTML basique (GLPI utilise un éditeur riche).
    .PARAMETER Type
        1 = Incident, 2 = Demande. Défaut : 1.
    .PARAMETER Urgence
        1 (Très faible) à 5 (Très haute). Défaut : 3 (Moyenne).
    .PARAMETER CategorieId
        ID de la catégorie ITIL (itilcategories_id).
    .PARAMETER AssigneeId
        ID de l'utilisateur GLPI auquel assigner le ticket.
    .PARAMETER GroupId
        ID du groupe auquel assigner le ticket (alternatif à AssigneeId).
    .PARAMETER BaseUrl
        URL de base de l'API GLPI.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Headers,
        [Parameter(Mandatory)][string]$Titre,
        [Parameter(Mandatory)][string]$Contenu,
        [ValidateSet(1, 2)][int]$Type     = 1,
        [ValidateRange(1, 5)][int]$Urgence = 3,
        [int]$CategorieId,
        [int]$AssigneeId,
        [int]$GroupId,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )

    # Corps du ticket
    $ticketBody = @{
        name             = $Titre
        content          = $Contenu
        type             = $Type
        urgency          = $Urgence
        status           = 1   # 1 = Nouveau
        entities_id      = 0   # Entité racine (0) — adapter si multi-entités
    }

    if ($CategorieId) { $ticketBody.itilcategories_id = $CategorieId }

    # Assignation technicien ou groupe
    $actors = @()
    if ($AssigneeId) {
        $actors += @{ users_id = $AssigneeId; type = 2 }  # type 2 = Assigned
    }
    if ($GroupId) {
        $actors += @{ groups_id = $GroupId; type = 2 }
    }

    $body = @{
        input = $ticketBody
    }

    $uri    = "$BaseUrl/Ticket"
    $result = Invoke-RestMethod -Method Post -Uri $uri -Headers $Headers -Body ($body | ConvertTo-Json -Depth 5)

    $ticketId = $result.id
    Write-Host "[OK] Ticket créé : #$ticketId — $Titre" -ForegroundColor Green

    # Assignation via Ticket_User si des acteurs ont été définis
    foreach ($actor in $actors) {
        $actorBody = @{ input = ($actor + @{ tickets_id = $ticketId }) }
        Invoke-RestMethod -Method Post `
            -Uri     "$BaseUrl/Ticket_User" `
            -Headers $Headers `
            -Body    ($actorBody | ConvertTo-Json) | Out-Null
    }

    return $ticketId
}

# Créer un incident depuis une alerte monitoring
$headers  = Get-Faitza_GLPI_Headers
$userId   = Get-Faitza_GLPI_UserEmailByName -Headers $headers -Email "[email protected]"

$id = New-Faitza_GLPI_Ticket `
    -Headers     $headers `
    -Titre       "[ALERTE] Serveur SRV-WEB-01 — CPU > 95% depuis 10 min" `
    -Contenu     "Alerte déclenchée automatiquement par le monitoring Zabbix.
Hôte : SRV-WEB-01
Métrique : system.cpu.util > 95%
Durée : 10 minutes
Heure : $(Get-Date -Format 'yyyy-MM-dd HH:mm')" ` -Type 1 ` -Urgence 4 ` -AssigneeId $userId
Valeurs d'énumération GLPI

Les champs comme urgency, impact, priority et status utilisent des entiers codifiés. Status : 1 Nouveau, 2 En cours (assigné), 3 En cours (planifié), 4 En attente, 5 Résolu, 6 Clos. Priority est généralement calculée automatiquement par GLPI en fonction de Urgency × Impact.

Lire les tickets

Deux fonctions complémentaires : Get-Faitza_GLPI_Ticket liste les tickets ouverts selon des critères simples, tandis que Get-Faitza_GLPI_TicketById retourne le détail complet d'un ticket par son identifiant — utile pour vérifier l'état après création ou pour extraire le contenu d'un ticket existant.

function Get-Faitza_GLPI_TicketById {
    <#
    .SYNOPSIS
        Retourne le détail complet d'un ticket GLPI par son ID.
    .PARAMETER Headers
        Headers de session GLPI.
    .PARAMETER TicketId
        ID numérique du ticket.
    .PARAMETER WithFollowups
        Si présent, inclut les suivis (ITILFollowup) dans la réponse.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Headers,
        [Parameter(Mandatory)][int]$TicketId,
        [switch]$WithFollowups,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )

    $ticket = Invoke-RestMethod -Method Get -Uri "$BaseUrl/Ticket/$TicketId" -Headers $Headers

    # Décodage du contenu HTML
    $ticket.content = ConvertFrom-Faitza_GLPI_Content -HtmlContent $ticket.content

    if ($WithFollowups) {
        $followups = Invoke-RestMethod `
            -Method Get `
            -Uri    "$BaseUrl/Ticket/$TicketId/ITILFollowup" `
            -Headers $Headers
        $ticket | Add-Member -NotePropertyName Followups -NotePropertyValue $followups
    }

    return $ticket
}

function Get-Faitza_GLPI_Ticket {
    <#
    .SYNOPSIS
        Liste les tickets GLPI selon des critères : statut, assigné, catégorie.
    .PARAMETER Headers
        Headers de session GLPI.
    .PARAMETER Status
        Filtre par statut (1=Nouveau, 2=En cours, 4=En attente, 5=Résolu, 6=Clos).
        Si absent, retourne tous les statuts sauf Clos.
    .PARAMETER AssigneeId
        Filtre par ID de technicien assigné.
    .PARAMETER Limit
        Nombre maximum de tickets à retourner. Défaut : 50.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Headers,
        [int[]]$Status,
        [int]$AssigneeId,
        [int]$Limit   = 50,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )

    # Construction des critères de recherche (SearchEngine GLPI)
    $criteria = @()
    $i = 0

    if ($Status) {
        foreach ($s in $Status) {
            $criteria += "criteria[$i][field]=12&criteria[$i][searchtype]=equals&criteria[$i][value]=$s"
            if ($i -gt 0) { $criteria[-1] = "criteria[$i][link]=OR&" + $criteria[-1] }
            $i++
        }
    } else {
        # Par défaut : tout sauf Clos (6)
        $criteria += "criteria[$i][field]=12&criteria[$i][searchtype]=notequals&criteria[$i][value]=6"
        $i++
    }

    if ($AssigneeId) {
        $criteria += "criteria[$i][link]=AND&criteria[$i][field]=5&criteria[$i][searchtype]=equals&criteria[$i][value]=$AssigneeId"
        $i++
    }

    $queryString = ($criteria -join "&") + "&range=0-$($Limit - 1)"
    $uri = "$BaseUrl/search/Ticket?$queryString&forcedisplay[0]=1&forcedisplay[1]=2&forcedisplay[2]=12&forcedisplay[3]=15&forcedisplay[4]=5"

    $result = Invoke-RestMethod -Method Get -Uri $uri -Headers $Headers
    return $result.data
}

# Lire un ticket par ID avec ses suivis
$ticket = Get-Faitza_GLPI_TicketById -Headers $headers -TicketId 1337 -WithFollowups
Write-Host "Titre : $($ticket.name)"
Write-Host "Statut : $($ticket.status)"
$ticket.Followups | ForEach-Object { Write-Host "→ $($_.content)" }

# Lister les tickets nouveaux assignés à un technicien
Get-Faitza_GLPI_Ticket -Headers $headers -Status @(1, 2) -AssigneeId $userId |
    Format-Table id, name, status, date -AutoSize

Ajouter un suivi

Add-Faitza_GLPI_TicketFollowup ajoute un commentaire/suivi (ITILFollowup) à un ticket existant. C'est l'action la plus fréquente dans les workflows automatisés : enregistrer le résultat d'une action corrective, notifier d'une progression, ou déclencher un changement de statut en même temps que l'ajout du commentaire.

function Add-Faitza_GLPI_TicketFollowup {
    <#
    .SYNOPSIS
        Ajoute un suivi (commentaire) à un ticket GLPI.
    .PARAMETER Headers
        Headers de session GLPI.
    .PARAMETER TicketId
        ID du ticket cible.
    .PARAMETER Contenu
        Texte du suivi. Accepte du HTML.
    .PARAMETER Prive
        Si $true, le suivi est marqué comme privé (visible techniciens uniquement).
    .PARAMETER NouveauStatut
        Si fourni, change le statut du ticket en même temps que l'ajout du suivi.
        Valeurs : 1 Nouveau, 2 En cours, 4 En attente, 5 Résolu, 6 Clos.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Headers,
        [Parameter(Mandatory)][int]$TicketId,
        [Parameter(Mandatory)][string]$Contenu,
        [bool]$Prive          = $false,
        [int]$NouveauStatut,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )

    $followupBody = @{
        input = @{
            items_id    = $TicketId
            itemtype    = "Ticket"
            content     = $Contenu
            is_private  = [int]$Prive
            requesttypes_id = 1   # 1 = Email (source du suivi)
        }
    }

    $result = Invoke-RestMethod `
        -Method Post `
        -Uri     "$BaseUrl/ITILFollowup" `
        -Headers $Headers `
        -Body    ($followupBody | ConvertTo-Json -Depth 5)

    Write-Host "[OK] Suivi ajouté au ticket #$TicketId (ID suivi : $($result.id))" -ForegroundColor Green

    # Changer le statut si demandé
    if ($NouveauStatut) {
        $statusBody = @{ input = @{ id = $TicketId; status = $NouveauStatut } }
        Invoke-RestMethod `
            -Method Put `
            -Uri     "$BaseUrl/Ticket/$TicketId" `
            -Headers $Headers `
            -Body    ($statusBody | ConvertTo-Json) | Out-Null

        $statusLabel = @{1="Nouveau";2="En cours";4="En attente";5="Résolu";6="Clos"}[$NouveauStatut]
        Write-Host "[OK] Statut du ticket #$TicketId → $statusLabel" -ForegroundColor Cyan
    }

    return $result.id
}

# Ajouter un suivi de résolution et clore le ticket
Add-Faitza_GLPI_TicketFollowup `
    -Headers       $headers `
    -TicketId      1337 `
    -Contenu       "Action corrective appliquée : redémarrage du service IIS à 14h32.
CPU retombé à 12% — surveillance maintenue pendant 30 min sans récidive.
Ticket clos." ` -NouveauStatut 6 # Suivi privé (note interne technicien) Add-Faitza_GLPI_TicketFollowup ` -Headers $headers ` -TicketId 1337 ` -Contenu "RCA en cours — suspicion de fuite mémoire dans l'application .NET. Ticket de suivi ouvert : #1342." ` -Prive $true

Export pour reporting

Get-Faitza_GLPI_AllTickets exporte l'intégralité des tickets (ou un sous-ensemble filtré par dates) dans un tableau plat, prêt pour un export CSV ou une alimentation de tableau de bord. La pagination est gérée automatiquement pour les bases de plusieurs milliers de tickets.

function Get-Faitza_GLPI_AllTickets {
    <#
    .SYNOPSIS
        Exporte tous les tickets GLPI dans un tableau plat pour reporting.
    .PARAMETER Headers
        Headers de session GLPI.
    .PARAMETER DateDebut
        Filtre — tickets créés après cette date (format "yyyy-MM-dd").
    .PARAMETER DateFin
        Filtre — tickets créés avant cette date.
    .PARAMETER PageSize
        Nombre de tickets par page d'API. Défaut : 100. Max recommandé : 200.
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][hashtable]$Headers,
        [datetime]$DateDebut,
        [datetime]$DateFin,
        [int]$PageSize = 100,
        [string]$BaseUrl = $env:GLPI_BASE_URL
    )

    # Champs à retourner (IDs SearchEngine Ticket)
    # 1=ID, 2=Titre, 12=Statut, 15=DateOuverture, 17=DateRésolution,
    # 5=AssignéTechnicien, 7=AssignéGroupe, 10=Urgence, 11=Impact, 13=Priorité,
    # 9=Catégorie, 83=Demandeur
    $displayFields = @(1, 2, 9, 10, 11, 12, 13, 15, 17, 5, 7, 83)
    $displayQuery  = ($displayFields | ForEach-Object { "forcedisplay[]=$_" }) -join "&"

    $criteriaQuery = ""
    $ci = 0

    if ($DateDebut) {
        $criteriaQuery += "criteria[$ci][field]=15&criteria[$ci][searchtype]=morethan&criteria[$ci][value]=$($DateDebut.ToString('yyyy-MM-dd'))&"
        $ci++
    }
    if ($DateFin) {
        $link = if ($ci -gt 0) { "criteria[$ci][link]=AND&" } else { "" }
        $criteriaQuery += "${link}criteria[$ci][field]=15&criteria[$ci][searchtype]=lessthan&criteria[$ci][value]=$($DateFin.ToString('yyyy-MM-dd'))&"
        $ci++
    }

    $allTickets = @()
    $offset     = 0

    do {
        $rangeEnd = $offset + $PageSize - 1
        $uri = "$BaseUrl/search/Ticket?$criteriaQuery$displayQuery&range=$offset-$rangeEnd&order=DESC&sort=15"

        Write-Verbose "Récupération tickets $offset → $rangeEnd..."
        $page = Invoke-RestMethod -Method Get -Uri $uri -Headers $Headers

        if (-not $page.data) { break }

        $allTickets += $page.data | ForEach-Object {
            [PSCustomObject]@{
                Id               = $_["1"]
                Titre            = $_["2"]
                Categorie        = $_["9"]
                Urgence          = $_["10"]
                Impact           = $_["11"]
                Statut           = $_["12"]
                Priorite         = $_["13"]
                DateOuverture    = $_["15"]
                DateResolution   = $_["17"]
                Technicien       = $_["5"]
                Groupe           = $_["7"]
                Demandeur        = $_["83"]
            }
        }

        $offset += $PageSize
        $total   = $page.totalcount

    } while ($offset -lt $total)

    Write-Host "[OK] $($allTickets.Count) tickets récupérés (total : $total)." -ForegroundColor Green
    return $allTickets
}

# Export mensuel pour tableau de bord
$debut = Get-Date -Year 2025 -Month 5 -Day 1
$fin   = Get-Date -Year 2025 -Month 6 -Day 1

$tickets = Get-Faitza_GLPI_AllTickets -Headers $headers -DateDebut $debut -DateFin $fin

# Statistiques rapides
$tickets | Group-Object Statut | Select-Object Name, Count | Sort-Object Count -Descending

# Export CSV
$tickets | Export-Csv ".\glpi_mai2025.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ";"
Performance sur gros volumes

Sur une base GLPI de plus de 10 000 tickets, l'export complet peut prendre plusieurs minutes. Préférez filtrer par plage de dates ou par statut. GLPI limite par défaut les résultats de recherche à 10 000 items — au-delà, envisagez une extraction directe en base via requête SQL (si vous avez accès) ou fragmentez les exports par mois.

Pièges classiques

Retour d'expérience sur les erreurs les plus fréquentes lors de l'intégration avec l'API REST GLPI, et les contournements éprouvés.

# ─── Piège 1 : Session expirée silencieusement ───────────────────────────────
# GLPI ne retourne pas toujours une erreur 401 claire sur session expirée.
# Parfois c'est un 200 avec un corps d'erreur JSON. Toujours tester la réponse.
function Test-Faitza_GLPI_Session {
    param([hashtable]$Headers, [string]$BaseUrl = $env:GLPI_BASE_URL)
    try {
        $r = Invoke-RestMethod -Method Get -Uri "$BaseUrl/getFullSession" -Headers $Headers
        return $null -ne $r.session
    } catch { return $false }
}

# En début de script critique : valider la session, la renouveler si besoin
if (-not (Test-Faitza_GLPI_Session -Headers $headers)) {
    Write-Warning "Session expirée — réauthentification..."
    $headers = Get-Faitza_GLPI_Headers
}

# ─── Piège 2 : HTML dans le contenu des tickets ──────────────────────────────
# GLPI stocke le contenu en HTML (éditeur TinyMCE). Tout texte brut saisi via
# l'API doit être encodé pour éviter les injections ou l'affichage cassé.
# Pour envoyer du texte brut propre :
$contenuBrut = "Alerte : disque /var à 95% sur srv-db-01."
$contenuHtml = $contenuBrut -replace "`n", "
" -replace "&", "&" -replace "<(?!br)", "<" # ─── Piège 3 : Champs inconnus retournés avec leur ID numérique ────────────── # Le SearchEngine retourne les données sous forme $_[""]. # Si un champ ID change selon la version GLPI, l'export se casse sans message d'erreur. # Toujours documenter les IDs utilisés et les vérifier lors des mises à jour GLPI. # ─── Piège 4 : Entité GLPI (entities_id) ────────────────────────────────────── # En mode multi-entités, un ticket créé dans l'entité 0 (racine) peut ne pas être # visible depuis les entités filles selon la configuration de visibilité. # Toujours spécifier l'entities_id correspondant au périmètre cible. # Lister les entités disponibles : Invoke-RestMethod -Method Get -Uri "$($env:GLPI_BASE_URL)/Entity" -Headers $headers | Select-Object -ExpandProperty data | Format-Table id, name # ─── Piège 5 : Ticket_User vs Ticket_Group — itemtype différents ───────────── # L'assignation de groupe se fait sur l'itemtype "Ticket_Group", pas "Ticket_User". # Type d'acteur : 1 = Demandeur, 2 = Assigné, 3 = Observateur. $groupAssign = @{ input = @{ tickets_id = 1337 groups_id = 12 # ID du groupe GLPI type = 2 # Assigné } } Invoke-RestMethod -Method Post ` -Uri "$($env:GLPI_BASE_URL)/Ticket_Group" ` -Headers $headers ` -Body ($groupAssign | ConvertTo-Json)
Ne pas stocker le Session-Token

Le Session-Token retourné par init_session est lié à la session HTTP de l'utilisateur GLPI. Il ne doit pas être mis en cache dans un fichier ou une variable d'environnement persistante — sa durée de vie est courte et il est invalidé dès l'appel à killSession. Régénérez-le à chaque exécution de script.

// Commentaires

Aucun commentaire pour l'instant.