PowerShell – ADK & MDT installation silencieuse automatique

Version mise à jour du script ici : https://obilan.be/update-powershell-adk-mdt-installation

 

Bonjour,

Voici un petit script créé la semaine dernière pour un besoin personnel. Ce script effectue le téléchargement, l’installation et la configuration basique de MDT (Microsoft Deployment Toolkit) et de sa dépendance ADK (Windows Assessment and Deployment Kit) pour le déploiement de Windows 10, version 1607.

Ce script ne tourne que sur Windows Server 2012 ou ultérieur, et ne fonctionnera pas entièrement si le rôle ADDS est installé.

Voici la liste ordonnée des opérations effectuées par le script :

  1. Exécution en admin (self elevation) ;
  2. Activation de la feature .NET Framework 3.5 ;
  3. Téléchargement et installation de l’ADK ;
  4. Téléchargement et installation de MDT ;
  5. Création d’un utilisateur local admin (mdt_admin), utilisé plus tard pour accéder au Deployment Share ;
  6. Création et configuration du Deployment Share ;
  7. Ajout de settings basiques dans les fichiers customsettings.ini et bootstrap.ini (dont la mention de l’utilisateur créé au step 5) ;
  8. Génération d’un rapport HTML simple reprenant le logging et le password aléatoirement généré pour l’utilisateur mdt_admin.

N’hésitez pas à changer la valeur des variables regroupées dans la région ‘Variables’ pour changer les répertoires d’installation par défaut.

J’essaierai de l’améliorer quelques peu dans les jours qui viennent 🙂

<# 
    .SYNOPSIS 
    Install and configure ADK and MDT silently 
    .DESCRIPTION
    This script will download sources, install and configure ADK and MDT with some basic settings
    .Notes 
    Author : Antoine DELRUE 
    WebSite: https://obilan.be 
#> 

#region Run script as admin
# Get the ID and security principal of the current user account
$myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent()
$myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID)
 
# Get the security principal for the Administrator role
$adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator
 
# Check to see if we are currently running "as Administrator"
if ($myWindowsPrincipal.IsInRole($adminRole))
   {
   # We are running "as Administrator" - so change the title and background color to indicate this
   $Host.UI.RawUI.WindowTitle = $myInvocation.MyCommand.Definition + "(Elevated)"
   $Host.UI.RawUI.BackgroundColor = "DarkBlue"
   clear-host
   }
else
   {
   # We are not running "as Administrator" - so relaunch as administrator
   
   # Create a new process object that starts PowerShell
   $newProcess = new-object System.Diagnostics.ProcessStartInfo "PowerShell";
   
   # Specify the current script path and name as a parameter
   $newProcess.Arguments = $myInvocation.MyCommand.Definition;
   
   # Indicate that the process should be elevated
   $newProcess.Verb = "runas";
   
   # Start the new process
   [System.Diagnostics.Process]::Start($newProcess);
   
   # Exit from the current, unelevated, process
   exit
   }
#endregion

#region Functions 

function Write-Log 
{ 
    [CmdletBinding()] 
    Param 
    ( 
        [Parameter(Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true)] 
        [ValidateNotNullOrEmpty()] 
        [Alias("LogContent")] 
        [string]$Message, 
 
        [Parameter(Mandatory=$false)] 
        [Alias('LogPath')] 
        [string]$Path='C:\Logs\PowerShellLog.log', 
         
        [Parameter(Mandatory=$false)] 
        [ValidateSet("Error","Warn","Info")] 
        [string]$Level="Info", 
         
        [Parameter(Mandatory=$false)] 
        [switch]$NoClobber 
    ) 
 
    Begin 
    { 
        # Set VerbosePreference to Continue so that verbose messages are displayed. 
        $VerbosePreference = 'Continue' 
    } 
    Process 
    { 
         
        # If the file already exists and NoClobber was specified, do not write to the log. 
        if ((Test-Path $Path) -AND $NoClobber) { 
            Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name." 
            Return 
            } 
 
        # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. 
        elseif (!(Test-Path $Path)) { 
            Write-Verbose "Creating $Path." 
            $NewLogFile = New-Item $Path -Force -ItemType File 
            } 
 
        else { 
            # Nothing to see here yet. 
            } 
 
        # Format Date for our Log File 
        $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 
 
        # Write message to error, warning, or verbose pipeline and specify $LevelText 
        switch ($Level) { 
            'Error' { 
                Write-Error $Message 
                $LevelText = 'ERROR:' 
                } 
            'Warn' { 
                Write-Warning $Message 
                $LevelText = 'WARNING:' 
                } 
            'Info' { 
                Write-Verbose $Message 
                $LevelText = 'INFO:' 
                } 
            } 
         
        # Write log entry to $Path 
        "$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append 
    } 
    End 
    { 
    } 
}

