(zopeedit-gpo)=

# Procédure de déploiement ZopeEdit 1.1.2 via GPO

**Référence** : IA.DOCS-GPO-DEPLOY-ZopeEdit-v1.0
**Auteur** : christophe Tourment
**Date** : 28 mai 2026
**Cible** : Windows 11 Pro
**Méthode** : GPO Startup Script (PowerShell)

---

## 1. Contexte et objectif

### 1.1 Objectif

Déployer automatiquement le package `ZopeEdit-1.1.2.msi` (généré via *Scalable Smart Package*) sur les postes Windows 11 Pro du domaine `votre.commune.be` via une GPO de type *Startup Script*.

### 1.2 Pourquoi un Startup Script et pas un Logon Script ?

| Type de script | Contexte d'exécution | Droits | Installation MSI machine |
|---|---|---|---|
| **Startup Script** | `NT AUTHORITY\SYSTEM` | Administrateur local | ✅ Oui |
| **Logon Script** | Utilisateur connecté | Limités | ❌ Non (sauf user admin) |

L'installation d'un MSI *machine-wide* requiert les droits `SYSTEM`, donc **uniquement un Startup Script** est viable.

### 1.3 Pourquoi un script PowerShell et pas l'option "Software Installation" native de la GPO ?

L'option *Computer Configuration → Software Settings → Software Installation* déploie nativement les MSI, mais :

- pas de gestion fine des versions ni des codes de retour MSI
- pas de log centralisé exploitable par Zabbix
- diagnostic difficile en cas d'échec
- pas de logique d'idempotence personnalisée

Le script PowerShell offre un contrôle complet et une journalisation détaillée.

---

## 2. Prérequis

### 2.1 Infrastructure

- ✅ Contrôleur de domaine `ad.votre.commune.be` opérationnel
- ✅ Partage SYSVOL accessible en lecture pour les machines du domaine
- ✅ GPO `{Unique ID}` créée et liée à l'OU contenant les postes Windows 11 cibles

### 2.2 Postes clients

- Windows 11 Pro joint au domaine `votre.commune.be`
- PowerShell 5.1 (présent par défaut sur Windows 11)
- ExecutionPolicy autorisant l'exécution de scripts (voir §4)

### 2.3 Fichiers

- `ZopeEdit-1.1.2.msi` (package MSI à déployer)
- `Deploy-ZopeEdit.ps1` (script de déploiement, fourni en §3)

---

## 3. Script de déploiement PowerShell

### 3.1 Emplacement

Le script et le MSI doivent être copiés ensemble dans le dossier *Startup* de la GPO :

```
\\votre.commune.be\SYSVOL\votre.commune.be\Policies\{Unique ID}\Machine\Scripts\Startup\
├── Deploy-ZopeEdit.ps1
└── ZopeEdit-1.1.2.msi
```

### 3.2 Code du script

Fichier : `Deploy-ZopeEdit.ps1`

