Ajout de la langue anglaise
Ajout d'un README.md avec le descriptif et les instructions L'ancien README.md s'appelle désormais CHANGELOG.md Activation du pavé numérique par défaut pour les installations sans assistance Nouveau design pour la fenêtre de déploiement. Ajout de paramètre pour pouvoir procéder à une installation totalement automatisée Ajout de paramètre pour copier au choix sources, drivers ou les deux sur C:\DeployVenter avant leur usage par DISM.
This commit is contained in:
1 parent
cb917c97ce
commit
2212e25d31
26 files changed
+2616
-821
No files matched your search
+325
@@ -0,0 +1,325 @@
|
||||
# DeployCenter — première version de la réécriture
|
||||
|
||||
## Contenu
|
||||
|
||||
- `DeployCenter.ps1` : point d'entrée et logique applicative existante.
|
||||
- `Modules/ThemeManager.psm1` : découverte, chargement et validation des thèmes PSD1, avec prise en charge des rôles de boutons.
|
||||
- `Modules/UI.psm1` : contrôles et fenêtres d’interface, construits selon le thème actif.
|
||||
- `Modules/Deployment.psm1` : partitionnement UEFI/GPT et installation de l’édition Windows sélectionnée.
|
||||
- `Modules/DriverManagement.psm1` : recherche des packs de pilotes correspondant au constructeur, au modèle et à l’édition Windows.
|
||||
- `Modules/Unattend.psm1` : découverte et validation des profils de réponse Windows, puis génération sécurisée du XML.
|
||||
- `Modules/Localization.psm1` et `Locales/*.psd1` : sélection de la langue depuis la culture UI de WinPE et ressources françaises/anglaises.
|
||||
- `Themes/Light.psd1` et `Themes/Dark.psd1` : palettes et styles des boutons.
|
||||
|
||||
## Lancement
|
||||
|
||||
Depuis le dossier DeployCenter, lancer :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1
|
||||
```
|
||||
|
||||
Pour lancer en fenêtre redimensionnable :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -Windowed
|
||||
```
|
||||
|
||||
### Déploiement sans interaction
|
||||
|
||||
Le mode `-NonInteractive` choisit les mêmes profils et exécute le même flux de déploiement que l’interface, avec une fenêtre de progression. Il exige que toutes les décisions de la première page soient fournies et que l’effacement du disque soit confirmé explicitement. Les paramètres sont :
|
||||
|
||||
- `-DiskNumber` : numéro du disque physique de destination.
|
||||
- `-ImagePath` : chemin d’un ISO, WIM, ESD ou dossier de sources Windows.
|
||||
- `-EditionIndex` : index de l’édition dans l’image.
|
||||
- `-PartitionProfileId` : ID du profil PSD1 dans `Partitioning/Profiles`.
|
||||
- `-UnattendProfileId` : ID du profil dans `Unattend/Profiles`, ou `OOBE` pour laisser Windows présenter l’expérience de première ouverture sans fichier Unattend.
|
||||
- `-HostName` : requis si le profil Unattend configure le nom de la machine.
|
||||
- `-CSizeGB` : requis pour un profil qui demande la taille de C: ; le reste est attribué à D:.
|
||||
- `-ConfirmDiskErase` : confirmation obligatoire, puisque le disque choisi est entièrement effacé.
|
||||
- `-RestartWhenComplete` : demande un redémarrage automatique après succès. Celui-ci est annulé si les recommandations WinRE ne sont pas respectées.
|
||||
- `-CopyLocally` : copie localement les éléments indiqués avant DISM. Valeurs acceptées : `Sources`, `Drivers`, ou les deux. Sans ce paramètre, DISM utilise directement les chemins sources pour l’image Windows et les pilotes.
|
||||
|
||||
Exemple avec le profil de partitionnement C:/D: et le profil `OEMInstallation` :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 `
|
||||
-NonInteractive -DiskNumber 0 `
|
||||
-ImagePath 'Y:\Windows11.iso' -EditionIndex 6 `
|
||||
-PartitionProfileId 'UEFI-GPT-WinRE-C-D' -CSizeGB 80 `
|
||||
-UnattendProfileId 'OEMInstallation' -HostName 'POSTE-001' `
|
||||
-ConfirmDiskErase -RestartWhenComplete -CopyLocally Sources,Drivers
|
||||
```
|
||||
|
||||
Pour laisser Windows gérer OOBE, remplacer les paramètres de profil et de nom d’hôte par `-UnattendProfileId OOBE`. La commande retourne le code de processus `0` en cas de succès et `1` si la préparation ou le déploiement échoue. Après une tentative d’installation, le journal complet est copié à la racine de `C:\`, en cas de succès comme d’échec ; l’original reste dans `Logs`.
|
||||
|
||||
Conserver à côté du script les ressources habituelles du projet (`Assets`, `uvnc`, `HardwareInfo.ps1`, etc.). Les dossiers `Modules` et `Themes` doivent rester à côté de `DeployCenter.ps1`.
|
||||
|
||||
## Mise en page
|
||||
|
||||
Le redimensionnement de la fenêtre recalcule désormais les largeurs des panneaux, la taille du tableau et du journal, ainsi que la position des boutons. Les marges et hauteurs des contrôles restent volontairement fixes pour préserver la lisibilité sous WinPE.
|
||||
|
||||
## Architecture et validation
|
||||
|
||||
`DeployCenter.ps1` orchestre les modules : `UI.psm1` porte les workflows et contrôles d’interface dans le scope de session partagé, `ThemeManager.psm1` charge les palettes, `HardwareInventory.psm1` collecte et exporte les informations matérielles, `Partitioning.psm1` valide les profils et calcule les capacités, `WindowsSources.psm1` analyse les images Windows, et `Deployment.psm1` applique le profil puis déploie l’image.
|
||||
|
||||
La syntaxe PowerShell a été vérifiée statiquement. Le déroulement DISM/DiskPart/reagentc et le démarrage UEFI restent à valider sur la machine WinPE cible avant le premier déploiement réel.
|
||||
|
||||
## Localisation
|
||||
|
||||
DeployCenter utilise la langue d’interface exposée par WinPE au démarrage : les cultures françaises chargent `Locales/fr-FR.psd1`, les autres chargent `Locales/en-US.psd1`. L’anglais est la langue source des textes de l’application ; `fr-FR.psd1` en fournit la traduction française. Les textes produits par DeployCenter dans l’interface et dans son journal sont traduits ; les lignes émises par DISM, DiskPart, reagentc et les autres commandes système restent dans leur langue d’origine. Il n’y a pas de sélecteur de langue ni de clé de langue à configurer dans `DeployCenter.ini`.
|
||||
|
||||
|
||||
## Mot de passe UltraVNC de test
|
||||
|
||||
Dans `DeployCenter.ini`, ajoutez la section suivante. Un champ `Password` non vide est utilisé tel quel au lieu de générer un mot de passe aléatoire ; `createpassword64.exe` continue à le convertir dans le format attendu par UltraVNC. Laissez `Password=` vide pour conserver le comportement par défaut.
|
||||
|
||||
```ini
|
||||
[UltraVNC]
|
||||
Password=MonMotDePasseDeTest
|
||||
```
|
||||
|
||||
Le mot de passe est stocké en clair dans `DeployCenter.ini` : utilisez cette option uniquement dans un environnement de test protégé.
|
||||
|
||||
## Taille de la partition WinRE
|
||||
|
||||
La taille configurée par défaut est de 1 Go (1024 Mo). Elle peut être remplacée dans `DeployCenter.ini`, sous `[Partitioning]`, par `WinRESizeMB=1536` par exemple. La valeur doit être un entier strictement positif. Après l’application de Windows, DeployCenter vérifie la taille réelle de `winre.wim` : le déploiement échoue seulement si l’espace libre ne permet pas de copier le fichier. Une partition plus petite que 990 Mo ou un espace libre après copie inférieur à 250 Mo génère un avertissement, puis le déploiement continue.
|
||||
|
||||
## Paramètres Unattend facultatifs
|
||||
|
||||
Sous `[Unattend]`, `SupportProvider` définit le nom du fournisseur, `SupportURL` désigne une page Web d’assistance complète (par exemple `https://support.example.org`) et `SupportAppURL` désigne le protocole d’une application d’assistance installée (par exemple `monapp-support:`). Ces paramètres sont facultatifs et omis lorsqu’ils sont vides. Par compatibilité, une ancienne URL HTTP(S) placée dans `SupportAppURL` est générée comme `SupportURL`. `WallpaperPath` désigne l’image `.png`, `.jpg` ou `.bmp` à utiliser dans le thème Windows par défaut ; le chemin peut être absolu ou relatif au dossier DeployCenter. `Assets\blorand.png` est la valeur par défaut et une valeur vide laisse le fond Windows inchangé. Le fond est appliqué aux déploiements avec profil Unattend ; « Avec OOBE » reste sans fichier Unattend. Le template `OEMInstallation.xml` utilise `fr-FR` pour `InputLocale`.
|
||||
|
||||
Pour produire `AdminPasswordBase64`, lancer `powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -EncodeUnattendPassword`, saisir le mot de passe dans l’invite masquée, puis copier la valeur Base64 affichée dans `DeployCenter.ini`. Pour le champ `LocalAccount/Password` avec `PlainText=false`, Windows SIM ajoute le suffixe littéral `Password` au mot de passe avant de convertir le tout en UTF-16LE puis en Base64. Base64 est réversible et ne chiffre pas le secret ; protéger le fichier INI en conséquence.
|
||||
|
||||
## Historique récent
|
||||
|
||||
- Création de `README.md` avec la présentation de DeployCenter, ses prérequis, son manuel de déploiement interactif et automatisé, la configuration, les pilotes et le dépannage.
|
||||
- Ajout du paramètre `-CopyLocally` (`Sources`, `Drivers`) : les sources sont utilisées directement par DISM par défaut ; les éléments choisis sont copiés par Robocopy sur la partition Windows avant traitement. Les contrôles d’espace et le calcul de taille des pilotes ne portent que sur les éléments à copier. La progression n’affiche les étapes de copie que lorsqu’elles sont demandées, et le journal précise quand DISM utilise directement une source.
|
||||
- L’en-tête de la fenêtre de déploiement et celui de la page principale sont construits par le même composant UI. La fenêtre de déploiement reprend le logo, le titre et le sous-titre, sans le sélecteur de thème ; la progression conserve son intitulé sous cet en-tête et compacte les étapes pour tenir en 800×600.
|
||||
- Les commandes natives sans pourcentage laissent la boucle WinForms traiter ses événements et leur code de sortie est écrit par un lanceur PowerShell temporaire, plutôt que lu depuis l’objet processus qui était vide dans certains WinPE. Si le code natif ne peut pas être récupéré, DeployCenter échoue avec un diagnostic explicite.
|
||||
- DISM `/Add-Driver` et les opérations `mountvol` pendant l’installation transmettent aussi le callback UI au lanceur, afin que le chrono continue de s’actualiser pendant leur attente.
|
||||
- L’étape active affiche le temps écoulé depuis son démarrage, à côté du pourcentage lorsqu’une progression réelle est disponible. Les commandes natives sans pourcentage laissent la boucle WinForms traiter ses événements pendant leur exécution, sans analyser leur sortie pour en extraire un progrès ; le pourcentage reste réservé à DISM `/Apply-Image` et aux copies Robocopy.
|
||||
- La fenêtre de progression conserve désormais le logo actif de l’application dans son en-tête. L’en-tête est pris en compte dans le calcul de la zone des étapes, dont les lignes sont compactées pour rester visibles en 800×600, en mode plein écran comme avec `-Windowed`.
|
||||
- La copie des pilotes locaux suit maintenant celle de l’image Windows et précède son application. Les fichiers de l’image et du pack cohabitent donc temporairement sur C: ; l’espace libre est vérifié pour leur taille cumulée. `C:\DeployCenter` est supprimé en une seule étape à la fin du déploiement.
|
||||
- En mode normal, la progression occupe l’écran et reprend le pied de page DeployCenter ; les lignes compactes tiennent sans défilement sur un écran 800×600. Avec `-Windowed`, elle reste centrée, adaptée à la fenêtre principale et dimensionnée pour afficher les étapes sans défilement lorsque la fenêtre principale couvre l’écran.
|
||||
- Le libellé de l’étape active reprend maintenant le texte localisé complet du message de déploiement, y compris les détails dynamiques comme la lettre du lecteur cible (`C:`) pendant l’application de Windows.
|
||||
- Déclaration préalable de la propriété `UpdateAction` dans l’objet de progression ; Windows PowerShell 5.1 ne permet pas d’ajouter cette propriété après la création du `PSCustomObject`.
|
||||
- La fenêtre de progression adapte sa taille à la zone d’écran et utilise des lignes plus compactes, tout en conservant le défilement pour les étapes nombreuses. Son callback encapsule aussi l’action d’actualisation dans l’objet UI afin qu’elle reste accessible depuis la portée isolée créée par `GetNewClosure()` sous Windows PowerShell 5.1.
|
||||
- La fenêtre de déploiement affiche toutes les étapes à venir et terminées, avec la durée observée pour chacune. DISM `/Apply-Image` et les copies Robocopy affichent leur pourcentage réel ; les autres étapes indiquent « En cours… ». Après réussite, cette même fenêtre conserve le récapitulatif et présente le message final, la case de redémarrage et le bouton OK ; elle ne se ferme qu’après validation.
|
||||
- La copie de `winre.wim` vers la partition de récupération utilise maintenant Robocopy avec progression et vérification de la taille ; un code de sortie Robocopy d’échec ou une copie incomplète interrompt la configuration WinRE.
|
||||
- La copie Robocopy reste suivie en progression sans journaliser chaque ligne de pourcentage. Son code de sortie est récupéré par un petit processus PowerShell enfant qui l’écrit dans un fichier temporaire ; si ce code reste indisponible, DeployCenter compare les fichiers source et destination (chemins et tailles) avant de poursuivre, et bloque en cas de différence.
|
||||
- Le parseur de la sortie `bcdedit` utilise maintenant le libellé `identifier` fourni par la localisation active (`identifier` en anglais, `identificateur` en français), au lieu d’inclure les deux formes dans le code.
|
||||
- Le libellé de secours « Preparing deployment… » est désormais localisé dès l’ouverture de la progression, y compris avant la réception du premier message d’étape.
|
||||
- Traduction complète de l’indication de recherche des pilotes affichée avant la sélection d’une édition Windows.
|
||||
- Les libellés et les valeurs fixes de la confirmation d’effacement (disque, profil, édition, tailles et configuration initiale) utilisent désormais les ressources de localisation ; les tailles C:/D: et l’espace restant affichent aussi l’unité traduite.
|
||||
- Renommage du profil Unattend en anglais idiomatique : `OEMInstallation.psd1`, ID `OEMInstallation` et template `OEMInstallation.xml` ; l’exemple de déploiement sans interaction utilise le nouvel ID.
|
||||
- Les intitulés des profils Unattend et le choix « Use OOBE » passent par la localisation avant leur affichage ; le profil OEM active NumLock sur l’écran d’ouverture de session et dans le profil utilisateur par défaut.
|
||||
- Correction du message d’erreur de sélection d’un profil Unattend pour qu’il soit entièrement rédigé en anglais et localisable.
|
||||
- Correction des accents manquants dans plusieurs traductions de `Locales/fr-FR.psd1`.
|
||||
- Ajout de la localisation française/anglaise selon la culture UI de WinPE. Les chaînes de l’interface, les statuts et les messages applicatifs consignés par DeployCenter sont traduits ; la sortie brute des commandes système reste inchangée.
|
||||
- L’anglais devient la langue source des textes, commentaires, profils et exemples de configuration du code ; `Locales/fr-FR.psd1` fournit la traduction de l’interface et des messages DeployCenter.
|
||||
- La copie des pilotes de déploiement utilise Robocopy avant leur injection DISM depuis `C:\DeployCenter\Drivers` ; les durées de copie et d’injection sont journalisées séparément.
|
||||
- L’image sélectionnée (`install.wim` ou `install.esd`) est copiée par Robocopy vers `C:\DeployCenter\Sources` avant `/Apply-Image`. La progression de cette copie mono-fichier est remontée en pourcentage ; DeployCenter supprime ensuite le fichier temporaire, y compris si l’application DISM échoue. Les codes Robocopy 0 à 7 sont traités comme des résultats sans échec.
|
||||
- La fenêtre de confirmation d’effacement est élargie et son bouton rouge dispose d’une largeur de 134 px ; son libellé « Effacer et installer » tient maintenant sur une seule ligne.
|
||||
- Correction du générateur `-EncodeUnattendPassword` : Windows SIM encode pour `LocalAccount/Password` le mot de passe suivi du suffixe littéral `Password`, en UTF-16LE puis Base64. La valeur actuelle de `DeployCenter.ini` a été régénérée avec ce format après l’échec `Failed to decode password (0x8007000d)`.
|
||||
- La boîte « Confirmer l’effacement du disque » mesure maintenant la hauteur du message et place les boutons en dessous ; les détails du profil Unattend et du nom d’hôte ne coupent plus l’avertissement final.
|
||||
- Correction de la hauteur du panneau « Préparation du déploiement » pendant le redimensionnement : elle correspond désormais à sa hauteur initiale de 270 px et laisse un espace avant les boutons d’action en bas, notamment à la résolution minimale 800 × 600.
|
||||
- Ajout du paramètre autonome `-EncodeUnattendPassword` : il demande le mot de passe sans l’afficher et renvoie la valeur Base64 UTF-16LE à placer dans `[Unattend] AdminPasswordBase64`. Les fichiers INI documentent aussi ce format et rappellent qu’il ne chiffre pas le mot de passe.
|
||||
- Ajout de `[Unattend] SupportURL` pour les pages Web d’assistance et distinction documentée avec `SupportAppURL`, réservé au protocole d’une application. Les anciennes valeurs HTTP(S) dans `SupportAppURL` restent compatibles et sont générées sous `SupportURL`.
|
||||
- Analyse des journaux Panther ajoutés dans `Logs` : Windows a trouvé et validé `C:\Windows\Panther\Unattend\unattend.xml`, puis la passe `specialize` s’est terminée avec succès. `setuperr.log` ne signale que « System disks found » pendant la détection WinRE ; les lignes adjacentes indiquent que WinRE est prêt et que la spécialisation WinRE renvoie 0.
|
||||
- Correction de `[Unattend] AdminGroup` dans les fichiers INI : `Administrators` est le nom neutre requis par Windows Setup, y compris pour une image française. Les noms localisés comme `Administrateurs` ne doivent pas être écrits dans `LocalAccount/Group`.
|
||||
- Ajout de la recherche automatique de packs de pilotes dans `Drivers/<Constructeur>/<Préfixe modèle ou SKU>/<Version Windows>` et de leur injection récursive par DISM après l’application de l’image. La racine est configurable dans `[Drivers] RootPath` ; les liens Lenovo, Dell, HP et Microsoft sont mémorisés dans `DeployCenter.ini` et son exemple. L’écran principal affiche le chemin inspecté et l’état de détection.
|
||||
- `DeployCenter.ps1` accepte les paramètres `-NonInteractive`, `-DiskNumber`, `-ImagePath`, `-EditionIndex`, `-PartitionProfileId`, `-UnattendProfileId`, `-HostName`, `-CSizeGB`, `-ConfirmDiskErase` et `-RestartWhenComplete` pour automatiser le déploiement. La confirmation explicite d’effacement est obligatoire ; les choix et tailles sont validés avant l’appel au déploiement. Le résultat est retourné par le code processus. Après une tentative, `Logging.psm1` copie le journal à la racine de C: en réussite comme en échec.
|
||||
- La touche Échap ferme désormais la fenêtre « Sources Windows » même lorsque le focus est dans un contrôle enfant.
|
||||
- La description du profil PSD1 `InstallationOEM` évite l’apostrophe typographique dans une chaîne entre apostrophes, pour rester analysable sous Windows PowerShell 5.1.
|
||||
- Le profil Unattend `LocalAdmin` a été renommé `InstallationOEM` : son ID, son fichier PSD1, son template XML et sa description sont alignés sur son rôle.
|
||||
- Le profil affiché sous le nom « Installation OEM » prépare le nom d’hôte, le compte local et les options OEM. À la fin du déploiement, les avertissements de recommandations WinRE sont rappelés ; la case de redémarrage automatique est décochée s’il y en a.
|
||||
- Correction de la chaîne d’erreur de validation du chemin du fond d’écran dans `Deployment.psm1` : elle est délimitée par des guillemets doubles pour éviter toute ambiguïté d’apostrophe dans Windows PowerShell 5.1.
|
||||
- L’image `Assets/blorand.png` devient le fond du thème Windows par défaut pour les nouveaux utilisateurs des déploiements utilisant un profil Unattend. `[Unattend] WallpaperPath` accepte un chemin absolu ou relatif au dossier DeployCenter ; une valeur vide désactive cette personnalisation. Le mode « Avec OOBE » reste sans fichier Unattend et conserve donc le fond Windows.
|
||||
- `DeployCenter.ini.example` documente désormais toutes les options reconnues par `Configuration.psm1`, y compris `SupportProvider`, `SupportURL` et `SupportAppURL` sous forme de clés explicites vides par défaut.
|
||||
- Le profil `InstallationOEM` utilise `fr-FR` pour `InputLocale`. Les clés facultatives `[Unattend] SupportProvider`, `SupportURL` et `SupportAppURL` ajoutent indépendamment le fournisseur, le site Web et le protocole d’application à `OEMInformation` ; chaque élément est omis lorsque sa clé est absente ou vide.
|
||||
- Le `clean` et la création des partitions sont exécutés en deux étapes DiskPart ; `Update-HostStorageCache` actualise les objets Storage entre les deux, puis après le partitionnement, les retraits de lettres et le `mountvol /D` de la source, avant les contrôles concernés. Si le cmdlet est indisponible dans WinPE, le déploiement continue avec un avertissement.
|
||||
- Le choix « Avec OOBE » ne génère ni n’applique de fichier de réponses et masque la saisie du nom d’hôte. Les profils `Unattend/Profiles/*.psd1` alimentent le menu de la page principale ; le profil `InstallationOEM` configure le nom de machine et crée un compte local de la configuration `[Unattend]`, sans activer le compte intégré Administrator.
|
||||
- Chaque profil Unattend déclare `Target = 'Workstation'` ou `Target = 'Server'`. Le menu propose les profils correspondant à l’édition retenue (le nom contenant « Server » sélectionne la catégorie serveur) ; avant le choix d’une édition, seul « Avec OOBE » est disponible.
|
||||
- L’indication « Environnement : WinPE » a été retirée de l’écran principal. Le bloc d’informations PC est moins haut et le bloc de préparation est remonté/compacté afin de dégager les boutons d’action sur les petites résolutions.
|
||||
- Le template Workstation renseigne les paramètres régionaux français et répond aux pages OOBE de licence, inscription OEM, compte en ligne, réseau sans fil et paramètres express. Le compte local est fourni dans `UserAccounts`; les paramètres `SkipMachineOOBE` et `SkipUserOOBE` ne sont pas utilisés.
|
||||
- À la fin d’un déploiement réussi, la boîte de confirmation propose le redémarrage WinPE avec une case cochée par défaut. Si elle reste cochée, DeployCenter ferme ses ressources, arrête VNC, démonte l’image source puis demande le redémarrage via `wpeutil`.
|
||||
- Le libellé « Configuration de première ouverture » a été raccourci d’un pixel en hauteur pour ne plus recouvrir le bord supérieur de sa liste déroulante.
|
||||
- Compatibilité Windows PowerShell 5.1 : les chaînes de diagnostic de `Unattend.psm1` évitent les apostrophes intégrées et le module est enregistré en UTF-8 avec BOM.
|
||||
- Le groupe local du compte est configuré avec le nom neutre `Administrators`, que Windows traduit selon la langue de l’image ; cela évite de dépendre du libellé français `Administrateurs`.
|
||||
- Les réponses `specialize` et `oobeSystem` sont placées dans `Windows\Panther\Unattend\Unattend.xml` pour être traitées par Windows Setup au premier démarrage. Le mot de passe du compte local provient de `AdminPasswordBase64` (Base64 UTF-16LE, réversible) et n’est jamais ajouté au journal.
|
||||
- La vérification du retrait des lettres EFI/WinRE s’appuie sur l’existence réelle des chemins de lecteur : dans WinPE, `Get-Partition` peut conserver une lettre Recovery dans ses métadonnées alors que `mountvol` et le système de fichiers confirment son retrait.
|
||||
- La lecture du magasin BCD accepte le libellé français `identificateur` de `bcdedit` en plus du libellé anglais `identifier`, pour retrouver l’entrée Windows hors ligne dans un WinPE francophone.
|
||||
- Correction du pipeline de diagnostic des lettres EFI/WinRE pour rester compatible avec le parseur Windows PowerShell 5.1.
|
||||
- DiskPart attribue la lettre temporaire de WinRE avant d’appliquer son type Recovery et ses attributs GPT, conformément à l’ordre des exemples Microsoft. En fin de déploiement réussi, la lettre est retirée en sélectionnant explicitement la partition ; `Get-Partition` peut ne pas exposer sa lettre dans WinPE.
|
||||
- Correction du texte de diagnostic WinRE pour éviter une apostrophe typographique dans une chaîne délimitée par apostrophes simples sous Windows PowerShell 5.1.
|
||||
- L’activation hors ligne de WinRE utilise désormais le GUID de l’entrée Windows relevé dans le magasin BCD créé par `bcdboot`, conformément aux paramètres REAgentC disponibles dans WinPE.
|
||||
- Le suivi de progression avec `Start-Process` est réservé à `dism.exe /Apply-Image`. Les autres commandes natives, y compris DISM hors application d’image, sont invoquées directement, leurs sorties sont journalisées et leur code est lu dans `$LASTEXITCODE`.
|
||||
- En fin de déploiement réussi, seules les lettres temporaires des partitions EFI et WinRE sont retirées. Les lettres Windows et Data définies par le profil restent attribuées ; en cas d’échec, le nettoyage ne retire aucune lettre pour préserver l’état utile au diagnostic.
|
||||
- Correction du retour du plan de partitionnement : conversion explicite de la liste générique en tableau pour éviter une erreur de types sous Windows PowerShell 5.1 avant l’exécution de DiskPart.
|
||||
- Sous certains WinPE, le code de sortie de DiskPart peut être absent malgré une exécution réussie. DeployCenter confirme désormais le partitionnement et le retrait des lettres en relisant l’état des partitions.
|
||||
- Remplacement de l’apostrophe typographique de « d’hôte » par une apostrophe ASCII simple dans le message de validation NetBIOS de `Deployment.psm1`.
|
||||
- Correction de la chaîne du journal DiskPart : les apostrophes typographiques dans une chaîne PowerShell délimitée par apostrophes simples provoquaient une erreur de syntaxe signalée plus loin dans le fichier.
|
||||
- Vérification des lettres de partition par requête directe `Get-Partition -DriveLetter`, puis contrôle du numéro de disque, afin de ne pas dépendre des lettres présentes dans l’instantané retourné par `Get-Partition -DiskNumber` sous WinPE.
|
||||
- La partition Recovery, marquée cachée par ses attributs GPT, peut ne pas être exposée par `Get-Partition -DriveLetter`. DeployCenter l’associe alors à sa position dans les partitions fraîchement créées par DiskPart.
|
||||
- Les partitions EFI et Recovery pouvant être absentes des requêtes `Get-Partition -DriveLetter` dans WinPE, l’association des rôles suit désormais l’ordre des partitions créées et vérifie l’accès effectif à chaque lettre temporaire.
|
||||
- Retrait de l’ajout automatique de `/ScratchDir` aux commandes DISM afin de tester le comportement sans cette redirection dans WinPE ; le journal DISM dédié est conservé.
|
||||
- La progression de l’application d’image lit maintenant les pourcentages émis par DISM pendant son exécution et les transmet à la barre WinForms. Si le code de sortie DISM est absent, le message de réussite explicite est aussi pris en compte.
|
||||
- Le pourcentage DISM est aussi affiché dans le libellé de l’étape de progression, sans ajouter de ligne de journal à chaque mise à jour.
|
||||
- Vérification de l’espace WinRE via `System.IO.DriveInfo` : `Get-Volume -DriveLetter` ne trouve pas toujours les volumes de récupération cachés dans WinPE, même lorsqu’ils sont montés et accessibles.
|
||||
- Le déploiement échoue uniquement si l’espace libre est insuffisant pour copier `winre.wim`. Les recommandations Microsoft de 990 Mo pour la partition et 250 Mo libres après copie produisent un avertissement et n’interrompent pas l’installation.
|
||||
- Sous certains WinPE, `bcdboot` peut confirmer la création des fichiers de démarrage sans que `Start-Process` fournisse son code de sortie. DeployCenter accepte alors le résultat si la sortie confirme la création ou si le magasin BCD est présent sur la partition EFI.
|
||||
- Correction du message de confirmation `bcdboot` et simplification des messages natifs ; les erreurs de parseur signalées plus loin étaient des conséquences de cette chaîne non terminée.
|
||||
- Rétablissement du BOM UTF-8 de `Deployment.psm1`, requis pour que Windows PowerShell 5.1 lise correctement les caractères accentués dans WinPE.
|
||||
- Décodage des sorties natives avec l’encodage de la console WinPE pour préserver les accents ; REAgentC est désormais recherché dans l’image Windows hors ligne avant WinPE.
|
||||
- Recherche de REAgentC reportée après l’application de Windows, lorsque `C:\Windows\System32\reagentc.exe` existe effectivement.
|
||||
- La configuration `WinRESizeMB` et les profils acceptent désormais toute taille strictement positive ; le seuil recommandé de 990 Mo est évalué après application de Windows comme avertissement, sans bloquer la configuration.
|
||||
|
||||
|
||||
### Correctifs v3
|
||||
- En-têtes du tableau des éditions Windows avec une couleur dédiée définie par le thème (`TableHeader`).
|
||||
- Sélection unique explicite dans la liste des éditions et dessin de la sélection basé sur l'élément réellement sélectionné.
|
||||
|
||||
### Correctif v12 — pied de page
|
||||
- La fenêtre principale affiche l'adresse IP de déploiement et le mot de passe UltraVNC dans son pied de page.
|
||||
- Le pied de page commun reste activé par défaut ; le paramètre `-ShowFooter $false` permet de le masquer dans la fenêtre « Éditions Windows ». Les autres fenêtres conservent leur comportement habituel.
|
||||
|
||||
### Correctif v15 — pied de page cohérent
|
||||
- Le pied de page commun affiche par défaut l’adresse IP et le mot de passe VNC dans toutes les fenêtres qui l’utilisent.
|
||||
- Seule la fenêtre de sélection « Éditions Windows » masque entièrement le pied de page via `-ShowFooter $false`.
|
||||
|
||||
|
||||
### Alternance des lignes des tableaux
|
||||
|
||||
Les tableaux utilisent une alternance paire/impaire définie dans chaque thème (`TableRowEven` et `TableRowOdd`). La sélection conserve la couleur d’accent du thème. Les valeurs sont partagées par les fenêtres Sources Windows et Éditions Windows.
|
||||
|
||||
|
||||
## Paramètre de démarrage VNC (v18)
|
||||
|
||||
- Sans paramètre : UltraVNC ne démarre pas.
|
||||
- Avec `-vnc` : UltraVNC démarre.
|
||||
|
||||
Exemple : `powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -vnc`
|
||||
|
||||
|
||||
## Options de lancement
|
||||
|
||||
- Sans paramètre : UltraVNC n'est pas démarré.
|
||||
- Avec `-Vnc` (alias PowerShell utilisable sous la forme `-vnc`) : UltraVNC est démarré.
|
||||
- Les fenêtres sont non redimensionnables par défaut via `Set-DeployCenterFormSizing`; passer `-Resizable` à cette fonction commune pour autoriser le redimensionnement.
|
||||
|
||||
|
||||
## Module VNC
|
||||
|
||||
La gestion d'UltraVNC est isolée dans `Modules/VNC.psm1`. Le fichier est chargé par dot-sourcing uniquement lorsque `-Vnc` est fourni, afin de conserver le contexte partagé de DeployCenter.
|
||||
|
||||
- Sans `-Vnc`, UltraVNC ne démarre pas et DeployCenter ne demande pas la désactivation du pare-feu.
|
||||
- Avec `-Vnc`, le module VNC exécute `wpeutil DisableFirewall` avant de lancer UltraVNC. Cette commande désactive le pare-feu WinPE dans son ensemble ; utilisez cette option sur un réseau de déploiement maîtrisé.
|
||||
- Le démarrage VNC et les erreurs sont consignés dans le journal DeployCenter.
|
||||
|
||||
|
||||
## Modules
|
||||
|
||||
- `Modules/Network.psm1` : détection de l’adresse IPv4 de déploiement, utilisée par le pied de page et le nom du journal.
|
||||
- `Modules/Logging.psm1` : initialisation et écriture des journaux. L’adresse IPv4 détectée lui est transmise par le script principal.
|
||||
- `Modules/Configuration.psm1` : lecture de `DeployCenter.ini` en lecture seule.
|
||||
- `Modules/ThemeManager.psm1` : chargement et validation des palettes déclarées dans `Themes/*.psd1`.
|
||||
- `Modules/UI.psm1` : contrôles, boîtes de dialogue, fenêtres Sources Windows et workflows d’interface.
|
||||
- `Modules/HardwareInventory.psm1` : collecte matérielle, sélection d’un disque et export consolidé de l’inventaire.
|
||||
- `Modules/Partitioning.psm1` : découverte et validation des profils PSD1, calcul de capacité et construction de l’état de dimensionnement.
|
||||
- `Modules/Deployment.psm1` : orchestration du partitionnement et de l’installation Windows pour les profils UEFI/GPT.
|
||||
- `Modules/Unattend.psm1` : découverte des profils de réponses Windows, validation de leur configuration et génération du XML utilisé au premier démarrage.
|
||||
- `Modules/WindowsSources.psm1` : recherche de supports Windows et lecture des éditions WIM/ESD.
|
||||
|
||||
## Profils de partitionnement
|
||||
|
||||
Les fichiers PSD1 de `Partitioning/Profiles` décrivent les schémas de partitionnement comme des données, sans exécuter de code contenu dans les profils. Le profil par défaut « UEFI / GPT — WinRE avant C: » crée EFI (300 Mo), MSR (16 Mo), WinRE (taille définie dans `DeployCenter.ini`, 1 Go par défaut), puis Windows sur l’espace restant. Le profil « UEFI / GPT — WinRE, C: et D: » demande la taille de C: et attribue le reste à D:, dans l’ordre physique C: puis D:. Les propriétés de taille déclarées dans le profil alimentent un formulaire générique.
|
||||
|
||||
Le bouton « Installer » demande une confirmation explicite avant d’effacer le disque sélectionné. Le flux vérifie le firmware, crée les partitions définies par le profil, applique l’image avec DISM, vérifie que WinRE tient dans sa partition, configure le nom d’hôte, crée les fichiers de démarrage UEFI et active WinRE. L’absence d’espace pour copier `winre.wim` interrompt le déploiement ; le non-respect des recommandations de taille et d’espace libre génère un avertissement. Les opérations longues affichent une fenêtre de progression et sont consignées dans le journal. Le moteur actuel prend en charge les profils UEFI/GPT et requiert une image contenant `Windows\System32\Recovery\winre.wim`.
|
||||
|
||||
## Pilotes matériels
|
||||
|
||||
Par défaut, DeployCenter recherche les packs dans `Drivers` au même niveau que son dossier. La clé `[Drivers] RootPath` de `DeployCenter.ini` permet de choisir une autre racine ; une valeur vide conserve le dossier par défaut et un chemin relatif est résolu depuis le dossier DeployCenter. Les clés `LenovoURL`, `DellURL`, `HPURL` et `MicrosoftURL` mémorisent les pages utiles pour retrouver les packs.
|
||||
|
||||
Organisez les dossiers ainsi : `Drivers\<Constructeur>\<Préfixe modèle ou SKU>\<Version Windows>\...`. Les noms de constructeur reconnus sont `Lenovo`, `Dell`, `HP` et `Microsoft`. Le préfixe de modèle/SKU est comparé au modèle et au SKU retournés par l’inventaire matériel ; la correspondance la plus spécifique est retenue. Les noms de version reconnus sont `Windows 10`, `Windows 11` et `Windows Server <année>`. Les fichiers INF peuvent être placés dans des sous-dossiers.
|
||||
|
||||
La page principale affiche le chemin examiné et le résultat de la recherche (le chemin complet est aussi disponible en info-bulle). Quand un pack avec des INF est trouvé, DeployCenter le copie après l’application de Windows par Robocopy dans `C:\DeployCenter\Drivers`, puis exécute `DISM /Image:<Windows hors ligne> /Add-Driver /Driver:C:\DeployCenter\Drivers /Recurse`. Le dossier temporaire est supprimé après l’injection, y compris si la copie ou DISM échoue. Le journal indique séparément le temps de copie et le temps d’injection. En l’absence de correspondance, le déploiement continue sans injection ; une erreur DISM pendant l’injection fait échouer le déploiement.
|
||||
|
||||
|
||||
## Historique des changements
|
||||
|
||||
### 2026-10-17
|
||||
|
||||
- Ajout de `SupportURL` dans `[Unattend]` et dans le template `InstallationOEM.xml`. `SupportURL` accepte l’URL d’un site Web, `SupportAppURL` le schéma d’une application ; les anciennes valeurs HTTP(S) de `SupportAppURL` sont converties pour compatibilité. Les journaux Panther indiquent que le fichier Unattend est validé et que `specialize` réussit.
|
||||
- `AdminGroup` est fixé à `Administrators` dans `DeployCenter.ini` et son exemple. Windows Setup exige les noms de groupes neutres anglais avant que la langue de l’interface soit appliquée.
|
||||
- Ajout de `DriverManagement.psm1`, d’une clé `[Drivers] RootPath` et d’URLs constructeur dans `DeployCenter.ini` et son exemple. La détection utilise fabricant, modèle/SKU et édition Windows ; les packs contenant des INF sont ajoutés à l’image hors ligne par DISM après son application. Le panneau principal montre le chemin recherché et le résultat.
|
||||
- La recherche des dossiers modèle ignore les valeurs Model/SKU absentes et refuse les correspondances ambiguës plutôt que de choisir un pack incertain.
|
||||
- Correction de l’échappement des arguments des commandes natives Windows. Les chemins terminés par une barre oblique inverse, comme `W:\`, ne ferment plus prématurément leur guillemet ; cela permet à `mountvol` de recevoir correctement sa lettre et le chemin de volume.
|
||||
- L’affectation temporaire d’une lettre source vérifie l’accès effectif au fichier WIM après `mountvol`, afin de gérer les environnements WinPE qui ne remontent pas correctement son code de sortie.
|
||||
- Le point de montage DISM temporaire est créé à la racine du lecteur système WinPE plutôt que sous `%TEMP%\DeployCenter`, après l’échec d’accès DISM dans ce dernier emplacement ; la libération de la lettre source est confirmée par l’inaccessibilité du WIM après `mountvol /D`.
|
||||
- La taille de WinRE est lue en montant l’édition sélectionnée avec DISM en lecture seule, pour les sources `.wim` comme `.esd`, puis en démontant l’image sans modification. Cette méthode remplace l’extraction directe de `winre.wim`, qui remplissait le disque RAM WinPE (erreur WIMGAPI `112`).
|
||||
- Remplacement de cette lecture préalable par une taille WinRE configurable dans `[Partitioning] WinRESizeMB` (1024 Mo par défaut). Le profil réserve cette taille sans monter ni extraire `install.wim` ; après l’application de Windows, DeployCenter mesure `winre.wim` et arrête la configuration WinRE si la partition est inférieure à 990 Mo ou ne conserverait pas 250 Mo libres après copie.
|
||||
- Correction de la fermeture du callback de progression : le callback de journalisation est maintenant capturé explicitement avant `GetNewClosure()`, afin qu’il reste accessible depuis le scope de l’action de progression.
|
||||
- Pour la préparation DISM, une source ISO exposée sous un chemin de volume GUID reçoit temporairement une lettre libre autre que C: ou D:. DISM reçoit aussi son répertoire temporaire et son propre journal pour faciliter le diagnostic des erreurs de montage.
|
||||
- Resserrement de l’espacement dans la confirmation d’effacement pour rapprocher l’avertissement du nom d’hôte.
|
||||
- Ajout du flux intégré « Installer » : confirmation avant effacement, tailles des partitions calculées depuis le profil, application DISM, configuration du nom d’hôte, fichiers de démarrage UEFI et activation de WinRE.
|
||||
- Création de `Modules/Deployment.psm1` pour isoler l’exécution du déploiement ; ajout dans `UI.psm1` des fenêtres thémées de confirmation et de progression. Le nouveau bouton d’installation suit le thème actif.
|
||||
- Suppression des objets de résultat de démontage ISO à la fois dans `WindowsSources.psm1` et aux appels UI, afin qu’ils ne s’affichent pas dans la console PowerShell.
|
||||
- Prise en charge des volumes ISO exposés par WinPE sans objet partition : recherche via le chemin de volume (GUID), puis libération de D: avec `mountvol` si cette lettre est attribuée à l’ISO.
|
||||
- Recherche de `install.wim`/`install.esd` à partir des volumes associés explicitement à l’ISO, puis association de chaque volume à sa partition.
|
||||
- Le montage ISO réutilise une lettre déjà attribuée si elle n’est ni C: ni D: ; si WinPE a attribué l’une de ces lettres, elle est retirée avant l’affectation temporaire.
|
||||
- Correction des appels `Add-PartitionAccessPath` et `Remove-PartitionAccessPath` : avec `-InputObject`, la lettre du lecteur doit être fournie comme `-AccessPath` pour respecter leurs jeux de paramètres.
|
||||
- Résolution du disque ISO par `Get-Disk -Number` à partir de `MSFT_DiskImage.Number`, sans pipeline ambigu dans certaines versions de WinPE.
|
||||
- Correction de l’association entre un ISO monté et son disque dans WinPE : résolution via `Get-DiskImage | Get-Disk` plutôt que par le chemin `\\.\CDROMn`.
|
||||
- Le montage ISO n’attribue plus de lettre automatiquement : une lettre temporaire disponible est choisie en excluant C: et D:. Une barre d’activité apparaît pendant l’analyse des éditions.
|
||||
- Renommage de la commande de nettoyage du montage ISO avec le verbe PowerShell approuvé `Dismount`, pour supprimer l’avertissement à l’import du module WindowsSources.
|
||||
- Le montage d’un ISO reste actif tant qu’il est la source Windows sélectionnée ; il est libéré après analyse annulée, remplacement par une autre source, erreur ou fermeture de DeployCenter.
|
||||
- Le callback de journalisation utilise `INFO` lorsque le niveau est omis ou vide, afin d’éviter une erreur de validation `ValidateSet` au démarrage.
|
||||
- Correction du callback de journalisation au démarrage : il conserve désormais le scope du script principal pour résoudre `Write-Log` depuis les événements WinForms. Les derniers appels directs de `DeployCenter.ps1` passent aussi par ce callback.
|
||||
|
||||
### 2026-10-15
|
||||
|
||||
- Les anciens exports de `HardwareInfo.ps1` ont d’abord été déplacés sous `%TEMP%\DeployCenter`, puis retirés au profit du seul inventaire consolidé décrit ci-dessous.
|
||||
- L’affichage des disques a d’abord utilisé `InterfaceType`, puis a été corrigé pour afficher `BusType` obtenu par `Get-Disk`.
|
||||
- Le module VNC réutilise l’adresse IPv4 détectée au démarrage ; la détection réseau n’est plus relancée lors du démarrage de VNC.
|
||||
- Les messages d’information et d’erreur utilisent maintenant `Show-DeployCenterMessage` dans `Modules/UI.psm1`, avec la palette active, au lieu des boîtes de dialogue natives de Windows.
|
||||
- Les erreurs de démarrage VNC indiquent maintenant l’étape concernée pour distinguer les échecs de nettoyage, de copie, de pare-feu et de lancement.
|
||||
- La sortie de `wpeutil DisableFirewall` et son code de retour sont consignés dans le journal DeployCenter au lieu d’apparaître dans la console PowerShell.
|
||||
- Le libellé de statut de la fenêtre Sources Windows est remonté et sa hauteur réduite pour mieux le séparer du pied de page.
|
||||
- L’inventaire matériel, l’adresse IP et les informations VNC sont réunis dans `%TEMP%\DeployCenter\DeployCenter_Inventory.json`. Les exports distincts `HardwareInfo.json`, `hardware.json` et `hardware.log` ne sont plus générés.
|
||||
- Les anciens fichiers temporaires peuvent rester jusqu’au redémarrage de WinPE, qui vide `%TEMP%`.
|
||||
- La collecte complète des disques utilise maintenant `Get-Disk` : numéro, nom, taille, type de bus, état opérationnel et état de santé. `Win32_DiskDrive` n’est plus utilisé pour l’inventaire des disques.
|
||||
- La détection IPv4 de `Modules/Network.psm1` utilise les API .NET `System.Net.NetworkInformation` au lieu de `Get-CimInstance Win32_NetworkAdapterConfiguration` ; la priorité aux interfaces avec passerelle et les exclusions d’adresses restent en place.
|
||||
- Ajout du chargement et de la validation de profils de partitionnement PSD1, ainsi que d’un sélecteur dans l’interface. Le profil UEFI/GPT place EFI, MSR et WinRE avant Windows, qui reçoit l’espace restant ; aucun script DiskPart n’est exécuté à cette étape.
|
||||
- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell et PSD1 pour préserver les caractères accentués lors de leur lecture par Windows PowerShell 5.1.
|
||||
- Correction de l’apostrophe dans la description du profil UEFI/GPT, qui empêchait le chargement du fichier PSD1.
|
||||
- Ajout du profil UEFI/GPT avec C: et D:. Les politiques de taille du PSD1 pilotent un formulaire générique : saisie de C: et calcul de D: comme espace restant estimé. Le profil place C: avant D:.
|
||||
- Correction d’une syntaxe PowerShell incompatible dans les filtres de politique de taille du profil, qui empêchait le lancement du script.
|
||||
- Correction du message de sélection du disque : son apostrophe typographique fermait prématurément une chaîne PowerShell et bloquait l’analyse du script.
|
||||
- Correction du placement d’un libellé du formulaire C:/D: : l’expression de coordonnées était interprétée comme un troisième argument du constructeur `Point`. Le profil PSD1 reçoit un BOM UTF-8 et utilise des guillemets doubles autour de sa description, qui contient une apostrophe typographique.
|
||||
- Le formulaire du profil C:/D: recalcule D: pendant la saisie de C: et accepte désormais une taille minimale de 40 Go pour C:.
|
||||
- Réorganisation de la première page : sélection du disque, du système, du profil et du nom d’hôte dans le panneau de préparation ; le journal est accessible dans une fenêtre dédiée depuis la barre d’actions. Les nouveaux contrôles et la fenêtre du journal sont construits dans `UI.psm1` et suivent le thème actif.
|
||||
- Intégration du nouveau panneau de préparation dans la fenêtre principale : le profil est activé après la sélection d’une édition Windows, et le journal est ouvert depuis le bouton placé à gauche des actions principales.
|
||||
- Le journal reste alimenté par `Logging.psm1` ; `UI.psm1` fournit uniquement sa fenêtre thémée et son contrôle d’affichage.
|
||||
- Le libellé du système sélectionné conserve la couleur de statut du thème et tronque les chemins longs sans déformer le panneau.
|
||||
- Le constructeur de ComboBox accepte une collection vide à l’initialisation du sélecteur de disque, ensuite alimentée par l’inventaire matériel.
|
||||
- Le champ du nom d’hôte est dimensionné pour un nom NetBIOS et limite la saisie à 15 caractères.
|
||||
- Le champ du nom d’hôte est aligné à droite du panneau avec un espacement réservé après son libellé afin d’éviter tout chevauchement.
|
||||
- Dans Sources Windows, le double-clic sur une ligne sélectionnée lance aussi l’affichage des éditions, comme le bouton dédié.
|
||||
- Après validation d’une édition Windows, la fenêtre Sources Windows se ferme pour revenir directement à l’écran principal.
|
||||
|
||||
- Extraction de la recherche, de la lecture des éditions et de la résolution des images Windows vers Modules/WindowsSources.psm1. DeployCenter.ps1 charge ce module après l’initialisation du journal.
|
||||
- Le module Sources Windows reçoit explicitement un callback de journalisation fourni par Logging.psm1, sans dépendre implicitement de la portée du script principal.
|
||||
|
||||
- Déplacement dans UI.psm1 des constructeurs génériques de labels, groupes et boutons, du pied de page de session et du réglage de fenêtre. Les fonctions reçoivent explicitement thème, police et données de session.
|
||||
- Extraction dans UI.psm1 de la sélection du logo selon le thème et de son remplacement ; chemins et PictureBox sont transmis en paramètres.
|
||||
- Création de Modules/Configuration.psm1 : la lecture de DeployCenter.ini est isolée et retourne un objet de préférences sans modifier le fichier.
|
||||
- Répartition des fonctions restantes : workflows de fenêtre et de thème dans UI.psm1, préférences dans Configuration.psm1, collecte/export/lecture disque dans HardwareInventory.psm1. Les profils et calculs de capacité restent dans Partitioning.psm1.
|
||||
- Configuration.psm1 renvoie désormais un objet de configuration avec des valeurs par défaut et ne dépend plus de variables script externes.
|
||||
- HardwareInventory.psm1 exporte la collecte et l’export avec des paramètres explicites ; HardwareInfo.ps1 reste un point d’entrée de compatibilité.
|
||||
- Nettoyage des espaces en fin de ligne dans les modules UI et Partitioning après leur déplacement.
|
||||
- Partitioning.psm1 construit désormais l’état des tailles choisi à partir du profil, du disque et du résultat du dialogue UI.
|
||||
- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell pour Windows PowerShell 5.1 et tolérance de l’absence temporaire d’inventaire lors de la résolution d’un disque.
|
||||
- Mise à jour de la description d’architecture et de la liste des modules après l’extraction des fonctions restantes.
|
||||
- Correction des appels d’export d’inventaire depuis le module VNC : les chemins requis et les données de session sont transmis explicitement, sans invite interactive au lancement.
|
||||
- Les workflows UI et VNC transmettent désormais leurs messages au callback de Logging.psm1, sans appeler Write-Log directement depuis leurs scopes.
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
@echo off
|
||||
Start powershell.exe -executionpolicy Unrestricted -WorkingDirectory y:\deploycenter -Command .\DeployCenter.ps1 -windowed -vnc
|
||||
Start powershell.exe -executionpolicy Unrestricted -WorkingDirectory y:\deploycenter -Command .\DeployCenter.ps1 -vnc
|
||||
+39
-22
@@ -1,42 +1,59 @@
|
||||
; Preferences DeployCenter
|
||||
; Le fichier est lu au démarrage et n'est jamais modifié automatiquement.
|
||||
; Copiez ce fichier sous le nom DeployCenter.ini puis adaptez les valeurs.
|
||||
; This file is read at startup and is never modified automatically.
|
||||
; Copy it to DeployCenter.ini and adjust the values.
|
||||
[General]
|
||||
; Nom d'un fichier Themes\<Nom>.psd1 sans extension. Valeur par défaut : Light.
|
||||
; Name of a file in Themes\<Name>.psd1 format, without the extension. Default: Light.
|
||||
Theme=Light
|
||||
|
||||
[Sources]
|
||||
; Racines à analyser, séparées par des points-virgules.
|
||||
; %DEPLOY_VOLUME% désigne le lecteur qui héberge DeployCenter.
|
||||
; Roots to scan, separated by semicolons.
|
||||
; %DEPLOY_VOLUME% is the drive that hosts DeployCenter.
|
||||
Paths=%DEPLOY_VOLUME%
|
||||
|
||||
[UltraVNC]
|
||||
; Laisser vide pour générer un mot de passe aléatoire à chaque lancement.
|
||||
; Pour les tests, renseigner un mot de passe ici. Il sera converti dans ultravnc.ini par createpassword64.exe.
|
||||
; Leave empty to generate a random password at each startup.
|
||||
; For testing, enter a password here. createpassword64.exe converts it for ultravnc.ini.
|
||||
Password=
|
||||
|
||||
[Partitioning]
|
||||
; Taille réservée à WinRE en Mo. Valeur par défaut : 1024 (1 Go).
|
||||
; La valeur doit être un entier strictement positif ; Windows recommande au moins 990 Mo.
|
||||
; Space reserved for WinRE in MB. Default: 1024 (1 GB).
|
||||
; Must be a positive integer; Microsoft recommends at least 990 MB.
|
||||
; WinRESizeMB=1024
|
||||
|
||||
[Drivers]
|
||||
; Root folder containing Manufacturer\Model\Windows version\*.inf.
|
||||
; Empty = the Drivers folder next to DeployCenter.
|
||||
RootPath=
|
||||
; Official references for finding manufacturer driver packs.
|
||||
LenovoURL=https://docs.lenovocdrt.com/dpm/deploy/recipe-cards.html
|
||||
DellURL=https://www.dell.com/support/kbdoc/en-us/000124139/dell-command-deploy-driver-packs-for-enterprise-client-os-deployment
|
||||
HPURL=https://ftp.hp.com/pub/caps-softpaq/cmit/HP_Driverpack_Matrix_x64.html
|
||||
MicrosoftURL=https://support.microsoft.com/en-us/surface/drivers-firmware/download-drivers-and-firmware-for-surface
|
||||
|
||||
[Unattend]
|
||||
; Utilisés par les profils Unattend qui créent un compte administrateur local.
|
||||
; AdminName : nom de connexion (défaut : admin).
|
||||
; Used by Unattend profiles that create a local administrator account.
|
||||
; AdminName: logon name (default: admin).
|
||||
AdminName=admin
|
||||
; AdminDisplayName : nom affiché (défaut : Administrateur local).
|
||||
AdminDisplayName=Administrateur local
|
||||
; AdminGroup : groupe local ; utiliser le nom neutre anglais reconnu sur toutes les langues Windows.
|
||||
; AdminDisplayName: display name (default: Local Administrator).
|
||||
AdminDisplayName=Local Administrator
|
||||
; AdminGroup: local group; use the English name recognized across Windows languages.
|
||||
; The localized name of the Administrators group can vary by system language.
|
||||
AdminGroup=Administrators
|
||||
; Base64 du mot de passe encodé en UTF-16LE (ce n'est pas un chiffrement).
|
||||
; Vide par défaut : le profil créant un compte local sera indisponible tant qu'un mot de passe n'est pas fourni.
|
||||
; Generate the value with: powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -EncodeUnattendPassword
|
||||
; Enter the password in the hidden prompt, then copy the displayed value here.
|
||||
; For LocalAccount/Password with PlainText=false, Windows SIM expects UTF-16LE(Base64) of the password followed by Password.
|
||||
; This format is reversible and is not encryption; protect this file as a secret.
|
||||
; Empty by default: profiles that create a local account remain unavailable until a password is provided.
|
||||
AdminPasswordBase64=
|
||||
; Nom facultatif affiché comme fournisseur de support Windows.
|
||||
; Si vide ou absent, Windows utilise le fabricant système.
|
||||
; Optional name displayed as the Windows support provider.
|
||||
; If empty or missing, Windows uses the system manufacturer.
|
||||
SupportProvider=
|
||||
; URI facultative de l'application ou du site de support Windows.
|
||||
; Optional support website URL (e.g. https://support.example.org).
|
||||
SupportURL=
|
||||
; Protocol URI for an installed support app (e.g. myapp-support:).
|
||||
; Leave empty if no dedicated support app is installed.
|
||||
SupportAppURL=
|
||||
; Chemin du fond d'écran, absolu ou relatif au dossier DeployCenter.
|
||||
; L'image est copiée dans Windows et définie dans le thème par défaut des nouveaux utilisateurs.
|
||||
; Vide = fond Windows inchangé. Cette option nécessite un profil Unattend ; « Avec OOBE » reste sans Unattend.
|
||||
; Wallpaper path, absolute or relative to the DeployCenter folder.
|
||||
; The image is copied to Windows and set as the default theme wallpaper for new users.
|
||||
; Empty = keep the Windows wallpaper unchanged. This option requires an Unattend profile; “Use OOBE” does not use an answer file.
|
||||
WallpaperPath=Assets\blorand.png
|
||||
+446
-189
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Point d'entrée historique conservé pour les appels directs à HardwareInfo.ps1.
|
||||
# Legacy entry point retained for direct calls to HardwareInfo.ps1.
|
||||
$hardwareInventoryModule = Join-Path $PSScriptRoot 'Modules\HardwareInventory.psm1'
|
||||
Import-Module $hardwareInventoryModule -Force
|
||||
Get-HardwareInfo
|
||||
@@ -0,0 +1,4 @@
|
||||
@{
|
||||
Culture = 'en-US'
|
||||
Strings = @{}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
@{
|
||||
Culture = 'fr-FR'
|
||||
Strings = @{
|
||||
" (remaining space)" = " (espace restant)"
|
||||
" INF file(s) from " = " fichier(s) INF depuis "
|
||||
" to " = " vers "
|
||||
"{0} edition(s) found in {1}. No new edition selected." = "{0} édition(s) trouvée(s) dans {1}. Aucune nouvelle édition sélectionnée."
|
||||
"{0} edition(s) found in {1}. Select an edition." = "{0} édition(s) détectée(s) dans {1}. Sélectionne une édition."
|
||||
"{0} result(s) shown out of {1}. Instant filter; no files changed." = "{0} résultat(s) affiché(s) sur {1}. Filtre instantané ; aucun fichier modifié."
|
||||
"{0}: {1:N1} GB (remaining space)" = "{0} : {1:N1} Go (espace restant)"
|
||||
"All contents of the disk will be erased. This operation cannot be undone." = "Tout le contenu du disque sera effacé. Cette opération est irréversible."
|
||||
"GB" = "Go"
|
||||
"Analyzing editions in" = "Analyse des éditions de"
|
||||
"Analyzing editions in {0}..." = "Analyse des éditions de {0} en cours..."
|
||||
"Applying Windows to the partition" = "Application de Windows sur la partition"
|
||||
"Architecture" = "Architecture"
|
||||
"Architecture unavailable" = "Architecture non disponible"
|
||||
"Architecture:" = "Architecture :"
|
||||
"Argument types do not match." = "Les types des arguments ne correspondent pas."
|
||||
"Automatic restart cancelled:" = "Redémarrage automatique annulé :"
|
||||
"Available Windows editions" = "Éditions Windows disponibles"
|
||||
"Browse…" = "Choisir…"
|
||||
"C: size" = "Taille de C :"
|
||||
"Cancel" = "Annuler"
|
||||
"Checking WinRE partition capacity…" = "Vérification de la capacité de la partition WinRE…"
|
||||
"Close" = "Fermer"
|
||||
"Computer name" = "Nom d`’hôte"
|
||||
"Computer name:" = "Nom d`’hôte :"
|
||||
"Configured WinRE size:" = "Taille WinRE configurée :"
|
||||
"Configured WinRE size: {1} MB." = "Taille WinRE configurée : {1} Mo."
|
||||
"Configuring the WinRE recovery partition…" = "Configuration de la partition de récupération WinRE…"
|
||||
"Configuring UEFI boot…" = "Configuration du démarrage UEFI…"
|
||||
"Confirm disk erasure" = "Confirmer l`’effacement du disque"
|
||||
"Copying " = "Copie de "
|
||||
"Copying drivers locally…" = "Copie locale des pilotes…"
|
||||
"Copying image {0} from {1} to {2} locally." = "Copie locale de l`’image {0} depuis {1} vers {2}."
|
||||
"Copying the log to " = "Copie du journal vers "
|
||||
"Copying the Windows image locally…" = "Copie locale de l`’image Windows…"
|
||||
"Using the Windows image directly from " = "Utilisation directe de l`’image Windows depuis "
|
||||
"Using the driver pack directly from " = "Utilisation directe du pack de pilotes depuis "
|
||||
"Copying winre.wim…" = "Copie de winre.wim…"
|
||||
"Cleaning up temporary files…" = "Nettoyage des fichiers temporaires…"
|
||||
"Creating partitions…" = "Création des partitions…"
|
||||
"D: size" = "Taille de D :"
|
||||
"D: size (remaining space)" = "Taille de D: (espace restant)"
|
||||
"DeployCenter error" = "Erreur DeployCenter"
|
||||
"DeployCenter log" = "Journal DeployCenter"
|
||||
"DeployCenter started." = "DeployCenter démarre."
|
||||
"Deployment complete" = "Déploiement terminé"
|
||||
"Deployment complete: Windows" = "Déploiement terminé : Windows"
|
||||
"Deployment complete: Windows " = "Déploiement terminé : Windows "
|
||||
"Deployment failed" = "Échec du déploiement"
|
||||
"Deployment failed:" = "Échec du déploiement :"
|
||||
"Deployment failed." = "Le déploiement a échoué."
|
||||
"Deployment IP unavailable" = "IP de déploiement indisponible"
|
||||
"Deployment IP:" = "IP de déploiement :"
|
||||
"Deployment is ready." = "Le déploiement est prêt."
|
||||
"Deployment preparation" = "Préparation du déploiement"
|
||||
"Deployment steps" = "Étapes du déploiement"
|
||||
"Upcoming" = "À venir"
|
||||
"In progress…" = "En cours…"
|
||||
"Completed" = "Terminé"
|
||||
"Failed" = "Échec"
|
||||
"Temporary C:\DeployCenter directory removed." = "Dossier temporaire C:\DeployCenter supprimé."
|
||||
"Unable to remove the temporary C:\DeployCenter directory: " = "Impossible de supprimer le dossier temporaire C:\DeployCenter : "
|
||||
"Registering the WinRE image…" = "Enregistrement de l’image WinRE…"
|
||||
"Enabling WinRE…" = "Activation de WinRE…"
|
||||
"Deployment started." = "Déploiement démarré."
|
||||
"Detection complete:" = "Détection terminée :"
|
||||
"Device" = "Périphérique"
|
||||
"Disk" = "Disque"
|
||||
"DISM confirms Windows was applied, but the exit code is unavailable in WinPE." = "DISM confirme que Windows a été appliqué, mais le code de sortie est absent dans WinPE."
|
||||
"DISM could not read metadata for index " = "DISM n’a pas pu lire les métadonnées de l’index "
|
||||
"DISM driver injection completed in " = "Injection DISM des pilotes terminée en "
|
||||
"DISM returned no usable editions for:" = "DISM n`’a retourné aucune édition exploitable pour :"
|
||||
"Driver search failed:" = "Recherche des pilotes impossible :"
|
||||
"Drivers found:" = "Pilotes trouvés :"
|
||||
"Drivers searched under:" = "Pilotes recherchés sous :"
|
||||
"Drivers: select a Windows edition to search for a driver pack." = "Pilotes : sélectionne une édition Windows pour rechercher un pack."
|
||||
"Edition" = "Édition"
|
||||
"Edition analysis" = "Analyse des éditions"
|
||||
"Edition analysis complete:" = "Analyse des éditions terminée :"
|
||||
"Edition analysis error" = "Erreur d’analyse des éditions"
|
||||
"Edition analysis error " = "Erreur d’analyse des éditions "
|
||||
"Edition selected:" = "Édition sélectionnée :"
|
||||
"Edition:" = "Édition :"
|
||||
"edition(s) detected in" = "édition(s) détectée(s) dans"
|
||||
"Entered size for " = "Taille saisie pour "
|
||||
"Erase and install" = "Effacer et installer"
|
||||
"Erasing and partitioning the disk…" = "Effacement et partitionnement du disque…"
|
||||
"Erasing disk" = "Effacement du disque"
|
||||
"Error" = "Erreur"
|
||||
"Estimated capacity after EFI, MSR and WinRE reservations:" = "Capacité estimée après réservation EFI, MSR et WinRE :"
|
||||
"Estimated capacity after EFI, MSR and WinRE reservations: {0:N1} GB.``r``nConfigured WinRE size: {1} MB." = "Capacité estimée après réservation EFI, MSR et WinRE : {0:N1} Go.``r``nTaille WinRE configurée : {1} Mo."
|
||||
"Failed to analyze editions in {0}." = "Échec de l’analyse des éditions de {0}."
|
||||
"Failed to request restart:" = "Échec de la demande de redémarrage :"
|
||||
"File:" = "Fichier :"
|
||||
"Filter list:" = "Filtrer la liste :"
|
||||
"Finalizing partitions…" = "Finalisation des partitions…"
|
||||
"Firmware:" = "Firmware :"
|
||||
"First-run configuration" = "Configuration de première ouverture"
|
||||
"Initial configuration:" = "Configuration initiale :"
|
||||
"Folder:" = "Dossier :"
|
||||
"Get-WindowsImage unavailable for " = "Get-WindowsImage indisponible pour "
|
||||
"Hardware inventory refreshed." = "Inventaire matériel actualisé."
|
||||
"Image file not found:" = "Fichier image introuvable :"
|
||||
"Index" = "Index"
|
||||
"identifier" = "identificateur"
|
||||
"Information" = "Information"
|
||||
"Injecting driver pack:" = "Injection du pack de pilotes :"
|
||||
"Install" = "Installer"
|
||||
"Installing hardware drivers…" = "Installation des pilotes matériels…"
|
||||
"Installing Windows drivers…" = "Installation des pilotes Windows…"
|
||||
"Insufficient space on " = "Espace insuffisant sur "
|
||||
"Insufficient space on the WinRE partition to copy winre.wim:" = "Espace insuffisant sur la partition WinRE pour copier winre.wim :"
|
||||
"Invalid computer name" = "Nom d`’hôte invalide"
|
||||
"Invalid Unattend configuration" = "Configuration Unattend invalide"
|
||||
"Inventory error:" = "Erreur inventaire :"
|
||||
"Local account password" = "Le mot de passe du compte local"
|
||||
"Local copy of image " = "Copie locale de l`’image "
|
||||
"Local driver copy completed in " = "Copie locale des pilotes terminée en "
|
||||
"Local Windows image copy completed in " = "Copie locale de l`’image Windows terminée en "
|
||||
"Local Windows image copy completed in {0:N1} s ({1} MB)." = "Copie locale de l`’image Windows terminée en {0:N1} s ({1} Mo)."
|
||||
"Log" = "Journal"
|
||||
"Log copy skipped:" = "Copie du journal ignorée :"
|
||||
"Log copy skipped: no log file is available." = "Copie du journal ignorée : aucun fichier de journal disponible."
|
||||
"Log:" = "Journal :"
|
||||
"Manufacturer:" = "Fabricant :"
|
||||
"Model:" = "Modèle :"
|
||||
"Name" = "Nom"
|
||||
"Name unavailable" = "Nom non disponible"
|
||||
"Native command:" = "Commande native :"
|
||||
"Network detection:" = "Détection réseau :"
|
||||
"No deployment IPv4 address detected." = "Aucune adresse IPv4 de déploiement détectée."
|
||||
"No drivers found (" = "Pilotes absents ("
|
||||
"No ISO/WIM/ESD source or extracted installation was found in the accessible paths." = "Aucune source ISO/WIM/ESD ou installation extraite détectée dans les chemins accessibles."
|
||||
"No matching driver pack; injection skipped." = "Aucun pack de pilotes correspondant ; injection ignorée."
|
||||
"No physical disk detected." = "Aucun disque physique détecté."
|
||||
"No results for" = "Aucun résultat pour"
|
||||
"No results for `“{0}`”. Clear the filter to see the list again." = "Aucun résultat pour « {0} ». Efface le filtre pour revoir la liste."
|
||||
"No Sources\install.wim or Sources\install.esd file in:" = "Aucun fichier Sources\install.wim ou Sources\install.esd dans :"
|
||||
"No Sources\install.wim or Sources\install.esd file was found in the ISO." = "Aucun fichier Sources\install.wim ou Sources\install.esd trouvé dans l`’ISO."
|
||||
"No system selected" = "Aucun système sélectionné"
|
||||
"No Windows source found in the accessible paths." = "Aucune source Windows détectée dans les chemins accessibles."
|
||||
"Non-interactive mode prepared:" = "Mode sans interaction préparé :"
|
||||
"None of the paths is accessible:" = "Aucun des chemins n'est accessible :"
|
||||
"OEM installation" = "Installation OEM"
|
||||
"OK" = "OK"
|
||||
"Partition sizes" = "Tailles des partitions"
|
||||
"Partitioning" = "Partitionnement"
|
||||
"Partitioning disk" = "Partitionnement du disque"
|
||||
"Partitioning disk " = "Partitionnement du disque "
|
||||
"Partitioning profile" = "Profil de partitionnement"
|
||||
"Partitioning profile " = "Profil de partitionnement "
|
||||
"Partitioning profile:" = "Profil de partitionnement :"
|
||||
"Remaining space:" = "Espace restant :"
|
||||
"Partitioning unavailable" = "Partitionnement impossible"
|
||||
"Path" = "Chemin"
|
||||
"Path:" = "Chemin :"
|
||||
"Paths to scan (separated by ;):" = "Chemins à analyser (séparés par ;) :"
|
||||
"PC information" = "Informations du PC"
|
||||
"Physical disk" = "Disque physique"
|
||||
"Physical disk:" = "Disque physique :"
|
||||
"Preparing deployment…" = "Préparation du déploiement…"
|
||||
"Preparing the Unattend file…" = "Préparation du fichier Unattend…"
|
||||
"Preparing the Windows answer file…" = "Préparation du fichier de réponses Windows…"
|
||||
"Preparing Windows boot…" = "Préparation du démarrage Windows…"
|
||||
"Read-only scan of the DeployCenter drive and/or SMB share. Separate multiple paths with semicolons." = "Analyse en lecture seule du lecteur de DeployCenter et/ou du partage SMB. Plusieurs chemins peuvent être séparés par un point-virgule."
|
||||
"Refresh" = "Actualiser"
|
||||
"Restart this computer when closing" = "Redémarrer cet ordinateur après fermeture"
|
||||
"Robocopy exited with code " = "Robocopy s`’est terminé avec le code "
|
||||
"Robocopy did not report an exit code and file verification failed." = "Robocopy n’a pas fourni de code de sortie et la vérification des fichiers a échoué."
|
||||
"Robocopy did not report an exit code; file verification succeeded." = "Robocopy n’a pas fourni de code de sortie ; la vérification des fichiers a réussi."
|
||||
"Search" = "Rechercher"
|
||||
"Search failed. See the message." = "Échec de la recherche. Consulte le message."
|
||||
"Searching editions" = "Recherche des éditions"
|
||||
"Searching for Windows sources in:" = "Recherche des sources Windows dans :"
|
||||
"Searching the specified paths..." = "Recherche en cours sur les chemins indiqués..."
|
||||
"Select" = "Valider"
|
||||
"Select a physical disk first." = "Sélectionne d'abord un disque physique."
|
||||
"Select a source from the list first." = "Sélectionne d`’abord une source dans la liste."
|
||||
"Select a source from the list." = "Sélectionne une source dans la liste."
|
||||
"Select a Windows edition to install." = "Sélectionne une édition Windows à installer."
|
||||
"Select a Windows edition to check" = "Sélectionne une édition Windows pour vérifier les pilotes."
|
||||
"Select an edition." = "Sélectionne une édition."
|
||||
"Select edition" = "Valider l`’édition"
|
||||
"Select the destination disk." = "Sélectionne le disque de destination."
|
||||
"Selected edition:" = "Édition sélectionnée :"
|
||||
"Selected partitioning profile:" = "Profil de partitionnement sélectionné :"
|
||||
"Selected:" = "Sélectionnée :"
|
||||
"Sizes:" = "Tailles :"
|
||||
"Sizes defined by the profile" = "Tailles définies par le profil"
|
||||
"Serial number:" = "Numéro de série :"
|
||||
"Show editions" = "Afficher les éditions"
|
||||
"Size" = "Taille"
|
||||
"Source detection" = "Détection des sources"
|
||||
"Source detection error:" = "Erreur de détection des sources :"
|
||||
"Source inaccessible, skipped:" = "Source inaccessible, ignorée :"
|
||||
"Source unavailable, skipped:" = "Source inaccessible, ignorée :"
|
||||
"Source:" = "Source :"
|
||||
"Starting deployment" = "Démarrage du déploiement"
|
||||
"Starting deployment…" = "Démarrage du déploiement…"
|
||||
"Stopping UltraVNC:" = "Arrêt UltraVNC :"
|
||||
"System to install" = "Système à installer"
|
||||
"System:" = "Système :"
|
||||
"Temporary local driver copy removed." = "Copie locale temporaire des pilotes supprimée."
|
||||
"Temporary local Windows image copy removed." = "Copie locale temporaire de l`’image Windows supprimée."
|
||||
"The [Unattend] AdminPasswordBase64 setting is required for this profile." = "Le paramètre [Unattend] AdminPasswordBase64 est requis pour ce profil."
|
||||
"The computer name must follow NetBIOS rules and not exceed 15 characters." = "Le nom de machine doit respecter les règles NetBIOS et ne pas dépasser 15 caractères."
|
||||
"The local driver copy is incomplete:" = "La copie locale des pilotes est incomplète :"
|
||||
"The local Windows image copy is incomplete:" = "La copie locale de l`’image Windows est incomplète :"
|
||||
"The log " = "Le journal "
|
||||
"The partition sizes were calculated for another disk." = "La taille des partitions correspond à un autre disque."
|
||||
"The Unattend profile" = "Le profil Unattend"
|
||||
"The WinRE partition" = "La partition WinRE"
|
||||
"Theme" = "Thème"
|
||||
"Type" = "Type"
|
||||
"UEFI / GPT — WinRE before C:" = "UEFI / GPT — WinRE avant C:"
|
||||
"UEFI / GPT — WinRE, C: and D:" = "UEFI / GPT — WinRE, C: et D:"
|
||||
"UltraVNC started. IP:" = "UltraVNC démarré. IP :"
|
||||
"UltraVNC startup failed at stage " = "Échec du démarrage UltraVNC à l'étape "
|
||||
"UltraVNC was not started:" = "UltraVNC non démarré :"
|
||||
"UltraVNC was not started: -vnc parameter was not supplied." = "UltraVNC non démarré : paramètre -vnc absent."
|
||||
"Unable to copy the log to " = "Impossible de copier le journal vers "
|
||||
"Unable to dismount the ISO after analysis:" = "Impossible de démonter l`’ISO après l`’analyse :"
|
||||
"Unable to dismount the ISO on close:" = "Impossible de démonter l'ISO à la fermeture :"
|
||||
"Unable to load theme " = "Impossible de charger le thème "
|
||||
"Unable to prepare non-interactive mode:" = "Préparation du mode sans interaction impossible :"
|
||||
"Unable to remove the previous inventory export " = "Impossible de supprimer l'ancien export d'inventaire "
|
||||
"Unable to remove the temporary local driver copy:" = "Impossible de supprimer la copie locale temporaire des pilotes :"
|
||||
"Unable to remove the temporary local Windows image copy:" = "Impossible de supprimer la copie locale temporaire de l`’image Windows :"
|
||||
"Unable to start VNC:" = "Impossible de démarrer VNC :"
|
||||
"Unable to write the temporary inventory:" = "Impossible d'écrire l'inventaire temporaire :"
|
||||
"Unattend profile" = "Profil Unattend"
|
||||
"Unattend profile " = "Profil Unattend "
|
||||
"Unattend profile:" = "Profil Unattend :"
|
||||
"Unattend profiles were skipped because their display name is duplicated:" = "Profils Unattend ignorés car leur nom affiché est en double :"
|
||||
"Unavailable" = "Non disponible"
|
||||
"Unsupported source type for analysis:" = "Type de source non pris en charge pour l`’analyse :"
|
||||
"Use OOBE" = "Avec OOBE"
|
||||
"Version" = "Version"
|
||||
"Version unavailable" = "Version non disponible"
|
||||
"VNC password:" = "Mot de passe VNC :"
|
||||
"Wallpaper image copied to Windows:" = "Image de fond copiée dans Windows :"
|
||||
"Warning" = "Avertissement"
|
||||
"Windows answer file prepared with profile " = "Fichier de réponses Windows préparé avec le profil "
|
||||
"Windows deployment" = "Déploiement Windows"
|
||||
"Windows deployment environment" = "Environnement de déploiement Windows"
|
||||
"Windows editions" = "Éditions Windows"
|
||||
"Windows sources" = "Sources Windows"
|
||||
"Windows sources - DeployCenter drive and SMB share" = "Sources Windows - lecteur DeployCenter et partage SMB"
|
||||
"Windows was applied successfully. Deployment is ready." = "Windows a été appliqué avec succès. Le déploiement est prêt."
|
||||
"Unknown" = "Inconnu"
|
||||
"Invalid profile name:" = "Nom de profil invalide :"
|
||||
"Profile not found:" = "Profil introuvable :"
|
||||
"Invalid Unattend profile name:" = "Nom de profil Unattend invalide :"
|
||||
"Unattend profile not found:" = "Profil Unattend introuvable :"
|
||||
"Unattend profile ID " = "L’identifiant du profil Unattend "
|
||||
"The Target property of Unattend profile " = "La propriété Target du profil Unattend "
|
||||
"Invalid XML template name in profile " = "Nom de modèle XML invalide dans le profil "
|
||||
"XML template not found for profile " = "Modèle XML introuvable pour le profil "
|
||||
"Invalid UTF-16LE Base64 format." = "Format Base64 UTF-16LE invalide."
|
||||
"The template for profile " = "Le modèle du profil "
|
||||
"The profile " = "Le profil "
|
||||
"Unknown size policy in profile " = "Politique de taille inconnue dans le profil "
|
||||
"Unsupported partition size policy:" = "Politique de taille de partition non prise en charge :"
|
||||
"No free drive letter is available for partition " = "Aucune lettre de lecteur libre n’est disponible pour la partition "
|
||||
"No temporary drive letter is available to access the Windows source." = "Aucune lettre de lecteur temporaire n’est disponible pour accéder à la source Windows."
|
||||
"No temporary drive letter is available to mount the ISO." = "Aucune lettre de lecteur temporaire n’est disponible pour monter l’ISO."
|
||||
"Windows did not expose any ISO volumes." = "Windows n’a exposé aucun volume ISO."
|
||||
"Specify at least one local path and/or SMB share, separated by semicolons." = "Indique au moins un chemin local et/ou un partage SMB, séparés par des points-virgules."
|
||||
"DISM could not read the image:" = "DISM n’a pas pu lire l’image :"
|
||||
"Check that the local drive and SMB share are available." = "Vérifie que le lecteur local et le partage SMB sont accessibles."
|
||||
"ISO mounting is unavailable in this environment" = "Le montage ISO n’est pas disponible dans cet environnement"
|
||||
"The HardwareInventory module does not provide Get-HardwareInfo." = "Le module HardwareInventory ne fournit pas Get-HardwareInfo."
|
||||
"Unsupported manufacturer" = "Fabricant non pris en charge"
|
||||
"Unrecognized Windows version" = "Version de Windows non reconnue"
|
||||
"Version folder not found" = "Dossier de version introuvable"
|
||||
"No INF files" = "Aucun fichier INF"
|
||||
"The available space on this disk is insufficient for profile " = "L’espace disponible sur ce disque est insuffisant pour le profil "
|
||||
"The size for partition " = "La taille de la partition "
|
||||
"Partition " = "La partition "
|
||||
"Applying Windows to " = "Application de Windows sur "
|
||||
"WinPE storage cache refresh is unavailable:" = "L’actualisation du cache de stockage WinPE est indisponible :"
|
||||
"Robocopy.exe was not found in WinPE." = "Robocopy.exe est introuvable dans WinPE."
|
||||
"Wallpaper image not found:" = "Image de fond introuvable :"
|
||||
"Invalid partition name in profile:" = "Nom de partition invalide dans le profil :"
|
||||
"The source file is still accessible." = "Le fichier source est toujours accessible."
|
||||
"The Windows source is not accessible after temporarily assigning " = "La source Windows n’est pas accessible après l’affectation temporaire de "
|
||||
"The profile must define at least two partitions." = "Le profil doit définir au moins deux partitions."
|
||||
"The profile must define exactly one Windows partition." = "Le profil doit définir exactement une partition Windows."
|
||||
"The WinRE free-space allowance must be at least 250 MB." = "La réserve d’espace libre WinRE doit être d’au moins 250 Mo."
|
||||
"The user-size input must define MinimumGB and a DefaultPercent from 1 to 99." = "La saisie de taille utilisateur doit définir MinimumGB et DefaultPercent entre 1 et 99."
|
||||
"The disk is too small for the profile reserves " = "Le disque est trop petit pour les réserves du profil "
|
||||
"Unsupported partitioning profile" = "Profil de partitionnement non pris en charge"
|
||||
"Partitioning profile not found or ambiguous:" = "Profil de partitionnement introuvable ou ambigu :"
|
||||
"Unattend profile not found or ambiguous:" = "Profil Unattend introuvable ou ambigu :"
|
||||
"The selected Unattend profile was not found or is ambiguous." = "Le profil Unattend sélectionné est introuvable ou ambigu."
|
||||
"Partitioning profile '" = "Le profil de partitionnement '"
|
||||
"Non-interactive mode supports only one user-defined size input for C: in profile " = "Le mode sans interaction ne prend en charge qu’une seule saisie de taille utilisateur pour C: dans le profil "
|
||||
"Non-interactive mode prepared: disk " = "Mode sans interaction préparé : disque "
|
||||
"DeployCenter.ini parameter '" = "Le paramètre DeployCenter.ini '"
|
||||
"PC model:" = "Modèle du PC :"
|
||||
"Disk inventory:" = "Inventaire des disques :"
|
||||
"Firmware mode unavailable" = "Mode firmware indisponible"
|
||||
"createpassword64.exe absent" = "createpassword64.exe absent"
|
||||
"WinPE firewall disabled at the request of -Vnc" = "Pare-feu WinPE désactivé à la demande de -Vnc"
|
||||
"WinPE firewall disabled at the request of -Vnc (wpeutil exit code:" = "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil :"
|
||||
"WinPE restart requested through wpeutil." = "Redémarrage de WinPE demandé via wpeutil."
|
||||
"WinPE storage cache refreshed." = "Cache de stockage WinPE actualisé."
|
||||
"WinPE storage cache unavailable:" = "Cache de stockage WinPE indisponible :"
|
||||
"WinRE reminder:" = "Rappel WinRE :"
|
||||
"WinRE validation succeeded:" = "Vérification WinRE réussie :"
|
||||
"WinRE warning:" = "Avertissement WinRE :"
|
||||
"WinRE warning: Microsoft recommendations are not all met (" = "Avertissement WinRE : les recommandations Microsoft ne sont pas toutes respectées ("
|
||||
"wpeutil DisableFirewall:" = "wpeutil DisableFirewall :"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
# DeployCenter - Lecture des préférences utilisateur
|
||||
# Lecture seule : ce module ne modifie jamais DeployCenter.ini.
|
||||
# DeployCenter - Read user preferences
|
||||
# Read-only: this module never modifies DeployCenter.ini.
|
||||
|
||||
function Read-DeployCenterConfig {
|
||||
[CmdletBinding()]
|
||||
@@ -11,12 +11,15 @@ function Read-DeployCenterConfig {
|
||||
VncPassword = ''
|
||||
WinRESizeMB = 1024
|
||||
UnattendAdminName = 'admin'
|
||||
UnattendAdminDisplayName = 'Administrateur local'
|
||||
UnattendAdminDisplayName = 'Local Administrator'
|
||||
UnattendAdminGroup = 'Administrators'
|
||||
UnattendAdminPasswordBase64 = ''
|
||||
UnattendSupportProvider = ''
|
||||
UnattendSupportURL = ''
|
||||
UnattendSupportAppURL = ''
|
||||
UnattendWallpaperPath = 'Assets\blorand.png'
|
||||
DriverRootPath = ''
|
||||
DriverUrls = @{ Lenovo = ''; Dell = ''; HP = ''; Microsoft = '' }
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { return $config }
|
||||
|
||||
@@ -38,7 +41,7 @@ function Read-DeployCenterConfig {
|
||||
} elseif ($section -ieq 'Partitioning' -and $key -ieq 'WinRESizeMB') {
|
||||
$sizeMB = 0
|
||||
if (-not [int]::TryParse($value, [ref]$sizeMB) -or $sizeMB -le 0) {
|
||||
throw "Le paramètre [Partitioning] WinRESizeMB doit être un entier strictement positif. Valeur reçue : '$value'."
|
||||
throw "[Partitioning] WinRESizeMB must be a positive integer. Value received: '$value'."
|
||||
}
|
||||
$config.WinRESizeMB = $sizeMB
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'AdminName') {
|
||||
@@ -51,10 +54,16 @@ function Read-DeployCenterConfig {
|
||||
$config.UnattendAdminPasswordBase64 = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'SupportProvider') {
|
||||
$config.UnattendSupportProvider = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'SupportURL') {
|
||||
$config.UnattendSupportURL = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'SupportAppURL') {
|
||||
$config.UnattendSupportAppURL = $value
|
||||
} elseif ($section -ieq 'Unattend' -and $key -ieq 'WallpaperPath') {
|
||||
$config.UnattendWallpaperPath = $value
|
||||
} elseif ($section -ieq 'Drivers' -and $key -ieq 'RootPath') {
|
||||
$config.DriverRootPath = $value
|
||||
} elseif ($section -ieq 'Drivers' -and $key -match '^(Lenovo|Dell|HP|Microsoft)URL$') {
|
||||
$config.DriverUrls[$matches[1]] = $value
|
||||
}
|
||||
}
|
||||
return $config
|
||||
|
||||
+420
-93
@@ -1,11 +1,11 @@
|
||||
# DeployCenter - Partitionnement et installation d'une edition Windows
|
||||
# Compatible Windows PowerShell 5.1 / WinPE.
|
||||
# DeployCenter - Windows partitioning and installation
|
||||
# Compatible with Windows PowerShell 5.1 / WinPE.
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'Unattend.psm1') -Force
|
||||
|
||||
function Write-DeployCenterDeploymentLog {
|
||||
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO')
|
||||
if ($LogAction) { & $LogAction $Message $Level }
|
||||
param([AllowNull()][scriptblock]$LogAction, [string]$Message, [string]$Level = 'INFO', [switch]$SystemOutput)
|
||||
if ($LogAction) { & $LogAction $Message $Level ([bool]$SystemOutput) }
|
||||
}
|
||||
|
||||
function Update-DeployCenterHostStorageCache {
|
||||
@@ -14,11 +14,11 @@ function Update-DeployCenterHostStorageCache {
|
||||
try {
|
||||
Update-HostStorageCache -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'Cache de stockage WinPE actualisé.' -Level 'INFO'
|
||||
-Message 'WinPE storage cache refreshed.' -Level 'INFO'
|
||||
return $true
|
||||
} catch {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Actualisation du cache de stockage WinPE indisponible : {0}" -f $_.Exception.Message) `
|
||||
-Message ("WinPE storage cache refresh is unavailable: {0}" -f $_.Exception.Message) `
|
||||
-Level 'WARNING'
|
||||
return $false
|
||||
}
|
||||
@@ -30,19 +30,19 @@ function Restart-DeployCenterWinPE {
|
||||
|
||||
$wpeutilPath = Join-Path $env:WINDIR 'System32\wpeutil.exe'
|
||||
if (-not (Test-Path -LiteralPath $wpeutilPath -PathType Leaf)) {
|
||||
throw 'wpeutil.exe est introuvable ; le redémarrage WinPE ne peut pas être demandé.'
|
||||
throw 'wpeutil.exe was not found; a WinPE restart cannot be requested.'
|
||||
}
|
||||
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'Redémarrage de WinPE demandé via wpeutil.' -Level 'INFO'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'WinPE restart requested through wpeutil.' -Level 'INFO'
|
||||
$output = @(& $wpeutilPath Reboot 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $output) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' -SystemOutput
|
||||
}
|
||||
}
|
||||
if ($null -ne $exitCode -and $exitCode -ne 0) {
|
||||
throw "wpeutil Reboot a renvoyé le code $exitCode."
|
||||
throw "wpeutil Reboot returned exit code $exitCode."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,13 +79,14 @@ function New-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ImagePath,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
$lease = [pscustomobject]@{ ImagePath = $ImagePath; DriveLetter = $null; VolumeRoot = $null }
|
||||
if (-not $ImagePath.StartsWith('\\?\Volume{', [StringComparison]::OrdinalIgnoreCase)) { return $lease }
|
||||
$volumeEnd = $ImagePath.IndexOf('}\', [StringComparison]::OrdinalIgnoreCase)
|
||||
if ($volumeEnd -lt 0) { throw "Chemin de volume GUID invalide pour l’image : $ImagePath" }
|
||||
if ($volumeEnd -lt 0) { throw "Invalid volume GUID path for image: $ImagePath" }
|
||||
$volumeRoot = $ImagePath.Substring(0, $volumeEnd + 2)
|
||||
$relativePath = $ImagePath.Substring($volumeEnd + 2)
|
||||
|
||||
@@ -103,7 +104,7 @@ function New-DeployCenterDismSourceAccess {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $letter) { throw 'Aucune lettre temporaire disponible pour accéder à la source Windows.' }
|
||||
if (-not $letter) { throw 'No temporary drive letter is available to access the Windows source.' }
|
||||
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolPath -PathType Leaf)) { $mountVolPath = 'mountvol.exe' }
|
||||
@@ -111,20 +112,20 @@ function New-DeployCenterDismSourceAccess {
|
||||
$mountError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), $volumeRoot) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
} catch {
|
||||
$mountError = $_.Exception
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $letterPath -PathType Leaf)) {
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $letter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
} catch { }
|
||||
throw "La source Windows n’est pas accessible après l’affectation temporaire de ${letter}:."
|
||||
throw "The Windows source is not accessible after temporarily assigning ${letter}:."
|
||||
}
|
||||
if ($mountError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source est accessible via {0}:. L’affectation est confirmée." -f $letter) `
|
||||
-Message ("mountvol did not provide a usable exit code, but the source is accessible through {0}:. The assignment is confirmed." -f $letter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
$lease.ImagePath = $letterPath
|
||||
@@ -137,7 +138,8 @@ function Remove-DeployCenterDismSourceAccess {
|
||||
param(
|
||||
[AllowNull()]$Lease,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
if ($null -eq $Lease -or -not $Lease.DriveLetter) { return }
|
||||
$mountVolPath = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
@@ -145,18 +147,18 @@ function Remove-DeployCenterDismSourceAccess {
|
||||
$releaseError = $null
|
||||
try {
|
||||
Invoke-DeployCenterNativeCommand -FilePath $mountVolPath -Arguments @(("{0}:\" -f $Lease.DriveLetter), '/D') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
} catch {
|
||||
$releaseError = $_.Exception
|
||||
}
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
if (Test-Path -LiteralPath ([string]$Lease.ImagePath) -PathType Leaf) {
|
||||
$details = if ($releaseError) { $releaseError.Message } else { 'Le fichier source reste accessible.' }
|
||||
$details = if ($releaseError) { $releaseError.Message } else { 'The source file is still accessible.' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message "Impossible de confirmer la libération de la lettre temporaire $($Lease.DriveLetter): $details" -Level 'WARNING'
|
||||
-Message "Could not confirm release of temporary drive letter $($Lease.DriveLetter): $details" -Level 'WARNING'
|
||||
} elseif ($releaseError) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("mountvol n’a pas fourni de code de sortie exploitable, mais la source n’est plus accessible via {0}:. La libération est confirmée." -f $Lease.DriveLetter) `
|
||||
-Message ("mountvol did not provide a usable exit code, but the source is no longer accessible through {0}:. The drive letter has been released." -f $Lease.DriveLetter) `
|
||||
-Level 'WARNING'
|
||||
}
|
||||
}
|
||||
@@ -183,6 +185,155 @@ function Get-DeployCenterNativeProgressPercent {
|
||||
return [int][math]::Max(0, [math]::Min(100, [math]::Floor($value)))
|
||||
}
|
||||
|
||||
function Test-DeployCenterRobocopyCopy {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SourceDirectory,
|
||||
[Parameter(Mandatory=$true)][string]$DestinationDirectory,
|
||||
[Parameter(Mandatory=$true)][string[]]$CopyArguments
|
||||
)
|
||||
|
||||
$sourceRoot = [IO.Path]::GetFullPath($SourceDirectory).TrimEnd('\')
|
||||
$fileFilters = @($CopyArguments | Where-Object { -not ([string]$_).StartsWith('/') })
|
||||
$sourceFiles = @(Get-ChildItem -LiteralPath $SourceDirectory -File -Recurse -ErrorAction Stop)
|
||||
if ($fileFilters.Count -gt 0) {
|
||||
$sourceFiles = @($sourceFiles | Where-Object {
|
||||
$fileName = $_.Name
|
||||
@($fileFilters | Where-Object { $fileName -like [string]$_ }).Count -gt 0
|
||||
})
|
||||
}
|
||||
if ($sourceFiles.Count -eq 0) { return $false }
|
||||
|
||||
foreach ($sourceFile in $sourceFiles) {
|
||||
$relativePath = $sourceFile.FullName.Substring($sourceRoot.Length).TrimStart('\')
|
||||
$destinationPath = Join-Path $DestinationDirectory $relativePath
|
||||
if (-not (Test-Path -LiteralPath $destinationPath -PathType Leaf)) { return $false }
|
||||
$destinationFile = Get-Item -LiteralPath $destinationPath -ErrorAction Stop
|
||||
if ([long]$sourceFile.Length -ne [long]$destinationFile.Length) { return $false }
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterPowerShellLiteral {
|
||||
param([AllowNull()][string]$Value)
|
||||
if ($null -eq $Value) { $Value = '' }
|
||||
return "'" + $Value.Replace("'", "''") + "'"
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterRobocopy {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$SourceDirectory,
|
||||
[Parameter(Mandatory=$true)][string]$DestinationDirectory,
|
||||
[Parameter(Mandatory=$true)][string[]]$CopyArguments,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $TemporaryRoot -PathType Container)) {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$robocopyPath = Join-Path $env:WINDIR 'System32\robocopy.exe'
|
||||
if (-not (Test-Path -LiteralPath $robocopyPath -PathType Leaf)) {
|
||||
$robocopyCommand = Get-Command 'robocopy.exe' -ErrorAction SilentlyContinue
|
||||
if ($null -eq $robocopyCommand) { throw 'Robocopy.exe was not found in WinPE.' }
|
||||
$robocopyPath = [string]$robocopyCommand.Path
|
||||
}
|
||||
|
||||
$id = [guid]::NewGuid().ToString('N')
|
||||
$stdoutPath = Join-Path $TemporaryRoot ("robocopy_{0}.out" -f $id)
|
||||
$stderrPath = Join-Path $TemporaryRoot ("robocopy_{0}.err" -f $id)
|
||||
$runnerPath = Join-Path $TemporaryRoot ("robocopy_{0}.ps1" -f $id)
|
||||
$exitCodePath = Join-Path $TemporaryRoot ("robocopy_{0}.exitcode" -f $id)
|
||||
$arguments = @($SourceDirectory, $DestinationDirectory) + @($CopyArguments)
|
||||
$argumentLine = ($arguments | ForEach-Object {
|
||||
ConvertTo-DeployCenterNativeArgument -Value ([string]$_)
|
||||
}) -join ' '
|
||||
$powerShellPath = Join-Path $PSHOME 'powershell.exe'
|
||||
if (-not (Test-Path -LiteralPath $powerShellPath -PathType Leaf)) {
|
||||
$powerShellCommand = Get-Command 'powershell.exe' -ErrorAction SilentlyContinue
|
||||
if ($null -eq $powerShellCommand) { throw 'Windows PowerShell was not found to capture the Robocopy exit code.' }
|
||||
$powerShellPath = [string]$powerShellCommand.Path
|
||||
}
|
||||
|
||||
try {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Native command: {0} {1}" -f $robocopyPath, $argumentLine)
|
||||
$runnerCommand = '& ' + (ConvertTo-DeployCenterPowerShellLiteral -Value $robocopyPath)
|
||||
foreach ($argument in $arguments) {
|
||||
$runnerCommand += ' ' + (ConvertTo-DeployCenterPowerShellLiteral -Value ([string]$argument))
|
||||
}
|
||||
$runnerLines = @(
|
||||
$runnerCommand,
|
||||
'$robocopyExitCode = $LASTEXITCODE',
|
||||
('[System.IO.File]::WriteAllText(' + (ConvertTo-DeployCenterPowerShellLiteral -Value $exitCodePath) + ', [string]$robocopyExitCode, [System.Text.Encoding]::ASCII)')
|
||||
)
|
||||
[System.IO.File]::WriteAllLines($runnerPath, [string[]]$runnerLines, (New-Object System.Text.UTF8Encoding($true)))
|
||||
$runnerArguments = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $runnerPath)
|
||||
$runnerArgumentLine = ($runnerArguments | ForEach-Object {
|
||||
ConvertTo-DeployCenterNativeArgument -Value ([string]$_)
|
||||
}) -join ' '
|
||||
$process = Start-Process -FilePath $powerShellPath -ArgumentList $runnerArgumentLine `
|
||||
-PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath -ErrorAction Stop
|
||||
while (-not $process.WaitForExit(250)) {
|
||||
if ($ProgressAction -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
|
||||
[System.Windows.Forms.Application]::DoEvents()
|
||||
$copyPercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $copyPercent) { & $ProgressAction $null $copyPercent }
|
||||
}
|
||||
}
|
||||
$process.WaitForExit()
|
||||
$exitCode = $null
|
||||
if (Test-Path -LiteralPath $exitCodePath -PathType Leaf) {
|
||||
$exitCodeText = [System.IO.File]::ReadAllText($exitCodePath).Trim()
|
||||
$parsedExitCode = 0
|
||||
if ([int]::TryParse($exitCodeText, [ref]$parsedExitCode)) { $exitCode = $parsedExitCode }
|
||||
}
|
||||
|
||||
$outputEncoding = [System.Console]::OutputEncoding
|
||||
if ($null -eq $outputEncoding) { $outputEncoding = [System.Text.Encoding]::Default }
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $outputEncoding)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
if ($line -match '^\s*\d{1,3}(?:[.,]\d+)?\s*%\s*$') { continue }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' -SystemOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ProgressAction -and (Test-Path -LiteralPath $stdoutPath -PathType Leaf)) {
|
||||
$copyPercent = Get-DeployCenterNativeProgressPercent -OutputPath $stdoutPath
|
||||
if ($null -ne $copyPercent) { & $ProgressAction $null $copyPercent }
|
||||
}
|
||||
# Robocopy uses codes 0 to 7 for non-failure results; 8 and above indicate an error.
|
||||
if ($null -eq $exitCode) {
|
||||
if (-not (Test-DeployCenterRobocopyCopy -SourceDirectory $SourceDirectory `
|
||||
-DestinationDirectory $DestinationDirectory -CopyArguments $CopyArguments)) {
|
||||
throw 'Robocopy did not report an exit code and file verification failed.'
|
||||
}
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'Robocopy did not report an exit code; file verification succeeded.' `
|
||||
-Level 'WARNING'
|
||||
return $null
|
||||
}
|
||||
if ([int]$exitCode -ge 8) {
|
||||
throw ("Robocopy exited with code {0}." -f $exitCode)
|
||||
}
|
||||
return [int]$exitCode
|
||||
} finally {
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $outputPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
foreach ($temporaryPath in @($runnerPath, $exitCodePath)) {
|
||||
if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-DeployCenterNativeCommand {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$FilePath,
|
||||
@@ -199,6 +350,8 @@ function Invoke-DeployCenterNativeCommand {
|
||||
$stdoutPath = Join-Path $TemporaryRoot ("native_{0}.out" -f $id)
|
||||
$stderrPath = Join-Path $TemporaryRoot ("native_{0}.err" -f $id)
|
||||
$dismLogPath = Join-Path $TemporaryRoot ("dism_{0}.log" -f $id)
|
||||
$runnerPath = Join-Path $TemporaryRoot ("native_{0}.ps1" -f $id)
|
||||
$exitCodePath = Join-Path $TemporaryRoot ("native_{0}.exitcode" -f $id)
|
||||
$isDism = [IO.Path]::GetFileName($FilePath) -ieq 'dism.exe'
|
||||
$isImageApply = $isDism -and @($Arguments | Where-Object { [string]$_ -ieq '/Apply-Image' }).Count -gt 0
|
||||
if ($isDism) {
|
||||
@@ -209,7 +362,7 @@ function Invoke-DeployCenterNativeCommand {
|
||||
}) -join ' '
|
||||
|
||||
try {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Commande native : {0} {1}" -f $FilePath, $argumentLine)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Native command: {0} {1}" -f $FilePath, $argumentLine)
|
||||
$nativeOutputLines = New-Object System.Collections.Generic.List[string]
|
||||
if ($isImageApply) {
|
||||
$process = Start-Process -FilePath $FilePath -ArgumentList $argumentLine `
|
||||
@@ -234,7 +387,7 @@ function Invoke-DeployCenterNativeCommand {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $nativeOutputEncoding)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' -SystemOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -245,17 +398,55 @@ function Invoke-DeployCenterNativeCommand {
|
||||
}
|
||||
if ($null -eq $exitCode -and @($nativeOutputLines | Where-Object { $_ -match '(?i)completed successfully|a r.ussi' }).Count -gt 0) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message 'DISM confirme que Windows a été appliqué, mais le code de sortie est absent dans WinPE.' `
|
||||
-Message 'DISM confirms Windows was applied, but the exit code is unavailable in WinPE.' `
|
||||
-Level 'WARNING'
|
||||
return
|
||||
}
|
||||
} else {
|
||||
$commandOutput = @(& $FilePath @Arguments 2>&1)
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $commandOutput) {
|
||||
$powerShellPath = Join-Path $PSHOME 'powershell.exe'
|
||||
if (-not (Test-Path -LiteralPath $powerShellPath -PathType Leaf)) {
|
||||
$powerShellCommand = Get-Command 'powershell.exe' -ErrorAction SilentlyContinue
|
||||
if ($null -eq $powerShellCommand) { throw 'Windows PowerShell was not found to capture the native command exit code.' }
|
||||
$powerShellPath = [string]$powerShellCommand.Path
|
||||
}
|
||||
$runnerCommand = '& ' + (ConvertTo-DeployCenterPowerShellLiteral -Value $FilePath)
|
||||
foreach ($argument in $Arguments) {
|
||||
$runnerCommand += ' ' + (ConvertTo-DeployCenterPowerShellLiteral -Value ([string]$argument))
|
||||
}
|
||||
$runnerLines = @(
|
||||
$runnerCommand,
|
||||
'$nativeCommandExitCode = $LASTEXITCODE',
|
||||
('[System.IO.File]::WriteAllText(' + (ConvertTo-DeployCenterPowerShellLiteral -Value $exitCodePath) + ', [string]$nativeCommandExitCode, [System.Text.Encoding]::ASCII)')
|
||||
)
|
||||
[System.IO.File]::WriteAllLines($runnerPath, [string[]]$runnerLines, (New-Object System.Text.UTF8Encoding($true)))
|
||||
$runnerArguments = @('-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', $runnerPath)
|
||||
$runnerArgumentLine = ($runnerArguments | ForEach-Object {
|
||||
ConvertTo-DeployCenterNativeArgument -Value ([string]$_)
|
||||
}) -join ' '
|
||||
$process = Start-Process -FilePath $powerShellPath -ArgumentList $runnerArgumentLine `
|
||||
-PassThru -WindowStyle Hidden -RedirectStandardOutput $stdoutPath `
|
||||
-RedirectStandardError $stderrPath -ErrorAction Stop
|
||||
while (-not $process.WaitForExit(250)) {
|
||||
if ($ProgressAction) { [System.Windows.Forms.Application]::DoEvents() }
|
||||
}
|
||||
$process.WaitForExit()
|
||||
$exitCode = $null
|
||||
if (Test-Path -LiteralPath $exitCodePath -PathType Leaf) {
|
||||
$exitCodeText = [System.IO.File]::ReadAllText($exitCodePath).Trim()
|
||||
$parsedExitCode = 0
|
||||
if ([int]::TryParse($exitCodeText, [ref]$parsedExitCode)) { $exitCode = $parsedExitCode }
|
||||
}
|
||||
if ($null -eq $exitCode) { throw ("Could not capture the exit code from {0}." -f [IO.Path]::GetFileName($FilePath)) }
|
||||
$nativeOutputEncoding = [System.Console]::OutputEncoding
|
||||
if ($null -eq $nativeOutputEncoding) { $nativeOutputEncoding = [System.Text.Encoding]::Default }
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
if (Test-Path -LiteralPath $outputPath -PathType Leaf) {
|
||||
foreach ($line in [System.IO.File]::ReadAllLines($outputPath, $nativeOutputEncoding)) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
[void]$nativeOutputLines.Add([string]$line)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' -SystemOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -264,11 +455,11 @@ function Invoke-DeployCenterNativeCommand {
|
||||
if ($isDism -and (Test-Path -LiteralPath $dismLogPath -PathType Leaf)) {
|
||||
$diagnostic = @(Get-Content -LiteralPath $dismLogPath -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object { [string]$_ })
|
||||
foreach ($line in $diagnostic) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("DISM : {0}" -f $line) -Level 'ERROR'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'ERROR' -SystemOutput
|
||||
}
|
||||
}
|
||||
$diagnosticMessage = $diagnostic -join ' | '
|
||||
throw ("La commande {0} s'est terminée avec le code {1}. {2}" -f $FilePath, $exitCode, $diagnosticMessage)
|
||||
throw ("Command {0} exited with code {1}. {2}" -f $FilePath, $exitCode, $diagnosticMessage)
|
||||
}
|
||||
} finally {
|
||||
foreach ($outputPath in @($stdoutPath, $stderrPath)) {
|
||||
@@ -279,6 +470,11 @@ function Invoke-DeployCenterNativeCommand {
|
||||
if (Test-Path -LiteralPath $dismLogPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $dismLogPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
foreach ($temporaryPath in @($runnerPath, $exitCodePath)) {
|
||||
if (Test-Path -LiteralPath $temporaryPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $temporaryPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,11 +482,12 @@ function Get-DeployCenterOfflineWindowsBootGuid {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$BcdStorePath,
|
||||
[Parameter(Mandatory=$true)][string]$WindowsDriveLetter,
|
||||
[Parameter(Mandatory=$true)][string]$IdentifierLabel,
|
||||
[AllowNull()][scriptblock]$LogAction = $null
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $BcdStorePath -PathType Leaf)) {
|
||||
throw "Magasin BCD introuvable après la création des fichiers de démarrage : $BcdStorePath"
|
||||
throw "BCD store was not found after boot files were created: $BcdStorePath"
|
||||
}
|
||||
$bcdeditPath = Join-Path $env:WINDIR 'System32\bcdedit.exe'
|
||||
if (-not (Test-Path -LiteralPath $bcdeditPath -PathType Leaf)) { $bcdeditPath = 'bcdedit.exe' }
|
||||
@@ -298,19 +495,20 @@ function Get-DeployCenterOfflineWindowsBootGuid {
|
||||
$exitCode = $LASTEXITCODE
|
||||
foreach ($line in $output) {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$line)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ([string]$line) -Level 'INFO' -SystemOutput
|
||||
}
|
||||
}
|
||||
if ($null -eq $exitCode -or [int]$exitCode -ne 0) {
|
||||
throw "bcdedit n’a pas pu lire le magasin BCD $BcdStorePath (code $exitCode)."
|
||||
throw "bcdedit could not read the BCD store $BcdStorePath (exit code $exitCode)."
|
||||
}
|
||||
|
||||
$targetLetter = $WindowsDriveLetter.TrimEnd(':').ToUpperInvariant()
|
||||
$entries = New-Object System.Collections.Generic.List[object]
|
||||
$currentEntry = $null
|
||||
$identifierPattern = '^\s*' + [regex]::Escape($IdentifierLabel) + '\s+(?<guid>\{[0-9A-Fa-f-]{36}\})\s*$'
|
||||
foreach ($lineObject in $output) {
|
||||
$line = [string]$lineObject
|
||||
if ($line -match '^\s*(?:identifier|identificateur)\s+(?<guid>\{[0-9A-Fa-f-]{36}\})\s*$') {
|
||||
if ($line -match $identifierPattern) {
|
||||
if ($null -ne $currentEntry) { $entries.Add($currentEntry) }
|
||||
$currentEntry = [pscustomobject]@{
|
||||
Guid = [string]$Matches['guid']
|
||||
@@ -332,7 +530,7 @@ function Get-DeployCenterOfflineWindowsBootGuid {
|
||||
$_.Device -eq $targetLetter -and $_.OsDevice -eq $targetLetter
|
||||
})
|
||||
if ($matchingEntries.Count -ne 1) {
|
||||
throw "Impossible d’identifier une entrée BCD unique pour Windows sur ${targetLetter}: (entrées correspondantes : $($matchingEntries.Count))."
|
||||
throw "Could not identify a unique BCD entry for Windows on ${targetLetter}: (matching entries: $($matchingEntries.Count))."
|
||||
}
|
||||
return [string]$matchingEntries[0].Guid
|
||||
}
|
||||
@@ -370,9 +568,9 @@ function Get-DeployCenterTemporaryDriveLetters {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $selected) { throw "Aucune lettre de lecteur libre pour la partition '$($partition.Name)'." }
|
||||
if (-not $selected) { throw "No free drive letter is available for partition '$($partition.Name)'." }
|
||||
}
|
||||
if ($selected -in $lettersByRole.Values) { throw "La lettre ${selected}: est affectée à plusieurs partitions du profil." }
|
||||
if ($selected -in $lettersByRole.Values) { throw "The drive letter ${selected}: is assigned to multiple partitions in the profile." }
|
||||
$lettersByRole[[string]$partition.Role] = $selected
|
||||
$occupied[$selected] = $true
|
||||
}
|
||||
@@ -387,7 +585,7 @@ function New-DeployCenterPartitionPlan {
|
||||
)
|
||||
|
||||
if ([string]$Profile.PartitionStyle -ne 'GPT' -or [string]$Profile.Firmware -ne 'UEFI') {
|
||||
throw "Le profil '$($Profile.Name)' n’est pas pris en charge par le moteur actuel (UEFI/GPT requis)."
|
||||
throw "The profile '$($Profile.Name)' is not supported by the current engine (UEFI/GPT required)."
|
||||
}
|
||||
$totalMB = [long][math]::Floor(([double]$DiskInfo.SizeBytes / 1MB))
|
||||
$availableMB = $totalMB - 2
|
||||
@@ -401,26 +599,26 @@ function New-DeployCenterPartitionPlan {
|
||||
'ConfiguredMB' { $sizeMB = [long]$partition.Size.Value }
|
||||
'UserInputGB' {
|
||||
if ($null -eq $SizingState -or [string]$SizingState.ProfileId -ne [string]$Profile.Id) {
|
||||
throw "Le profil '$($Profile.Name)' nécessite une taille utilisateur, qui n’a pas été validée."
|
||||
throw "Profile '$($Profile.Name)' requires a user-defined size, which has not been validated."
|
||||
}
|
||||
$role = [string]$partition.Role
|
||||
if (-not $SizingState.Inputs.Contains($role)) { throw "La taille de la partition '$role' est absente." }
|
||||
if (-not $SizingState.Inputs.Contains($role)) { throw "The size for partition '$role' is missing." }
|
||||
$sizeMB = [long][math]::Round(([double]$SizingState.Inputs[$role] * 1024), 0)
|
||||
if ($sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "La partition '$role' est sous sa taille minimale." }
|
||||
if ($sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) { throw "Partition '$role' is below its minimum size." }
|
||||
}
|
||||
{ $_ -in @('Remaining','RemainderAfterInputs') } {
|
||||
$sizeMB = $availableMB - $usedMB
|
||||
if ($sizeMB -lt 1) { throw "Le disque ne dispose plus d’espace pour la partition '$($partition.Name)'." }
|
||||
if ($sizeMB -lt 1) { throw "The disk no longer has enough space for partition '$($partition.Name)'." }
|
||||
if ($policy -eq 'RemainderAfterInputs' -and $sizeMB -lt ([long]$partition.Size.MinimumGB * 1024)) {
|
||||
throw "L’espace restant pour '$($partition.Name)' est inférieur au minimum du profil."
|
||||
throw "The remaining space for '$($partition.Name)' is below the profile minimum."
|
||||
}
|
||||
}
|
||||
default { throw "Politique de taille non prise en charge lors du partitionnement : $policy" }
|
||||
default { throw "Unsupported partition size policy: $policy" }
|
||||
}
|
||||
if ($null -ne $sizeMB) { $usedMB += [long]$sizeMB }
|
||||
$plan.Add([pscustomobject]@{ Definition = $partition; SizeMB = $sizeMB })
|
||||
}
|
||||
if ($usedMB -gt $availableMB) { throw "La taille demandée dépasse la capacité du disque sélectionné." }
|
||||
if ($usedMB -gt $availableMB) { throw "The requested size exceeds the selected disk capacity." }
|
||||
return $plan.ToArray()
|
||||
}
|
||||
|
||||
@@ -432,38 +630,49 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
[AllowNull()]$SizingState,
|
||||
[Parameter(Mandatory=$true)]$Edition,
|
||||
[AllowNull()][string]$HostName = $null,
|
||||
[Parameter(Mandatory=$true)][string]$IdentifierLabel,
|
||||
[AllowNull()][System.Collections.IDictionary]$UnattendProfile = $null,
|
||||
[AllowNull()][System.Collections.IDictionary]$UnattendConfiguration = $null,
|
||||
[Parameter(Mandatory=$true)][string]$Firmware,
|
||||
[Parameter(Mandatory=$true)][string]$TemporaryRoot,
|
||||
[AllowNull()][string]$DriverPath = $null,
|
||||
[int]$DriverInfCount = 0,
|
||||
[ValidateSet('Sources', 'Drivers')]
|
||||
[string[]]$CopyLocally = @(),
|
||||
[AllowNull()][scriptblock]$LogAction = $null,
|
||||
[AllowNull()][scriptblock]$ProgressAction = $null
|
||||
)
|
||||
|
||||
if ($null -ne $UnattendProfile) {
|
||||
if ($null -eq $UnattendConfiguration) { throw 'La configuration DeployCenter.ini est absente pour le profil Unattend sélectionné.' }
|
||||
if ($null -eq $UnattendConfiguration) { throw 'DeployCenter.ini configuration is missing for the selected Unattend profile.' }
|
||||
Test-DeployCenterUnattendConfiguration -Profile $UnattendProfile -Configuration $UnattendConfiguration -HostName ([string]$HostName)
|
||||
}
|
||||
$winreRecommendationWarnings = @()
|
||||
if ([string]$Firmware -ne [string]$Profile.Firmware) {
|
||||
throw ("Le profil {0} exige le firmware {1}, détecté : {2}." -f $Profile.Name, $Profile.Firmware, $Firmware)
|
||||
throw ("The profile {0} requires firmware {1}, detected: {2}." -f $Profile.Name, $Profile.Firmware, $Firmware)
|
||||
}
|
||||
foreach ($requiredRole in @('System','Reserved','Recovery','Windows')) {
|
||||
$roleCount = @($Profile.Partitions | Where-Object { [string]$_.Role -eq $requiredRole }).Count
|
||||
if ($roleCount -ne 1) { throw "Le profil doit définir exactement une partition de rôle '$requiredRole'. Le disque n’a pas été modifié." }
|
||||
if ($roleCount -ne 1) { throw "The profile must define exactly one partition with the '$requiredRole' role. The disk was not modified." }
|
||||
}
|
||||
$recoveryDefinitions = @($Profile.Partitions | Where-Object { [string]$_.Role -eq 'Recovery' })
|
||||
if ($recoveryDefinitions.Count -ne 1 -or [string]$recoveryDefinitions[0].Size.Policy -ne 'ConfiguredMB') {
|
||||
throw "Le profil doit définir une taille configurée pour la partition Recovery. Le disque n’a pas été modifié."
|
||||
throw "The profile must define a configured size for the Recovery partition. The disk was not modified."
|
||||
}
|
||||
$winrePartitionSizeMB = [long]$recoveryDefinitions[0].Size.Value
|
||||
if ($winrePartitionSizeMB -le 0) {
|
||||
throw "La taille configurée de la partition WinRE doit être strictement positive. Le disque n’a pas été modifié."
|
||||
throw "The configured WinRE partition size must be greater than zero. The disk was not modified."
|
||||
}
|
||||
if (-not $Edition.ImagePath -or -not (Test-Path -LiteralPath ([string]$Edition.ImagePath) -PathType Leaf)) {
|
||||
throw "Le chemin de l’image Windows sélectionnée est introuvable."
|
||||
throw "The selected Windows image path was not found."
|
||||
}
|
||||
if (-not $Edition.Index -or [int]$Edition.Index -lt 1) { throw "The Windows edition index is invalid." }
|
||||
if (-not [string]::IsNullOrWhiteSpace($DriverPath)) {
|
||||
if (-not (Test-Path -LiteralPath $DriverPath -PathType Container)) { throw "The selected driver folder was not found: $DriverPath" }
|
||||
if ($DriverInfCount -le 0 -or @(Get-ChildItem -LiteralPath $DriverPath -Filter '*.inf' -File -Recurse -ErrorAction SilentlyContinue).Count -eq 0) {
|
||||
throw "The selected driver folder no longer contains an INF file: $DriverPath"
|
||||
}
|
||||
}
|
||||
if (-not $Edition.Index -or [int]$Edition.Index -lt 1) { throw "L’index de l’édition Windows est invalide." }
|
||||
|
||||
$wallpaperSourcePath = $null
|
||||
if ($null -ne $UnattendProfile -and $null -ne $UnattendConfiguration) {
|
||||
@@ -476,16 +685,16 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$wallpaperSourcePath = [System.IO.Path]::GetFullPath((Join-Path $deployRoot $configuredWallpaperPath))
|
||||
}
|
||||
if ([System.IO.Path]::GetExtension($wallpaperSourcePath) -notin @('.png', '.jpg', '.bmp')) {
|
||||
throw 'WallpaperPath doit désigner un fichier .png, .jpg ou .bmp.'
|
||||
throw 'WallpaperPath must point to a .png, .jpg, or .bmp file.'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $wallpaperSourcePath -PathType Leaf)) {
|
||||
throw "Image de fond introuvable : $wallpaperSourcePath"
|
||||
throw "Wallpaper image not found: $wallpaperSourcePath"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$disk = Get-Disk -Number ([int]$DiskInfo.Index) -ErrorAction Stop
|
||||
if ($disk.IsBoot -or $disk.IsSystem) { throw 'Le disque sélectionné héberge le système WinPE et ne peut pas être effacé.' }
|
||||
if ($disk.IsBoot -or $disk.IsSystem) { throw 'The selected disk hosts WinPE and cannot be erased.' }
|
||||
if ($disk.IsOffline) { [void](Set-Disk -Number $disk.Number -IsOffline $false -ErrorAction Stop) }
|
||||
if ($disk.IsReadOnly) { [void](Set-Disk -Number $disk.Number -IsReadOnly $false -ErrorAction Stop) }
|
||||
|
||||
@@ -493,7 +702,8 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
New-Item -Path $TemporaryRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
$sourceLease = New-DeployCenterDismSourceAccess -ImagePath ([string]$Edition.ImagePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$windowsDeployCenterPath = $null
|
||||
try {
|
||||
$deploymentImagePath = [string]$sourceLease.ImagePath
|
||||
$partitionSizingDisk = [pscustomobject]@{ Index = [int]$disk.Number; SizeBytes = [long]$disk.Size }
|
||||
@@ -524,7 +734,7 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
}
|
||||
if ($role -eq 'Reserved') { continue }
|
||||
$label = [string]$definition.Name
|
||||
if ($label -notmatch '^[A-Za-z0-9_-]{1,32}$') { throw "Nom de partition invalide dans le profil : $label" }
|
||||
if ($label -notmatch '^[A-Za-z0-9_-]{1,32}$') { throw "Invalid partition name in profile: $label" }
|
||||
if ([string]$definition.FileSystem) {
|
||||
$scriptLines.Add("format fs=$($definition.FileSystem) quick label=$label")
|
||||
}
|
||||
@@ -544,8 +754,8 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$unattendPath = Join-Path $TemporaryRoot ("unattend_{0}.xml" -f ([guid]::NewGuid().ToString('N')))
|
||||
try {
|
||||
[System.IO.File]::WriteAllLines($cleanPath, $cleanLines, [System.Text.Encoding]::ASCII)
|
||||
if ($ProgressAction) { & $ProgressAction 'Effacement et partitionnement du disque…' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Partitionnement du disque {0} avec le profil '{1}' ; partition WinRE configurée : {2} Mo." -f $disk.Number, $Profile.Name, $winrePartitionSizeMB)
|
||||
if ($ProgressAction) { & $ProgressAction 'Erasing and partitioning the disk…' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Partitioning disk {0} with profile '{1}' ; configured WinRE partition: {2} MB." -f $disk.Number, $Profile.Name, $winrePartitionSizeMB)
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $cleanPath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
@@ -556,7 +766,7 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
$createdPartitions = @(Get-Partition -DiskNumber ([int]$disk.Number) -ErrorAction Stop)
|
||||
if ($createdPartitions.Count -ne $partitionPlan.Count) {
|
||||
throw "DiskPart a créé $($createdPartitions.Count) partition(s), alors que le profil en prévoit $($partitionPlan.Count). L’installation est interrompue."
|
||||
throw "DiskPart created $($createdPartitions.Count) partition(s), but the profile specifies $($partitionPlan.Count). Installation has been stopped."
|
||||
}
|
||||
$orderedCreatedPartitions = @($createdPartitions | Sort-Object -Property PartitionNumber)
|
||||
$recoveryPlanIndex = -1
|
||||
@@ -566,12 +776,12 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($recoveryPlanIndex -lt 0) { throw 'La partition Recovery est absente du plan exécuté.' }
|
||||
if ($recoveryPlanIndex -lt 0) { throw 'The Recovery partition is missing from the executed plan.' }
|
||||
$recoveryPartition = $orderedCreatedPartitions[$recoveryPlanIndex]
|
||||
$recoveryLetter = [string]$driveLetters['Recovery']
|
||||
$recoveryDriveRoot = '{0}:\' -f $recoveryLetter
|
||||
if (-not (Test-Path -LiteralPath $recoveryDriveRoot -PathType Container)) {
|
||||
throw "La lettre temporaire ${recoveryLetter}: de WinRE n’est pas accessible après le partitionnement."
|
||||
throw "The temporary drive letter ${recoveryLetter}: de WinRE is not accessible after partitioning."
|
||||
}
|
||||
$partitionsByLetter = @{}
|
||||
for ($index = 0; $index -lt $partitionPlan.Count; $index++) {
|
||||
@@ -580,21 +790,22 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$letter = [string]$driveLetters[$role]
|
||||
$driveRoot = '{0}:\' -f $letter
|
||||
if (-not (Test-Path -LiteralPath $driveRoot -PathType Container)) {
|
||||
throw "La lettre temporaire ${letter}: n’est pas accessible après le partitionnement. L’installation est interrompue."
|
||||
throw "The temporary drive letter ${letter}: is not accessible after partitioning. Installation has been stopped."
|
||||
}
|
||||
# WinPE peut ne pas exposer les partitions EFI/Recovery dans les
|
||||
# requêtes Get-Partition par lettre. Leur ordre sur le disque suit
|
||||
# directement celui des définitions exécutées par DiskPart.
|
||||
# WinPE may not expose EFI/Recovery partitions in
|
||||
# Get-Partition queries by drive letter. Their disk order follows
|
||||
# the order of the definitions executed by DiskPart.
|
||||
$partitionsByLetter[$letter] = $orderedCreatedPartitions[$index]
|
||||
}
|
||||
|
||||
$windowsLetter = [string]$driveLetters['Windows']
|
||||
$windowsPath = "${windowsLetter}:\"
|
||||
$windowsRoot = Join-Path $windowsPath 'Windows'
|
||||
$windowsDeployCenterPath = Join-Path $windowsPath 'DeployCenter'
|
||||
$recoveryPath = "${recoveryLetter}:\Recovery\WindowsRE"
|
||||
$efiLetter = [string]$driveLetters['System']
|
||||
if (-not $windowsLetter -or -not $recoveryLetter -or -not $efiLetter) {
|
||||
throw "Le profil doit définir les rôles System, Windows et Recovery pour permettre l’installation UEFI avec WinRE."
|
||||
throw "The profile must define the System, Windows, and Recovery roles for UEFI installation with WinRE."
|
||||
}
|
||||
|
||||
$wallpaperTargetPath = $null
|
||||
@@ -604,31 +815,126 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$wallpaperDirectory = Join-Path $windowsRoot 'Web\Wallpaper\DeployCenter'
|
||||
$wallpaperDestination = Join-Path $wallpaperDirectory $wallpaperName
|
||||
$wallpaperTargetPath = '%WINDIR%\Web\Wallpaper\DeployCenter\' + $wallpaperName
|
||||
if ($wallpaperDestination.Length -gt 259) { throw "Le chemin configuré du fond d’écran dépasse 259 caractères dans Windows." }
|
||||
if ($wallpaperDestination.Length -gt 259) { throw "The configured wallpaper path exceeds 259 characters in Windows." }
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Application de Windows sur ${windowsLetter}:…" }
|
||||
$imageFileName = [System.IO.Path]::GetFileName($deploymentImagePath)
|
||||
if ([string]::IsNullOrWhiteSpace($imageFileName)) { throw "Could not determine the selected Windows image filename." }
|
||||
$imageStagingPath = $null
|
||||
$imageStagingDirectory = $null
|
||||
if ('Sources' -in $CopyLocally) {
|
||||
$imageStagingPath = Join-Path (Join-Path (Join-Path $windowsPath 'DeployCenter') 'Sources') $imageFileName
|
||||
$imageStagingDirectory = Split-Path -Path $imageStagingPath -Parent
|
||||
} else {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Using the Windows image directly from {0}." -f $deploymentImagePath) -Level 'INFO'
|
||||
}
|
||||
$imageFile = Get-Item -LiteralPath $deploymentImagePath -ErrorAction Stop
|
||||
$windowsDriveInfo = New-Object System.IO.DriveInfo -ArgumentList ("{0}:\" -f $windowsLetter)
|
||||
$driverPackSizeBytes = [long]0
|
||||
if (-not [string]::IsNullOrWhiteSpace($DriverPath) -and 'Drivers' -in $CopyLocally) {
|
||||
$driverFiles = @(Get-ChildItem -LiteralPath $DriverPath -Force -File -Recurse -ErrorAction Stop)
|
||||
$driverPackSizeBytes = [long](($driverFiles | Measure-Object -Property Length -Sum).Sum)
|
||||
}
|
||||
$requiredStagingBytes = [long]0
|
||||
if ('Sources' -in $CopyLocally) { $requiredStagingBytes += [long]$imageFile.Length }
|
||||
if ('Drivers' -in $CopyLocally) { $requiredStagingBytes += $driverPackSizeBytes }
|
||||
if ($requiredStagingBytes -gt 0 -and (-not $windowsDriveInfo.IsReady -or [long]$windowsDriveInfo.AvailableFreeSpace -lt $requiredStagingBytes)) {
|
||||
$availableImageSpaceMB = if ($windowsDriveInfo.IsReady) { [math]::Floor(([double]$windowsDriveInfo.AvailableFreeSpace / 1MB)) } else { 0 }
|
||||
$requiredStagingSpaceMB = [math]::Ceiling(([double]$requiredStagingBytes / 1MB))
|
||||
throw ("Insufficient space on ${windowsLetter}: to stage the Windows image and driver pack: {0} MB required, {1} MB available." -f $requiredStagingSpaceMB, $availableImageSpaceMB)
|
||||
}
|
||||
|
||||
if ('Sources' -in $CopyLocally) {
|
||||
$imageCopyTimer = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
try {
|
||||
New-Item -Path $imageStagingDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
if ($ProgressAction) { & $ProgressAction "Copying the Windows image locally…" }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Copying image {0} from {1} to {2} locally." -f $imageFileName, $deploymentImagePath, $imageStagingDirectory) -Level 'INFO'
|
||||
[void](Invoke-DeployCenterRobocopy -SourceDirectory (Split-Path -Path $deploymentImagePath -Parent) `
|
||||
-DestinationDirectory $imageStagingDirectory -CopyArguments @($imageFileName, '/J', '/R:2', '/W:2', '/ETA') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction)
|
||||
$stagedImage = Get-Item -LiteralPath $imageStagingPath -ErrorAction Stop
|
||||
if ([long]$stagedImage.Length -ne [long]$imageFile.Length) {
|
||||
throw ("The local Windows image copy is incomplete: {0} bytes copied of {1}." -f $stagedImage.Length, $imageFile.Length)
|
||||
}
|
||||
$imageCopyTimer.Stop()
|
||||
$imageSizeMB = [math]::Round(([double]$stagedImage.Length / 1MB), 0)
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Local Windows image copy completed in {0:N1} s ({1} MB)." -f $imageCopyTimer.Elapsed.TotalSeconds, $imageSizeMB) -Level 'SUCCESS'
|
||||
$deploymentImagePath = $imageStagingPath
|
||||
} finally {
|
||||
if ($imageCopyTimer.IsRunning) { $imageCopyTimer.Stop() }
|
||||
}
|
||||
}
|
||||
|
||||
$driverDeploymentPath = $DriverPath
|
||||
if (-not [string]::IsNullOrWhiteSpace($DriverPath) -and 'Drivers' -in $CopyLocally) {
|
||||
$driverStagingPath = Join-Path $windowsDeployCenterPath 'Drivers'
|
||||
$driverCopyTimer = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
try {
|
||||
if ($ProgressAction) { & $ProgressAction 'Copying drivers locally…' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Copying {0} INF file(s) from {1} to {2} before applying Windows." -f $DriverInfCount, $DriverPath, $driverStagingPath) -Level 'INFO'
|
||||
New-Item -Path $driverStagingPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
[void](Invoke-DeployCenterRobocopy -SourceDirectory $DriverPath -DestinationDirectory $driverStagingPath `
|
||||
-CopyArguments @('/E', '/J', '/R:2', '/W:2', '/ETA') -TemporaryRoot $TemporaryRoot `
|
||||
-LogAction $LogAction -ProgressAction $ProgressAction)
|
||||
$stagedInfCount = @(Get-ChildItem -LiteralPath $driverStagingPath -Filter '*.inf' -File -Recurse -ErrorAction Stop).Count
|
||||
if ($stagedInfCount -ne $DriverInfCount) {
|
||||
throw ("The local driver copy is incomplete: {0} INF file(s) copied of {1}." -f $stagedInfCount, $DriverInfCount)
|
||||
}
|
||||
$driverCopyTimer.Stop()
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Local driver copy completed in {0:N1} s ({1} INF file(s))." -f $driverCopyTimer.Elapsed.TotalSeconds, $stagedInfCount) -Level 'SUCCESS'
|
||||
$driverDeploymentPath = $driverStagingPath
|
||||
} finally {
|
||||
if ($driverCopyTimer.IsRunning) { $driverCopyTimer.Stop() }
|
||||
}
|
||||
} elseif (-not [string]::IsNullOrWhiteSpace($DriverPath)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Using the driver pack directly from {0}." -f $DriverPath) -Level 'INFO'
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction "Applying Windows to ${windowsLetter}:…" }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
'/Apply-Image', "/ImageFile:$deploymentImagePath", "/Index:$([int]$Edition.Index)", "/ApplyDir:$windowsPath", '/CheckIntegrity'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($DriverDeploymentPath)) {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'No matching driver pack; injection skipped.' -Level 'INFO'
|
||||
} else {
|
||||
$driverInstallTimer = $null
|
||||
try {
|
||||
if ($ProgressAction) { & $ProgressAction 'Installing hardware drivers…' }
|
||||
$driverInstallTimer = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
Invoke-DeployCenterNativeCommand -FilePath $dismPath -Arguments @(
|
||||
"/Image:$windowsPath", '/Add-Driver', "/Driver:$driverDeploymentPath", '/Recurse'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
$driverInstallTimer.Stop()
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("DISM driver injection completed in {0:N1} s." -f $driverInstallTimer.Elapsed.TotalSeconds) -Level 'SUCCESS'
|
||||
} finally {
|
||||
if ($driverInstallTimer -and $driverInstallTimer.IsRunning) { $driverInstallTimer.Stop() }
|
||||
}
|
||||
}
|
||||
|
||||
if ($wallpaperSourcePath) {
|
||||
New-Item -Path $wallpaperDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $wallpaperSourcePath -Destination $wallpaperDestination -Force -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Image de fond copiée dans Windows : {0}." -f $wallpaperDestination) -Level 'INFO'
|
||||
-Message ("Wallpaper image copied to Windows: {0}." -f $wallpaperDestination) -Level 'INFO'
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Vérification de la capacité de la partition WinRE…' }
|
||||
if ($ProgressAction) { & $ProgressAction 'Checking WinRE partition capacity…' }
|
||||
$sourceWinre = Join-Path $windowsRoot 'System32\Recovery\winre.wim'
|
||||
if (-not (Test-Path -LiteralPath $sourceWinre -PathType Leaf)) {
|
||||
throw "winre.wim est absent de l’image Windows appliquée ; impossible de configurer WinRE."
|
||||
throw "winre.wim is missing from the applied Windows image; WinRE cannot be configured."
|
||||
}
|
||||
$winreFile = Get-Item -LiteralPath $sourceWinre -ErrorAction Stop
|
||||
$recoveryPartition = $partitionsByLetter[$recoveryLetter]
|
||||
$recoveryDriveInfo = New-Object System.IO.DriveInfo -ArgumentList "${recoveryLetter}:\"
|
||||
if (-not $recoveryDriveInfo.IsReady) {
|
||||
throw "Le volume de récupération ${recoveryLetter}: n’est pas prêt pour vérifier l’espace WinRE."
|
||||
throw "The recovery volume ${recoveryLetter}: is not ready to check WinRE space."
|
||||
}
|
||||
$minimumPartitionBytes = [long](990 * 1MB)
|
||||
$minimumFreeAfterCopyBytes = [long](250 * 1MB)
|
||||
@@ -638,29 +944,29 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
$availableFreeMB = [math]::Floor(([double]$recoveryFreeBytes / 1MB))
|
||||
$availableAfterCopyMB = [math]::Floor(([double]$expectedFreeBytes / 1MB))
|
||||
if ($recoveryFreeBytes -lt [long]$winreFile.Length) {
|
||||
$message = "Espace insuffisant sur la partition WinRE pour copier winre.wim : fichier de $winreSizeForLog Mo, espace disponible $availableFreeMB Mo. Le déploiement ne peut pas configurer WinRE."
|
||||
$message = "Insufficient space on the WinRE partition to copy winre.wim: file size: $winreSizeForLog MB, available space $availableFreeMB MB. Deployment cannot configure WinRE."
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'ERROR'
|
||||
throw $message
|
||||
}
|
||||
$recommendationWarnings = New-Object System.Collections.Generic.List[string]
|
||||
if ([long]$recoveryPartition.Size -lt $minimumPartitionBytes) {
|
||||
$actualPartitionMB = [math]::Floor(([double]$recoveryPartition.Size / 1MB))
|
||||
[void]$recommendationWarnings.Add("taille de partition : $actualPartitionMB Mo, minimum recommandé : 990 Mo")
|
||||
[void]$recommendationWarnings.Add("partition size: $actualPartitionMB MB, recommended minimum: 990 MB")
|
||||
}
|
||||
if ($expectedFreeBytes -lt $minimumFreeAfterCopyBytes) {
|
||||
[void]$recommendationWarnings.Add("espace libre après copie : $availableAfterCopyMB Mo, minimum recommandé : 250 Mo")
|
||||
[void]$recommendationWarnings.Add("free space after copy: $availableAfterCopyMB MB, recommended minimum: 250 MB")
|
||||
}
|
||||
if ($recommendationWarnings.Count -gt 0) {
|
||||
$warningDetails = $recommendationWarnings -join '; '
|
||||
$winreRecommendationWarnings = @($recommendationWarnings.ToArray())
|
||||
$message = "Avertissement WinRE : les recommandations Microsoft ne sont pas toutes respectées ($warningDetails). La copie de winre.wim reste possible ; le déploiement continue."
|
||||
$message = "WinRE warning: Microsoft recommendations are not all met ($warningDetails). winre.wim can still be copied; deployment will continue."
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message $message -Level 'WARNING'
|
||||
} else {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Vérification WinRE réussie : partition {0} Mo, winre.wim {1} Mo, espace restant estimé après copie {2} Mo." -f $winrePartitionSizeMB, $winreSizeForLog, $availableAfterCopyMB) -Level 'SUCCESS'
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("WinRE validation succeeded: partition {0} MB, winre.wim {1} MB, estimated free space after copy {2} MB." -f $winrePartitionSizeMB, $winreSizeForLog, $availableAfterCopyMB) -Level 'SUCCESS'
|
||||
}
|
||||
|
||||
if ($null -ne $UnattendProfile) {
|
||||
if ($ProgressAction) { & $ProgressAction 'Préparation du fichier de réponses Windows…' }
|
||||
if ($ProgressAction) { & $ProgressAction 'Preparing the Windows answer file…' }
|
||||
$editionArchitecture = if ($Edition.Edition -and $Edition.Edition.Architecture) { [string]$Edition.Edition.Architecture } else { [string]$Edition.Architecture }
|
||||
$unattendXml = New-DeployCenterUnattendXml -Profile $UnattendProfile `
|
||||
-Configuration $UnattendConfiguration -HostName ([string]$HostName) `
|
||||
@@ -670,35 +976,45 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
New-Item -Path $pantherUnattendDirectory -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $unattendPath -Destination (Join-Path $pantherUnattendDirectory 'Unattend.xml') -Force -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Fichier de réponses Windows préparé avec le profil '{0}' (nom d’hôte : {1})." -f $UnattendProfile.Name, $HostName) -Level 'INFO'
|
||||
-Message ("Windows answer file prepared with profile '{0}' (computer name: {1})." -f $UnattendProfile.Name, $HostName) -Level 'INFO'
|
||||
}
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration du démarrage UEFI…' }
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuring UEFI boot…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $bcdbootPath -Arguments @(
|
||||
("{0}:\Windows" -f $windowsLetter), '/s', ("{0}:" -f $efiLetter), '/f', 'UEFI'
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuration de la partition de récupération WinRE…' }
|
||||
if ($ProgressAction) { & $ProgressAction 'Configuring the WinRE recovery partition…' }
|
||||
New-Item -Path $recoveryPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
Copy-Item -LiteralPath $sourceWinre -Destination (Join-Path $recoveryPath 'winre.wim') -Force -ErrorAction Stop
|
||||
if ($ProgressAction) { & $ProgressAction 'Copying winre.wim…' }
|
||||
$recoveryImagePath = Join-Path $recoveryPath 'winre.wim'
|
||||
[void](Invoke-DeployCenterRobocopy -SourceDirectory (Split-Path -Path $sourceWinre -Parent) `
|
||||
-DestinationDirectory $recoveryPath -CopyArguments @('winre.wim', '/J', '/R:2', '/W:2', '/ETA') `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction)
|
||||
$copiedWinre = Get-Item -LiteralPath $recoveryImagePath -ErrorAction Stop
|
||||
if ([long]$copiedWinre.Length -ne [long]$winreFile.Length) {
|
||||
throw ("The WinRE image copy is incomplete: {0} bytes copied of {1}." -f $copiedWinre.Length, $winreFile.Length)
|
||||
}
|
||||
$reagentcPath = Join-Path $windowsRoot 'System32\reagentc.exe'
|
||||
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) {
|
||||
$reagentcPath = Join-Path $env:WINDIR 'System32\reagentc.exe'
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $reagentcPath -PathType Leaf)) {
|
||||
throw 'REAgentC.exe est introuvable dans Windows appliqué et dans WinPE.'
|
||||
throw 'REAgentC.exe was not found in the applied Windows image or WinPE.'
|
||||
}
|
||||
$bcdStorePath = "${efiLetter}:\EFI\Microsoft\Boot\BCD"
|
||||
$windowsBootGuid = Get-DeployCenterOfflineWindowsBootGuid -BcdStorePath $bcdStorePath `
|
||||
-WindowsDriveLetter $windowsLetter -LogAction $LogAction
|
||||
-WindowsDriveLetter $windowsLetter -IdentifierLabel $IdentifierLabel -LogAction $LogAction
|
||||
if ($ProgressAction) { & $ProgressAction 'Registering the WinRE image…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/SetReImage', '/Path', $recoveryPath, '/Target', $windowsRoot
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
if ($ProgressAction) { & $ProgressAction 'Enabling WinRE…' }
|
||||
Invoke-DeployCenterNativeCommand -FilePath $reagentcPath -Arguments @(
|
||||
'/Enable', '/OSGUID', $windowsBootGuid
|
||||
) -TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
|
||||
if ($ProgressAction) { & $ProgressAction 'Finalisation des partitions…' }
|
||||
if ($ProgressAction) { & $ProgressAction 'Finalizing partitions…' }
|
||||
$removeLines = New-Object System.Collections.Generic.List[string]
|
||||
$lettersToRemove = @()
|
||||
foreach ($roleToRemove in @('System', 'Recovery')) {
|
||||
@@ -719,28 +1035,39 @@ function Invoke-DeployCenterWindowsDeployment {
|
||||
Invoke-DeployCenterNativeCommand -FilePath 'diskpart.exe' -Arguments @('/s', $removePath) `
|
||||
-TemporaryRoot $TemporaryRoot -LogAction $LogAction -ProgressAction $ProgressAction
|
||||
[void](Update-DeployCenterHostStorageCache -LogAction $LogAction)
|
||||
# Get-Partition peut conserver la lettre Recovery dans ses propriétés
|
||||
# après que DiskPart l'a effectivement retirée sous WinPE.
|
||||
# Get-Partition may retain the Recovery drive letter in its properties
|
||||
# after DiskPart has actually removed it in WinPE.
|
||||
$remainingPaths = @($lettersToRemove | Where-Object {
|
||||
[System.IO.Directory]::Exists(('{0}:\' -f $_))
|
||||
})
|
||||
if ($remainingPaths.Count -gt 0) {
|
||||
$remaining = (@($remainingPaths | ForEach-Object { ([string]$_).ToUpperInvariant() } | Select-Object -Unique)) -join ', '
|
||||
throw "DiskPart n’a pas libéré toutes les lettres temporaires EFI/WinRE : $remaining."
|
||||
throw "DiskPart did not release all temporary EFI/WinRE drive letters: $remaining."
|
||||
}
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $removePath -PathType Leaf) { Remove-Item -LiteralPath $removePath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
$completionMode = if ($null -ne $UnattendProfile) { "profil Unattend '$($UnattendProfile.Name)'" } else { 'Avec OOBE' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Déploiement terminé : Windows {0}, disque {1}, mode {2}." -f $Edition.Name, $disk.Number, $completionMode) -Level 'SUCCESS'
|
||||
$completionMode = if ($null -ne $UnattendProfile) { "Unattend profile '$($UnattendProfile.Name)'" } else { 'Use OOBE' }
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message ("Deployment complete: Windows {0}, disk {1}, mode {2}." -f $Edition.Name, $disk.Number, $completionMode) -Level 'SUCCESS'
|
||||
if ($ProgressAction) { & $ProgressAction 'Cleaning up temporary files…' }
|
||||
} finally {
|
||||
foreach ($temporaryFile in @($cleanPath, $diskpartPath, $unattendPath)) {
|
||||
if (Test-Path -LiteralPath $temporaryFile -PathType Leaf) { Remove-Item -LiteralPath $temporaryFile -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Remove-DeployCenterDismSourceAccess -Lease $sourceLease -TemporaryRoot $TemporaryRoot -LogAction $LogAction
|
||||
if ($windowsDeployCenterPath -and (Test-Path -LiteralPath $windowsDeployCenterPath -PathType Container)) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $windowsDeployCenterPath -Recurse -Force -ErrorAction Stop
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction -Message 'Temporary C:\DeployCenter directory removed.' -Level 'INFO'
|
||||
} catch {
|
||||
Write-DeployCenterDeploymentLog -LogAction $LogAction `
|
||||
-Message ("Unable to remove the temporary C:\DeployCenter directory: {0}" -f $_.Exception.Message) -Level 'WARNING'
|
||||
}
|
||||
}
|
||||
Remove-DeployCenterDismSourceAccess -Lease $sourceLease -TemporaryRoot $TemporaryRoot `
|
||||
-LogAction $LogAction -ProgressAction $ProgressAction
|
||||
}
|
||||
return [pscustomobject]@{
|
||||
WinRERecommendationWarnings = @($winreRecommendationWarnings)
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# DeployCenter - offline driver pack detection.
|
||||
|
||||
function ConvertTo-DeployCenterDriverKey {
|
||||
param([AllowNull()][string]$Value)
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return '' }
|
||||
return ([regex]::Replace($Value, '[^A-Za-z0-9]', '')).ToUpperInvariant()
|
||||
}
|
||||
|
||||
function Get-DeployCenterDriverManufacturerFolder {
|
||||
param([AllowNull()][string]$Manufacturer)
|
||||
if ($Manufacturer -match '(?i)lenovo') { return 'Lenovo' }
|
||||
if ($Manufacturer -match '(?i)dell') { return 'Dell' }
|
||||
if ($Manufacturer -match '(?i)hewlett|\bHP\b') { return 'HP' }
|
||||
if ($Manufacturer -match '(?i)microsoft') { return 'Microsoft' }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-DeployCenterDriverWindowsFolder {
|
||||
param([AllowNull()][string]$EditionName)
|
||||
if ([string]::IsNullOrWhiteSpace($EditionName)) { return $null }
|
||||
if ($EditionName -match '(?i)Windows\s+Server\s+(\d{4})') { return "Windows Server $($matches[1])" }
|
||||
if ($EditionName -match '(?i)Windows\s+11') { return 'Windows 11' }
|
||||
if ($EditionName -match '(?i)Windows\s+10') { return 'Windows 10' }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-DeployCenterDriverPack {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$DriverRoot,
|
||||
[AllowNull()][string]$Manufacturer,
|
||||
[AllowNull()][string]$Model,
|
||||
[AllowNull()][string]$SystemSKU,
|
||||
[AllowNull()][string]$EditionName
|
||||
)
|
||||
|
||||
$vendor = Get-DeployCenterDriverManufacturerFolder -Manufacturer $Manufacturer
|
||||
$osFolder = Get-DeployCenterDriverWindowsFolder -EditionName $EditionName
|
||||
$vendorRoot = if ($vendor) { Join-Path $DriverRoot $vendor } else { Join-Path $DriverRoot '_unknown_vendor' }
|
||||
$modelFolder = $null
|
||||
if (Test-Path -LiteralPath $vendorRoot -PathType Container) {
|
||||
$prefixes = @()
|
||||
$modelKey = ConvertTo-DeployCenterDriverKey $Model
|
||||
$skuKey = ConvertTo-DeployCenterDriverKey $SystemSKU
|
||||
if ($modelKey) { $prefixes += $modelKey }
|
||||
if ($skuKey) { $prefixes += $skuKey }
|
||||
$candidates = @()
|
||||
foreach ($directory in @(Get-ChildItem -LiteralPath $vendorRoot -Directory -ErrorAction SilentlyContinue)) {
|
||||
$key = ConvertTo-DeployCenterDriverKey $directory.Name
|
||||
if (-not $key) { continue }
|
||||
foreach ($prefix in $prefixes) {
|
||||
if ($prefix.StartsWith($key, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
$candidates += [pscustomobject]@{ Path = $directory.FullName; KeyLength = $key.Length }
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
$longest = ($candidates | Measure-Object -Property KeyLength -Maximum).Maximum
|
||||
$best = @($candidates | Where-Object { $_.KeyLength -eq $longest })
|
||||
if ($best.Count -eq 1) { $modelFolder = $best[0].Path }
|
||||
elseif ($best.Count -gt 1) {
|
||||
return [pscustomobject]@{ Found=$false; Status='Ambiguous model match'; DriverPath=$null; SearchPath=$vendorRoot; InfCount=0 }
|
||||
}
|
||||
}
|
||||
$searchPath = if ($modelFolder -and $osFolder) { Join-Path $modelFolder $osFolder } elseif ($vendor -and $osFolder) { Join-Path $vendorRoot ('<model>\' + $osFolder) } elseif ($vendor) { Join-Path $vendorRoot '<Windows version>' } else { Join-Path $DriverRoot '<vendor>\<model>\<Windows version>' }
|
||||
if (-not $vendor) { return [pscustomobject]@{ Found=$false; Status='Unsupported manufacturer'; DriverPath=$null; SearchPath=$searchPath; InfCount=0 } }
|
||||
if (-not $osFolder) { return [pscustomobject]@{ Found=$false; Status='Unrecognized Windows version'; DriverPath=$null; SearchPath=$searchPath; InfCount=0 } }
|
||||
if (-not $modelFolder) { return [pscustomobject]@{ Found=$false; Status='Model not found'; DriverPath=$null; SearchPath=$searchPath; InfCount=0 } }
|
||||
$searchPath = Join-Path $modelFolder $osFolder
|
||||
if (-not (Test-Path -LiteralPath $searchPath -PathType Container)) { return [pscustomobject]@{ Found=$false; Status='Version folder not found'; DriverPath=$null; SearchPath=$searchPath; InfCount=0 } }
|
||||
$infCount = @(Get-ChildItem -LiteralPath $searchPath -Filter '*.inf' -File -Recurse -ErrorAction SilentlyContinue).Count
|
||||
if ($infCount -eq 0) { return [pscustomobject]@{ Found=$false; Status='No INF files'; DriverPath=$null; SearchPath=$searchPath; InfCount=0 } }
|
||||
return [pscustomobject]@{ Found=$true; Status='Driver pack found'; DriverPath=$searchPath; SearchPath=$searchPath; InfCount=$infCount }
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Find-DeployCenterDriverPack
|
||||
@@ -5,17 +5,18 @@ function Get-HardwareInfo {
|
||||
param()
|
||||
|
||||
$info = [ordered]@{
|
||||
Manufacturer = "Inconnu"
|
||||
Model = "Inconnu"
|
||||
SerialNumber = "Inconnu"
|
||||
Firmware = "Inconnu"
|
||||
Manufacturer = "Unknown"
|
||||
Model = "Unknown"
|
||||
SystemSKU = ""
|
||||
SerialNumber = "Unknown"
|
||||
Firmware = "Unknown"
|
||||
Architecture = $env:PROCESSOR_ARCHITECTURE
|
||||
WinPE = $false
|
||||
Disks = @()
|
||||
Errors = @()
|
||||
}
|
||||
|
||||
# Fabricant et modele du PC
|
||||
# PC manufacturer and model
|
||||
try {
|
||||
$computer = Get-CimInstance `
|
||||
-ClassName Win32_ComputerSystem `
|
||||
@@ -28,12 +29,15 @@ function Get-HardwareInfo {
|
||||
if ($computer.Model) {
|
||||
$info.Model = $computer.Model.Trim()
|
||||
}
|
||||
if ($computer.SystemSKUNumber) {
|
||||
$info.SystemSKU = ([string]$computer.SystemSKUNumber).Trim()
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Modele du PC : $($_.Exception.Message)"
|
||||
$info.Errors += "PC model: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Numero de serie
|
||||
# Serial number
|
||||
try {
|
||||
$bios = Get-CimInstance `
|
||||
-ClassName Win32_BIOS `
|
||||
@@ -47,7 +51,7 @@ function Get-HardwareInfo {
|
||||
$info.Errors += "Numero de serie : $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Detection du firmware
|
||||
# Firmware detection
|
||||
try {
|
||||
$firmwareKey = Get-ItemProperty `
|
||||
-Path "HKLM:\SYSTEM\CurrentControlSet\Control" `
|
||||
@@ -61,20 +65,20 @@ function Get-HardwareInfo {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Mode firmware indisponible"
|
||||
$info.Errors += "Firmware mode unavailable"
|
||||
}
|
||||
|
||||
# Detection de WinPE
|
||||
# WinPE detection
|
||||
$info.WinPE = Test-Path `
|
||||
"HKLM:\SYSTEM\CurrentControlSet\Control\MiniNT"
|
||||
|
||||
# Inventaire des disques physiques
|
||||
# Physical disk inventory
|
||||
try {
|
||||
$disks = @(Get-Disk -ErrorAction Stop)
|
||||
|
||||
foreach ($disk in $disks) {
|
||||
$operationalStatus = @($disk.OperationalStatus | ForEach-Object { [string]$_ } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
$status = if ($operationalStatus.Count -gt 0) { $operationalStatus -join ', ' } else { 'Inconnu' }
|
||||
$status = if ($operationalStatus.Count -gt 0) { $operationalStatus -join ', ' } else { 'Unknown' }
|
||||
|
||||
$info.Disks += [PSCustomObject]@{
|
||||
Index = [int]$disk.Number
|
||||
@@ -90,10 +94,10 @@ function Get-HardwareInfo {
|
||||
}
|
||||
}
|
||||
catch {
|
||||
$info.Errors += "Inventaire des disques : $($_.Exception.Message)"
|
||||
$info.Errors += "Disk inventory: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
# Retourner un objet exploitable par les autres modules
|
||||
# Return an object that other modules can use.
|
||||
[PSCustomObject]$info
|
||||
}
|
||||
|
||||
@@ -119,7 +123,7 @@ function Write-DeployInventory {
|
||||
$json = $inventory | ConvertTo-Json -Depth 8
|
||||
Set-Content -LiteralPath $InventoryFile -Value $json -Encoding UTF8
|
||||
|
||||
# Nettoyer les anciens exports séparés après l'écriture de l'inventaire consolidé.
|
||||
# Remove the old separate exports after writing the consolidated inventory.
|
||||
$legacyInventoryFiles = @(
|
||||
(Join-Path $env:TEMP 'HardwareInfo.json'),
|
||||
(Join-Path $InventoryDir 'hardware.json')
|
||||
@@ -129,12 +133,12 @@ function Write-DeployInventory {
|
||||
try {
|
||||
Remove-Item -LiteralPath $legacyFile -Force -ErrorAction Stop
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible de supprimer l'ancien export d'inventaire '$legacyFile' : $($_.Exception.Message)" 'WARNING' }
|
||||
if ($LogAction) { & $LogAction "Unable to remove the previous inventory export '$legacyFile' : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Impossible d'écrire l'inventaire temporaire : $($_.Exception.Message)" 'WARNING' }
|
||||
if ($LogAction) { & $LogAction "Unable to write the temporary inventory: $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# DeployCenter - localize application text using the current WinPE language.
|
||||
|
||||
$script:DeployCenterLocale = 'fr-FR'
|
||||
$script:DeployCenterText = @{}
|
||||
|
||||
function Initialize-DeployCenterLocalization {
|
||||
[CmdletBinding()]
|
||||
param([AllowNull()][string]$CultureName = [Globalization.CultureInfo]::CurrentUICulture.Name)
|
||||
|
||||
$culture = if ([string]::IsNullOrWhiteSpace($CultureName)) { 'fr-FR' } else { $CultureName }
|
||||
$localeName = if ($culture.StartsWith('fr', [StringComparison]::OrdinalIgnoreCase)) { 'fr-FR' } else { 'en-US' }
|
||||
$resourcePath = Join-Path $PSScriptRoot (Join-Path '..' (Join-Path 'Locales' ($localeName + '.psd1')))
|
||||
$resourcePath = [IO.Path]::GetFullPath($resourcePath)
|
||||
if (-not (Test-Path -LiteralPath $resourcePath -PathType Leaf)) {
|
||||
throw "Localization resource not found: $resourcePath"
|
||||
}
|
||||
|
||||
$resource = Import-PowerShellDataFile -LiteralPath $resourcePath
|
||||
$script:DeployCenterLocale = $localeName
|
||||
$script:DeployCenterText = $resource.Strings
|
||||
return $localeName
|
||||
}
|
||||
|
||||
function Get-DeployCenterText {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][AllowEmptyString()][string]$Text,
|
||||
[AllowEmptyCollection()][object[]]$FormatArguments = @()
|
||||
)
|
||||
|
||||
$localized = $Text
|
||||
if ($script:DeployCenterText.ContainsKey($Text)) {
|
||||
$localized = [string]$script:DeployCenterText[$Text]
|
||||
} else {
|
||||
foreach ($key in @($script:DeployCenterText.Keys | Sort-Object { $_.Length } -Descending)) {
|
||||
if ($Text.StartsWith([string]$key, [StringComparison]::Ordinal)) {
|
||||
$localized = [string]$script:DeployCenterText[$key] + $Text.Substring(([string]$key).Length)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($FormatArguments.Count -gt 0) {
|
||||
return [string]::Format([Globalization.CultureInfo]::CurrentCulture, $localized, $FormatArguments)
|
||||
}
|
||||
return $localized
|
||||
}
|
||||
|
||||
function ConvertTo-DeployCenterLocalizedLogMessage {
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory=$true)][AllowEmptyString()][string]$Message)
|
||||
return Get-DeployCenterText -Text $Message
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Initialize-DeployCenterLocalization, Get-DeployCenterText, ConvertTo-DeployCenterLocalizedLogMessage
|
||||
+31
-5
@@ -1,5 +1,5 @@
|
||||
# DeployCenter - Gestion des journaux
|
||||
# Compatible Windows PowerShell 5.1
|
||||
# DeployCenter - Log management
|
||||
# Compatible with Windows PowerShell 5.1
|
||||
|
||||
function Initialize-DeployCenterLogging {
|
||||
[CmdletBinding()]
|
||||
@@ -16,11 +16,11 @@ function Initialize-DeployCenterLogging {
|
||||
New-Item -Path $script:LogRoot -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
|
||||
# L'adresse IPv4 est fournie par Modules\\Network.psm1.
|
||||
# The IPv4 address is provided by Modules\\Network.psm1.
|
||||
$script:LogIPv4 = $IPv4
|
||||
|
||||
$logIpTag = if ([string]::IsNullOrWhiteSpace($script:LogIPv4)) {
|
||||
'sans-ip'
|
||||
'no-ip'
|
||||
} else {
|
||||
$script:LogIPv4 -replace '[^0-9A-Za-z.-]', '_'
|
||||
}
|
||||
@@ -48,7 +48,7 @@ function Write-Log {
|
||||
[System.Text.Encoding]::UTF8
|
||||
)
|
||||
} catch {
|
||||
# Le journal graphique reste disponible si le fichier n'est pas accessible.
|
||||
# The graphical log remains available if the file cannot be accessed.
|
||||
}
|
||||
|
||||
if ($null -ne $script:txtLog -and -not $script:txtLog.IsDisposed) {
|
||||
@@ -57,3 +57,29 @@ function Write-Log {
|
||||
$script:txtLog.ScrollToCaret()
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-DeployCenterLog {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$DestinationRoot
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace([string]$script:LogFile) -or
|
||||
-not (Test-Path -LiteralPath $script:LogFile -PathType Leaf)) {
|
||||
Write-Log -Message (ConvertTo-DeployCenterLocalizedLogMessage -Message 'Log copy skipped: no log file is available.') -Level 'WARNING'
|
||||
return $null
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $DestinationRoot -PathType Container)) {
|
||||
throw "Destination folder is not accessible: $DestinationRoot"
|
||||
}
|
||||
$destinationPath = Join-Path $DestinationRoot ([System.IO.Path]::GetFileName($script:LogFile))
|
||||
Write-Log -Message (ConvertTo-DeployCenterLocalizedLogMessage -Message "Copying the log to $destinationPath.") -Level 'INFO'
|
||||
Copy-Item -LiteralPath $script:LogFile -Destination $destinationPath -Force -ErrorAction Stop | Out-Null
|
||||
return $destinationPath
|
||||
} catch {
|
||||
Write-Log -Message (ConvertTo-DeployCenterLocalizedLogMessage -Message "Unable to copy the log to '$DestinationRoot' : $($_.Exception.Message)") -Level 'WARNING'
|
||||
return $null
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
# DeployCenter - Détection réseau
|
||||
# Compatible Windows PowerShell 5.1
|
||||
# DeployCenter - Network detection
|
||||
# Compatible with Windows PowerShell 5.1
|
||||
|
||||
function Get-DeployNetworkIPv4 {
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
# Priorité à une interface avec passerelle par défaut ; exclut loopback,
|
||||
# APIPA et adresses IPv4 non routables couramment inutiles en WinPE.
|
||||
# Prefer an interface with a default gateway; exclude loopback,
|
||||
# APIPA, and non-routable IPv4 addresses that are generally useless in WinPE.
|
||||
$candidates = @()
|
||||
try {
|
||||
foreach ($interface in [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces()) {
|
||||
@@ -36,9 +36,9 @@ function Get-DeployNetworkIPv4 {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# La détection réseau ne doit pas empêcher le démarrage de DeployCenter.
|
||||
# Network detection must not prevent DeployCenter from starting.
|
||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||
Write-Log "Détection réseau : $($_.Exception.Message)" 'WARNING'
|
||||
Write-Log (ConvertTo-DeployCenterLocalizedLogMessage -Message "Network detection: $($_.Exception.Message)") 'WARNING'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+21
-21
@@ -1,5 +1,5 @@
|
||||
# DeployCenter - Chargement et validation des profils de partitionnement
|
||||
# Les profils PSD1 sont des fichiers de données ; ils ne contiennent aucun code exécuté.
|
||||
# DeployCenter - Load and validate partitioning profiles
|
||||
# PSD1 profiles are data files; they contain no executable code.
|
||||
|
||||
function Get-DeployCenterPartitionProfileNames {
|
||||
[CmdletBinding()]
|
||||
@@ -20,61 +20,61 @@ function Import-DeployCenterPartitionProfile {
|
||||
[Parameter(Mandatory=$true)][string]$ProfileName
|
||||
)
|
||||
|
||||
if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de profil invalide : $ProfileName" }
|
||||
if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid profile name: $ProfileName" }
|
||||
$path = Join-Path $ProfilesRoot ($ProfileName + '.psd1')
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profil introuvable : $ProfileName" }
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profile not found: $ProfileName" }
|
||||
|
||||
$profile = Import-PowerShellDataFile -LiteralPath $path
|
||||
foreach ($key in @('Id','Name','Description','Firmware','PartitionStyle','Partitions')) {
|
||||
if (-not $profile.ContainsKey($key)) { throw "Le profil '$ProfileName' ne contient pas la clé '$key'." }
|
||||
if (-not $profile.ContainsKey($key)) { throw "The profile '$ProfileName' does not contain key '$key'." }
|
||||
}
|
||||
if ([string]$profile.Id -ne $ProfileName) { throw "L'identifiant du profil '$ProfileName' ne correspond pas au nom du fichier." }
|
||||
if (@($profile.Partitions).Count -lt 2) { throw "Le profil '$ProfileName' doit définir au moins deux partitions." }
|
||||
if ([string]$profile.Id -ne $ProfileName) { throw "Profile ID '$ProfileName' does not match the file name." }
|
||||
if (@($profile.Partitions).Count -lt 2) { throw 'The profile must define at least two partitions.' }
|
||||
if (@($profile.Partitions | Where-Object { [string]$_.Role -eq 'Windows' }).Count -ne 1) {
|
||||
throw "Le profil '$ProfileName' doit définir exactement une partition Windows."
|
||||
throw 'The profile must define exactly one Windows partition.'
|
||||
}
|
||||
$userInputPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'UserInputGB' })
|
||||
$remainderPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'RemainderAfterInputs' })
|
||||
$remainingPartitions = @($profile.Partitions | Where-Object { [string]$_.Size.Policy -eq 'Remaining' })
|
||||
if ($userInputPartitions.Count -gt 0) {
|
||||
if ($remainderPartitions.Count -ne 1 -or $remainingPartitions.Count -gt 0) {
|
||||
throw "Le profil '$ProfileName' doit associer les saisies utilisateur à une partition de reste unique."
|
||||
throw "Profile '$ProfileName' must associate user inputs with exactly one remaining-space partition."
|
||||
}
|
||||
} elseif ($remainingPartitions.Count -ne 1 -or [string]$profile.Partitions[-1].Size.Policy -ne 'Remaining') {
|
||||
throw "Le profil '$ProfileName' doit placer une unique partition utilisant l'espace restant en dernière position."
|
||||
throw "Profile '$ProfileName' must place exactly one remaining-space partition last."
|
||||
}
|
||||
foreach ($partition in $profile.Partitions) {
|
||||
foreach ($key in @('Name','Role','FileSystem','Size')) {
|
||||
if (-not $partition.ContainsKey($key)) { throw "Une partition du profil '$ProfileName' ne contient pas la clé '$key'." }
|
||||
if (-not $partition.ContainsKey($key)) { throw "A profile partition '$ProfileName' does not contain key '$key'." }
|
||||
}
|
||||
if (-not $partition.Size.ContainsKey('Policy')) { throw "La taille d'une partition du profil '$ProfileName' ne définit pas Policy." }
|
||||
if (-not $partition.Size.ContainsKey('Policy')) { throw "The size of a profile partition '$ProfileName' does not define Policy." }
|
||||
switch ([string]$partition.Size.Policy) {
|
||||
'FixedMB' {
|
||||
if ([int]$partition.Size.Value -le 0) { throw "Une taille fixe du profil '$ProfileName' doit être supérieure à zéro." }
|
||||
if ([int]$partition.Size.Value -le 0) { throw "A fixed size in profile '$ProfileName' must be greater than zero." }
|
||||
}
|
||||
'ImageSizePlusFreeSpace' {
|
||||
if ([int]$partition.Size.FreeSpaceMB -lt 250) { throw "La marge libre WinRE du profil '$ProfileName' doit être d'au moins 250 Mo." }
|
||||
if ([int]$partition.Size.PreviewMB -le 0) { throw "Le profil '$ProfileName' doit définir une réserve PreviewMB pour estimer la capacité disponible." }
|
||||
if ([int]$partition.Size.FreeSpaceMB -lt 250) { throw 'The WinRE free-space allowance must be at least 250 MB.' }
|
||||
if ([int]$partition.Size.PreviewMB -le 0) { throw "Profile '$ProfileName' must define a PreviewMB reserve to estimate available capacity." }
|
||||
}
|
||||
'ConfiguredMB' {
|
||||
if ([string]::IsNullOrWhiteSpace([string]$partition.Size.ConfigKey) -or
|
||||
[int]$partition.Size.Value -le 0 -or [int]$partition.Size.PreviewMB -le 0) {
|
||||
throw "La taille configurée de la partition '$($partition.Name)' du profil '$ProfileName' doit définir ConfigKey et des tailles strictement positives."
|
||||
throw "The configured size of partition '$($partition.Name)' in profile '$ProfileName' must define ConfigKey and strictly positive sizes."
|
||||
}
|
||||
}
|
||||
'UserInputGB' {
|
||||
if ([int]$partition.Size.MinimumGB -le 0 -or [int]$partition.Size.DefaultPercent -le 0 -or [int]$partition.Size.DefaultPercent -ge 100) {
|
||||
throw "La saisie de taille utilisateur du profil '$ProfileName' doit définir MinimumGB et un DefaultPercent entre 1 et 99."
|
||||
throw 'The user-size input must define MinimumGB and a DefaultPercent from 1 to 99.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { throw "La saisie de taille utilisateur du profil '$ProfileName' doit définir Label." }
|
||||
if ([string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) { throw "The profile user-size input '$ProfileName' must define Label." }
|
||||
}
|
||||
'RemainderAfterInputs' {
|
||||
if ([double]$partition.Size.MinimumGB -lt 0 -or [string]::IsNullOrWhiteSpace([string]$partition.Size.Label)) {
|
||||
throw "La partition de reste du profil '$ProfileName' doit définir un MinimumGB non négatif et un Label."
|
||||
throw "The remaining-space partition in profile '$ProfileName' must define a non-negative MinimumGB and a Label."
|
||||
}
|
||||
}
|
||||
'Remaining' { }
|
||||
default { throw "Politique de taille inconnue dans le profil '$ProfileName' : $($partition.Size.Policy)" }
|
||||
default { throw "Unknown size policy in profile '$ProfileName': $($partition.Size.Policy)" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ function Get-DeployCenterPartitionSizingCapacity {
|
||||
}
|
||||
|
||||
$capacityBytes = $DiskSizeBytes - [long][math]::Ceiling($reservedMB * 1MB)
|
||||
if ($capacityBytes -le 0) { throw "Le disque est trop petit pour les réserves du profil '$($Profile.Name)'." }
|
||||
if ($capacityBytes -le 0) { throw "The disk is too small for the profile reserves '$($Profile.Name)'." }
|
||||
return [pscustomobject]@{
|
||||
UsableBytes = [long]$capacityBytes
|
||||
UsableGB = [math]::Round(($capacityBytes / 1GB), 1)
|
||||
|
||||
@@ -4,7 +4,7 @@ function ConvertFrom-ThemeColor {
|
||||
param([Parameter(Mandatory=$true)][string]$Value)
|
||||
$hex = $Value.Trim().TrimStart('#')
|
||||
if ($hex -notmatch '^[0-9A-Fa-f]{6}$') {
|
||||
throw "Couleur invalide dans un thème : '$Value'. Utiliser #RRGGBB."
|
||||
throw "Invalid color in theme: '$Value'. Use #RRGGBB."
|
||||
}
|
||||
return [System.Drawing.Color]::FromArgb(
|
||||
[Convert]::ToInt32($hex.Substring(0,2),16),
|
||||
@@ -27,13 +27,13 @@ function Import-DeployCenterTheme {
|
||||
[Parameter(Mandatory=$true)][string]$ThemesRoot,
|
||||
[Parameter(Mandatory=$true)][string]$ThemeName
|
||||
)
|
||||
if ($ThemeName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de thème invalide : $ThemeName" }
|
||||
if ($ThemeName -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid theme name: $ThemeName" }
|
||||
$path = Join-Path $ThemesRoot ($ThemeName + '.psd1')
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Thème introuvable : $ThemeName" }
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Theme not found: $ThemeName" }
|
||||
$data = Import-PowerShellDataFile -LiteralPath $path
|
||||
$required = @('Background','Panel','PanelAlt','Text','Secondary','Accent','Success','Border','Danger')
|
||||
foreach ($key in $required) {
|
||||
if (-not $data.ContainsKey($key)) { throw "Le thème '$ThemeName' ne contient pas la clé '$key'." }
|
||||
if (-not $data.ContainsKey($key)) { throw "Theme '$ThemeName' does not contain key '$key'." }
|
||||
}
|
||||
$theme = @{}
|
||||
foreach ($key in $required) { $theme[$key] = ConvertFrom-ThemeColor -Value ([string]$data[$key]) }
|
||||
|
||||
+571
-165
File diff suppressed because it is too large.
Load diff
+30
-22
@@ -1,5 +1,5 @@
|
||||
# DeployCenter - Découverte et génération des profils de réponses Windows.
|
||||
# Les profils PSD1 restent des fichiers de données ; les réponses sont construites comme XML.
|
||||
# DeployCenter - Discover and generate Windows answer-file profiles.
|
||||
# PSD1 profiles remain data files; answer files are built as XML.
|
||||
|
||||
function Get-DeployCenterUnattendProfileNames {
|
||||
[CmdletBinding()]
|
||||
@@ -20,20 +20,20 @@ function Import-DeployCenterUnattendProfile {
|
||||
[Parameter(Mandatory=$true)][string]$ProfileName
|
||||
)
|
||||
|
||||
if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Nom de profil Unattend invalide : $ProfileName" }
|
||||
if ($ProfileName -notmatch '^[A-Za-z0-9_-]+$') { throw "Invalid Unattend profile name: $ProfileName" }
|
||||
$path = Join-Path $ProfilesRoot ($ProfileName + '.psd1')
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Profil Unattend introuvable : $ProfileName" }
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "Unattend profile not found: $ProfileName" }
|
||||
$profile = Import-PowerShellDataFile -LiteralPath $path
|
||||
foreach ($key in @('Id','Name','Description','Target','Template','ComputerName','LocalAccount')) {
|
||||
if (-not $profile.ContainsKey($key)) { throw ('Le profil Unattend {0} ne contient pas la cle {1}.' -f $ProfileName, $key) }
|
||||
if (-not $profile.ContainsKey($key)) { throw ('Unattend profile {0} does not contain key {1}.' -f $ProfileName, $key) }
|
||||
}
|
||||
if ([string]$profile.Id -ne $ProfileName) { throw ('L identifiant du profil Unattend {0} ne correspond pas au nom du fichier.' -f $ProfileName) }
|
||||
if ([string]$profile.Target -notin @('Workstation', 'Server')) { throw ('La propriete Target du profil Unattend {0} doit etre Workstation ou Server.' -f $ProfileName) }
|
||||
if ([string]$profile.Template -notmatch '^[A-Za-z0-9_-]+\.xml$') { throw ('Nom de template XML invalide dans le profil {0}.' -f $ProfileName) }
|
||||
if ([string]$profile.Id -ne $ProfileName) { throw ('Unattend profile ID {0} does not match the file name.' -f $ProfileName) }
|
||||
if ([string]$profile.Target -notin @('Workstation', 'Server')) { throw ('The Target property of Unattend profile {0} must be Workstation or Server.' -f $ProfileName) }
|
||||
if ([string]$profile.Template -notmatch '^[A-Za-z0-9_-]+\.xml$') { throw ('Invalid XML template name in profile {0}.' -f $ProfileName) }
|
||||
$templatesRoot = Join-Path (Split-Path -Parent $ProfilesRoot) 'Templates'
|
||||
$templatePath = Join-Path $templatesRoot ([string]$profile.Template)
|
||||
if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) { throw "Template XML introuvable pour le profil '$ProfileName' : $templatePath" }
|
||||
if (-not [string]$profile.Name -or [string]::IsNullOrWhiteSpace([string]$profile.Name)) { throw ('Le profil Unattend {0} doit definir un nom affichable.' -f $ProfileName) }
|
||||
if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) { throw "XML template not found for profile '$ProfileName': $templatePath" }
|
||||
if (-not [string]$profile.Name -or [string]::IsNullOrWhiteSpace([string]$profile.Name)) { throw ('Unattend profile {0} must define a display name.' -f $ProfileName) }
|
||||
$profile.TemplatePath = $templatePath
|
||||
return $profile
|
||||
}
|
||||
@@ -47,25 +47,25 @@ function Test-DeployCenterUnattendConfiguration {
|
||||
)
|
||||
|
||||
if ($Profile.ComputerName -and $HostName -notmatch '^(?=.*[A-Za-z])[A-Za-z0-9](?:[A-Za-z0-9-]{0,13}[A-Za-z0-9])?$') {
|
||||
throw 'Le nom de machine doit respecter les règles NetBIOS et ne pas dépasser 15 caractères.'
|
||||
throw 'The computer name must follow NetBIOS rules and not exceed 15 characters.'
|
||||
}
|
||||
if ($Profile.LocalAccount) {
|
||||
$accountName = [string]$Configuration.UnattendAdminName
|
||||
$displayName = [string]$Configuration.UnattendAdminDisplayName
|
||||
$groupName = [string]$Configuration.UnattendAdminGroup
|
||||
$passwordBase64 = [string]$Configuration.UnattendAdminPasswordBase64
|
||||
if ($accountName -notmatch '^[^"/\[\]:;|=,+*?<>\\]{1,20}$') { throw 'Le paramètre [Unattend] AdminName doit contenir de 1 à 20 caractères et éviter les caractères interdits par Windows.' }
|
||||
if ([string]::IsNullOrWhiteSpace($displayName)) { throw 'Le paramètre [Unattend] AdminDisplayName ne peut pas être vide.' }
|
||||
if ([string]::IsNullOrWhiteSpace($groupName)) { throw 'Le paramètre [Unattend] AdminGroup ne peut pas être vide.' }
|
||||
if ([string]::IsNullOrWhiteSpace($passwordBase64)) { throw 'Le paramètre [Unattend] AdminPasswordBase64 est requis pour ce profil.' }
|
||||
if ($accountName -notmatch '^[^"/\[\]:;|=,+*?<>\\]{1,20}$') { throw '[Unattend] AdminName must be 1 to 20 characters and must not contain characters prohibited by Windows.' }
|
||||
if ([string]::IsNullOrWhiteSpace($displayName)) { throw '[Unattend] AdminDisplayName cannot be empty.' }
|
||||
if ([string]::IsNullOrWhiteSpace($groupName)) { throw '[Unattend] AdminGroup cannot be empty.' }
|
||||
if ([string]::IsNullOrWhiteSpace($passwordBase64)) { throw 'The [Unattend] AdminPasswordBase64 setting is required for this profile.' }
|
||||
try {
|
||||
$passwordBytes = [Convert]::FromBase64String($passwordBase64)
|
||||
if ($passwordBytes.Length -eq 0 -or ($passwordBytes.Length % 2) -ne 0 -or
|
||||
[string]::IsNullOrEmpty([Text.Encoding]::Unicode.GetString($passwordBytes))) {
|
||||
throw 'Format UTF-16LE Base64 invalide.'
|
||||
throw 'Invalid UTF-16LE Base64 format.'
|
||||
}
|
||||
} catch {
|
||||
throw 'Le paramètre [Unattend] AdminPasswordBase64 doit être un mot de passe UTF-16LE encodé en Base64.'
|
||||
throw '[Unattend] AdminPasswordBase64 must be a UTF-16LE password encoded as Base64.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ function New-DeployCenterUnattendXml {
|
||||
}
|
||||
if ($Profile.ComputerName) {
|
||||
$node = $document.SelectSingleNode('/u:unattend/u:settings[@pass="specialize"]/u:component[@name="Microsoft-Windows-Shell-Setup"]/u:ComputerName', $namespaces)
|
||||
if ($null -eq $node) { throw ('Le template du profil {0} ne definit pas le noeud ComputerName dans specialize.' -f $Profile.Id) }
|
||||
if ($null -eq $node) { throw ('The template for profile {0} does not define the ComputerName node in specialize.' -f $Profile.Id) }
|
||||
$node.InnerText = $HostName
|
||||
}
|
||||
if ($Profile.LocalAccount) {
|
||||
@@ -111,20 +111,28 @@ function New-DeployCenterUnattendXml {
|
||||
foreach ($name in $values.Keys) {
|
||||
$field = if ($name -eq 'Password') { 'Password/u:Value' } else { $name }
|
||||
$node = $document.SelectSingleNode("/u:unattend/u:settings[@pass='oobeSystem']/u:component[@name='Microsoft-Windows-Shell-Setup']/u:UserAccounts/u:LocalAccounts/u:LocalAccount/u:$field", $namespaces)
|
||||
if ($null -eq $node) { throw ('Le template du profil {0} ne definit pas le champ de compte {1}.' -f $Profile.Id, $name) }
|
||||
if ($null -eq $node) { throw ('The template for profile {0} does not define the account field {1}.' -f $Profile.Id, $name) }
|
||||
$node.InnerText = $values[$name]
|
||||
}
|
||||
}
|
||||
$supportAppUrl = [string]$Configuration.UnattendSupportAppURL
|
||||
$supportUrl = [string]$Configuration.UnattendSupportURL
|
||||
# Compatibility: before SupportURL was added, a web URL could be provided in SupportAppURL.
|
||||
if ($supportAppUrl -match '^https?://') {
|
||||
if ([string]::IsNullOrWhiteSpace($supportUrl)) { $supportUrl = $supportAppUrl }
|
||||
$supportAppUrl = ''
|
||||
}
|
||||
$supportSettings = @(
|
||||
@{ Name = 'SupportProvider'; Value = [string]$Configuration.UnattendSupportProvider },
|
||||
@{ Name = 'SupportAppURL'; Value = [string]$Configuration.UnattendSupportAppURL }
|
||||
@{ Name = 'SupportURL'; Value = $supportUrl },
|
||||
@{ Name = 'SupportAppURL'; Value = $supportAppUrl }
|
||||
)
|
||||
$oemInformationPath = '/u:unattend/u:settings[@pass="oobeSystem"]/u:component[@name="Microsoft-Windows-Shell-Setup"]/u:OEMInformation'
|
||||
foreach ($supportSetting in $supportSettings) {
|
||||
$supportNode = $document.SelectSingleNode(($oemInformationPath + '/u:' + $supportSetting.Name), $namespaces)
|
||||
if ($null -eq $supportNode) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($supportSetting.Value)) {
|
||||
throw ('Le template du profil {0} ne definit pas le noeud {1}.' -f $Profile.Id, $supportSetting.Name)
|
||||
throw ('The template for profile {0} does not define the {1} node.' -f $Profile.Id, $supportSetting.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -140,7 +148,7 @@ function New-DeployCenterUnattendXml {
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($WallpaperPath)) {
|
||||
$shellSetupNode = $document.SelectSingleNode('/u:unattend/u:settings[@pass="oobeSystem"]/u:component[@name="Microsoft-Windows-Shell-Setup"]', $namespaces)
|
||||
if ($null -eq $shellSetupNode) { throw ('Le template du profil {0} ne definit pas le composant Shell-Setup dans oobeSystem.' -f $Profile.Id) }
|
||||
if ($null -eq $shellSetupNode) { throw ('The template for profile {0} does not define the Shell-Setup component in oobeSystem.' -f $Profile.Id) }
|
||||
$themesNode = $shellSetupNode.SelectSingleNode('u:Themes', $namespaces)
|
||||
if ($null -eq $themesNode) {
|
||||
$themesNode = $document.CreateElement('Themes', 'urn:schemas-microsoft-com:unattend')
|
||||
|
||||
+33
-33
@@ -1,5 +1,5 @@
|
||||
# Module VNC de DeployCenter
|
||||
# Exécuté uniquement avec -Vnc dans le scope principal pour partager le contexte WinPE.
|
||||
# DeployCenter VNC module
|
||||
# Run only with -Vnc in the main scope to share the WinPE context.
|
||||
|
||||
function Start-DeployCenterVnc {
|
||||
$parentRoot = Split-Path -Path $script:DeployRoot -Parent
|
||||
@@ -9,36 +9,36 @@ function Start-DeployCenterVnc {
|
||||
$sourceIni = Join-Path $sourceVnc 'ultravnc.ini'
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($script:DeployIPv4)) {
|
||||
& $script:SourceLogAction 'Aucune adresse IPv4 de déploiement détectée.' 'WARNING'
|
||||
& $script:SourceLogAction 'No deployment IPv4 address detected.' 'WARNING'
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceExe -PathType Leaf)) {
|
||||
& $script:SourceLogAction "UltraVNC non démarré : exécutable absent ($sourceExe)." 'WARNING'
|
||||
& $script:SourceLogAction "UltraVNC was not started: executable not found ($sourceExe)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $passwordTool -PathType Leaf)) {
|
||||
& $script:SourceLogAction "UltraVNC non démarré : createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
& $script:SourceLogAction "UltraVNC was not started: createpassword64.exe absent ($passwordTool)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $sourceIni -PathType Leaf)) {
|
||||
& $script:SourceLogAction "UltraVNC non démarré : fichier absent ($sourceIni)." 'WARNING'
|
||||
& $script:SourceLogAction "UltraVNC was not started: file not found ($sourceIni)." 'WARNING'
|
||||
Write-DeployInventory -InventoryDir $script:InventoryDir -InventoryFile $script:InventoryFile -HardwareInfo $script:HardwareInventory -DeploymentIPv4 $script:DeployIPv4 -VncPassword $script:VncPassword -LogAction $script:SourceLogAction
|
||||
return
|
||||
}
|
||||
|
||||
$vncStage = 'préparation du dossier temporaire'
|
||||
$vncStage = 'preparing the temporary folder'
|
||||
try {
|
||||
if (Test-Path -LiteralPath $script:VncDirectory) {
|
||||
Remove-Item -LiteralPath $script:VncDirectory -Recurse -Force -ErrorAction Stop
|
||||
}
|
||||
New-Item -Path $script:VncDirectory -ItemType Directory -Force | Out-Null
|
||||
$vncStage = "copie des fichiers depuis $sourceVnc vers $script:VncDirectory"
|
||||
$vncStage = "copying files from $sourceVnc to $script:VncDirectory"
|
||||
Copy-Item -Path (Join-Path $sourceVnc '*') -Destination $script:VncDirectory -Recurse -Force
|
||||
# WinPE ne permet pas toujours d'ajouter des règles de pare-feu.
|
||||
# La désactivation globale n'est effectuée que lorsque VNC est demandé.
|
||||
$vncStage = 'désactivation du pare-feu WinPE'
|
||||
# WinPE does not always allow firewall rules to be added.
|
||||
# The firewall is disabled globally only when VNC is requested.
|
||||
$vncStage = 'disabling the WinPE firewall'
|
||||
$wpeutil = Join-Path $env:WINDIR 'System32\wpeutil.exe'
|
||||
if (-not (Test-Path -LiteralPath $wpeutil -PathType Leaf)) {
|
||||
$wpeutil = 'wpeutil.exe'
|
||||
@@ -48,58 +48,58 @@ function Start-DeployCenterVnc {
|
||||
foreach ($outputLine in $wpeutilOutput) {
|
||||
$outputText = [string]$outputLine
|
||||
if (-not [string]::IsNullOrWhiteSpace($outputText)) {
|
||||
& $script:SourceLogAction "wpeutil DisableFirewall : $outputText" 'INFO'
|
||||
& $script:SourceLogAction "wpeutil DisableFirewall: $outputText" 'INFO' $true
|
||||
}
|
||||
}
|
||||
if ($wpeutilExitCode -ne 0) {
|
||||
throw "wpeutil DisableFirewall a renvoyé le code $wpeutilExitCode."
|
||||
throw "wpeutil DisableFirewall returned exit code $wpeutilExitCode."
|
||||
}
|
||||
& $script:SourceLogAction "Pare-feu WinPE désactivé à la demande de -Vnc (code wpeutil : $wpeutilExitCode)." 'WARNING'
|
||||
& $script:SourceLogAction "WinPE firewall disabled at the request of -Vnc (wpeutil exit code: $wpeutilExitCode)." 'WARNING'
|
||||
|
||||
# Force le mode portable : l'INI actif est celui placé à côté de winvnc.exe.
|
||||
$vncStage = 'activation du mode portable UltraVNC'
|
||||
# Force portable mode: the active INI is the one next to winvnc.exe.
|
||||
$vncStage = 'enabling UltraVNC portable mode'
|
||||
$portableFlag = Join-Path $script:VncDirectory 'ultravnc.portable'
|
||||
Set-Content -LiteralPath $portableFlag -Value '' -Encoding ASCII
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$script:Config.VncPassword)) {
|
||||
$script:VncPassword = [string]$script:Config.VncPassword
|
||||
$passwordSource = 'configuré dans DeployCenter.ini'
|
||||
$passwordSource = 'configured in DeployCenter.ini'
|
||||
}
|
||||
else {
|
||||
$script:VncPassword = -join (1..8 | ForEach-Object {
|
||||
$alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789'
|
||||
$alphabet[(Get-Random -Minimum 0 -Maximum $alphabet.Length)]
|
||||
})
|
||||
$passwordSource = 'généré automatiquement'
|
||||
$passwordSource = 'generated automatically'
|
||||
}
|
||||
|
||||
# createpassword64 encode le mot de passe choisi dans le champ passwd d'ultravnc.ini.
|
||||
$vncStage = 'génération du mot de passe UltraVNC'
|
||||
# createpassword64 encodes the selected password in the passwd field of ultravnc.ini.
|
||||
$vncStage = 'generating the UltraVNC password'
|
||||
$tool = Join-Path $script:VncDirectory 'createpassword64.exe'
|
||||
$script:VncProcess = Start-Process -FilePath $tool -ArgumentList @($script:VncPassword) `
|
||||
-WorkingDirectory $script:VncDirectory -Wait -PassThru -WindowStyle Hidden
|
||||
if ($script:VncProcess.ExitCode -ne 0) {
|
||||
throw "createpassword64.exe a renvoyé le code $($script:VncProcess.ExitCode)."
|
||||
throw "createpassword64.exe returned exit code $($script:VncProcess.ExitCode)."
|
||||
}
|
||||
|
||||
$activeIni = Join-Path $script:VncDirectory 'ultravnc.ini'
|
||||
if (-not (Test-Path -LiteralPath $activeIni -PathType Leaf) -or
|
||||
-not (Select-String -LiteralPath $activeIni -Pattern '^passwd\s*=' -Quiet)) {
|
||||
throw "createpassword64.exe n'a pas créé le champ passwd dans ultravnc.ini."
|
||||
throw "createpassword64.exe did not create the passwd field in ultravnc.ini."
|
||||
}
|
||||
|
||||
$server = Join-Path $script:VncDirectory 'winvnc.exe'
|
||||
$vncStage = 'démarrage du serveur UltraVNC'
|
||||
$vncStage = 'starting the UltraVNC server'
|
||||
$script:VncProcess = Start-Process -FilePath $server -ArgumentList @('-run') `
|
||||
-WorkingDirectory $script:VncDirectory -PassThru -WindowStyle Hidden
|
||||
Start-Sleep -Milliseconds 800
|
||||
if ($script:VncProcess.HasExited) {
|
||||
throw "winvnc.exe s'est arrêté immédiatement (code $($script:VncProcess.ExitCode))."
|
||||
throw "winvnc.exe exited immediately (code $($script:VncProcess.ExitCode))."
|
||||
}
|
||||
$script:VncStarted = $true
|
||||
& $script:SourceLogAction "UltraVNC démarré. IP : $($script:DeployIPv4) ; mot de passe $passwordSource." 'SUCCESS'
|
||||
& $script:SourceLogAction "UltraVNC started. IP: $($script:DeployIPv4) ; password $passwordSource." 'SUCCESS'
|
||||
} catch {
|
||||
& $script:SourceLogAction "Échec du démarrage UltraVNC à l'étape '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
& $script:SourceLogAction "UltraVNC startup failed at stage '$vncStage' : $($_.Exception.Message)" 'ERROR'
|
||||
$script:VncStarted = $false
|
||||
$script:VncPassword = $null
|
||||
}
|
||||
@@ -112,20 +112,20 @@ function Stop-DeployCenterVnc {
|
||||
$proc = Get-Process -Id $script:VncProcess.Id -ErrorAction SilentlyContinue
|
||||
if ($null -ne $proc) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
# Au cas où winvnc lance un processus serveur distinct, ne cibler que les
|
||||
# processus dont le chemin correspond à notre copie temporaire.
|
||||
# If winvnc starts a separate server process, target only processes
|
||||
# whose executable path matches our temporary copy.
|
||||
foreach ($proc in (Get-CimInstance Win32_Process -Filter "Name='winvnc.exe'" -ErrorAction SilentlyContinue)) {
|
||||
if ($proc.ExecutablePath -and $proc.ExecutablePath.StartsWith($script:VncDirectory, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
& $script:SourceLogAction "Arrêt UltraVNC : $($_.Exception.Message)" 'WARNING'
|
||||
& $script:SourceLogAction "Stopping UltraVNC: $($_.Exception.Message)" 'WARNING'
|
||||
}
|
||||
# Le processus winvnc peut conserver brièvement un handle sur son dossier.
|
||||
# Le dossier VNC est propre à ce processus PowerShell (uvnc_<PID>) : on ne
|
||||
# supprime pas récursivement le dossier partagé InventoryDir, ce qui évite
|
||||
# de bloquer le lancement suivant si un handle est encore ouvert.
|
||||
# The winvnc process may briefly keep a handle open on its folder.
|
||||
# The VNC folder belongs to this PowerShell process (uvnc_<PID>); do not
|
||||
# recursively delete the shared InventoryDir folder. This avoids
|
||||
# blocking the next launch if a handle is still open.
|
||||
try {
|
||||
if (Test-Path -LiteralPath $script:InventoryFile -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $script:InventoryFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
+37
-37
@@ -1,5 +1,5 @@
|
||||
# DeployCenter - Recherche et lecture des sources Windows
|
||||
# Compatible Windows PowerShell 5.1
|
||||
# DeployCenter - Find and read Windows sources
|
||||
# Compatible with Windows PowerShell 5.1
|
||||
|
||||
function Find-WindowsSources {
|
||||
param(
|
||||
@@ -14,20 +14,20 @@ function Find-WindowsSources {
|
||||
$candidate
|
||||
} | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
|
||||
if ($roots.Count -eq 0) {
|
||||
throw 'Indique au moins un chemin local et/ou un partage SMB, separes par un point-virgule.'
|
||||
throw 'Specify at least one local path and/or SMB share, separated by semicolons.'
|
||||
}
|
||||
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||
if ($LogAction) { & $LogAction "Source inaccessible, ignoree : $root" 'WARNING' }
|
||||
if ($LogAction) { & $LogAction "Source inaccessible, skipped: $root" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
if ($LogAction) { & $LogAction "Recherche des sources Windows dans : $root" 'INFO' }
|
||||
# Parcours en lecture seule. Les erreurs d'acces aux sous-dossiers sont ignorees.
|
||||
# Afficher les ISO et les fichiers WIM/ESD autonomes.
|
||||
# Exception : masquer boot.wim uniquement lorsqu'il fait partie d'un
|
||||
# support Windows extrait dont le même dossier Sources contient install.wim/esd.
|
||||
if ($LogAction) { & $LogAction "Searching for Windows sources in: $root" 'INFO' }
|
||||
# Read-only scan. Errors accessing subfolders are ignored.
|
||||
# Show ISO files and standalone WIM/ESD files.
|
||||
# Exception: hide boot.wim only when it is part of an
|
||||
# extracted Windows medium whose Sources folder also contains install.wim/esd.
|
||||
$allFiles = @(Get-ChildItem -LiteralPath $root -Recurse -File -ErrorAction SilentlyContinue)
|
||||
$files = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($file in $allFiles) {
|
||||
@@ -62,24 +62,24 @@ function Find-WindowsSources {
|
||||
'.esd' { 'Image ESD Windows' }
|
||||
}
|
||||
$detail = if ($file.Name -ieq 'install.wim' -or $file.Name -ieq 'install.esd') {
|
||||
"Image d’installation dans un support extrait"
|
||||
"Installation image on extracted media"
|
||||
} elseif ($file.Extension -ieq '.iso') {
|
||||
"Support d’installation ISO"
|
||||
"ISO installation media"
|
||||
} else {
|
||||
"Image WIM/ESD autonome"
|
||||
}
|
||||
$results.Add([pscustomobject]@{
|
||||
Type = $kind; Nom = $file.Name; Chemin = $file.FullName
|
||||
Taille = ('{0:N2} Go' -f ($file.Length / 1GB)); Detail = $detail
|
||||
Type = $kind; Name = $file.Name; Path = $file.FullName
|
||||
Size = ('{0:N2} GB' -f ($file.Length / 1GB)); Detail = $detail
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (-not ($roots | Where-Object { Test-Path -LiteralPath $_ -PathType Container })) {
|
||||
throw "Aucun des chemins n'est accessible : $($roots -join '; ')`nVerifie que le lecteur local et le partage SMB sont accessibles."
|
||||
throw "None of the paths is accessible: $($roots -join '; ')`nCheck that the local drive and SMB share are available."
|
||||
}
|
||||
if ($LogAction) { & $LogAction ("Detection terminee : {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' }
|
||||
return @($results | Sort-Object Type, Nom, Chemin -Unique)
|
||||
if ($LogAction) { & $LogAction ("Detection complete: {0} element(s) trouve(s)." -f $results.Count) 'SUCCESS' }
|
||||
return @($results | Sort-Object Type, Name, Path -Unique)
|
||||
}
|
||||
|
||||
function Get-WindowsImageEditions {
|
||||
@@ -89,7 +89,7 @@ function Get-WindowsImageEditions {
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
|
||||
throw "Fichier image introuvable : $ImagePath"
|
||||
throw "Image file not found: $ImagePath"
|
||||
}
|
||||
|
||||
$normalized = @()
|
||||
@@ -138,18 +138,18 @@ function Get-WindowsImageEditions {
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if ($LogAction) { & $LogAction "Get-WindowsImage indisponible pour $ImagePath : $($_.Exception.Message)" 'WARNING' }
|
||||
if ($LogAction) { & $LogAction "Get-WindowsImage unavailable for $ImagePath : $($_.Exception.Message)" 'WARNING' }
|
||||
}
|
||||
}
|
||||
|
||||
$dism = Join-Path $env:WINDIR 'System32\dism.exe'
|
||||
if (-not (Test-Path -LiteralPath $dism)) { $dism = 'dism.exe' }
|
||||
|
||||
# Si le module PowerShell n'a pas renvoyé les index/noms, les récupérer avec DISM.
|
||||
# If the PowerShell module did not return indexes/names, retrieve them with DISM.
|
||||
if ($normalized.Count -eq 0) {
|
||||
$output = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "DISM n'a pas pu lire l'image : $ImagePath`n$($output -join "`n")"
|
||||
throw "DISM could not read the image: $ImagePath`n$($output -join "`n")"
|
||||
}
|
||||
|
||||
$current = $null
|
||||
@@ -168,17 +168,17 @@ function Get-WindowsImageEditions {
|
||||
}
|
||||
|
||||
if ($normalized.Count -eq 0) {
|
||||
throw "DISM n’a retourné aucune édition exploitable pour : $ImagePath"
|
||||
throw "DISM returned no usable editions for: $ImagePath"
|
||||
}
|
||||
|
||||
# Interroger DISM pour chaque index même si Get-WindowsImage a fourni les noms :
|
||||
# selon la version du module, les propriétés Architecture/Version peuvent être absentes.
|
||||
# Query DISM for every index even if Get-WindowsImage returned the names:
|
||||
# Architecture/Version properties may be missing in some module versions.
|
||||
foreach ($edition in $normalized) {
|
||||
if ($edition.Architecture -and $edition.Version) { continue }
|
||||
|
||||
$detailOutput = & $dism /English /Get-WimInfo "/WimFile:$ImagePath" "/Index:$($edition.Index)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
if ($LogAction) { & $LogAction "DISM n'a pas pu lire les metadonnees de l'index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' }
|
||||
if ($LogAction) { & $LogAction "DISM could not read metadata for index $($edition.Index) : $($detailOutput -join ' ')" 'WARNING' }
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ function Get-WindowsImageEditions {
|
||||
$insideImageDetails = $true
|
||||
continue
|
||||
}
|
||||
# Ignorer la version de DISM affichée au début de sa sortie.
|
||||
# Ignore the DISM version printed at the start of its output.
|
||||
if (-not $insideImageDetails) { continue }
|
||||
|
||||
if (-not $edition.Architecture -and $detailText -match '^\s*Architecture\s*:\s*(.*?)\s*$') {
|
||||
@@ -215,8 +215,8 @@ function Get-WindowsImageEditions {
|
||||
}
|
||||
}
|
||||
|
||||
# Version globale de l'image : ne l'afficher que si les métadonnées des index
|
||||
# donnent une seule version cohérente. Ne pas répéter cette valeur sur chaque ligne.
|
||||
# Overall image version: show it only if the index metadata
|
||||
# reports one consistent version. Do not repeat it on every row.
|
||||
$versionValues = @($normalized | ForEach-Object {
|
||||
if (-not [string]::IsNullOrWhiteSpace([string]$_.Version)) { [string]$_.Version }
|
||||
} | Select-Object -Unique)
|
||||
@@ -259,7 +259,7 @@ function Resolve-InstallImagePath {
|
||||
return [pscustomobject]@{ SourcePath = $SelectedPath; ImagePath = $candidate; MountedImage = $null }
|
||||
}
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd dans : $SelectedPath"
|
||||
throw "No Sources\install.wim or Sources\install.esd file in: $SelectedPath"
|
||||
}
|
||||
|
||||
if ([IO.Path]::GetExtension($SelectedPath) -ieq '.iso') {
|
||||
@@ -269,9 +269,9 @@ function Resolve-InstallImagePath {
|
||||
return $ExistingImage
|
||||
}
|
||||
if (-not (Get-Command Mount-DiskImage -ErrorAction SilentlyContinue)) {
|
||||
throw "Le montage ISO n'est pas disponible dans cet environnement (commande Mount-DiskImage absente). Pour analyser cette source, utilise le dossier extrait ou un fichier install.wim/esd."
|
||||
throw "ISO mounting is unavailable in this environment (Mount-DiskImage command not found). Use an extracted folder or an install.wim/esd file to analyze this source."
|
||||
}
|
||||
# Ne pas laisser Windows attribuer une lettre arbitraire (souvent D:) à l'ISO.
|
||||
# Do not let Windows assign an arbitrary drive letter (often D:) to the ISO.
|
||||
$diskImage = Mount-DiskImage -ImagePath $SelectedPath -Access ReadOnly -NoDriveLetter -PassThru -ErrorAction Stop
|
||||
$mountInfo = [pscustomobject]@{
|
||||
SourcePath = $SelectedPath
|
||||
@@ -281,10 +281,10 @@ function Resolve-InstallImagePath {
|
||||
DriveLetter = $null
|
||||
}
|
||||
try {
|
||||
# Partir des volumes explicitement associés à cette image évite de
|
||||
# confondre le lecteur CDROM virtuel et un disque physique dans WinPE.
|
||||
# Start with the volumes explicitly associated with this image to avoid
|
||||
# confusing the virtual CD-ROM drive with a physical disk in WinPE.
|
||||
$volumes = @(Get-Volume -DiskImage $diskImage -ErrorAction Stop)
|
||||
if ($volumes.Count -eq 0) { throw 'Aucun volume ISO expose par Windows.' }
|
||||
if ($volumes.Count -eq 0) { throw 'Windows did not expose any ISO volumes.' }
|
||||
$usedLetters = @(
|
||||
Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | ForEach-Object { ([string]$_.DriveLetter).ToUpperInvariant() }
|
||||
Get-PSDrive -PSProvider FileSystem -ErrorAction SilentlyContinue | ForEach-Object { ([string]$_.Name).ToUpperInvariant() }
|
||||
@@ -296,7 +296,7 @@ function Resolve-InstallImagePath {
|
||||
break
|
||||
}
|
||||
}
|
||||
if (-not $availableLetter) { throw 'Aucune lettre de lecteur temporaire disponible pour le montage ISO.' }
|
||||
if (-not $availableLetter) { throw 'No temporary drive letter is available to mount the ISO.' }
|
||||
|
||||
$checkedPaths = @()
|
||||
foreach ($volume in $volumes) {
|
||||
@@ -319,7 +319,7 @@ function Resolve-InstallImagePath {
|
||||
$mountVolCommand = Join-Path $env:WINDIR 'System32\mountvol.exe'
|
||||
if (-not (Test-Path -LiteralPath $mountVolCommand -PathType Leaf)) { $mountVolCommand = 'mountvol.exe' }
|
||||
$removeOutput = & $mountVolCommand 'D:\' '/D' 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { throw "Impossible de libérer D: pour le volume ISO : $($removeOutput -join ' ')" }
|
||||
if ($LASTEXITCODE -ne 0) { throw "Could not release D: for the ISO volume: $($removeOutput -join ' ')" }
|
||||
}
|
||||
$mountInfo.ImagePath = $candidate
|
||||
return $mountInfo
|
||||
@@ -358,14 +358,14 @@ function Resolve-InstallImagePath {
|
||||
$mountInfo.MountedPartition = $null
|
||||
$mountInfo.DriveLetter = $null
|
||||
}
|
||||
throw "Aucun fichier Sources\install.wim ou Sources\install.esd trouvé dans l'ISO. Chemins vérifiés : $($checkedPaths -join ', ')."
|
||||
throw "No Sources\install.wim or Sources\install.esd file was found in the ISO. Checked paths: $($checkedPaths -join ', ')."
|
||||
} catch {
|
||||
try { [void](Dismount-DeployCenterInstallImage -Image $mountInfo -ErrorAction SilentlyContinue) } catch { }
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
throw "Type de source non pris en charge pour l'analyse : $SelectedPath"
|
||||
throw "Unsupported source type for analysis: $SelectedPath"
|
||||
}
|
||||
|
||||
function Dismount-DeployCenterInstallImage {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@{
|
||||
Id = 'UEFI-GPT-WinRE-C-D'
|
||||
Name = 'UEFI / GPT — WinRE, C: et D:'
|
||||
Description = "Crée EFI, MSR et WinRE, puis C: selon la taille choisie et D: avec l’espace restant."
|
||||
Name = 'UEFI / GPT — WinRE, C: and D:'
|
||||
Description = "Creates EFI, MSR, and WinRE partitions, then C: at the selected size and D: with the remaining space."
|
||||
Default = $false
|
||||
Firmware = 'UEFI'
|
||||
PartitionStyle = 'GPT'
|
||||
@@ -35,7 +35,7 @@
|
||||
GptTypeId = 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'
|
||||
DriveLetter = 'C'
|
||||
DriveLetterAfterReboot = 'C'
|
||||
Size = @{ Policy = 'UserInputGB'; MinimumGB = 40; DefaultPercent = 60; Label = 'Taille de C:' }
|
||||
Size = @{ Policy = 'UserInputGB'; MinimumGB = 40; DefaultPercent = 60; Label = 'C: size' }
|
||||
}
|
||||
@{
|
||||
Name = 'Data'
|
||||
@@ -44,7 +44,7 @@
|
||||
GptTypeId = 'EBD0A0A2-B9E5-4433-87C0-68B6B72699C7'
|
||||
DriveLetter = 'D'
|
||||
DriveLetterAfterReboot = 'D'
|
||||
Size = @{ Policy = 'RemainderAfterInputs'; MinimumGB = 1; Label = 'Taille de D: (espace restant)' }
|
||||
Size = @{ Policy = 'RemainderAfterInputs'; MinimumGB = 1; Label = 'D: size (remaining space)' }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
@{
|
||||
Id = 'UEFI-GPT-WinRE-before-C'
|
||||
Name = 'UEFI / GPT — WinRE avant C:'
|
||||
Description = "Crée EFI, MSR et WinRE avant la partition Windows qui occupe l’espace restant."
|
||||
Name = 'UEFI / GPT — WinRE before C:'
|
||||
Description = "Creates EFI, MSR, and WinRE partitions before the Windows partition, which uses the remaining space."
|
||||
Default = $true
|
||||
Firmware = 'UEFI'
|
||||
PartitionStyle = 'GPT'
|
||||
|
||||
@@ -1,239 +1,187 @@
|
||||
# DeployCenter — première version de la réécriture
|
||||
# DeployCenter
|
||||
|
||||
## Contenu
|
||||
DeployCenter est un outil PowerShell de préparation et de déploiement de Windows depuis Windows PE (WinPE). Il détecte le matériel, analyse les sources Windows disponibles, propose une édition et un schéma de partitionnement, puis applique Windows et configure le démarrage UEFI et l’environnement de récupération WinRE.
|
||||
|
||||
- `DeployCenter.ps1` : point d'entrée et logique applicative existante.
|
||||
- `Modules/ThemeManager.psm1` : découverte, chargement et validation des thèmes PSD1, avec prise en charge des rôles de boutons.
|
||||
- `Modules/UI.psm1` : contrôles et fenêtres d’interface, construits selon le thème actif.
|
||||
- `Modules/Deployment.psm1` : partitionnement UEFI/GPT et installation de l’édition Windows sélectionnée.
|
||||
- `Modules/Unattend.psm1` : découverte et validation des profils de réponse Windows, puis génération sécurisée du XML.
|
||||
- `Themes/Light.psd1` et `Themes/Dark.psd1` : palettes et styles des boutons.
|
||||
L’interface est localisée selon la langue d’interface de WinPE (français ou anglais). Les thèmes, profils de partitionnement et profils Unattend sont chargés depuis des fichiers de données afin de faciliter leur personnalisation.
|
||||
|
||||
## Lancement
|
||||
> **Attention :** l’installation efface entièrement le disque physique sélectionné. Vérifiez soigneusement le numéro, le modèle et la capacité du disque avant de confirmer. Une erreur de sélection entraîne la perte de ses données.
|
||||
|
||||
Depuis le dossier DeployCenter, lancer :
|
||||
## Fonctionnalités
|
||||
|
||||
- Inventaire matériel et informations sur les disques physiques.
|
||||
- Recherche de sources Windows sur les chemins configurés, avec prise en charge des images ISO, WIM et ESD ainsi que des sources extraites.
|
||||
- Sélection de l’édition Windows contenue dans une image.
|
||||
- Profils de partitionnement UEFI/GPT avec WinRE avant C:, avec ou sans partition D:.
|
||||
- Installation de pilotes INF correspondant au constructeur, au modèle/SKU et à la version de Windows.
|
||||
- Génération facultative d’un fichier de réponses Windows (Unattend), notamment pour configurer le nom d’hôte et un compte local.
|
||||
- Configuration UEFI, copie de `winre.wim`, enregistrement et activation de WinRE.
|
||||
- Progression du déploiement, journalisation et copie du journal à la racine de C: après une tentative d’installation.
|
||||
- Mode interactif ou déploiement automatisé en ligne de commande.
|
||||
|
||||
## Prérequis et fichiers
|
||||
|
||||
DeployCenter est conçu pour fonctionner dans WinPE avec Windows PowerShell 5.1 et les outils Windows nécessaires au déploiement : DISM, DiskPart, `mountvol`, `bcdboot`, `reagentc`, Robocopy et les cmdlets de gestion du stockage. Les images, les partages et les pilotes doivent être accessibles depuis WinPE.
|
||||
|
||||
Conservez la structure du dépôt lors de la copie dans l’environnement WinPE :
|
||||
|
||||
```text
|
||||
DeployCenter/
|
||||
├── DeployCenter.ps1
|
||||
├── DeployCenter.ini.example
|
||||
├── HardwareInfo.ps1
|
||||
├── Assets/
|
||||
├── Locales/
|
||||
├── Modules/
|
||||
├── Partitioning/Profiles/
|
||||
├── Themes/
|
||||
└── Unattend/
|
||||
├── Profiles/
|
||||
└── Templates/
|
||||
```
|
||||
|
||||
Les dossiers de modules, thèmes, profils, modèles XML, langues et ressources graphiques sont requis pour les fonctionnalités correspondantes. Le dossier `Drivers` est normalement placé au même niveau que le dossier `DeployCenter`.
|
||||
|
||||
## Installation et premier lancement
|
||||
|
||||
1. Copiez `DeployCenter.ini.example` sous le nom `DeployCenter.ini` dans le dossier DeployCenter.
|
||||
2. Modifiez les chemins de recherche des sources et les paramètres nécessaires. Le fichier INI est lu au démarrage et n’est jamais modifié par l’application.
|
||||
3. Démarrez WinPE, connectez les partages réseau nécessaires et lancez PowerShell dans le dossier DeployCenter.
|
||||
4. Exécutez :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1
|
||||
```
|
||||
|
||||
Pour lancer en fenêtre redimensionnable :
|
||||
Pour ouvrir l’interface dans une fenêtre redimensionnable, notamment pour le diagnostic :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -Windowed
|
||||
```
|
||||
|
||||
Conserver à côté du script les ressources habituelles du projet (`Assets`, `uvnc`, `HardwareInfo.ps1`, etc.). Les dossiers `Modules` et `Themes` doivent rester à côté de `DeployCenter.ps1`.
|
||||
UltraVNC est désactivé par défaut. L’option `-Vnc` le démarre au lancement ; dans ce cas, DeployCenter désactive le pare-feu WinPE afin de permettre la connexion VNC. Utilisez cette option uniquement sur un réseau de déploiement maîtrisé : la désactivation concerne le pare-feu WinPE dans son ensemble.
|
||||
|
||||
## Mise en page
|
||||
## Déploiement interactif
|
||||
|
||||
Le redimensionnement de la fenêtre recalcule désormais les largeurs des panneaux, la taille du tableau et du journal, ainsi que la position des boutons. Les marges et hauteurs des contrôles restent volontairement fixes pour préserver la lisibilité sous WinPE.
|
||||
1. Vérifiez les informations matérielles et sélectionnez le **disque physique de destination**.
|
||||
2. Cliquez sur **Choisir…** pour ouvrir Sources Windows. Les chemins à analyser sont séparés par des points-virgules ; **Rechercher** actualise les résultats. La liste peut être filtrée, et un double-clic sur une image permet également de choisir une édition.
|
||||
3. Sélectionnez l’édition Windows voulue. DeployCenter recherche alors, si disponible, un pack de pilotes correspondant au matériel et à cette édition.
|
||||
4. Choisissez un profil de partitionnement. Le profil par défaut réserve EFI, MSR et WinRE avant C:, qui reçoit l’espace restant. Le profil C:/D: demande la taille de C: et attribue le reste à D:.
|
||||
5. Choisissez un profil de première ouverture. **Use OOBE** laisse Windows gérer l’expérience de première ouverture sans générer de fichier Unattend ni demander de nom d’hôte. Le profil **OEM installation** configure le nom d’hôte et un compte local à partir de `DeployCenter.ini`.
|
||||
6. Cliquez sur **Installer**, relisez les informations de confirmation et confirmez uniquement si le disque choisi peut être effacé.
|
||||
7. Suivez les étapes dans la fenêtre de progression. Après réussite, vérifiez le résultat et le rappel éventuel concernant la taille de WinRE avant de valider le redémarrage.
|
||||
|
||||
## Architecture et validation
|
||||
L’installation nécessite une image UEFI/GPT compatible et contenant `Windows\System32\Recovery\winre.wim`. Le moteur actuel ne propose pas de profil MBR/BIOS. Un manque de place empêchant la copie de `winre.wim` fait échouer le déploiement ; le non-respect des recommandations Microsoft de taille ou d’espace libre WinRE déclenche un avertissement. En présence de cet avertissement, le redémarrage automatique n’est pas présélectionné.
|
||||
|
||||
`DeployCenter.ps1` orchestre les modules : `UI.psm1` porte les workflows et contrôles d’interface dans le scope de session partagé, `ThemeManager.psm1` charge les palettes, `HardwareInventory.psm1` collecte et exporte les informations matérielles, `Partitioning.psm1` valide les profils et calcule les capacités, `WindowsSources.psm1` analyse les images Windows, et `Deployment.psm1` applique le profil puis déploie l’image.
|
||||
## Sources Windows
|
||||
|
||||
La syntaxe PowerShell a été vérifiée statiquement. Le déroulement DISM/DiskPart/reagentc et le démarrage UEFI restent à valider sur la machine WinPE cible avant le premier déploiement réel.
|
||||
|
||||
|
||||
## Mot de passe UltraVNC de test
|
||||
|
||||
Dans `DeployCenter.ini`, ajoutez la section suivante. Un champ `Password` non vide est utilisé tel quel au lieu de générer un mot de passe aléatoire ; `createpassword64.exe` continue à le convertir dans le format attendu par UltraVNC. Laissez `Password=` vide pour conserver le comportement par défaut.
|
||||
Dans `DeployCenter.ini`, la section `[Sources]` accepte plusieurs racines séparées par des points-virgules. `%DEPLOY_VOLUME%` désigne le lecteur qui héberge DeployCenter :
|
||||
|
||||
```ini
|
||||
[UltraVNC]
|
||||
Password=MonMotDePasseDeTest
|
||||
[Sources]
|
||||
Paths=%DEPLOY_VOLUME%;Z:\
|
||||
```
|
||||
|
||||
Le mot de passe est stocké en clair dans `DeployCenter.ini` : utilisez cette option uniquement dans un environnement de test protégé.
|
||||
DeployCenter recherche les fichiers ISO, WIM et ESD ainsi que les dossiers de sources extraits. Pour les ISO, le fichier est monté en lecture seule pendant l’analyse et le déploiement, puis démonté lorsque l’opération se termine ou est annulée.
|
||||
|
||||
## Taille de la partition WinRE
|
||||
Par défaut, DISM travaille directement depuis les chemins source. En mode automatisé, le paramètre `-CopyLocally` permet de demander une copie Robocopy vers le disque Windows avant le traitement : `Sources` pour l’image, `Drivers` pour le pack de pilotes, ou les deux. Cela nécessite suffisamment d’espace libre sur la partition Windows. Le paramètre absent n’effectue pas ces copies. La copie de `winre.wim` vers la partition de récupération reste nécessaire dans tous les cas.
|
||||
|
||||
La taille configurée par défaut est de 1 Go (1024 Mo). Elle peut être remplacée dans `DeployCenter.ini`, sous `[Partitioning]`, par `WinRESizeMB=1536` par exemple. La valeur doit être un entier strictement positif. Après l’application de Windows, DeployCenter vérifie la taille réelle de `winre.wim` : le déploiement échoue seulement si l’espace libre ne permet pas de copier le fichier. Une partition plus petite que 990 Mo ou un espace libre après copie inférieur à 250 Mo génère un avertissement, puis le déploiement continue.
|
||||
## Pilotes
|
||||
|
||||
## Paramètres Unattend facultatifs
|
||||
Par défaut, la racine des packs est `Drivers` au même niveau que `DeployCenter`. Elle peut être redéfinie par `[Drivers] RootPath` dans le fichier INI. Les chemins relatifs sont résolus depuis le dossier DeployCenter.
|
||||
|
||||
Sous `[Unattend]`, `SupportProvider` définit le nom de fournisseur de support et `SupportAppURL` l’URI de l’application ou du site de support dans `OEMInformation` du fichier de réponses. Ces clés sont facultatives ; chaque élément vide ou absent est omis indépendamment. `WallpaperPath` désigne l’image `.png`, `.jpg` ou `.bmp` à utiliser dans le thème Windows par défaut ; le chemin peut être absolu ou relatif au dossier DeployCenter. `Assets\blorand.png` est la valeur par défaut et une valeur vide laisse le fond Windows inchangé. Le fond est appliqué aux déploiements avec profil Unattend ; « Avec OOBE » reste sans fichier Unattend. Le template `InstallationOEM.xml` utilise `fr-FR` pour `InputLocale`.
|
||||
Organisez les packs de cette manière :
|
||||
|
||||
## Historique récent
|
||||
```text
|
||||
Drivers/
|
||||
└── Lenovo/
|
||||
└── <préfixe du modèle ou SKU>/
|
||||
└── Windows 11/
|
||||
├── ...
|
||||
└── pilote.inf
|
||||
```
|
||||
|
||||
- La description du profil PSD1 `InstallationOEM` évite l’apostrophe typographique dans une chaîne entre apostrophes, pour rester analysable sous Windows PowerShell 5.1.
|
||||
- Le profil Unattend `LocalAdmin` a été renommé `InstallationOEM` : son ID, son fichier PSD1, son template XML et sa description sont alignés sur son rôle.
|
||||
- Le profil affiché sous le nom « Installation OEM » prépare le nom d’hôte, le compte local et les options OEM. À la fin du déploiement, les avertissements de recommandations WinRE sont rappelés ; la case de redémarrage automatique est décochée s’il y en a.
|
||||
- Correction de la chaîne d’erreur de validation du chemin du fond d’écran dans `Deployment.psm1` : elle est délimitée par des guillemets doubles pour éviter toute ambiguïté d’apostrophe dans Windows PowerShell 5.1.
|
||||
- L’image `Assets/blorand.png` devient le fond du thème Windows par défaut pour les nouveaux utilisateurs des déploiements utilisant un profil Unattend. `[Unattend] WallpaperPath` accepte un chemin absolu ou relatif au dossier DeployCenter ; une valeur vide désactive cette personnalisation. Le mode « Avec OOBE » reste sans fichier Unattend et conserve donc le fond Windows.
|
||||
- `DeployCenter.ini.example` documente désormais toutes les options reconnues par `Configuration.psm1`, y compris `SupportProvider` et `SupportAppURL` sous forme de clés explicites vides par défaut.
|
||||
- Le profil `InstallationOEM` utilise `fr-FR` pour `InputLocale`. Les clés facultatives `[Unattend] SupportProvider` et `SupportAppURL` ajoutent indépendamment le fournisseur et l’URI de support à `OEMInformation` ; chaque élément est omis lorsque sa clé est absente ou vide.
|
||||
- Le `clean` et la création des partitions sont exécutés en deux étapes DiskPart ; `Update-HostStorageCache` actualise les objets Storage entre les deux, puis après le partitionnement, les retraits de lettres et le `mountvol /D` de la source, avant les contrôles concernés. Si le cmdlet est indisponible dans WinPE, le déploiement continue avec un avertissement.
|
||||
- Le choix « Avec OOBE » ne génère ni n’applique de fichier de réponses et masque la saisie du nom d’hôte. Les profils `Unattend/Profiles/*.psd1` alimentent le menu de la page principale ; le profil `InstallationOEM` configure le nom de machine et crée un compte local de la configuration `[Unattend]`, sans activer le compte intégré Administrator.
|
||||
- Chaque profil Unattend déclare `Target = 'Workstation'` ou `Target = 'Server'`. Le menu propose les profils correspondant à l’édition retenue (le nom contenant « Server » sélectionne la catégorie serveur) ; avant le choix d’une édition, seul « Avec OOBE » est disponible.
|
||||
- L’indication « Environnement : WinPE » a été retirée de l’écran principal. Le bloc d’informations PC est moins haut et le bloc de préparation est remonté/compacté afin de dégager les boutons d’action sur les petites résolutions.
|
||||
- Le template Workstation renseigne les paramètres régionaux français et répond aux pages OOBE de licence, inscription OEM, compte en ligne, réseau sans fil et paramètres express. Le compte local est fourni dans `UserAccounts`; les paramètres `SkipMachineOOBE` et `SkipUserOOBE` ne sont pas utilisés.
|
||||
- À la fin d’un déploiement réussi, la boîte de confirmation propose le redémarrage WinPE avec une case cochée par défaut. Si elle reste cochée, DeployCenter ferme ses ressources, arrête VNC, démonte l’image source puis demande le redémarrage via `wpeutil`.
|
||||
- Le libellé « Configuration de première ouverture » a été raccourci d’un pixel en hauteur pour ne plus recouvrir le bord supérieur de sa liste déroulante.
|
||||
- Compatibilité Windows PowerShell 5.1 : les chaînes de diagnostic de `Unattend.psm1` évitent les apostrophes intégrées et le module est enregistré en UTF-8 avec BOM.
|
||||
- Le groupe local du compte est configuré avec le nom neutre `Administrators`, que Windows traduit selon la langue de l’image ; cela évite de dépendre du libellé français `Administrateurs`.
|
||||
- Les réponses `specialize` et `oobeSystem` sont placées dans `Windows\Panther\Unattend\Unattend.xml` pour être traitées par Windows Setup au premier démarrage. Le mot de passe du compte local provient de `AdminPasswordBase64` (Base64 UTF-16LE, réversible) et n’est jamais ajouté au journal.
|
||||
- La vérification du retrait des lettres EFI/WinRE s’appuie sur l’existence réelle des chemins de lecteur : dans WinPE, `Get-Partition` peut conserver une lettre Recovery dans ses métadonnées alors que `mountvol` et le système de fichiers confirment son retrait.
|
||||
- La lecture du magasin BCD accepte le libellé français `identificateur` de `bcdedit` en plus du libellé anglais `identifier`, pour retrouver l’entrée Windows hors ligne dans un WinPE francophone.
|
||||
- Correction du pipeline de diagnostic des lettres EFI/WinRE pour rester compatible avec le parseur Windows PowerShell 5.1.
|
||||
- DiskPart attribue la lettre temporaire de WinRE avant d’appliquer son type Recovery et ses attributs GPT, conformément à l’ordre des exemples Microsoft. En fin de déploiement réussi, la lettre est retirée en sélectionnant explicitement la partition ; `Get-Partition` peut ne pas exposer sa lettre dans WinPE.
|
||||
- Correction du texte de diagnostic WinRE pour éviter une apostrophe typographique dans une chaîne délimitée par apostrophes simples sous Windows PowerShell 5.1.
|
||||
- L’activation hors ligne de WinRE utilise désormais le GUID de l’entrée Windows relevé dans le magasin BCD créé par `bcdboot`, conformément aux paramètres REAgentC disponibles dans WinPE.
|
||||
- Le suivi de progression avec `Start-Process` est réservé à `dism.exe /Apply-Image`. Les autres commandes natives, y compris DISM hors application d’image, sont invoquées directement, leurs sorties sont journalisées et leur code est lu dans `$LASTEXITCODE`.
|
||||
- En fin de déploiement réussi, seules les lettres temporaires des partitions EFI et WinRE sont retirées. Les lettres Windows et Data définies par le profil restent attribuées ; en cas d’échec, le nettoyage ne retire aucune lettre pour préserver l’état utile au diagnostic.
|
||||
- Correction du retour du plan de partitionnement : conversion explicite de la liste générique en tableau pour éviter une erreur de types sous Windows PowerShell 5.1 avant l’exécution de DiskPart.
|
||||
- Sous certains WinPE, le code de sortie de DiskPart peut être absent malgré une exécution réussie. DeployCenter confirme désormais le partitionnement et le retrait des lettres en relisant l’état des partitions.
|
||||
- Remplacement de l’apostrophe typographique de « d’hôte » par une apostrophe ASCII simple dans le message de validation NetBIOS de `Deployment.psm1`.
|
||||
- Correction de la chaîne du journal DiskPart : les apostrophes typographiques dans une chaîne PowerShell délimitée par apostrophes simples provoquaient une erreur de syntaxe signalée plus loin dans le fichier.
|
||||
- Vérification des lettres de partition par requête directe `Get-Partition -DriveLetter`, puis contrôle du numéro de disque, afin de ne pas dépendre des lettres présentes dans l’instantané retourné par `Get-Partition -DiskNumber` sous WinPE.
|
||||
- La partition Recovery, marquée cachée par ses attributs GPT, peut ne pas être exposée par `Get-Partition -DriveLetter`. DeployCenter l’associe alors à sa position dans les partitions fraîchement créées par DiskPart.
|
||||
- Les partitions EFI et Recovery pouvant être absentes des requêtes `Get-Partition -DriveLetter` dans WinPE, l’association des rôles suit désormais l’ordre des partitions créées et vérifie l’accès effectif à chaque lettre temporaire.
|
||||
- Retrait de l’ajout automatique de `/ScratchDir` aux commandes DISM afin de tester le comportement sans cette redirection dans WinPE ; le journal DISM dédié est conservé.
|
||||
- La progression de l’application d’image lit maintenant les pourcentages émis par DISM pendant son exécution et les transmet à la barre WinForms. Si le code de sortie DISM est absent, le message de réussite explicite est aussi pris en compte.
|
||||
- Le pourcentage DISM est aussi affiché dans le libellé de l’étape de progression, sans ajouter de ligne de journal à chaque mise à jour.
|
||||
- Vérification de l’espace WinRE via `System.IO.DriveInfo` : `Get-Volume -DriveLetter` ne trouve pas toujours les volumes de récupération cachés dans WinPE, même lorsqu’ils sont montés et accessibles.
|
||||
- Le déploiement échoue uniquement si l’espace libre est insuffisant pour copier `winre.wim`. Les recommandations Microsoft de 990 Mo pour la partition et 250 Mo libres après copie produisent un avertissement et n’interrompent pas l’installation.
|
||||
- Sous certains WinPE, `bcdboot` peut confirmer la création des fichiers de démarrage sans que `Start-Process` fournisse son code de sortie. DeployCenter accepte alors le résultat si la sortie confirme la création ou si le magasin BCD est présent sur la partition EFI.
|
||||
- Correction du message de confirmation `bcdboot` et simplification des messages natifs ; les erreurs de parseur signalées plus loin étaient des conséquences de cette chaîne non terminée.
|
||||
- Rétablissement du BOM UTF-8 de `Deployment.psm1`, requis pour que Windows PowerShell 5.1 lise correctement les caractères accentués dans WinPE.
|
||||
- Décodage des sorties natives avec l’encodage de la console WinPE pour préserver les accents ; REAgentC est désormais recherché dans l’image Windows hors ligne avant WinPE.
|
||||
- Recherche de REAgentC reportée après l’application de Windows, lorsque `C:\Windows\System32\reagentc.exe` existe effectivement.
|
||||
- La configuration `WinRESizeMB` et les profils acceptent désormais toute taille strictement positive ; le seuil recommandé de 990 Mo est évalué après application de Windows comme avertissement, sans bloquer la configuration.
|
||||
Les constructeurs reconnus sont Lenovo, Dell, HP et Microsoft. Les dossiers de version reconnus incluent `Windows 10`, `Windows 11` et `Windows Server <année>`. Les fichiers INF peuvent se trouver dans des sous-dossiers ; DISM les ajoute récursivement à l’image Windows hors ligne. L’écran principal affiche le chemin recherché et indique si un pack correspondant a été trouvé.
|
||||
|
||||
## Configuration de DeployCenter.ini
|
||||
|
||||
### Correctifs v3
|
||||
- En-têtes du tableau des éditions Windows avec une couleur dédiée définie par le thème (`TableHeader`).
|
||||
- Sélection unique explicite dans la liste des éditions et dessin de la sélection basé sur l'élément réellement sélectionné.
|
||||
`DeployCenter.ini.example` décrit toutes les options et leurs valeurs par défaut. Les principales sections sont :
|
||||
|
||||
### Correctif v12 — pied de page
|
||||
- La fenêtre principale affiche l'adresse IP de déploiement et le mot de passe UltraVNC dans son pied de page.
|
||||
- Le pied de page commun reste activé par défaut ; le paramètre `-ShowFooter $false` permet de le masquer dans la fenêtre « Éditions Windows ». Les autres fenêtres conservent leur comportement habituel.
|
||||
- `[General] Theme` : thème chargé depuis `Themes/<nom>.psd1` (Light par défaut).
|
||||
- `[Sources] Paths` : racines de recherche séparées par des points-virgules.
|
||||
- `[Partitioning] WinRESizeMB` : taille réservée à WinRE en Mo (1024 Mo par défaut).
|
||||
- `[Drivers] RootPath` : racine personnalisée des pilotes, vide pour utiliser le dossier `Drivers` voisin ; les clés URL sont des liens de référence constructeur.
|
||||
- `[Unattend]` : compte local, mot de passe encodé, informations d’assistance et fond d’écran.
|
||||
- `[UltraVNC] Password` : mot de passe de test facultatif ; vide, un mot de passe aléatoire est généré.
|
||||
|
||||
### Correctif v15 — pied de page cohérent
|
||||
- Le pied de page commun affiche par défaut l’adresse IP et le mot de passe VNC dans toutes les fenêtres qui l’utilisent.
|
||||
- Seule la fenêtre de sélection « Éditions Windows » masque entièrement le pied de page via `-ShowFooter $false`.
|
||||
Le mot de passe d’un compte local Unattend se prépare avec :
|
||||
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -EncodeUnattendPassword
|
||||
```
|
||||
|
||||
### Alternance des lignes des tableaux
|
||||
Saisissez le mot de passe à l’invite masquée, puis placez la valeur produite dans `[Unattend] AdminPasswordBase64`. Cette valeur Base64 est réversible et ne constitue pas un chiffrement ; protégez `DeployCenter.ini` en conséquence. Ne confondez pas ce réglage avec `[UltraVNC] Password`, qui est une option de test distincte.
|
||||
|
||||
Les tableaux utilisent une alternance paire/impaire définie dans chaque thème (`TableRowEven` et `TableRowOdd`). La sélection conserve la couleur d’accent du thème. Les valeurs sont partagées par les fenêtres Sources Windows et Éditions Windows.
|
||||
Les clés `SupportProvider`, `SupportURL` et `SupportAppURL` sont facultatives. `SupportURL` correspond à un site Web d’assistance ; `SupportAppURL` correspond au protocole d’une application d’assistance. `WallpaperPath` accepte un chemin `.png`, `.jpg` ou `.bmp`, absolu ou relatif au dossier DeployCenter ; vide, il laisse le fond Windows inchangé. Ces personnalisations sont appliquées avec un profil Unattend, pas avec **Use OOBE**.
|
||||
|
||||
## Déploiement automatisé
|
||||
|
||||
## Paramètre de démarrage VNC (v18)
|
||||
`-NonInteractive` lance le déploiement sans demander les choix de la première page. Tous les paramètres requis doivent être fournis et `-ConfirmDiskErase` est obligatoire. L’application affiche toujours la fenêtre de progression.
|
||||
|
||||
- Sans paramètre : UltraVNC ne démarre pas.
|
||||
- Avec `-vnc` : UltraVNC démarre.
|
||||
Exemple pour Windows 11 sur le disque 0, avec le profil C:/D: et le profil Unattend OEM :
|
||||
|
||||
Exemple : `powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 -vnc`
|
||||
```powershell
|
||||
powershell.exe -ExecutionPolicy Bypass -File .\DeployCenter.ps1 `
|
||||
-NonInteractive -DiskNumber 0 `
|
||||
-ImagePath 'Y:\Windows11.iso' -EditionIndex 6 `
|
||||
-PartitionProfileId 'UEFI-GPT-WinRE-C-D' -CSizeGB 80 `
|
||||
-UnattendProfileId 'OEMInstallation' -HostName 'POSTE-001' `
|
||||
-ConfirmDiskErase -RestartWhenComplete `
|
||||
-CopyLocally Sources,Drivers
|
||||
```
|
||||
|
||||
Paramètres requis :
|
||||
|
||||
## Options de lancement
|
||||
- `-DiskNumber` : numéro du disque physique à effacer.
|
||||
- `-ImagePath` : chemin d’un ISO, WIM, ESD ou d’un dossier de sources Windows.
|
||||
- `-EditionIndex` : index de l’édition dans l’image.
|
||||
- `-PartitionProfileId` : ID d’un fichier PSD1 dans `Partitioning/Profiles`.
|
||||
- `-UnattendProfileId` : ID d’un profil dans `Unattend/Profiles`, ou `OOBE` pour ne pas appliquer d’Unattend.
|
||||
- `-ConfirmDiskErase` : confirmation explicite de l’effacement du disque.
|
||||
|
||||
- Sans paramètre : UltraVNC n'est pas démarré.
|
||||
- Avec `-Vnc` (alias PowerShell utilisable sous la forme `-vnc`) : UltraVNC est démarré.
|
||||
- Les fenêtres sont non redimensionnables par défaut via `Set-DeployCenterFormSizing`; passer `-Resizable` à cette fonction commune pour autoriser le redimensionnement.
|
||||
Paramètres conditionnels et facultatifs :
|
||||
|
||||
- `-HostName` : requis lorsqu’un profil Unattend configure le nom de machine ; le nom doit respecter la limite NetBIOS de 15 caractères.
|
||||
- `-CSizeGB` : requis par un profil qui demande la taille de C: ; le profil C:/D: attribue le reste à D:.
|
||||
- `-RestartWhenComplete` : demande le redémarrage après réussite ; les avertissements de capacité WinRE annulent cette demande.
|
||||
- `-CopyLocally Sources,Drivers` : copie l’image et les pilotes localement. Les valeurs possibles sont `Sources` et `Drivers`, individuellement ou ensemble. Sans ce paramètre, DISM utilise directement les chemins source.
|
||||
|
||||
## Module VNC
|
||||
`-CopyLocally` est un paramètre de déploiement : il s’utilise avec `-NonInteractive`. Pour laisser Windows gérer la première ouverture, utilisez `-UnattendProfileId OOBE` et omettez `-HostName`. Le processus retourne `0` en cas de réussite et `1` si la préparation ou le déploiement échoue. Après une tentative d’installation, le journal est également copié à la racine de `C:\`.
|
||||
|
||||
La gestion d'UltraVNC est isolée dans `Modules/VNC.psm1`. Le fichier est chargé par dot-sourcing uniquement lorsque `-Vnc` est fourni, afin de conserver le contexte partagé de DeployCenter.
|
||||
## Journaux et dépannage
|
||||
|
||||
- Sans `-Vnc`, UltraVNC ne démarre pas et DeployCenter ne demande pas la désactivation du pare-feu.
|
||||
- Avec `-Vnc`, le module VNC exécute `wpeutil DisableFirewall` avant de lancer UltraVNC. Cette commande désactive le pare-feu WinPE dans son ensemble ; utilisez cette option sur un réseau de déploiement maîtrisé.
|
||||
- Le démarrage VNC et les erreurs sont consignés dans le journal DeployCenter.
|
||||
Le bouton **Journal** ouvre le journal de session. Les fichiers sont créés dans `Logs` avec l’adresse IPv4 détectée et l’horodatage. Après une tentative de déploiement, une copie du journal est placée à la racine de `C:\` pour rester accessible même si le partage réseau n’est plus disponible.
|
||||
|
||||
En cas d’échec :
|
||||
|
||||
## Modules
|
||||
1. Relevez le message final et l’étape en échec dans la fenêtre de progression.
|
||||
2. Consultez le journal DeployCenter et, si nécessaire, les journaux système DISM/Windows Setup sur la machine cible.
|
||||
3. Vérifiez l’accès réseau aux sources, l’index d’édition, l’espace du disque cible et la présence des fichiers INF attendus.
|
||||
4. Après un échec de partitionnement, les lettres et partitions sont conservées autant que possible pour faciliter le diagnostic. Ne relancez pas l’installation avant d’avoir vérifié le disque sélectionné et son état.
|
||||
|
||||
- `Modules/Network.psm1` : détection de l’adresse IPv4 de déploiement, utilisée par le pied de page et le nom du journal.
|
||||
- `Modules/Logging.psm1` : initialisation et écriture des journaux. L’adresse IPv4 détectée lui est transmise par le script principal.
|
||||
- `Modules/Configuration.psm1` : lecture de `DeployCenter.ini` en lecture seule.
|
||||
- `Modules/ThemeManager.psm1` : chargement et validation des palettes déclarées dans `Themes/*.psd1`.
|
||||
- `Modules/UI.psm1` : contrôles, boîtes de dialogue, fenêtres Sources Windows et workflows d’interface.
|
||||
- `Modules/HardwareInventory.psm1` : collecte matérielle, sélection d’un disque et export consolidé de l’inventaire.
|
||||
- `Modules/Partitioning.psm1` : découverte et validation des profils PSD1, calcul de capacité et construction de l’état de dimensionnement.
|
||||
- `Modules/Deployment.psm1` : orchestration du partitionnement et de l’installation Windows pour les profils UEFI/GPT.
|
||||
- `Modules/Unattend.psm1` : découverte des profils de réponses Windows, validation de leur configuration et génération du XML utilisé au premier démarrage.
|
||||
- `Modules/WindowsSources.psm1` : recherche de supports Windows et lecture des éditions WIM/ESD.
|
||||
Les messages de DeployCenter suivent la langue de WinPE. Les sorties DISM, DiskPart et des autres outils Windows restent dans la langue de ces outils.
|
||||
|
||||
## Profils de partitionnement
|
||||
## Modules et personnalisation
|
||||
|
||||
Les fichiers PSD1 de `Partitioning/Profiles` décrivent les schémas de partitionnement comme des données, sans exécuter de code contenu dans les profils. Le profil par défaut « UEFI / GPT — WinRE avant C: » crée EFI (300 Mo), MSR (16 Mo), WinRE (taille définie dans `DeployCenter.ini`, 1 Go par défaut), puis Windows sur l’espace restant. Le profil « UEFI / GPT — WinRE, C: et D: » demande la taille de C: et attribue le reste à D:, dans l’ordre physique C: puis D:. Les propriétés de taille déclarées dans le profil alimentent un formulaire générique.
|
||||
- `DeployCenter.ps1` : point d’entrée, initialisation et orchestration de l’interface.
|
||||
- `Modules/UI.psm1` : fenêtres, contrôles et workflows d’interface.
|
||||
- `Modules/Deployment.psm1` : partitionnement et application/configuration de Windows.
|
||||
- `Modules/WindowsSources.psm1` : détection des sources, montage ISO et lecture des éditions.
|
||||
- `Modules/Partitioning.psm1` et `Partitioning/Profiles/*.psd1` : lecture et validation des profils de partitionnement.
|
||||
- `Modules/DriverManagement.psm1` : recherche des packs selon le matériel et l’édition Windows.
|
||||
- `Modules/Unattend.psm1`, `Unattend/Profiles/*.psd1` et `Unattend/Templates/*.xml` : profils et génération du fichier de réponses.
|
||||
- `Modules/Configuration.psm1` : lecture de `DeployCenter.ini`.
|
||||
- `Modules/Logging.psm1` : journalisation et copie finale du journal.
|
||||
- `Modules/Localization.psm1` et `Locales/*.psd1` : langue de l’interface et des messages DeployCenter.
|
||||
- `Modules/ThemeManager.psm1` et `Themes/*.psd1` : palettes de l’interface.
|
||||
|
||||
Le bouton « Installer » demande une confirmation explicite avant d’effacer le disque sélectionné. Le flux vérifie le firmware, crée les partitions définies par le profil, applique l’image avec DISM, vérifie que WinRE tient dans sa partition, configure le nom d’hôte, crée les fichiers de démarrage UEFI et active WinRE. L’absence d’espace pour copier `winre.wim` interrompt le déploiement ; le non-respect des recommandations de taille et d’espace libre génère un avertissement. Les opérations longues affichent une fenêtre de progression et sont consignées dans le journal. Le moteur actuel prend en charge les profils UEFI/GPT et requiert une image contenant `Windows\System32\Recovery\winre.wim`.
|
||||
|
||||
|
||||
## Historique des changements
|
||||
|
||||
### 2026-10-17
|
||||
|
||||
- Correction de l’échappement des arguments des commandes natives Windows. Les chemins terminés par une barre oblique inverse, comme `W:\`, ne ferment plus prématurément leur guillemet ; cela permet à `mountvol` de recevoir correctement sa lettre et le chemin de volume.
|
||||
- L’affectation temporaire d’une lettre source vérifie l’accès effectif au fichier WIM après `mountvol`, afin de gérer les environnements WinPE qui ne remontent pas correctement son code de sortie.
|
||||
- Le point de montage DISM temporaire est créé à la racine du lecteur système WinPE plutôt que sous `%TEMP%\DeployCenter`, après l’échec d’accès DISM dans ce dernier emplacement ; la libération de la lettre source est confirmée par l’inaccessibilité du WIM après `mountvol /D`.
|
||||
- La taille de WinRE est lue en montant l’édition sélectionnée avec DISM en lecture seule, pour les sources `.wim` comme `.esd`, puis en démontant l’image sans modification. Cette méthode remplace l’extraction directe de `winre.wim`, qui remplissait le disque RAM WinPE (erreur WIMGAPI `112`).
|
||||
- Remplacement de cette lecture préalable par une taille WinRE configurable dans `[Partitioning] WinRESizeMB` (1024 Mo par défaut). Le profil réserve cette taille sans monter ni extraire `install.wim` ; après l’application de Windows, DeployCenter mesure `winre.wim` et arrête la configuration WinRE si la partition est inférieure à 990 Mo ou ne conserverait pas 250 Mo libres après copie.
|
||||
- Correction de la fermeture du callback de progression : le callback de journalisation est maintenant capturé explicitement avant `GetNewClosure()`, afin qu’il reste accessible depuis le scope de l’action de progression.
|
||||
- Pour la préparation DISM, une source ISO exposée sous un chemin de volume GUID reçoit temporairement une lettre libre autre que C: ou D:. DISM reçoit aussi son répertoire temporaire et son propre journal pour faciliter le diagnostic des erreurs de montage.
|
||||
- Resserrement de l’espacement dans la confirmation d’effacement pour rapprocher l’avertissement du nom d’hôte.
|
||||
- Ajout du flux intégré « Installer » : confirmation avant effacement, tailles des partitions calculées depuis le profil, application DISM, configuration du nom d’hôte, fichiers de démarrage UEFI et activation de WinRE.
|
||||
- Création de `Modules/Deployment.psm1` pour isoler l’exécution du déploiement ; ajout dans `UI.psm1` des fenêtres thémées de confirmation et de progression. Le nouveau bouton d’installation suit le thème actif.
|
||||
- Suppression des objets de résultat de démontage ISO à la fois dans `WindowsSources.psm1` et aux appels UI, afin qu’ils ne s’affichent pas dans la console PowerShell.
|
||||
- Prise en charge des volumes ISO exposés par WinPE sans objet partition : recherche via le chemin de volume (GUID), puis libération de D: avec `mountvol` si cette lettre est attribuée à l’ISO.
|
||||
- Recherche de `install.wim`/`install.esd` à partir des volumes associés explicitement à l’ISO, puis association de chaque volume à sa partition.
|
||||
- Le montage ISO réutilise une lettre déjà attribuée si elle n’est ni C: ni D: ; si WinPE a attribué l’une de ces lettres, elle est retirée avant l’affectation temporaire.
|
||||
- Correction des appels `Add-PartitionAccessPath` et `Remove-PartitionAccessPath` : avec `-InputObject`, la lettre du lecteur doit être fournie comme `-AccessPath` pour respecter leurs jeux de paramètres.
|
||||
- Résolution du disque ISO par `Get-Disk -Number` à partir de `MSFT_DiskImage.Number`, sans pipeline ambigu dans certaines versions de WinPE.
|
||||
- Correction de l’association entre un ISO monté et son disque dans WinPE : résolution via `Get-DiskImage | Get-Disk` plutôt que par le chemin `\\.\CDROMn`.
|
||||
- Le montage ISO n’attribue plus de lettre automatiquement : une lettre temporaire disponible est choisie en excluant C: et D:. Une barre d’activité apparaît pendant l’analyse des éditions.
|
||||
- Renommage de la commande de nettoyage du montage ISO avec le verbe PowerShell approuvé `Dismount`, pour supprimer l’avertissement à l’import du module WindowsSources.
|
||||
- Le montage d’un ISO reste actif tant qu’il est la source Windows sélectionnée ; il est libéré après analyse annulée, remplacement par une autre source, erreur ou fermeture de DeployCenter.
|
||||
- Le callback de journalisation utilise `INFO` lorsque le niveau est omis ou vide, afin d’éviter une erreur de validation `ValidateSet` au démarrage.
|
||||
- Correction du callback de journalisation au démarrage : il conserve désormais le scope du script principal pour résoudre `Write-Log` depuis les événements WinForms. Les derniers appels directs de `DeployCenter.ps1` passent aussi par ce callback.
|
||||
|
||||
### 2026-10-15
|
||||
|
||||
- Les anciens exports de `HardwareInfo.ps1` ont d’abord été déplacés sous `%TEMP%\DeployCenter`, puis retirés au profit du seul inventaire consolidé décrit ci-dessous.
|
||||
- L’affichage des disques a d’abord utilisé `InterfaceType`, puis a été corrigé pour afficher `BusType` obtenu par `Get-Disk`.
|
||||
- Le module VNC réutilise l’adresse IPv4 détectée au démarrage ; la détection réseau n’est plus relancée lors du démarrage de VNC.
|
||||
- Les messages d’information et d’erreur utilisent maintenant `Show-DeployCenterMessage` dans `Modules/UI.psm1`, avec la palette active, au lieu des boîtes de dialogue natives de Windows.
|
||||
- Les erreurs de démarrage VNC indiquent maintenant l’étape concernée pour distinguer les échecs de nettoyage, de copie, de pare-feu et de lancement.
|
||||
- La sortie de `wpeutil DisableFirewall` et son code de retour sont consignés dans le journal DeployCenter au lieu d’apparaître dans la console PowerShell.
|
||||
- Le libellé de statut de la fenêtre Sources Windows est remonté et sa hauteur réduite pour mieux le séparer du pied de page.
|
||||
- L’inventaire matériel, l’adresse IP et les informations VNC sont réunis dans `%TEMP%\DeployCenter\DeployCenter_Inventory.json`. Les exports distincts `HardwareInfo.json`, `hardware.json` et `hardware.log` ne sont plus générés.
|
||||
- Les anciens fichiers temporaires peuvent rester jusqu’au redémarrage de WinPE, qui vide `%TEMP%`.
|
||||
- La collecte complète des disques utilise maintenant `Get-Disk` : numéro, nom, taille, type de bus, état opérationnel et état de santé. `Win32_DiskDrive` n’est plus utilisé pour l’inventaire des disques.
|
||||
- La détection IPv4 de `Modules/Network.psm1` utilise les API .NET `System.Net.NetworkInformation` au lieu de `Get-CimInstance Win32_NetworkAdapterConfiguration` ; la priorité aux interfaces avec passerelle et les exclusions d’adresses restent en place.
|
||||
- Ajout du chargement et de la validation de profils de partitionnement PSD1, ainsi que d’un sélecteur dans l’interface. Le profil UEFI/GPT place EFI, MSR et WinRE avant Windows, qui reçoit l’espace restant ; aucun script DiskPart n’est exécuté à cette étape.
|
||||
- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell et PSD1 pour préserver les caractères accentués lors de leur lecture par Windows PowerShell 5.1.
|
||||
- Correction de l’apostrophe dans la description du profil UEFI/GPT, qui empêchait le chargement du fichier PSD1.
|
||||
- Ajout du profil UEFI/GPT avec C: et D:. Les politiques de taille du PSD1 pilotent un formulaire générique : saisie de C: et calcul de D: comme espace restant estimé. Le profil place C: avant D:.
|
||||
- Correction d’une syntaxe PowerShell incompatible dans les filtres de politique de taille du profil, qui empêchait le lancement du script.
|
||||
- Correction du message de sélection du disque : son apostrophe typographique fermait prématurément une chaîne PowerShell et bloquait l’analyse du script.
|
||||
- Correction du placement d’un libellé du formulaire C:/D: : l’expression de coordonnées était interprétée comme un troisième argument du constructeur `Point`. Le profil PSD1 reçoit un BOM UTF-8 et utilise des guillemets doubles autour de sa description, qui contient une apostrophe typographique.
|
||||
- Le formulaire du profil C:/D: recalcule D: pendant la saisie de C: et accepte désormais une taille minimale de 40 Go pour C:.
|
||||
- Réorganisation de la première page : sélection du disque, du système, du profil et du nom d’hôte dans le panneau de préparation ; le journal est accessible dans une fenêtre dédiée depuis la barre d’actions. Les nouveaux contrôles et la fenêtre du journal sont construits dans `UI.psm1` et suivent le thème actif.
|
||||
- Intégration du nouveau panneau de préparation dans la fenêtre principale : le profil est activé après la sélection d’une édition Windows, et le journal est ouvert depuis le bouton placé à gauche des actions principales.
|
||||
- Le journal reste alimenté par `Logging.psm1` ; `UI.psm1` fournit uniquement sa fenêtre thémée et son contrôle d’affichage.
|
||||
- Le libellé du système sélectionné conserve la couleur de statut du thème et tronque les chemins longs sans déformer le panneau.
|
||||
- Le constructeur de ComboBox accepte une collection vide à l’initialisation du sélecteur de disque, ensuite alimentée par l’inventaire matériel.
|
||||
- Le champ du nom d’hôte est dimensionné pour un nom NetBIOS et limite la saisie à 15 caractères.
|
||||
- Le champ du nom d’hôte est aligné à droite du panneau avec un espacement réservé après son libellé afin d’éviter tout chevauchement.
|
||||
- Dans Sources Windows, le double-clic sur une ligne sélectionnée lance aussi l’affichage des éditions, comme le bouton dédié.
|
||||
- Après validation d’une édition Windows, la fenêtre Sources Windows se ferme pour revenir directement à l’écran principal.
|
||||
|
||||
- Extraction de la recherche, de la lecture des éditions et de la résolution des images Windows vers Modules/WindowsSources.psm1. DeployCenter.ps1 charge ce module après l’initialisation du journal.
|
||||
- Le module Sources Windows reçoit explicitement un callback de journalisation fourni par Logging.psm1, sans dépendre implicitement de la portée du script principal.
|
||||
|
||||
- Déplacement dans UI.psm1 des constructeurs génériques de labels, groupes et boutons, du pied de page de session et du réglage de fenêtre. Les fonctions reçoivent explicitement thème, police et données de session.
|
||||
- Extraction dans UI.psm1 de la sélection du logo selon le thème et de son remplacement ; chemins et PictureBox sont transmis en paramètres.
|
||||
- Création de Modules/Configuration.psm1 : la lecture de DeployCenter.ini est isolée et retourne un objet de préférences sans modifier le fichier.
|
||||
- Répartition des fonctions restantes : workflows de fenêtre et de thème dans UI.psm1, préférences dans Configuration.psm1, collecte/export/lecture disque dans HardwareInventory.psm1. Les profils et calculs de capacité restent dans Partitioning.psm1.
|
||||
- Configuration.psm1 renvoie désormais un objet de configuration avec des valeurs par défaut et ne dépend plus de variables script externes.
|
||||
- HardwareInventory.psm1 exporte la collecte et l’export avec des paramètres explicites ; HardwareInfo.ps1 reste un point d’entrée de compatibilité.
|
||||
- Nettoyage des espaces en fin de ligne dans les modules UI et Partitioning après leur déplacement.
|
||||
- Partitioning.psm1 construit désormais l’état des tailles choisi à partir du profil, du disque et du résultat du dialogue UI.
|
||||
- Ajout du BOM UTF-8 aux nouveaux fichiers PowerShell pour Windows PowerShell 5.1 et tolérance de l’absence temporaire d’inventaire lors de la résolution d’un disque.
|
||||
- Mise à jour de la description d’architecture et de la liste des modules après l’extraction des fonctions restantes.
|
||||
- Correction des appels d’export d’inventaire depuis le module VNC : les chemins requis et les données de session sont transmis explicitement, sans invite interactive au lancement.
|
||||
- Les workflows UI et VNC transmettent désormais leurs messages au callback de Logging.psm1, sans appeler Write-Log directement depuis leurs scopes.
|
||||
Les profils PSD1 sont des données de configuration ; gardez leur structure et leurs identifiants cohérents. Les fichiers PowerShell et profils utilisés par Windows PowerShell 5.1 doivent rester encodés en UTF-8 avec BOM pour préserver correctement les caractères accentués.
|
||||
@@ -1,9 +0,0 @@
|
||||
@{
|
||||
Id = 'InstallationOEM'
|
||||
Name = 'Installation OEM'
|
||||
Description = 'Configure un poste Windows avec le nom de machine, un compte local défini dans DeployCenter.ini et les personnalisations OEM sélectionnées.'
|
||||
Target = 'Workstation'
|
||||
Template = 'InstallationOEM.xml'
|
||||
ComputerName = $true
|
||||
LocalAccount = $true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@{
|
||||
Id = 'OEMInstallation'
|
||||
Name = 'OEM installation'
|
||||
Description = 'Configures a Windows workstation with a computer name, a local account from DeployCenter.ini, and the selected OEM customizations.'
|
||||
Target = 'Workstation'
|
||||
Template = 'OEMInstallation.xml'
|
||||
ComputerName = $true
|
||||
LocalAccount = $true
|
||||
}
|
||||
@@ -5,6 +5,20 @@
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<ComputerName></ComputerName>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Description>Enable NumLock on the sign-in screen</Description>
|
||||
<Path>cmd.exe /c reg add "HKU\.DEFAULT\Control Panel\Keyboard" /v InitialKeyboardIndicators /t REG_SZ /d 2 /f</Path>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Description>Enable NumLock for new users</Description>
|
||||
<Path>cmd.exe /c reg load HKU\DeployCenterDefaultUser "%SystemDrive%\Users\Default\NTUSER.DAT" && reg add "HKU\DeployCenterDefaultUser\Control Panel\Keyboard" /v InitialKeyboardIndicators /t REG_SZ /d 2 /f && reg unload HKU\DeployCenterDefaultUser</Path>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
@@ -16,6 +30,7 @@
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
|
||||
<OEMInformation>
|
||||
<SupportProvider></SupportProvider>
|
||||
<SupportURL></SupportURL>
|
||||
<SupportAppURL></SupportAppURL>
|
||||
</OEMInformation>
|
||||
<OOBE>
|
||||
Reference in new issue
Block a user