function New-SWRandomPassword {
    <#
    .Synopsis
       Generates one or more complex passwords designed to fulfill the requirements for Active Directory
    .DESCRIPTION
       Generates one or more complex passwords designed to fulfill the requirements for Active Directory
    .EXAMPLE
       New-SWRandomPassword
       C&3SX6Kn

       Will generate one password with a length between 8  and 12 chars.
    .EXAMPLE
       New-SWRandomPassword -MinPasswordLength 8 -MaxPasswordLength 12 -Count 4
       7d&5cnaB
       !Bh776T"Fw
       9"C"RxKcY
       %mtM7#9LQ9h

       Will generate four passwords, each with a length of between 8 and 12 chars.
    .EXAMPLE
       New-SWRandomPassword -InputStrings abc, ABC, 123 -PasswordLength 4
       3ABa

       Generates a password with a length of 4 containing atleast one char from each InputString
    .EXAMPLE
       New-SWRandomPassword -InputStrings abc, ABC, 123 -PasswordLength 4 -FirstChar abcdefghijkmnpqrstuvwxyzABCEFGHJKLMNPQRSTUVWXYZ
       3ABa

       Generates a password with a length of 4 containing atleast one char from each InputString that will start with a letter from 
       the string specified with the parameter FirstChar
    .OUTPUTS
       [String]
    .NOTES
       Written by Simon Wåhlin, blog.simonw.se
       I take no responsibility for any issues caused by this script.
    .FUNCTIONALITY
       Generates random passwords
    .LINK
       http://blog.simonw.se/powershell-generating-random-password-for-active-directory/
   
    #>
    [CmdletBinding(DefaultParameterSetName='FixedLength',ConfirmImpact='None')]
    [OutputType([String])]
    Param
    (
        # Specifies minimum password length
        [Parameter(Mandatory=$false,
                   ParameterSetName='RandomLength')]
        [ValidateScript({$_ -gt 0})]
        [Alias('Min')] 
        [int]$MinPasswordLength = 8,
        
        # Specifies maximum password length
        [Parameter(Mandatory=$false,
                   ParameterSetName='RandomLength')]
        [ValidateScript({
                if($_ -ge $MinPasswordLength){$true}
                else{Throw 'Max value cannot be lesser than min value.'}})]
        [Alias('Max')]
        [int]$MaxPasswordLength = 12,

        # Specifies a fixed password length
        [Parameter(Mandatory=$false,
                   ParameterSetName='FixedLength')]
        [ValidateRange(1,2147483647)]
        [int]$PasswordLength = 8,
        
        # Specifies an array of strings containing charactergroups from which the password will be generated.
        # At least one char from each group (string) will be used.
        [String[]]$InputStrings = @('abcdefghijkmnpqrstuvwxyz', 'ABCEFGHJKLMNPQRSTUVWXYZ', '23456789', '!"#%&'),

        # Specifies a string containing a character group from which the first character in the password will be generated.
        # Useful for systems which requires first char in password to be alphabetic.
        [String] $FirstChar,
        
        # Specifies number of passwords to generate.
        [ValidateRange(1,2147483647)]
        [int]$Count = 1
    )
    Begin {
        Function Get-Seed{
            # Generate a seed for randomization
            $RandomBytes = New-Object -TypeName 'System.Byte[]' 4
            $Random = New-Object -TypeName 'System.Security.Cryptography.RNGCryptoServiceProvider'
            $Random.GetBytes($RandomBytes)
            [BitConverter]::ToUInt32($RandomBytes, 0)
        }
    }
    Process {
        For($iteration = 1;$iteration -le $Count; $iteration++){
            $Password = @{}
            # Create char arrays containing groups of possible chars
            [char[][]]$CharGroups = $InputStrings

            # Create char array containing all chars
            $AllChars = $CharGroups | ForEach-Object {[Char[]]$_}

            # Set password length
            if($PSCmdlet.ParameterSetName -eq 'RandomLength')
            {
                if($MinPasswordLength -eq $MaxPasswordLength) {
                    # If password length is set, use set length
                    $PasswordLength = $MinPasswordLength
                }
                else {
                    # Otherwise randomize password length
                    $PasswordLength = ((Get-Seed) % ($MaxPasswordLength + 1 - $MinPasswordLength)) + $MinPasswordLength
                }
            }

            # If FirstChar is defined, randomize first char in password from that string.
            if($PSBoundParameters.ContainsKey('FirstChar')){
                $Password.Add(0,$FirstChar[((Get-Seed) % $FirstChar.Length)])
            }
            # Randomize one char from each group
            Foreach($Group in $CharGroups) {
                if($Password.Count -lt $PasswordLength) {
                    $Index = Get-Seed
                    While ($Password.ContainsKey($Index)){
                        $Index = Get-Seed                        
                    }
                    $Password.Add($Index,$Group[((Get-Seed) % $Group.Count)])
                }
            }

            # Fill out with chars from $AllChars
            for($i=$Password.Count;$i -lt $PasswordLength;$i++) {
                $Index = Get-Seed
                While ($Password.ContainsKey($Index)){
                    $Index = Get-Seed                        
                }
                $Password.Add($Index,$AllChars[((Get-Seed) % $AllChars.Count)])
            }
            Write-Output -InputObject $(-join ($Password.GetEnumerator() | Sort-Object -Property Name | Select-Object -ExpandProperty Value))
        }
    }
}