```powershell
<#
.SYNOPSIS
    Déploiement du package ZopeEdit-1.1.2.msi via GPO Startup Script.

.DESCRIPTION
    Script de déploiement idempotent qui :
    - Vérifie si le MSI est déjà installé (via DisplayName + DisplayVersion)
    - Installe ou met à jour le package si nécessaire
    - Journalise les opérations dans %ProgramData%\IMIO\Logs
    - Doit s'exécuter en contexte SYSTEM (startup script GPO)

.NOTES
    Auteur      : Christophe Tourment / IMIO
    Version     : 1.0
    Cible       : Windows 11 Pro - PowerShell 5.1
#>

#Requires -Version 5.0

[CmdletBinding()]
param()

# ============================================================
# Configuration
# ============================================================
$ErrorActionPreference = 'Stop'

$MsiFileName     = 'ZopeEdit-1.1.2.msi'
$ProductName     = 'ZopeEdit'
$TargetVersion   = '1.1.2'
$LogDir          = Join-Path $env:ProgramData 'IMIO\Logs'
$LogFile         = Join-Path $LogDir 'ZopeEdit-Deploy.log'
$MsiLogFile      = Join-Path $LogDir "ZopeEdit-MsiInstall-$(Get-Date -Format 'yyyyMMdd-HHmmss').log"
$MarkerFile      = Join-Path $env:ProgramData 'IMIO\ZopeEdit.installed'

$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$MsiPath   = Join-Path $ScriptDir $MsiFileName

# ============================================================
# Fonctions
# ============================================================
function Write-Log {
    param(
        [Parameter(Mandatory)][string]$Message,
        [ValidateSet('INFO','WARN','ERROR','OK')][string]$Level = 'INFO'
    )
    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $line = "[$timestamp] [$Level] $Message"
    try {
        Add-Content -Path $LogFile -Value $line -Encoding UTF8 -ErrorAction Stop
    } catch {}
    Write-Verbose $line
}

function Get-InstalledProduct {
    param([Parameter(Mandatory)][string]$Name)

    $uninstallKeys = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )

    Get-ItemProperty -Path $uninstallKeys -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName -like "*$Name*" } |
        Select-Object DisplayName, DisplayVersion, PSChildName, UninstallString
}

function Compare-Version {
    param(
        [Parameter(Mandatory)][string]$Installed,
        [Parameter(Mandatory)][string]$Target
    )
    try {
        $vInstalled = [version]$Installed
        $vTarget    = [version]$Target
        return $vInstalled.CompareTo($vTarget)
    } catch {
        return [string]::Compare($Installed, $Target)
    }
}

# ============================================================
# Initialisation
# ============================================================
try {
    if (-not (Test-Path $LogDir)) {
        New-Item -Path $LogDir -ItemType Directory -Force | Out-Null
    }
} catch {}

Write-Log "===== Demarrage deploiement $ProductName v$TargetVersion =====" -Level INFO
Write-Log "Execute en tant que : $([Security.Principal.WindowsIdentity]::GetCurrent().Name)"
Write-Log "Machine             : $env:COMPUTERNAME"
Write-Log "Chemin du script    : $ScriptDir"
Write-Log "Chemin du MSI       : $MsiPath"

# ============================================================
# Vérifications préalables
# ============================================================
if (-not (Test-Path -LiteralPath $MsiPath)) {
    Write-Log "MSI introuvable : $MsiPath" -Level ERROR
    exit 1
}

# ============================================================
# Vérification de l'état d'installation
# ============================================================
$installed = Get-InstalledProduct -Name $ProductName

if ($installed) {
    foreach ($product in $installed) {
        Write-Log "Produit detecte : $($product.DisplayName) v$($product.DisplayVersion)" -Level INFO
    }

    $current = $installed | Select-Object -First 1

    if ($current.DisplayVersion) {
        $cmp = Compare-Version -Installed $current.DisplayVersion -Target $TargetVersion

        if ($cmp -ge 0) {
            Write-Log "Version installee ($($current.DisplayVersion)) >= version cible ($TargetVersion). Rien a faire." -Level OK
            exit 0
        } else {
            Write-Log "Version installee ($($current.DisplayVersion)) < version cible ($TargetVersion). Mise a jour requise." -Level INFO
        }
    }
} else {
    Write-Log "Aucune installation existante detectee. Installation initiale." -Level INFO
}

# ============================================================
# Installation du MSI
# ============================================================
Write-Log "Lancement de msiexec..." -Level INFO

$msiArgs = @(
    '/i'
    "`"$MsiPath`""
    '/qn'
    '/norestart'
    'REBOOT=ReallySuppress'
    "/L*V `"$MsiLogFile`""
)

Write-Log "Commande : msiexec.exe $($msiArgs -join ' ')"

try {
    $process = Start-Process -FilePath 'msiexec.exe' `
                             -ArgumentList $msiArgs `
                             -Wait -PassThru -NoNewWindow

    $exitCode = $process.ExitCode
    Write-Log "msiexec termine avec code : $exitCode"

    switch ($exitCode) {
        0 {
            Write-Log "Installation reussie." -Level OK
            try {
                "$ProductName v$TargetVersion installe le $(Get-Date -Format 'o')" |
                    Set-Content -Path $MarkerFile -Encoding UTF8
            } catch {}
            exit 0
        }
        1605 {
            Write-Log "Code 1605 : produit non trouve." -Level WARN
            exit $exitCode
        }
        1618 {
            Write-Log "Code 1618 : une autre installation MSI est en cours. Reessai au prochain demarrage." -Level WARN
            exit $exitCode
        }
        1641 {
            Write-Log "Code 1641 : installation reussie, reboot initie." -Level OK
            exit 0
        }
        3010 {
            Write-Log "Code 3010 : installation reussie, reboot requis." -Level OK
            exit 0
        }
        default {
            Write-Log "Echec installation. Consultez le log MSI : $MsiLogFile" -Level ERROR
            exit $exitCode
        }
    }
}
catch {
    Write-Log "Exception lors de l'execution de msiexec : $($_.Exception.Message)" -Level ERROR
    exit 1
}
```

