Configuration Manager – IIS Log Maintenance

The majority of SCCM administrators will have included a list of maintenance tasks for ensuring optimum performance of their environment. One area that often gets overlooked however is the maintenance of IIS logs. The logs are obviously useful for troubleshooting purposes, however do you really need to know what was happening say over 30 days ago. The truth is if there was an issue you probably would have spotted it very early on and dealt with it.

So how do you deal with log files within your IIS directory filling up your disks?. The answer can be to setup a configuration item and let SCCM take care of its own compliance.

Creating Your SCCM Server Device Collection

To start with you will need to create a Device Collection to deploy the Configuration Item and subsequent Baseline to. This can be achieved by creating a device collection using the following query;

select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,SMS_R_SYSTEM.ResourceDomainORWorkgroup,SMS_R_SYSTEM.Client from SMS_R_System where SMS_R_System.ResourceNames in (Select ServerName FROM SMS_DistributionPointInfo)

Creating Your SCCM IIS Prune Logs Configuration Item

Once you have the device collection created and populated you can go on to create the Configuration Item to be used. To speed up this process I have exported the CI CAB for you to download, below are some screen shots of the manual creation process;

Download – Prune IIS Logs

  1. Create Configuration Item

  2. Select Supported Platforms

    In this section we will pick which operating systems are applicable for SCCM distribution points

  3. Settings

    Here we will create a new setting entry that will contain both the discovery and remediation scripts required

    Discovery Script

    # Specify the maximum log age file to maintain
    $MaxDays = 7
    
    function DiscoverIISLogs ($MaxDays)
    {
    	try
    	{
    		# Import IIS WebAdmin Module
    		Import-Module WebAdministration
    		
    		# Return list of IIS webistes
    		$IISSites = Get-Website
    		# Loop for each IIS site
    		foreach ($Site in $IISSites)
    		{
    			# Return path for IIS logs
    			$IISLogs = $Site.LogFile.Directory
    			# Condition to replace DOS %SystemDrive% variable with Powershell variable
    			If ($IISLogs -like "*%SystemDrive%*")
    			{
    				$IISLogs = $IISLogs -replace "%SystemDrive%", "$env:SystemDrive"
    			}
    			# Count IIS Log files to prune
    			$LogCount = $LogCount + (Get-ChildItem -Path $IISLogs -Recurse -Filter "*.log" | Where-Object { $(Get-Date).Subtract($_.LastWriteTime).Days -gt $MaxDays}).count
    		}
    		Return $LogCount
    	}
    	catch { return -1 }
    }
    
    DiscoverIISLogs ($MaxDays)

    Remediation Script

    # Specify the maximum log age file to maintain
    $MaxDays = 30
    
    function PurgeIISLogs ($MaxDays)
    {
    	try
    	{
    		# Import IIS WebAdmin Module
    		Import-Module WebAdministration
    		
    		# Return list of IIS webistes
    		$IISSites = Get-Website
    		# Loop for each IIS site
    		foreach ($Site in $IISSites)
    		{
    			# Return path for IIS logs
    			$IISLogs = $Site.LogFile.Directory
    			# Condition to replace DOS %SystemDrive% variable with Powershell variable
    			If ($IISLogs -like "*%SystemDrive%*")
    			{
    				$IISLogs = $IISLogs -replace "%SystemDrive%", "$env:SystemDrive"
    			}
    			# Purge IIS Log files
    			Get-ChildItem -Path $IISLogs -Recurse -Filter "*.log" | Where-Object { $(Get-Date).Subtract($_.LastWriteTime).Days -gt $MaxDays } | Foreach-Object { Remove-Item $_.FullName -Force -Verbose }
    		}
    	}
    
    catch {  }
    }
    
    PurgeIISLogs ($MaxDays)
    

     

  4. Compliance Rules

    Here we will specify when to run the remediation script you created in the previous step

Configuration Baseline

Now in order to apply the Configuration Item we will need to create a Configuration Baseline for the SCCM Server Device Collection.

  1. Create a new Configuration Baseline

  2. Set Evaluation Conditions

    Here we will add the Configuration Baseline we created earlier;

  3. Deploy the Configuration Baseline

    Set your schedule and generate an alert if required..

Monitoring

Now sit back, relax and watch your good work take hold across your estate;

Savings Examples

Below we have the before and after results of a distribution point running a low workload;

Before (Manual Run)

After (Manual Run)

When you have a large environment with thousands of clients you can clearly see the storage recovery benefits!.

Manual Script