#endregion

#region Variables

$outputFolder = "c:/temp/Deployment_Server_Configurator"
$logpath = $outputFolder + "/scriptlog.log"
$adklog = $outputFolder + "/adk_Setup.log"
$ADK_temp = $outputFolder + "/ADK_TEMP"
$download_MDT = "https://download.microsoft.com/download/3/0/1/3012B93D-C445-44A9-8BFB-F28EB937B060/MicrosoftDeploymentToolkit2013_x64.msi"
$download_ADK = "http://download.microsoft.com/download/8/1/9/8197FEB9-FABE-48FD-A537-7D8709586715/adk/adksetup.exe”
$file_MDT = "$outputFolder/MDT2013x64.msi"
$file_ADK = "$outputFolder/ADK_Win10v1607.exe"
# Pay attention in the path below, you have to specify a 'backslash' (\), not a simple slash (/), or it will not share the folder
$deploymentshare = "c:\DeploymentShare"

#endregion

# STARTING SCRIPT
Write-Log -Message "SCRIPT => Deployment_Server_Automation.ps1 started on $env:COMPUTERNAME by $env:USERNAME" -Path $logpath -Level Info

#region Check if OS is Windows Server

$OStest = Get-WmiObject -Class win32_operatingsystem | ? {$_.caption -ilike "*Windows Server 201*"}

if (!$OStest) 
{
    Write-Log -Message "Script can't run on non-Windows Server computers, exiting" -Path $logpath -Level Error
    Invoke-Item $logpath
    break
}
else
{
    Write-Log -Message "Computer passed OS test" -Path $logpath -Level Info    
}

#endregion

#region .NET Framework 3.5 configuration

# Check if .NET Framework 3.5 is enabled on the server, if not DO IT
$featuretest = Get-WindowsFeature -Name net-framework-core | ? {$_.installed -eq $true}
if (!$featuretest)
{
    try {
        Write-Log -Message "CONFIGURING => .NET Framework 3.5 feature" -Path $logpath -Level Info
        Install-WindowsFeature net-framework-core -IncludeAllSubFeature
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        $FailedItem = $_.Exception.ItemName
        Write-Log -Message "Enabling net-framework-core feature failed : $FailedItem" -Path $logpath -Level Error
        break
    }

    Write-Log -Message "CONFIGURED => .NET Framework 3.5 feature" -Path $logpath -Level Info
}
else {Write-Log -Message "CONFIGURED => .NET Framework 3.5 feature already enabled on this computer" -Path $logpath -Level Info}

#endregion

#region ADK source & features download

# Check if ADK source file has already been downloaded before, if so do not download again