---

## 4. Configuration de la GPO

### 4.1 Ouvrir la console GPMC

Sur un contrôleur de domaine ou un poste avec RSAT :

```
Démarrer → Group Policy Management
```

### 4.2 Éditer la GPO cible

1. Naviguer jusqu'à la GPO `{Unique ID}`
2. Clic droit → **Edit**

### 4.3 Activer l'ExecutionPolicy PowerShell

Chemin dans l'éditeur GPO :

```
Computer Configuration
└── Policies
    └── Administrative Templates
        └── Windows Components
            └── Windows PowerShell
                └── Turn on Script Execution
```

Configuration :

- **Enabled**
- Execution Policy : `Allow local scripts and remote signed scripts`

> **Note** : sur les Startup Scripts, le script s'exécute depuis SYSVOL qui est considéré comme local. La policy `RemoteSigned` peut malgré tout bloquer selon le contexte ; `Allow local scripts and remote signed scripts` est le compromis sûr.

### 4.4 Ajouter le script PowerShell de démarrage

Chemin dans l'éditeur GPO :

```
Computer Configuration
└── Policies
    └── Windows Settings
        └── Scripts (Startup/Shutdown)
            └── Startup
```

Étapes :

1. Double-cliquer sur **Startup**
2. Sélectionner l'onglet **PowerShell Scripts** (⚠️ pas l'onglet *Scripts* classique)
3. Cliquer sur **Show Files...** → cela ouvre le dossier SYSVOL `...\Machine\Scripts\Startup\`
4. **Copier** `Deploy-ZopeEdit.ps1` et `ZopeEdit-1.1.2.msi` dans ce dossier
5. Fermer l'Explorateur
6. Cliquer sur **Add...**
7. **Browse...** → sélectionner `Deploy-ZopeEdit.ps1`
8. Laisser le champ *Script Parameters* vide
9. **OK**

### 4.5 Ordre d'exécution (optionnel)

Si plusieurs scripts PowerShell coexistent, configurer l'option :

```
Run Windows PowerShell scripts first : Enabled
```

(Onglet *PowerShell Scripts* → bouton **Properties** en bas).

### 4.6 Forcer la mise à jour de la GPO

Sur un poste client de test :

```powershell
gpupdate /force
```

Puis **redémarrer** le poste — les Startup Scripts ne s'exécutent qu'au démarrage de la machine, pas lors d'un simple `gpupdate`.

---

## 5. Vérification du déploiement

### 5.1 Vérifier l'application de la GPO sur le poste client

```powershell
gpresult /scope computer /h C:\Temp\gpo.html
Start-Process C:\Temp\gpo.html
```

Chercher la GPO `{Unique ID}` dans la section *Computer Configuration* → *Applied GPOs*.

### 5.2 Vérifier l'installation du logiciel

```powershell
Get-ItemProperty `
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', `
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' |
    Where-Object DisplayName -like '*Zope*' |
    Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