If you want to run the script manually or deploy as a scheduled task, you can use the below PowerShell script (Download from https://gallery.technet.microsoft.com/scriptcenter/IIS-Log-Maintenance-6f357306);

<#	
	.NOTES
	===========================================================================
	 Created on:   	07/11/2016 22:05
	 Created by:   	Terence Beggs & Maurice Daly - DeployEverything.com
	 Organization: 	
	 Filename:     	PruneIISLogs.ps1
	===========================================================================
	.DESCRIPTION
		Clear up script for IIS logs. Removes logs older than the value specified 
		in the MaxDays variable.

		The script can be used as part of a Configuration Item in SCCM or as a scheduled
		task.
#>

# Specify the maximum log age file to maintain
$MaxDays = 7

function DiscoverIISLogs ($MaxDays)
{
	try
	{
		# Import IIS WebAdmin Module
		Import-Module WebAdministration
		
		# Return list of IIS webistes
		$IISSites = Get-Website
		# Loop for each IIS site
		foreach ($Site in $IISSites)
		{
			# Return path for IIS logs
			$IISLogs = $Site.LogFile.Directory
			# Condition to replace DOS %SystemDrive% variable with Powershell variable
			If ($IISLogs -like "*%SystemDrive%*")
			{
				$IISLogs = $IISLogs -replace "%SystemDrive%", "$env:SystemDrive"
			}
			# Count IIS Log files to prune
			$LogCount = $LogCount + (Get-ChildItem -Path $IISLogs -Recurse -Filter "*.log" | Where-Object { $(Get-Date).Subtract($_.LastWriteTime).Days -gt $MaxDays}).count
		}
		Return $LogCount
	}
	catch { return -1 }
}

DiscoverIISLogs ($MaxDays)

function PurgeIISLogs ($MaxDays)
{
	try
	{
		# Import IIS WebAdmin Module
		Import-Module WebAdministration
		
		# Return list of IIS webistes
		$IISSites = Get-Website
		# Loop for each IIS site
		foreach ($Site in $IISSites)
		{
			# Return path for IIS logs
			$IISLogs = $Site.LogFile.Directory
			# Condition to replace DOS %SystemDrive% variable with Powershell variable
			If ($IISLogs -like "*%SystemDrive%*")
			{
				$IISLogs = $IISLogs -replace "%SystemDrive%", "$env:SystemDrive"
			}
			# Purge IIS Log files
			Get-ChildItem -Path $IISLogs -Recurse -Filter "*.log" | Where-Object { $(Get-Date).Subtract($_.LastWriteTime).Days -gt $MaxDays } | Foreach-Object { Remove-Item $_.FullName -Force -Verbose }
		}
	}

catch {  }
}

PurgeIISLogs ($MaxDays)

References

A special thanks goes out to Johan Arwidmark for sharing this clean up script on his DeploymentResearch blog (http://deploymentresearch.com/Research/Post/563/A-little-bit-of-ConfigMgr-housekeeping).

Why you should not use the ConfigMgr rebuild index task — Steve Thompson [MVP]

Rebuilding the database indexes is a good thing, yes? Absolutely. Why not use the ConfigMgr rebuild index maintenance task? When the maintenance task works, and I have proof that some of the time it fails to run on ConfigMgr: It REINDEXES all indexes with more than 10% fragmentation It does not update statistics Let’s break […]

via Why you should not use the ConfigMgr rebuild index task — Steve Thompson [MVP]

SCCM Viglen Client Driver Autodownload PowerShell Script

viglen

In October of 2016 I released a script that downloaded drivers and bios updates for Dell client systems and created associated driver packs in SCCM (https://modalyitblog.wordpress.com/2016/10/10/sccm-dell-client-bios-driver-autodownload-ps-script/).

A friend of mine works in the UK educational system as an admin and thought it would be nice to have a similar script for his environment, mainly consisting of the UK educational hardware builder Viglen.

So here is version 1.0 of the download script adjusted for Viglen client systems.

The below scripts requires you to specify your driver file share and your SCCM site server name as a variable, it then does the following;

  1. Queries SCCM for a full list of Viglen client products
  2. Downloads the driver INF zip file for each model
  3. Extract the driver INF zip
  4. Import the drivers in the extracted ZIP folder
  5. Create a Category based on the machine model
  6. Create a Driver Package based on the machine model
  7. Imports the associated drivers into the newly created Driver Package

The downloads are stored within sub-folders within the share you specified, e.g;

\\MySCCMServer\Drivers\Viglen 820s\Driver ZIP\
\\MySCCMServer\Drivers\Viglen 820s\Extracted Drivers\

This slideshow requires JavaScript.

PowerShell Script

To run the script use the following syntax;

.\ViglenSCCMDownload.ps1 -SiteServer YOURSITESERVER -RepositoryPath “\\YOURSERVER\DRIVERREPO\” -PackagePath “\\YOURSERVER\DRIVERPACKPATH”

The script has Windows 10 x64 specified, however you can change this under the #Define Operating Systems section. Note I have tested Windows 7 & 10 only during building this.


<#
.NOTES
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.128
Created on: 03/11/2016 13:00
Created by: Maurice Daly
Filename: ViglenSCCMDownloads.ps1
==========================================================================
.DESCRIPTION
This script allows you to automate the process of keeping your
driver sources up to date. The script reads the Viglen support download
site for models found within SCCM and then downloads the corresponding
drivers

Version 1.0
Initial release

Notes
You can skip the driver package creation process by changing the
$DriverPackageCreation variable to $False.
The system architecture can also be changed by modifying the
$Architecture variable and using x64 or x86
You can limit the number of concurrent jobs by specifying
your max value in the $MaxConcurrent jobs variable

To run the script use the following syntax;

.\ViglenSCCMDownload.ps1 -SiteServer YOURSITESERVER -RepositoryPath UNCTEMPDRIVERPATH -PackagePath UNCDRIVERPACKAGEPATH

To re-enable error messaging for troubleshooting purpose
comment out the Error and Warning Preference values below

Use : This script is provided as it and I accept no responsibility for
any issues arising from its use.

Twitter : @modaly_it
Blog : https://modalyitblog.wordpress.com/
#>

# Define SCCM Site Server
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[parameter(Mandatory = $true, HelpMessage = "Site server where the SMS Provider is installed", Position = 1)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Connection -ComputerName $_ -Count 1 -Quiet })]
[string]$SiteServer,
[parameter(Mandatory = $true, HelpMessage = "UNC path for downloading and extracting drivers")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$RepositoryPath,
[parameter(Mandatory = $true, HelpMessage = "UNC path of your driver package repository")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$PackagePath
)

$ErrorActionPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'

# Import SCCM PowerShell Module
Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"

# Define Maximum Number Of Simultaneously Running Jobs
$MaxConcurrentJobs = 5

# Query SCCM Site Code
function QuerySiteCode ($SiteServer)
{
Write-Verbose "Determining SiteCode for Site Server: '$($SiteServer)'"
$SiteCodeObjects = Get-WmiObject -Namespace "root\SMS" -Class SMS_ProviderLocation -ComputerName $SiteServer -ErrorAction Stop
foreach ($SiteCodeObject in $SiteCodeObjects)
{
if ($SiteCodeObject.ProviderForLocalSite -eq $true)
{
$SiteCode = $SiteCodeObject.SiteCode
Write-Debug "SiteCode: $($SiteCode)"

}
}
Return [string]$SiteCode
}

function QueryModels ($SiteCode)
{
# ArrayList to store the Viglen models in
$ViglenProducts = New-Object -TypeName System.Collections.ArrayList
# Enumerate through all models
#$Models = "VIG430P","VIG665W","Vig670W","Vig800S","Vig820S","Vig830S"
$Models = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_G_System_COMPUTER_SYSTEM | Select-Object -Property Model | Where-Object { $_.Model -like "*Vig*" }
# Add model to ArrayList if not present
if ($Models -ne $null)
{
foreach ($Model in $Models)
{
if ($Model.Model -notin $ViglenProducts)
{
$ViglenProducts.Add($Model.Model) | Out-Null
}
}
}
#$ViglenProducts = "VIG430P", "VIG665W", "Vig670W", "Vig800S", "Vig820S", "Vig830S"
Return $ViglenProducts
}

function StartDownloadAndPackage ($PackagePath, $RepositoryPath)
{
$RunDownloadJob = {
Param ($Model,
$SiteCode,
$PackagePath,
$RepositoryPath)

# =================== DEFINE VARIABLES =====================

# Directory used for driver downloads
$DriverRepositoryRoot = ($RepositoryPath.Trimend("\") + "\Viglen\")
Write-Host "Driver package path set to $DriverRepositoryRoot"

# Directory used by SCCM for driver package
$DriverPackageRoot = ($PackagePath.Trimend("\") + "\Viglen\")
Write-Host "Driver package path set to $DriverPackageRoot"

# Define Operating System
$OperatingSystem = "Windows"
$OperatingSystemVersion = "10"
$Architecture = "64"

# Define Viglen Download Sources
$ViglenBaseURL = "http://download.viglen.co.uk"
$ViglenDownloadList = "/files/Motherboards/"
$ViglenBIOSFolder = "BIOS/"
$ViglenDriverFolder = "Drivers/"

# Import Driver Packs?
$DriverPackCreation = $true

# Import SCCM PowerShell Module
Import-Module "C:\Program Files (x86)\Microsoft Configuration Manager\AdminConsole\bin\ConfigurationManager.psd1"

# =================== INITIATE DOWNLOADS ===================

Write-Host "Getting download URL for Viglen client model: $Model"
$ModelLink = (Invoke-WebRequest -Uri ($ViglenBaseURL + $ViglenDownloadList) -UseBasicParsing).Links | Where-Object { $_.href -like "*$Model*" }
$ModelURL = ($ViglenBaseURL + $ModelLink.href)

# Correct slash direction issues
$ModelURL = $ModelURL.Replace("\", "/")

# ============= SCCM Driver Cab Download ==================

Write-Host "Getting SCCM driver pack link for model: $Model"
$ViglenInfFolder = (((Invoke-WebRequest -Uri ($ModelURL + $ViglenDriverFolder) -UseBasicParsing).links | Where-Object { $_.href -like "*INF*" }).href | Split-Path -Leaf) + "/"
$SCCMDriverDownload = ((Invoke-WebRequest -Uri ($ModelURL + $ViglenDriverFolder + $ViglenInfFolder) -UseBasicParsing).Links | Where-Object { $_.href -like "*$($OperatingSystem.trim("dows"))*$OperatingSystemVersion*.zip" } | Select-Object -Last 1).href
if ($SCCMDriverDownload -eq $null)
{
# Check for driver folder for specified OS
$DriverOSSubFolder = ((Invoke-WebRequest -Uri ($ModelURL + $ViglenDriverFolder + $ViglenInfFolder) -UseBasicParsing).Links | Where-Object { $_.outerHTML -like "*$OperatingSystem*$OperatingSystemVersion*" }).href | Split-Path -Leaf
if ($DriverOSSubFolder -eq $null)
{
Write-Host -ForegroundColor White -BackgroundColor Red "Driver does not exist for $Model running $OperatingSystem $OperatingSystemVersion"
Break
}
else
{
$SCCMDriverDownload = ((Invoke-WebRequest -Uri ($ModelURL + $ViglenDriverFolder + $ViglenInfFolder + $DriverOSSubFolder) -UseBasicParsing).Links | Where-Object { $_.href -like "*$($OperatingSystem.trim("dows"))*$OperatingSystemVersion*.zip" } | Select-Object -Last 1).href
}
}
else
{
# $SCCMDriverDownload = $SCCMDriverDownload.href
}

$SCCMDriverZIP = ($SCCMDriverDownload | Split-Path -Leaf)

# Check for destination directory, create if required and download the driver zip
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\Driver ZIP\")) -eq $true)
{
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\Driver ZIP\" + $SCCMDriverZIP)) -eq $true)
{
Write-Host -ForegroundColor Yellow "Skipping $SCCMDriverZIP... File already downloaded..."
$SkipDriver = $true
}
else
{
Start-BitsTransfer -Source ($ViglenBaseURL + $SCCMDriverDownload) -Destination ($DriverRepositoryRoot + $Model + "\Driver ZIP\" + $SCCMDriverZIP) -DisplayName "Downloading $Model driver ZIP file"
$SkipDriver = $false
}
}
else
{
Write-Host -ForegroundColor Green "Creating $Model download folder"
New-Item -Type dir -Path ($DriverRepositoryRoot + $Model + "\Driver ZIP")
Start-BitsTransfer -Source ($ViglenBaseURL + $SCCMDriverDownload) -Destination ($DriverRepositoryRoot + $Model + "\Driver ZIP\" + $SCCMDriverZIP) -DisplayName "Downloading $Model driver ZIP file"
}

# =================== CREATE DRIVER PACKAGE AND IMPORT DRIVERS ===================

Write-Host -ForegroundColor Green "Starting extract and import process"
$DriverSourceFile = ($DriverRepositoryRoot + $Model + "\Driver ZIP\" + $SCCMDriverZIP)
$DriverExtractDest = ($DriverRepositoryRoot + $Model + "\Extracted Drivers")
$DriverPackageDir = ($DriverSourceFile | Split-Path -Leaf)
$DriverPackageDir = $DriverPackageDir.Substring(0, $DriverPackageDir.length - 4)
$DriverFileDest = $DriverPackageRoot + "Viglen\" + $DriverPackageDir

if ($DriverPackCreation -eq $true)
{
if ((Test-Path -Path $DriverExtractDest) -eq $false)
{
New-Item -Type dir -Path $DriverExtractDest
}
else
{
Get-ChildItem -Path $DriverExtractDest -Recurse | Remove-Item -Recurse -Force
}
New-Item -Type dir -Path $DriverFileDest
Set-Location -Path ($SiteCode + ":")
$CMDDriverPackage = "Viglen " + $Model + " " + $OperatingSystem + " " + $OperatingSystemVersion + " " + $Architecture + "bit"
if (Get-CMDriverPackage -Name $CMDDriverPackage)
{
Write-Host -ForegroundColor Yellow "Skipping.. Driver package already exists.."
}
else
{
Write-Host -ForegroundColor Green "Creating driver package"
Set-Location -Path $env:SystemDrive
Add-Type -assembly "system.io.compression.filesystem"
[io.compression.zipfile]::ExtractToDirectory($DriverSourceFile, $DriverExtractDest)
$DriverINFFiles = Get-ChildItem -Path $DriverExtractDest -Recurse -Filter "*.inf"
}
Set-Location -Path ($SiteCode + ":")
New-CMDriverPackage -Name $CMDDriverPackage -path ($DriverPackageRoot + $Model + "\" + $OperatingSystem + " " + $OperatingSystemVersion + "\" + $Architecture)
if (Get-CMCategory -CategoryType DriverCategories -name ("Viglen " + $Model))
{
Write-Host -ForegroundColor Yellow "Category already exists"
$DriverCategory = Get-CMCategory -CategoryType DriverCategories -name ("Viglen " + $Model)
}
else
{
Write-Host -ForegroundColor Green "Creating category"
$DriverCategory = New-CMCategory -CategoryType DriverCategories -name ("Viglen " + $Model)
}
$DriverPackage = Get-CMDriverPackage -Name $CMDDriverPackage
foreach ($DriverINF in $DriverINFFiles)
{
$DriverInfo = Import-CMDriver -UncFileLocation ($DriverINF.FullName) -ImportDuplicateDriverOption AppendCategory -EnableAndAllowInstall $True -AdministrativeCategory $DriverCategory | Select-Object *
Add-CMDriverToDriverPackage -DriverID $DriverInfo.CI_ID -DriverPackageName $CMDDriverPackage
}
}
Set-Location -Path $env:SystemDrive
}
$TotalModelCount = $ViglenProducts.Count
$RemainingModels = $TotalModelCount
foreach ($Model in $ViglenProducts)
{
write-progress -activity "Initiate Driver Download & Driver Package Jobs" -status "Progress:" -percentcomplete (($TotalModelCount - $RemainingModels)/$TotalModelCount * 100)
$RemainingModels--
$Check = $false
while ($Check -eq $false)
{
if ((Get-Job -State 'Running').Count -lt $MaxConcurrentJobs)
{
Start-Job -ScriptBlock $RunDownloadJob -ArgumentList $Model, $SiteCode, $PackagePath, $RepositoryPath -Name ($Model + " Download")
$Check = $true
}
}
}
Get-Job | Wait-Job | Receive-Job
Get-Job | Remove-Job
}


# Get SCCM Site Code
$SiteCode = QuerySiteCode ($SiteServer)

Write-Debug $PackagePath
Write-Debug $RepositoryPath

if ($SiteCode -ne $null)
{
# Query Viglen Products in SCCM using QueryModels function
$ViglenProducts = QueryModels ($SiteCode)
# Output the members of the ArrayList
if ($ViglenProducts.Count -ge 1)
{
foreach ($ModelItem in $ViglenProducts)
{
$PSObject = [PSCustomObject]@{
"Viglen Models Found" = $ModelItem
}
Write-Output $PSObject
}
}
# Start download, extract, import and package process
Write-Host -ForegroundColor Green "Starting download, extract, import and driver package build process.."
StartDownloadAndPackage ($PackagePath) ($RepositoryPath) ($SiteCode)
}

Updating SCCM Package Source Locations

I’ve recently seen people asking on forums about how to modify the source settings of SCCM packages when migrating software installation files from an old server to a new one. Obviously manually updating packages is an option, but this will take time.. so this is achieved very easily by running the PowerShell code below


<#
.NOTES
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.128
Created on: 31/10/2016 14:13
Created by: Maurice.Daly
Organization:
Filename: UpdatePkgSource.ps1
===========================================================================
.DESCRIPTION
Updates the source location for SCCM packages and update your distribution points
Provided as is with no support. Run at your own risk.
#>
$OldSource = "\\YOUROLDSERVER\PACKAGESOURCE"
$NewSource = "\\YOURNEWSERVER\PACKAGESOURCE"

foreach ($Package in (Get-CMPackage | Where-Object { $_.PkgSourcePath -like "*$OldSource*" }))
{
Write-Host "Modifying $($Package.name) with new location $NewSource"
$UpdatedSource = $Package.PkgSourcePath -replace $OldSource, $NewSource
# Update source location of package
Set-CMPackage -id $Package.ID -Path $UpdatedSource
# Force update of distribution points
Get-CMPackage -id $Package.ID | Update-CMDistriubtionPoint
}

SCCM Dell Client Bios & Driver Autodownload PowerShell Script

dell
If you are a Dell hardware house then this script might come in useful for you.

As you are probably aware Dell provide excellent support for SCCM deployments via their Dell Command integration software and up to date driver cab files via their Enterprise Client Deployment site at http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment.

dellsccmscreen1

When I was refreshing my driver and bios update file repository I got thinking wouldn’t it be nice if I could just run a script that would download these update files based on the models of Dell client systems listed in my SCCM device collections?.

I found a script on Dustin Hedges blog (https://deploymentramblings.wordpress.com/2014/04/17/downloading-dell-driver-cab-files-automagically-with-the-driver-pack-catalog/) but I wanted to automate this further.

So here is my resulting effort. The below scripts requires you to specify your driver file share and your SCCM site server name as a variable, it then does the following;

  1. Queries SCCM for a full list of Dell enterprise client products (Optiplex & Latitude)
  2. Downloads BIOS updates for each model
  3. Downloads the driver CAB for each model
  4. Extract the driver CAB
  5. Import the drivers in the extracted CAB folder
  6. Create a Category based on the machine model
  7. Create a Driver Package based on the machine model and filename
  8. Imports the associated drivers into the newly created Driver Package
  9. Creates a BIOS Update Package based on machine model
  10. Creates a BIOS update deployment PowerShell script for each model using the latest BIOS update and silent switches

Progress bars have also been added for both the system model and driver import stage.

The downloads are stored within sub-folders within the share you specified, e.g;

\\MySCCMServer\Drivers\Dell Optiplex 7040\BIOS
\\MySCCMServer\Drivers\Dell Optiplex 7040\Driver Cabs\

This slideshow requires JavaScript.

Automatically created SCCM Driver Packages:

dellsccmscreen7

SCCM Driver Package Contents;

dellsccmscreen8

 

Multi-Threaded Script

To run the script use the following syntax;

.\DellDownloads.ps1 -SiteServer YOURSITESERVER -RepositoryPath “\\YOURSERVER\DRIVERREPO\” -PackagePath “\\YOURSERVER\DRIVERPACKPATH”

dellmultithread
Multi-Thread Script In Use (Running Code in PS Console)

Task Sequence BIOS Update Script

In the latest release BIOS packaging is included, it also generates a PowerShell script for use at deployment time which is contained within the BIOS folder of the model and uses the latest BIOS exe with silent switches for a silent upgrade.

Note: If you are using a BIOS setup password (which you should be), you will need to specify this within the script (unless you want me to update the script to look for this run running it from the shell).

Modify the following line – $BIOSSwitches = ” -noreboot -nopause /p=%YOURBIOSPASSWORD ”

This slideshow requires JavaScript.

 

UPDATE LOG

08/11/2016
The script has been updated with the following functionality;

  1. Creates BIOS packages for each model downloaded
  2. Creates a deployment PowerShell script containing the latest BIOS exe name and switches for a silent / no reboot update of the BIOS
  3. If the script is re-run it will automatically update the BIOS exe to use in the deployment PS script and update the distribution points.

28/10/2016
As a response to feedback, I have added a $MaxConcurrent jobs variable into the multi-threaded script that lets you specify the max number of jobs in order to control CPU utilization.

26/10/2016
I have added in an additional script below which is multi-threaded. This should help reduce the overall time to download, extract and create the driver packages in large environments with a wide range of models.

16/10/2016
Additional functionality has been added to now automate the process of extracting the CAB, creating computer categories, import the drivers into SCCM and create a driver pack for each of the models / driver packs downloaded.

Script Download Link – https://gallery.technet.microsoft.com/scriptcenter/SCCM-Dell-Client-Bios-ee577b04?redir=0

<#
.NOTES
===========================================================================
Created with: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.128
Created on: 16/10/2016 13:00
Created by: Maurice Daly
Filename: DellDownloads.ps1
===========================================================================
.DESCRIPTION
This script allows you to automate the process of keeping your Dell
driver and BIOS update sources up to date. The script reads the Dell
SCCM driver pack site for models you have specified and then downloads
the corresponding latest driver packs and BIOS updates.

Version 1.0
Retreive Dell models and download BIOS and Driver Packs
Version 2.0
Added driver CAB file extract, create new driver pack, category creation
and import driver functions.
Version 2.1
Added multi-threading
Version 2.2
Added Max Concurrent jobs setting for limiting CPU utilisation
Version 2.3
Replaced Invoke-WebRequest download with BITS enabled downloads for
improved performance
Version 2.4
Updated code and separated functions. Added required variables via commandline
Version 3.0
Creates BIOS Packages for each model and writes update powershell file for deployment
with SCCM.

Notes
You can skip the driver package creation process by changing the
$DriverPackageCreation variable to $False.
The system architecture can also be changed by modifying the
$Architecture variable and using x64 or x86

To re-enable error messaging for troubleshooting purpose
comment out the Error and Warning Preference values below

Use : This script is provided as it and I accept no responsibility for
any issues arising from its use.

Twitter : @modaly_it
Blog : https://modalyitblog.com/
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param (
[parameter(Mandatory = $true, HelpMessage = "Site server where the SMS Provider is installed", Position = 1)]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Connection -ComputerName $_ -Count 1 -Quiet })]
[string]$SiteServer,
[parameter(Mandatory = $true, HelpMessage = "UNC path for downloading and extracting drivers")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$RepositoryPath,
[parameter(Mandatory = $true, HelpMessage = "UNC path of your driver package repository")]
[ValidateNotNullOrEmpty()]
[ValidateScript({ Test-Path $_ })]
[string]$PackagePath
)

$ErrorActionPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'

# Define Maximum Number Of Simultaneously Running Jobs
$MaxConcurrentJobs = 5

$ErrorActionPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'

# Import SCCM PowerShell Module
$ModuleName = (get-item $env:SMS_ADMIN_UI_PATH).parent.FullName + "\ConfigurationManager.psd1"
Import-Module $ModuleName

# Query SCCM Site Code
function QuerySiteCode ($SiteServer)
{
Write-Debug "Determining SiteCode for Site Server: '$($SiteServer)'"
$SiteCodeObjects = Get-WmiObject -Namespace "root\SMS" -Class SMS_ProviderLocation -ComputerName $SiteServer -ErrorAction Stop
foreach ($SiteCodeObject in $SiteCodeObjects)
{
if ($SiteCodeObject.ProviderForLocalSite -eq $true)
{
$SiteCode = $SiteCodeObject.SiteCode
Write-Debug "SiteCode: $($SiteCode)"

}
}
Return [string]$SiteCode
}

function QueryModels ($SiteCode)
{
# ArrayList to store the Dell models in
$DellProducts = New-Object -TypeName System.Collections.ArrayList
# Enumerate through all models
$Models = Get-WmiObject -Namespace "root\SMS\site_$($SiteCode)" -Class SMS_G_System_COMPUTER_SYSTEM | Select-Object -Property Model | Where-Object { ($_.Model -like "*Optiplex*") -or ($_.Model -like "*Latitude*") }
# Add model to ArrayList if not present
if ($Models -ne $null)
{
foreach ($Model in $Models)
{
if ($Model.Model -notin $DellProducts)
{
$DellProducts.Add($Model.Model) | Out-Null
}
}
}
Return $DellProducts
}

function StartDownloadAndPackage ($PackagePath, $RepositoryPath, $SiteCode, $DellProducts)
{
$RunDownloadJob = {
Param ($Model,
$SiteCode,
$PackagePath,
$RepositoryPath)

# =================== DEFINE VARIABLES =====================
# Import SCCM PowerShell Module
$ModuleName = (get-item $env:SMS_ADMIN_UI_PATH).parent.FullName + "\ConfigurationManager.psd1"
Import-Module $ModuleName

# Directory used for driver downloads
$DriverRepositoryRoot = ($RepositoryPath.Trimend("\") + "\Dell\")
Write-Host "Driver package path set to $DriverRepositoryRoot"

# Directory used by SCCM for driver package
$DriverPackageRoot = $PackagePath
Write-Host "Driver package path set to $DriverPackageRoot"

# Define Operating System
$OperatingSystem = "Windows 10"
$Architecture = "x64"

# Define Dell Download Sources
$DellDownloadList = "http://downloads.dell.com/published/Pages/index.html"
$DellDownloadBase = "http://downloads.dell.com"
$DellSCCMDriverList = "http://en.community.dell.com/techcenter/enterprise-client/w/wiki/2065.dell-command-deploy-driver-packs-for-enterprise-client-os-deployment"
$DellSCCMBase = "http://en.community.dell.com"

# Import Driver Packs?
$DriverPackCreation = $true

# =================== INITIATE DOWNLOADS ===================

# ============= BIOS Upgrade Download ==================

Write-Host "Getting download URL for Dell client model: $Model"
$ModelLink = (Invoke-WebRequest -Uri $DellDownloadList).Links | Where-Object { $_.outerText -eq $Model }
$ModelURL = (Split-Path $DellDownloadList -Parent) + "/" + ($ModelLink.href)

# Correct slash direction issues
$ModelURL = $ModelURL.Replace("\", "/")
$BIOSDownload = (Invoke-WebRequest -Uri $ModelURL -UseBasicParsing).Links | Where-Object { ($_.outerHTML -like "*BIOS*") -and ($_.outerHTML -like "*WINDOWS*") } | select -First 1
$BIOSFile = $BIOSDownload.href | Split-Path -Leaf

# Check for destination directory, create if required and download the BIOS upgrade file
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\BIOS")) -eq $true)
{
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\BIOS\" + $BIOSFile)) -eq $false)
{
Write-Host -ForegroundColor Green "Downloading $($BIOSFile) BIOS update file"
# Invoke-WebRequest ($DellDownloadBase + $BIOSDownload.href) -OutFile ($DriverRepositoryRoot + $Model + "\BIOS\" + $BIOSFile) -UseBasicParsing
Start-BitsTransfer ($DellDownloadBase + $BIOSDownload.href) -Destination ($DriverRepositoryRoot + $Model + "\BIOS\" + $BIOSFile)
}
else
{
Write-Host -ForegroundColor Yellow "Skipping $BIOSFile... File already downloaded..."
}
}
else
{
Write-Host -ForegroundColor Green "Creating $Model download folder"
New-Item -Type dir -Path ($DriverRepositoryRoot + $Model + "\BIOS")
Write-Host -ForegroundColor Green "Downloading $($BIOSFile) BIOS update file"
# Invoke-WebRequest ($DellDownloadBase + $BIOSDownload.href) -OutFile ($DriverRepositoryRoot + $Model + "\BIOS\" + $BIOSFile) -UseBasicParsing
Start-BitsTransfer ($DellDownloadBase + $BIOSDownload.href) -Destination ($DriverRepositoryRoot + $Model + "\BIOS\" + $BIOSFile)
}

# ============= SCCM Driver Cab Download ==================

Write-Host "Getting SCCM driver pack link for model: $Model"
$ModelLink = (Invoke-WebRequest -Uri $DellSCCMDriverList -UseBasicParsing).Links | Where-Object { ($_.outerHTML -like "*$Model*") -and ($_.outerHTML -like "*$OperatingSystem*") } | select -First 1
$ModelURL = $DellSCCMBase + ($ModelLink.href)

# Correct slash direction issues
$ModelURL = $ModelURL.Replace("\", "/")
$SCCMDriverDownload = (Invoke-WebRequest -Uri $ModelURL -UseBasicParsing).Links | Where-Object { $_.href -like "*.cab" }
$SCCMDriverCab = $SCCMDriverDownload.href | Split-Path -Leaf

# Check for destination directory, create if required and download the driver cab
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\Driver Cab\")) -eq $true)
{
if ((Test-Path -Path ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab)) -eq $false)
{
Write-Host -ForegroundColor Green "Downloading $($SCCMDriverCab) driver cab file"
# Invoke-WebRequest ($SCCMDriverDownload.href) -OutFile ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab) -UseBasicParsing
Start-BitsTransfer -Source ($SCCMDriverDownload.href) -Destination ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab)
$SkipDriver = $false
}
else
{
Write-Host -ForegroundColor Yellow "Skipping $SCCMDriverCab... File already downloaded..."
$SkipDriver = $true
}
}
else
{
Write-Host -ForegroundColor Green "Creating $Model download folder"
New-Item -Type dir -Path ($DriverRepositoryRoot + $Model + "\Driver Cab")
Write-Host -ForegroundColor Green "Downloading $($SCCMDriverCab) driver cab file"
#Invoke-WebRequest ($SCCMDriverDownload.href) -OutFile ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab)
Start-BitsTransfer -Source ($SCCMDriverDownload.href) -Destination ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab)
}

# =================== CREATE BIOS UPDATE PACKAGE ===========================

$BIOSUpdatePackage = ("Dell" + " " + $Model + " " + "BIOS UPDATE")
$BIOSUpdateRoot = ($DriverRepositoryRoot + $Model + "\BIOS\")

Set-Location -Path ($SiteCode + ":")
if ((Get-CMPackage -name $BIOSUpdatePackage) -eq $null)
{
Write-Host -ForegroundColor Green "Creating BIOS Package"
New-CMPackage -Name "$BIOSUpdatePackage" -Path $BIOSUpdateRoot -Description "Dell $Model BIOS Updates" -Manufacturer "Dell" -Language English
}
Set-Location -Path $env:SystemDrive
$BIOSUpdateScript = ($BIOSUpdateRoot + "BIOSUpdate.ps1")
$CurrentBIOSFile = Get-ChildItem -Path $BIOSUpdateRoot -Filter *.exe -Recurse | Sort-Object $_.LastWriteTime | select -First 1
if ((Test-Path -Path $BIOSUpdateScript) -eq $False)
{
# Create BIOSUpdate.ps1 Deployment Script
New-Item -Path ($BIOSUpdateRoot + "BIOSUpdate.ps1") -ItemType File
$BIOSSwitches = " -noreboot -nopause "
Add-Content -Path $BIOSUpdateScript ('$CurrentBIOSFile=' + '"' + $($CurrentBIOSFile.name) + '"')
Add-Content -Path $BIOSUpdateScript ('$BIOSSwitches=' + '"' + $($BIOSSwitches) + '"')
Add-Content -Path $BIOSUpdateScript ('Start-Process $CurrentBIOSFile -ArgumentList $BIOSSwitches')
}
else
{
# Check if older BIOS update exists and update BIOSUpdate deployment script
$BIOSFileCount = (Get-ChildItem -Path $BIOSUpdateRoot -Filter *.exe -Recurse).count
if ($BIOSFileCount -gt 1)
{
$OldBIOSFiles = Get-ChildItem -Path $BIOSUpdateRoot -Filter *.exe -Recurse | Where-Object { $_.Name -ne $CurrentBIOSFile.name }

foreach ($OldBIOS in $OldBIOSFiles)
{
(Get-Content -Path $BIOSUpdateScript) -replace $OldBIOS.name, $CurrentBIOSFile.name | Set-Content -Path $BIOSUpdateScript
}
}
}
# Refresh Distribution Points
Get-CMPackage -name $BIOSUpdatePackage | Update-CMDistributionPoint
}

# =================== CREATE DRIVER PACKAGE AND IMPORT DRIVERS ===================

$DriverSourceCab = ($DriverRepositoryRoot + $Model + "\Driver Cab\" + $SCCMDriverCab)
$DriverExtractDest = ($DriverRepositoryRoot + $Model + "\Extracted Drivers")
$DriverPackageDir = ($DriverSourceCab | Split-Path -Leaf)
$DriverPackageDir = $DriverPackageDir.Substring(0, $DriverPackageDir.length - 4)
$DriverCabDest = $DriverPackageRoot + "\Dell\" + $DriverPackageDir

if ($DriverPackCreation -eq $true)
{
if ((Test-Path -Path $DriverExtractDest) -eq $false)
{
New-Item -Type dir -Path $DriverExtractDest
}
else
{
Get-ChildItem -Path $DriverExtractDest -Recurse | Remove-Item -Recurse -Force
}
New-Item -Type dir -Path $DriverCabDest
Set-Location -Path ($SiteCode + ":")
$CMDDriverPackage = "Dell " + $Model + " " + "(" + $DriverPackageDir + ")" + " " + $Architecture
if (Get-CMDriverPackage -Name $CMDDriverPackage)
{
Write-Host -ForegroundColor Yellow "Skipping.. Driver package already exists.."
}
else
{
Write-Host -ForegroundColor Green "Creating driver package"
Set-Location -Path $env:SystemDrive
Expand "$DriverSourceCab" -F:* "$DriverExtractDest"
$DriverINFFiles = Get-ChildItem -Path $DriverExtractDest -Recurse -Filter "*.inf" | Where-Object { $_.FullName -like "*$Architecture*" }
Set-Location -Path ($SiteCode + ":")
# Get-Location | Out-File -FilePath C:\Location2.txt
New-CMDriverPackage -Name $CMDDriverPackage -path ($DriverPackageRoot + "\Dell\" + $DriverPackageDir + "\" + $OperatingSystem + "\" + $Architecture)
if (Get-CMCategory -CategoryType DriverCategories -name ("Dell " + $Model))
{
Write-Host -ForegroundColor Yellow "Category already exists"
$DriverCategory = Get-CMCategory -CategoryType DriverCategories -name ("Dell " + $Model)
}
else
{
Write-Host -ForegroundColor Green "Creating category"
$DriverCategory = New-CMCategory -CategoryType DriverCategories -name ("Dell " + $Model)
}
$DriverPackage = Get-CMDriverPackage -Name $CMDDriverPackage
foreach ($DriverINF in $DriverINFFiles)
{
$DriverInfo = Import-CMDriver -UncFileLocation ($DriverINF.FullName) -ImportDuplicateDriverOption AppendCategory -EnableAndAllowInstall $True -AdministrativeCategory $DriverCategory | Select-Object *
Add-CMDriverToDriverPackage -DriverID $DriverInfo.CI_ID -DriverPackageName $CMDDriverPackage
}
}
Set-Location -Path $env:SystemDrive
}

$TotalModelCount = $DellProducts.Count
$RemainingModels = $TotalModelCount
foreach ($Model in $DellProducts)
{
write-progress -activity "Initiate Driver Download &amp;amp;amp; Driver Package Jobs" -status "Progress:" -percentcomplete (($TotalModelCount - $RemainingModels)/$TotalModelCount * 100)
$RemainingModels--
$Check = $false
while ($Check -eq $false)
{
if ((Get-Job -State 'Running').Count -lt $MaxConcurrentJobs)
{
Start-Job -ScriptBlock $RunDownloadJob -ArgumentList $Model, $SiteCode, $PackagePath, $RepositoryPath -Name ($Model + " Download")
$Check = $true
}
}
}
Get-Job | Wait-Job | Receive-Job
Get-Job | Remove-Job
}

# Get SCCM Site Code
$SiteCode = QuerySiteCode ($SiteServer)

Write-Debug $PackagePath
Write-Debug $RepositoryPath

if ($SiteCode -ne $null)
{
# Query Dell Products in SCCM using QueryModels function
$DellProducts = QueryModels ($SiteCode)
# Output the members of the ArrayList
if ($DellProducts.Count -ge 1)
{
foreach ($ModelItem in $DellProducts)
{
$PSObject = [PSCustomObject]@{
"Dell Models Found" = $ModelItem
}
Write-Output $PSObject
Write-Debug $PSObject
}
}
# Start download, extract, import and package process
Write-Host -ForegroundColor Green "Starting download, extract, import and driver package build process.."
StartDownloadAndPackage ($PackagePath) ($RepositoryPath) ($SiteCode) ($DellProducts)
}

Custom PowerShell Reboot GUI

When deploying software via SCCM I thought wouldn’t it be nice if there was greater flexibility regarding system reboot prompts for the end user. Sure you can enable a maintenance window and push your software out during that time, but we have at times been caught where a software push is needed during business hours.

So I came up with this PowerShell script which you can run as part of a task sequence when deploying emergency/unscheduled software installs. The script generates a GUI which provides the end-user with three options;

  1. Restart the computer
  2. Schedule a restart (note in here I have hard-coded this for 6pm)
  3. Cancel the restart

The script also starts a count-down timer to automatically restart the computer after 3 minutes if no user interaction occurs.

customrestart

Example Script Use – SCCM TS

In the below example we are going to create a Package in SCCM which contains the script file, you will also need to include two exe files from MDT which allow you to run the script in interactive mode.

Locate ServiceUI.exe and TSProgressUI.exe (obviously picking the x86 or x64 where applicable) and add these into your package source. You should have something that looks like this ;

rebootfiles

Now add a Run Command Line entry into your TS and use the following command line;

ServiceUI.exe -process:TSProgressUI.exe %SYSTEMROOT%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File CustomRestart.ps1

rebootts

When the Task Sequence is run, you should now have the restart prompt appear;

rebootcapture

Script Source


<#
 .NOTES
 --------------------------------------------------------------------------------
 Code generated by: SAPIEN Technologies, Inc., PowerShell Studio 2016 v5.2.128
 Generated on: 04/10/2016 10:13
 Generated by: Maurice.Daly
 --------------------------------------------------------------------------------
 .DESCRIPTION
 Provides an reboot prompt which counts down from 3 minutes and allows the
 end user to schedule or cancel the reboot.
#>

#----------------------------------------------
#region Import Assemblies
#----------------------------------------------
[void][Reflection.Assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
[void][Reflection.Assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
[void][Reflection.Assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
#endregion Import Assemblies

#Define a Param block to use custom parameters in the project
#Param ($CustomParameter)

function Main {
<#
 .SYNOPSIS
 The Main function starts the project application.

 .PARAMETER Commandline
 $Commandline contains the complete argument string passed to the script packager executable.

 .NOTES
 Use this function to initialize your script and to call GUI forms.

 .NOTES
 To get the console output in the Packager (Forms Engine) use:
 $ConsoleOutput (Type: System.Collections.ArrayList)
#>
 Param ([String]$Commandline)

 #--------------------------------------------------------------------------
 #TODO: Add initialization script here (Load modules and check requirements)

 #--------------------------------------------------------------------------

 if((Call-MainForm_psf) -eq 'OK')
 {

 }

 $global:ExitCode = 0 #Set the exit code for the Packager
}

#endregion Source: Startup.pss

#region Source: MainForm.psf
function Call-MainForm_psf
{

 #----------------------------------------------
 #region Import the Assemblies
 #----------------------------------------------
 [void][reflection.assembly]::Load('System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
 [void][reflection.assembly]::Load('System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089')
 [void][reflection.assembly]::Load('System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a')
 #endregion Import Assemblies

 #----------------------------------------------
 #region Generated Form Objects
 #----------------------------------------------
 [System.Windows.Forms.Application]::EnableVisualStyles()
 $MainForm = New-Object 'System.Windows.Forms.Form'
 $panel2 = New-Object 'System.Windows.Forms.Panel'
 $ButtonCancel = New-Object 'System.Windows.Forms.Button'
 $ButtonSchedule = New-Object 'System.Windows.Forms.Button'
 $ButtonRestartNow = New-Object 'System.Windows.Forms.Button'
 $panel1 = New-Object 'System.Windows.Forms.Panel'
 $labelITSystemsMaintenance = New-Object 'System.Windows.Forms.Label'
 $labelSecondsLeftToRestart = New-Object 'System.Windows.Forms.Label'
 $labelTime = New-Object 'System.Windows.Forms.Label'
 $labelInOrderToApplySecuri = New-Object 'System.Windows.Forms.Label'
 $timerUpdate = New-Object 'System.Windows.Forms.Timer'
 $InitialFormWindowState = New-Object 'System.Windows.Forms.FormWindowState'
 #endregion Generated Form Objects

 #----------------------------------------------
 # User Generated Script
 #----------------------------------------------
 $TotalTime = 180 #in seconds

 $MainForm_Load={
 #TODO: Initialize Form Controls here
 $labelTime.Text = "{0:D2}" -f $TotalTime #$TotalTime
 #Add TotalTime to current time
 $script:StartTime = (Get-Date).AddSeconds($TotalTime)
 #Start the timer
 $timerUpdate.Start()
 }

 $timerUpdate_Tick={
 # Define countdown timer
 [TimeSpan]$span = $script:StartTime - (Get-Date)
 #Update the display
 $labelTime.Text = "{0:N0}" -f $span.TotalSeconds
 $timerUpdate.Start()
 if ($span.TotalSeconds -le 0)
 {
 $timerUpdate.Stop()
 Restart-Computer -Force
 }

 }

 $ButtonRestartNow_Click = {
 # Restart the computer immediately
 Restart-Computer -Force
 }

 $ButtonSchedule_Click={
 # Schedule restart for 6pm
 (schtasks /create /sc once /tn "Post Maintenance Restart" /tr "shutdown - r -f ""restart""" /st 18:00 /f)
 $MainForm.Close()
 }

 $ButtonCancel_Click={
 #TODO: Place custom script here
 $MainForm.Close()
 }

 $labelITSystemsMaintenance_Click={
 #TODO: Place custom script here

 }

 $panel2_Paint=[System.Windows.Forms.PaintEventHandler]{
 #Event Argument: $_ = [System.Windows.Forms.PaintEventArgs]
 #TODO: Place custom script here

 }

 $labelTime_Click={
 #TODO: Place custom script here

 }
 # --End User Generated Script--
 #----------------------------------------------
 #region Generated Events
 #----------------------------------------------

 $Form_StateCorrection_Load=
 {
 #Correct the initial state of the form to prevent the .Net maximized form issue
 $MainForm.WindowState = $InitialFormWindowState
 }

 $Form_StoreValues_Closing=
 {
 #Store the control values
 }

 $Form_Cleanup_FormClosed=
 {
 #Remove all event handlers from the controls
 try
 {
 $ButtonCancel.remove_Click($buttonCancel_Click)
 $ButtonSchedule.remove_Click($ButtonSchedule_Click)
 $ButtonRestartNow.remove_Click($ButtonRestartNow_Click)
 $panel2.remove_Paint($panel2_Paint)
 $labelITSystemsMaintenance.remove_Click($labelITSystemsMaintenance_Click)
 $labelTime.remove_Click($labelTime_Click)
 $MainForm.remove_Load($MainForm_Load)
 $timerUpdate.remove_Tick($timerUpdate_Tick)
 $MainForm.remove_Load($Form_StateCorrection_Load)
 $MainForm.remove_Closing($Form_StoreValues_Closing)
 $MainForm.remove_FormClosed($Form_Cleanup_FormClosed)
 }
 catch [Exception]
 { }
 }
 #endregion Generated Events

 #----------------------------------------------
 #region Generated Form Code
 #----------------------------------------------
 $MainForm.SuspendLayout()
 $panel2.SuspendLayout()
 $panel1.SuspendLayout()
 #
 # MainForm
 #
 $MainForm.Controls.Add($panel2)
 $MainForm.Controls.Add($panel1)
 $MainForm.Controls.Add($labelSecondsLeftToRestart)
 $MainForm.Controls.Add($labelTime)
 $MainForm.Controls.Add($labelInOrderToApplySecuri)
 $MainForm.AutoScaleDimensions = '6, 13'
 $MainForm.AutoScaleMode = 'Font'
 $MainForm.BackColor = 'White'
 $MainForm.ClientSize = '373, 279'
 $MainForm.MaximizeBox = $False
 $MainForm.MinimizeBox = $False
 $MainForm.Name = 'MainForm'
 $MainForm.ShowIcon = $False
 $MainForm.ShowInTaskbar = $False
 $MainForm.StartPosition = 'CenterScreen'
 $MainForm.Text = 'Systems Maintenance'
 $MainForm.TopMost = $True
 $MainForm.add_Load($MainForm_Load)
 #
 # panel2
 #
 $panel2.Controls.Add($ButtonCancel)
 $panel2.Controls.Add($ButtonSchedule)
 $panel2.Controls.Add($ButtonRestartNow)
 $panel2.BackColor = 'ScrollBar'
 $panel2.Location = '0, 205'
 $panel2.Name = 'panel2'
 $panel2.Size = '378, 80'
 $panel2.TabIndex = 9
 $panel2.add_Paint($panel2_Paint)
 #
 # ButtonCancel
 #
 $ButtonCancel.Location = '250, 17'
 $ButtonCancel.Name = 'ButtonCancel'
 $ButtonCancel.Size = '77, 45'
 $ButtonCancel.TabIndex = 7
 $ButtonCancel.Text = 'Cancel'
 $ButtonCancel.UseVisualStyleBackColor = $True
 $ButtonCancel.add_Click($buttonCancel_Click)
 #
 # ButtonSchedule
 #
 $ButtonSchedule.Font = 'Microsoft Sans Serif, 8.25pt, style=Bold'
 $ButtonSchedule.Location = '139, 17'
 $ButtonSchedule.Name = 'ButtonSchedule'
 $ButtonSchedule.Size = '105, 45'
 $ButtonSchedule.TabIndex = 6
 $ButtonSchedule.Text = 'Schedule - 6pm'
 $ButtonSchedule.UseVisualStyleBackColor = $True
 $ButtonSchedule.add_Click($ButtonSchedule_Click)
 #
 # ButtonRestartNow
 #
 $ButtonRestartNow.Font = 'Microsoft Sans Serif, 8.25pt, style=Bold'
 $ButtonRestartNow.ForeColor = 'DarkRed'
 $ButtonRestartNow.Location = '42, 17'
 $ButtonRestartNow.Name = 'ButtonRestartNow'
 $ButtonRestartNow.Size = '91, 45'
 $ButtonRestartNow.TabIndex = 0
 $ButtonRestartNow.Text = 'Restart Now'
 $ButtonRestartNow.UseVisualStyleBackColor = $True
 $ButtonRestartNow.add_Click($ButtonRestartNow_Click)
 #
 # panel1
 #
 $panel1.Controls.Add($labelITSystemsMaintenance)
 $panel1.BackColor = '0, 114, 198'
 $panel1.Location = '0, 0'
 $panel1.Name = 'panel1'
 $panel1.Size = '375, 67'
 $panel1.TabIndex = 8
 #
 # labelITSystemsMaintenance
 #
 $labelITSystemsMaintenance.Font = 'Microsoft Sans Serif, 14.25pt'
 $labelITSystemsMaintenance.ForeColor = 'White'
 $labelITSystemsMaintenance.Location = '11, 18'
 $labelITSystemsMaintenance.Name = 'labelITSystemsMaintenance'
 $labelITSystemsMaintenance.Size = '269, 23'
 $labelITSystemsMaintenance.TabIndex = 1
 $labelITSystemsMaintenance.Text = 'IT Systems Maintenance'
 $labelITSystemsMaintenance.TextAlign = 'MiddleLeft'
 $labelITSystemsMaintenance.add_Click($labelITSystemsMaintenance_Click)
 #
 # labelSecondsLeftToRestart
 #
 $labelSecondsLeftToRestart.AutoSize = $True
 $labelSecondsLeftToRestart.Font = 'Microsoft Sans Serif, 9pt, style=Bold'
 $labelSecondsLeftToRestart.Location = '87, 176'
 $labelSecondsLeftToRestart.Name = 'labelSecondsLeftToRestart'
 $labelSecondsLeftToRestart.Size = '155, 15'
 $labelSecondsLeftToRestart.TabIndex = 5
 $labelSecondsLeftToRestart.Text = 'Seconds left to restart :'
 #
 # labelTime
 #
 $labelTime.AutoSize = $True
 $labelTime.Font = 'Microsoft Sans Serif, 9pt, style=Bold'
 $labelTime.ForeColor = '192, 0, 0'
 $labelTime.Location = '237, 176'
 $labelTime.Name = 'labelTime'
 $labelTime.Size = '43, 15'
 $labelTime.TabIndex = 3
 $labelTime.Text = '00:60'
 $labelTime.TextAlign = 'MiddleCenter'
 $labelTime.add_Click($labelTime_Click)
 #
 # labelInOrderToApplySecuri
 #
 $labelInOrderToApplySecuri.Font = 'Microsoft Sans Serif, 9pt'
 $labelInOrderToApplySecuri.Location = '12, 84'
 $labelInOrderToApplySecuri.Name = 'labelInOrderToApplySecuri'
 $labelInOrderToApplySecuri.Size = '350, 83'
 $labelInOrderToApplySecuri.TabIndex = 2
 $labelInOrderToApplySecuri.Text = 'In order to apply security patches and updates for your system, your machine must be restarted. 

If you do not wish to restart you computer at this time please click on the cancel button below.'
 #
 # timerUpdate
 #
 $timerUpdate.add_Tick($timerUpdate_Tick)
 $panel1.ResumeLayout()
 $panel2.ResumeLayout()
 $MainForm.ResumeLayout()
 #endregion Generated Form Code

 #----------------------------------------------

 #Save the initial state of the form
 $InitialFormWindowState = $MainForm.WindowState
 #Init the OnLoad event to correct the initial state of the form
 $MainForm.add_Load($Form_StateCorrection_Load)
 #Clean up the control events
 $MainForm.add_FormClosed($Form_Cleanup_FormClosed)
 #Store the control values when form is closing
 $MainForm.add_Closing($Form_StoreValues_Closing)
 #Show the Form
 return $MainForm.ShowDialog()

}
#endregion Source: MainForm.psf

#Start the application
Main ($CommandLine)

Download Link
The script is available to download from:
https://gallery.technet.microsoft.com/scriptcenter/Custom-PowerShell-GUI-7c7fbda8

WMUG 10th Anniversary Event – 13th July

WMUG

1_454x340

The WMUG team are hosting their 10th anniversary event in Microsoft, Paddington on the 13th of July 2016. The event is shaping up to be one of the biggest so far with guest speakers including;

  • Aaron Czechowski – @AaronCzechowski
    Senior Product Manager at Microsoft for both ConfigMgr and MDT
  • Nickolaj Andersen  – @NickolajA
    Senior consultant with Lumagate in Sweden, specialising in ConfigMgr – blog http://www.scconfigmgr.com/)
  • Marcus Robinson – @techdiction
    Technical Evangelist with Microsoft UK

From the WMUG team, Peter Egerton will be running a session pitting the audience “Geeks” against the panel of experts “Guests” and I am honoured to be co-hosting a session on Azure Multi Factor Authentication with Terence Beggs.

1E are sponsoring the event and will be providing free lunch and refreshments throughout the day.

The full agenda is as follows;

Speaker name Session title Abstract Time
  Registration & Coffee   09:00
WMUG Welcome A quick welcome and introduction from the WMUG team 09:30
1E 1e Products A technical dive into the 1E product suite. 09:45
  BREAK BREAK 10:30
Nickolaj Andersen PowerShell and Configuration Manager An overview of Powershell coolness with Configuration Manager 10:45
Marcus Robinson Azure Automation DSC Azure Automation DSC for server based configuration management 11:30
  LUNCH LUNCH 12:15
Maurice Daly and  Terence Beggs MFA Goodness Microsoft Azure Multi-factor Authentication 13:00
Robert Marshall TBC TBC 13:45
  BREAK BREAK 14:30
Peter Egerton Geeks vs Guests We put the audience head to head against our panel of experts to see who knows more about being an IT Pro. 14:45
Q&A Open questions A chance to ask questions, get answers and openly discuss any thoughts you may have around Windows Management. 15:30
Giveaways Prize giveaways We have a System Center Universe Europe ticket to give away. 16:00
Aaron Czechowski What’s new in Configuration Manager Live and direct from Redmond, Aaron will tell us what we can look forward to in Configuration Manager. 16:10
  Close & Thanks   16:55

Registration is now open on the WMUG site – http://wmug.co.uk/c/e/10

We will also be giving away a FREE ticket to System Center Universe Europe in Berlin to one lucky attendee.

 

 

MDT 2013 Update 1 – Static IP Address Issue

After upgrading our MDT 2013 environment with the update 1 release I was eager to build my Windows 10 image, until I came up against this issue while attempting to set a static IP for the VM –

MDTError

I checked the bootstrap.ini, updated the deployment share and boot images but the issue persisted, so as a work around I used F8 to bring up the command prompt and the following commands to manually set the IP address details:

netsh interface ip set address name=”Ethernet” static (IP Address) (Subnet Mask) (Gateway)
netsh interface ip set dnsservers name=”Ethernet” source=static (IP Address of DNS Server)

Doing this allowed me to get past this and build / capture the image, however it since turns out that this is know listed on a recent TechNet Blog (http://blogs.technet.com/b/msdeployment/archive/2015/08/25/mdt-2013-update-1-release-notes-and-known-issues.aspx).

Fingers crossed that a patch is on the way!.

Machine AD Auto Renaming – MDT/SCCM Deployment

I have seen a lot of scripts around for automatically generating the next available computer name for use with SCCM during the deployment stage, however I was looking for something that would read from Active Directory and find out if any gaps existed such as where an old machine was retired and then use the next sequence number. Example:

LT-DEMO-1000
LT-DEMO-1002
LT-DEMO-1003

Given that in the above example LT-DEMO-1001 is available.

Our existing SCCM/MDT solution already auto creates PC names based on their location, machine type and the last four digits of their serial number. I wanted to add functionality to take this information, query active directory and rename the machine using the next truly available name in our naming sequence. I created the below script based on a minimum sequence value of 1000 and an upper value of 1999, the original machine type and location/dept. in my naming convention is extracted and then the machine is renamed using the next number available:

$DC = $env:LOGONSERVER.Substring(2)
$ADPowerShell = New-PSSession -ComputerName $DC
Invoke-Command -Session $ADPowerShell -scriptblock {import-module ActiveDirectory}
Import-PSSession -Session $ADPowerShell -Module ActiveDirectory -AllowClobber -ErrorAction Stop
$TypeLocation = $env:COMPUTERNAME.Substring(0,$env:COMPUTERNAME.Length-4)
$UsedNumbers = Get-ADComputer -Filter * -Properties Name | Where-Object {$_.Name -like ("$TypeLocation" + "*")} | ForEach-Object {Write-Output $($_.Name.Substring($_.Name.length-4,4))}
$NextNumber = 1000
$values = 1000..1999
Do
{
$values[$NextNumber]
$NextNumber ++
} Until ($UsedNumbers.Contains("$NextNumber") -eq $false)
$ComputerName = ("$TypeLocation" + "$NextNumber")
Exit-PSSession
Rename-Computer -ComputerName $env:COMPUTERNAME -NewName $ComputerName -Restart

Obviously the script has to be run with an account with rights to rename the computer object in Active Directory. Note that this script can be run inside the Windows environment and not the task sequence.

UPDATE – 28/11/2014

After many hours of testing, I have modified the original rename script to run inside the task sequence.

The script requires sufficient rights to connect to a named domain controller via PowerShell and rename the workstation. For security purposes I am opting obviously not to embed the password in clear text so a secondary script is run to generate a text file with a secure string password first and uploaded to a file share.

It works by passing the local machine variables through to a PS session on your DC, finding the next available computer name and then issuing a rename-computer command to the machine being imaged. In order to achieve this the script also disables and re-enables the local Windows firewall on the machine being build in order for the WMI command to be issued from the DC.

To deploy the script, add it to a package, distribute it and then insert a Run PowerShell Script command within your task sequence (setting the execution policy to bypass) as per the screenshot below:

Machine Rename Task Sequence Command

Export Credential Script (run this to export the password to a secure string in a file on your file server share):

Script Download Link – https://gallery.technet.microsoft.com/scriptcenter/SCCM-MDT-Computer-Auto-7969a373

$key = (1..16)
$securepass = Read-Host -AsSecureString -Prompt "Please enter the required account password" | ConvertFrom-SecureString -Key $Key
$encrypted = $securepass | Out-File -FilePath '\\fileserver\yourshare\Required.txt'

Machine Rename Task Sequence Script:

$Username = 'yourdomain\useraccount'
$encrypted = Get-Content -Path '\\fileserver\yourshare\Required.txt'
$key = (1..16)
$Password = $encrypted | ConvertTo-SecureString -Key $key
$Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $Password
$DC = "YourDomainControllerName"
$ADPowerShell = New-PSSession -ComputerName $DC -Credential $Credentials
$CurrentName = $env:COMPUTERNAME
netsh advfirewall set allprofiles state off
Invoke-Command -Session $ADPowerShell -scriptblock {
	import-module ActiveDirectory
	$clientname = $($args[0])
	$Username = 'yourdomain\useraccount'
	$encrypted = Get-Content -Path '\\fileserver\yourshare\Required.txt'
	$key = (1..16)
	$password = $encrypted | ConvertTo-SecureString -Key $key
	$DomainCredentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Username, $Password
	$TypeLocation = $ClientName.Substring(0, $ClientName.Length - 4)
	$UsedNumbers = Get-ADComputer -Filter * -Properties Name | Where-Object { $_.Name -like ("$TypeLocation" + "*") } | ForEach-Object { Write-Output $($_.Name.Substring($_.Name.length - 4, 4)) }
	$values = 1000..1999
	$adcheck = 0
	Do
	{
		# Set beginning of sequence number
		$NextNumber = 1000
		# Increment active directory check flag
		$adcheck++
		Do
		{
			$values[$NextNumber]
			$NextNumber++
		}
		Until ($UsedNumbers.Contains("$NextNumber") -eq $false)
		$ComputerName = ("$TypeLocation" + "$NextNumber")
		start-sleep -Seconds 2
	}
	while ($adcheck -le 3)
	Rename-Computer -ComputerName $ClientName -NewName $ComputerName -DomainCredential $DomainCredentials
	Exit-PSSession
} -ArgumentList $CurrentName
Remove-PSSession $ADPowerShell
# Re-enable the local firewall
netsh advfirewall set allprofiles state on