$testADK = Test-Path $ADK_temp/* -Include *.exe

if ($testADK -eq $false)
{
    try {
        Write-Log -Message "DOWNLOADING => ADK Setup Launcher" -Path $logpath -Level Info
        New-Item -Path $outputFolder -ItemType Directory -Force | Out-Null
        Invoke-WebRequest -Uri $download_ADK -OutFile $file_ADK
        sleep -Seconds 5
        Write-Log -Message "DOWNLOADING => ADK features sources to $ADK_temp" -Path $logpath -Level Info
        Write-Verbose -Message "This operation can take a lot of time, depending of your network connection" -Verbose
        & $file_ADK /quiet /layout $ADK_temp | Out-Null

    }
    catch {
        $ErrorMessage = $_.Exception.Message
        $FailedItem = $_.Exception.ItemName
        Write-Log -Message "Download failed : $FailedItem" -Path $logpath -Level Error
        break
    }
}

else {Write-Log -Message "DOWNLOADED => ADK features sources already present in $ADK_temp" -Path $logpath -Level Info}

#endregion

#region MDT sources download

# Download MDT sources files from internet

try {
    Write-Log -Message "DOWNLOADING => MDT setup sources" -Path $logpath -Level Info
    New-Item -Path $outputFolder -ItemType Directory -Force | Out-Null
    Invoke-WebRequest -Uri $download_MDT -OutFile $file_MDT
}
catch {
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    Write-Log -Message "Download failed : $FailedItem" -Path $logpath -Level Error
    break
}

Write-Log -Message "DOWNLOADED => MDT setup sources in $outputFolder" -Path $logpath -Level Info

sleep -s 5

#endregion

#region Install ADK with the required features

# Install ADK with the required features, will use the previously downloaded source files

try {
    Write-Log -Message "INSTALLING => Ongoing ADK installation, this will take a while" -Path $logpath -Level Info
    $exe_ADK = $ADK_temp + "/adksetup.exe"
    & $exe_ADK /quiet /features OptionId.DeploymentTools OptionId.WindowsPreinstallationEnvironment OptionId.UserStateMigrationTool /ceip off /log $adklog | Out-Null
    
}
catch {
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    Write-Log -Message "ADK installation failed : $FailedItem, review log at : $adklog" -Path $logpath -Level Error
    break
}

Write-Log -Message "INSTALLED => ADK installation complete, review log at : $adklog" -Path $logpath -Level Info

sleep -s 5

#endregion

#region Install MDT

try {
    Write-Log -Message "INSTALLING => Ongoing MDT installation" -Path $logpath -Level Info
    & $file_MDT /qn
    
}
catch {
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    Write-Log -Message "MDT installation failed : $FailedItem" -Path $logpath -Level Error
    break
}

Write-Log -Message "INSTALLED => MDT installation complete" -Path $logpath -Level Info
sleep -s 5

#endregion

#region Create the local user used by the MDT deployment share

$name = "mdt_admin"
$password = New-SWRandomPassword -MinPasswordLength 8 -MaxPasswordLength 12
$localuser = Get-WmiObject -Class Win32_UserAccount -Filter "LocalAccount='True'" | ? {$_.Name -eq "$name"}

if (!$localuser)
{
    try {
        Write-Log -Message "CONFIGURING => Local user not found, creating it" -Path $logpath -Level Info
        $server=[adsi]"WinNT://$env:computername"
        $user=$server.Create("User","$name")
        $user.SetPassword($password)
        $user.SetInfo()
 
        # add extra info
        $user.Put('Description','Microsoft Deployment Tools administrator')
        $flag=$user.UserFlags.Value -bor 0x800000
        $user.put('userflags',$flag)
        $user.SetInfo()
 
        # add user to mandatory local group
        $group=[adsi]"WinNT://$env:computername/Users,Group"
        $group.Add($user.path)

        # add user to administrators local group
        $group=[adsi]"WinNT://$env:computername/Administrators,Group"
        $group.Add($user.path)
    } 
    catch {
        $ErrorMessage = $_.Exception.Message
        $FailedItem = $_.Exception.ItemName
        Write-Log -Message "MDT user creation failed" -Path $logpath -Level Error
        break
    }    
}
else 
{
    try {
        Write-Log -Message "CONFIGURING => Local user already exists, reseting password" -Path $logpath -Level Info
        ([adsi]“WinNT://$env:computername/$name”).SetPassword(“$password”)
    }
    catch {
        $ErrorMessage = $_.Exception.Message
        $FailedItem = $_.Exception.ItemName
        Write-Log -Message "MDT user password reset failed" -Path $logpath -Level Error
        break
    }
}

#endregion

#region Create and configure the MDT Deployment Share

try {
    Write-Log -Message "CONFIGURING => MDT deployment share configuration ongoing" -Path $logpath -Level Info
    New-Item -Path $deploymentshare -ItemType directory -Force | Out-Null
    New-SmbShare -Name "DeploymentShare$" -Path $deploymentshare -FullAccess Administrators
    Import-Module "C:\Program Files\Microsoft Deployment Toolkit\bin\MicrosoftDeploymentToolkit.psd1"
    New-PSDrive -Name "DS001" -PSProvider "MDTProvider" -Root $deploymentshare -Description "MDT Deployment Share" -NetworkPath "\\$env:COMPUTERNAME\DeploymentShare$" -Verbose | Add-MDTPersistentDrive -Verbose | Out-Null
}
catch {
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    Write-Log -Message "MDT configuration failed : $FailedItem" -Path $logpath -Level Error
    break
}

Write-Log -Message "CONFIGURED => MDT deployment share created" -Path $logpath -Level Info

#endregion

#region Performing the basic configuration of MDT (INI files)

$customsettings = $deploymentshare + "/Control/CustomSettings.ini"
$bootstrap = $deploymentshare + "/Control/Bootstrap.ini"

$bootcontent = @"
[Settings]
Priority=Default

[Default]
DeployRoot=\\$env:computername\DeploymentShare$
UserID=$name
UserPassword=$password
UserDomain=$env:COMPUTERNAME
SkipBDDWelcome=YES
"@

$customcontent = @"
[Settings]
Priority=Default
Properties=MyCustomProperty

[Default]
_SMSTSORGNAME=%TaskSequenceName%
OSInstall=Y
SkipCapture=YES
SkipAdminPassword=YES
[email protected]
SkipProductKey=YES
SkipBitLocker=YES
SkipComputerBackup=YES
SkipComputername=YES
SkipDomainMembership=YES
SkipUserData=YES
SkipLocaleSelection=YES
SkipTimeZone=YES
SkipSummary =YES
UILanguage=en-US
UserLocale=fr-BE
KeyboardLocale=080c:0000080c
TimeZoneName = Romance Standard Time
TimeZone=105
FinishAction=REBOOT
"@

try {
    Write-Log -Message "CONFIGURING => MDT .INI files creation with basic values" -Path $logpath -Level Info
    Remove-Item -Path $customsettings -Force
    Remove-Item -Path $bootstrap -Force

    Add-Content -Path $bootstrap -Value $bootcontent -Force
    Add-Content -Path $customsettings -Value $customcontent -Force
} 
catch {
    $ErrorMessage = $_.Exception.Message
    $FailedItem = $_.Exception.ItemName
    Write-Log -Message "MDT configuration failed : $FailedItem" -Path $logpath -Level Error
    break
}

Write-Log -Message "CONFIGURED => MDT installation & configuration is now COMPLETE" -Path $logpath -Level Info

#endregion

#region Generate a HTML report

Write-Log -Message "LOGGING => Generating HTML report" -Path $logpath -Level Info
$body = get-content $logpath
$FileLine = @()
Foreach ($Line in $body) {
    $MyObject = New-Object -TypeName PSObject
    Add-Member -InputObject $MyObject -Type NoteProperty -Name Logfile -Value $Line
    $FileLine += $MyObject
}

$loglinks = $null
$loglinks = @()
$loglinks += "<p>"

$logs = Get-ChildItem -Path $outputFolder\* -Include *.log
$logtitle = $logs.Name
$logFullname = $logs.fullname

$head = @"
<title>MDT & ADK Setup Report</title>
<meta name="description" content="MDT & ADK Automatic setup report">
<meta name="author" content="An awsome PowerShell Script">
<style>
    body {width: 90%; margin: auto; border: 3px solid black; padding: 10px;}
    table {border-collapse: collapse; table, th, td {border: 1px solid black;}
    h3 {color:blue;}
</style>
"@

$precontent = "<h1>MDT & ADK Automatic setup report</h1><br />"
$linklogadk = '"' + "$adklog" + '"'
$targettag = "target=" + '"' + "_blank" + '"'
$pub = "<a href=" + '"' + "https://obilan.be" + '"' + ">https://obilan.be</a>"

foreach ($log in $logs)
{
    $logtitle = $log.Name
    $logFullname = $log.fullname
    $link = "<a href=" + '"' + "$logFullname" + '"' + " $targettag>$logtitle</a><br >"
    $loglinks += $link
}

$loglinks += "</p>"

$postcontent = "<hr /><br ><h3>User information :</h3><br ><strong>New MDT user :</strong> $name<br ><strong>Password :</strong> $password<br ><strong>Deployment share path :</strong> $deploymentshare<br ><strong>Deployment share UNC :</strong> \\$env:COMPUTERNAME\DeploymentShare$<hr /><br ><h3>Log files :</h3>$loglinks<hr ><br ><p>Report generated by an Obilan's awsome script, check it at $pub</p>"

$FileLine | ConvertTo-Html -Head $head -Property Logfile -PreContent $precontent -PostContent $postcontent | set-content $outputFolder/report.html -Force
Invoke-Item $outputFolder/report.html

#endregion

# SCRIPT END
Write-Log -Message "SCRIPT => Script ENDED" -Path $logpath -Level Info

Si vous avez des améliorations à proposer ou d’autres remarques, n’hésitez pas à me contacter !

Laisser un commentaire