```

### 5.3 Consulter les logs

| Fichier | Contenu |
|---|---|
| `C:\ProgramData\IMIO\Logs\ZopeEdit-Deploy.log` | Journal du script PowerShell |
| `C:\ProgramData\IMIO\Logs\ZopeEdit-MsiInstall-<timestamp>.log` | Log verbeux msiexec |
| `C:\ProgramData\IMIO\ZopeEdit.installed` | Marker file (présent = installé) |

### 5.4 Vérifier le marker file

```powershell
Get-Content C:\ProgramData\IMIO\ZopeEdit.installed
```

Sortie attendue :

```
ZopeEdit v1.1.2 installe le 2026-05-28T08:15:32.0000000+02:00
```

---

## 6. Diagnostic et résolution de problèmes

### 6.1 Codes de retour MSI

| Code | Signification | Action |
|---|---|---|
| `0` | Installation réussie | Aucune |
| `1603` | Erreur fatale d'installation | Consulter le log MSI verbeux |
| `1605` | Produit non trouvé (pour désinstall) | Normal lors de la première installation |
| `1618` | Autre installation MSI en cours | Réessai automatique au prochain démarrage |
| `1619` | MSI introuvable ou inaccessible | Vérifier le chemin SYSVOL |
| `1638` | Version déjà installée (autre instance) | Vérifier le ProductCode du MSI |
| `1641` | Installation réussie, reboot initié | Aucune |
| `3010` | Installation réussie, reboot requis | Reboot manuel ou planifié |

### 6.2 Le script ne s'exécute pas

Vérifier dans l'ordre :

1. **GPO bien liée** à l'OU contenant le poste :
   ```powershell
   gpresult /r /scope computer
   ```
2. **Fichiers présents** dans SYSVOL :
   ```powershell
   Test-Path '\\votre.commune.be\SYSVOL\votre.commune.be\Policies\{Unique ID}\Machine\Scripts\Startup\Deploy-ZopeEdit.ps1'
   Test-Path '\\votre.commune.be\SYSVOL\votre.commune.be\Policies\{Unique ID}\Machine\Scripts\Startup\ZopeEdit-1.1.2.msi'
   ```
3. **Réplication SYSVOL** complète entre tous les DC (DFSR opérationnel)
4. **ExecutionPolicy** correctement appliquée :
   ```powershell
   Get-ExecutionPolicy -List
   ```
5. **Journal Événements Windows** :
   ```
   Event Viewer → Applications and Services Logs → Microsoft → Windows → GroupPolicy → Operational
   ```

### 6.3 Le MSI s'installe mais échoue

1. Consulter le log MSI verbeux dans `C:\ProgramData\IMIO\Logs\`
2. Rechercher les chaînes `Return value 3` (point d'échec) et `MainEngineThread is returning` (code final)
3. Tester l'installation manuelle en local en tant qu'Administrateur :
   ```powershell
   msiexec.exe /i "C:\Temp\ZopeEdit-1.1.2.msi" /qn /L*V "C:\Temp\manual-install.log"
   ```

### 6.4 Cas particulier : DisplayName ne contient pas "ZopeEdit"

Adapter la variable `$ProductName` dans le script en fonction du DisplayName réel détecté :

```powershell
Get-ItemProperty `
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', `
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' |
    Where-Object DisplayName |
    Select-Object DisplayName, DisplayVersion |
    Sort-Object DisplayName
```

---

## 7. Désinstallation / rollback

### 7.1 Désinstallation manuelle sur un poste

```powershell
# Récupérer le ProductCode
$product = Get-ItemProperty `
    'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', `
    'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' |
    Where-Object DisplayName -like '*ZopeEdit*' |
    Select-Object -First 1

if ($product) {
    $productCode = $product.PSChildName
    Start-Process msiexec.exe -ArgumentList "/x $productCode /qn /norestart" -Wait
    Remove-Item 'C:\ProgramData\IMIO\ZopeEdit.installed' -ErrorAction SilentlyContinue
}
```

### 7.2 Désactivation du déploiement

Pour stopper le déploiement sur de nouveaux postes :

1. Éditer la GPO `{Unique ID}`
2. Onglet **PowerShell Scripts** → sélectionner `Deploy-ZopeEdit.ps1` → **Remove**
3. `gpupdate /force` sur les DC

---

## 8. Annexes

### 8.1 Arborescence finale dans SYSVOL

```
\\votre.commune.be\SYSVOL\votre.commune.be\Policies\{Unique ID}\
└── Machine\
    └── Scripts\
        └── Startup\
            ├── Deploy-ZopeEdit.ps1
            └── ZopeEdit-1.1.2.msi
```

### 8.2 Arborescence finale sur le poste client

```
C:\ProgramData\IMIO\
├── ZopeEdit.installed
└── Logs\
    ├── ZopeEdit-Deploy.log
    └── ZopeEdit-MsiInstall-20260528-081530.log
```

### 8.3 Références

- Documentation MSI Exit Codes : https://learn.microsoft.com/windows/win32/msi/error-codes
- GPO PowerShell Startup Scripts : https://learn.microsoft.com/windows-server/administration/windows-commands/gpresult
- Scalable Smart Package : documentation interne IMIO

---

*Fin du document – DOC-IMIO-DEPLOY-ZopeEdit-v1.1.2*