Michael Niehaus' Windows and Office deployment ramblings
I’ve been toying around with using a web service that could be used to implement RIS-style computer naming. Unfortunately, I haven’t had time to work on it much in the last year or so, so I’ll post the code as-is and tell you up front that it’s not really complete – you might need to make some changes to it to meet your specific needs. So here’s the code (written using Visual Studio 2008 and .NET 3.5), which shows how easy it is to do LDAP queries and object creation using .NET:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.DirectoryServices; using System.Data; using System.Linq; using System.Web; using System.Web.Services; using System.Web.Services.Protocols; using System.Xml.Linq; namespace DemoWebService { [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [ToolboxItem(false)] public class NameService : System.Web.Services.WebService { [WebMethod] public String GenerateName(String dnsDomain, String prefix, String uuid, String machineObjectOU) { // Build the search DirectoryEntry entry = new DirectoryEntry("LDAP://" + dnsDomain); DirectorySearcher search = new DirectorySearcher(entry, "(name=" + prefix + "*)"); // Execute the search and build a list of the matching names and their UUIDs Dictionary<String, Guid> existingNames = new Dictionary<String, Guid>(); foreach (SearchResult result in search.FindAll()) { String name = result.Properties["name"][0].ToString().ToUpper(); Guid netbootGuid = new Guid(); if (result.Properties["netbootGuid"].Count > 0) netbootGuid = new Guid((byte[])result.Properties["netbootGuid"][0]); Trace.WriteLine("Found computer " + name + " with GUID " + netbootGuid.ToString()); existingNames.Add(name, netbootGuid); } // See if we can find an existing match. If so, return it. Guid existingUuid = new Guid(uuid); if (existingNames.ContainsValue(existingUuid)) { foreach (String name in existingNames.Keys) if (existingNames[name] == existingUuid) { // TODO: Maybe we want to move the computer object to the specified OU return name; } } // Find the first available name in sequence String nextName = null; for (Int32 i = 1; i <= 999; i++) { String testName = prefix + i.ToString("000"); if (!existingNames.ContainsKey(testName)) { nextName = testName; break; } } if (nextName == null) return null; // All names were taken // Add the computer to AD try { DirectoryEntry dirEntry = new DirectoryEntry("LDAP://" + machineObjectOU); DirectoryEntry newUser = dirEntry.Children.Add("CN=" + nextName, "computer"); newUser.Properties["samAccountName"].Value = nextName + "$"; newUser.Properties["netbootGUID"].Value = existingUuid.ToByteArray(); newUser.Properties["description"].Value = "Added by MDT"; newUser.CommitChanges(); newUser.Close(); } catch (Exception e) { Trace.WriteLine("Unable to add computer: " + e.ToString()); } // Return the name return nextName; } } }
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.DirectoryServices;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
namespace DemoWebService
{
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class NameService : System.Web.Services.WebService
[WebMethod]
public String GenerateName(String dnsDomain, String prefix, String uuid, String machineObjectOU)
// Build the search
DirectoryEntry entry =
new DirectoryEntry("LDAP://" + dnsDomain);
DirectorySearcher search = new DirectorySearcher(entry,
"(name=" + prefix + "*)");
// Execute the search and build a list of the matching names and their UUIDs
Dictionary<String, Guid> existingNames = new Dictionary<String, Guid>();
foreach (SearchResult result in search.FindAll())
String name = result.Properties["name"][0].ToString().ToUpper();
Guid netbootGuid = new Guid();
if (result.Properties["netbootGuid"].Count > 0)
netbootGuid = new Guid((byte[])result.Properties["netbootGuid"][0]);
Trace.WriteLine("Found computer " + name + " with GUID " + netbootGuid.ToString());
existingNames.Add(name, netbootGuid);
}
// See if we can find an existing match. If so, return it.
Guid existingUuid = new Guid(uuid);
if (existingNames.ContainsValue(existingUuid))
foreach (String name in existingNames.Keys)
if (existingNames[name] == existingUuid)
// TODO: Maybe we want to move the computer object to the specified OU
return name;
// Find the first available name in sequence
String nextName = null;
for (Int32 i = 1; i <= 999; i++)
String testName = prefix + i.ToString("000");
if (!existingNames.ContainsKey(testName))
nextName = testName;
break;
if (nextName == null)
return null; // All names were taken
// Add the computer to AD
try
DirectoryEntry dirEntry = new DirectoryEntry("LDAP://" + machineObjectOU);
DirectoryEntry newUser = dirEntry.Children.Add("CN=" + nextName, "computer");
newUser.Properties["samAccountName"].Value = nextName + "$";
newUser.Properties["netbootGUID"].Value = existingUuid.ToByteArray();
newUser.Properties["description"].Value = "Added by MDT";
newUser.CommitChanges();
newUser.Close();
catch (Exception e)
Trace.WriteLine("Unable to add computer: " + e.ToString());
// Return the name
return nextName;
To use this, you would add an entry to CustomSettings.ini to call the web service. That would look something like this:
[Settings] Priority=Default, GetName Properties=DnsDomain, Prefix [Default] Prefix=MDTTEST DnsDomain=mydomain.com MachineObjectOU=OU=Workstations,DC=mydomain,DC=com [GetName] WebService=http://myserver/NameService.asmx/GenerateName Parameters=DnsDomain, Prefix, UUID, MachineObjectOU OSDComputerName=string
[Settings] Priority=Default, GetName Properties=DnsDomain, Prefix
[Default] Prefix=MDTTEST DnsDomain=mydomain.com MachineObjectOU=OU=Workstations,DC=mydomain,DC=com
[GetName] WebService=http://myserver/NameService.asmx/GenerateName Parameters=DnsDomain, Prefix, UUID, MachineObjectOU OSDComputerName=string
The web service would be passed the DNS domain name (mydomain.com), computer prefix (MDTTEST), the current machine’s SMBIOS UUID, and the OU to which new computers should be added. It will return a name starting with the specified prefix and ending with the next available three-digit number. So the first machine would be MDTTEST001, the second MDTTEST002, etc. These computer names are added to Active Directory with the “netbootGUID” attribute set, so that if the machine is ever rebuilt it will use the same computer name again. (The code purposely doesn’t try to find any computer object with that SMBIOS UUID. Instead, it only looks for computers with the right prefix that have a matching UUID. This might not be the behavior you want, but it was the behavior I wanted.)
The output from ZTIGather.wsf when processing this INI file would look something like this:
Added new custom property DNSDOMAIN Added new custom property PREFIX Using from [Settings]: Rule Priority = DEFAULT, GETNAME ------ Processing the [DEFAULT] section ------ Property MACHINEOBJECTOU is now = OU=Workstations,DC=mydomain,DC=com Using from [DEFAULT]: MACHINEOBJECTOU = OU=Workstations,DC=mydomain,DC=com Property DNSDOMAIN is now = mydomain.com Using from [DEFAULT]: DNSDOMAIN = mydomain.com Property PREFIX is now = MDTTEST Using from [DEFAULT]: PREFIX = MDTTEST ------ Processing the [GETNAME] section ------ Determining the INI file to use. Using COMMAND LINE ARG: Ini file = CS.ini Finished determining the INI file to use. Using COMMAND LINE ARG: Ini file = CS.ini CHECKING the [GETNAME] section About to execute web service call using method POST to http://server/NameService.asmx/GenerateName: DnsDomain=mydomain.com&Prefix=MDTTEST&UUID=814100CD-CE48-CB11-A536-B7561D1E4450&MachineObjectOU=OU=Workstations,DC=mydomain,DC=com Response from web service: 200 OK Successfully executed the web service. Property OSDCOMPUTERNAME is now = MDTTEST001 Obtained OSDCOMPUTERNAME value from web service: string = MDTTEST001
This isn’t quite as flexible as the RIS naming, where you could use other variables in the computer name, but there’s no reason you couldn’t add more logic to cover those cases too. There’s also no guarantee that the web service will add the computer account to the same DC that the computer ends up using, which could cause some naming conflicts if the new computer object doesn’t replicate before the computer tries to join the domain. Use at your own risk :-)
When you perform a Lite Touch deployment, the task sequence runs through to completion, and then you will see a summary wizard that shows you any errors or warnings that may have occurred during the process. That wizard could be skipped by setting “SkipFinalSummary=YES” in CustomSettings.ini. (One behavior worth pointing out: With MDT 2010 Beta 2, if the task sequence fails, the SkipFinalSummary setting will be ignored and the wizard will be shown anyway.)
A common request we received was to provide a mechanism for specifying what should be done after the wizard is completed (or if it is skipped). With MDT 2010 Beta 2, we have provided that through a “FinishAction” variable that you can set in CustomSettings.ini. Valid values include:
If you don’t specify any value, the default is to just exit like was done in MDT 2008. The shutdown, restart, or logoff activities happen after the Lite Touch scripts have cleaned up the machine (copying logs to the network, removing MININT, removing autologon settings, etc.). In the case of restart or logoff, the machine will end up at the logon screen, ready for a user to log on.
I mentioned a while back that I wanted to do a blog post talking about how to troubleshoot the new MDT 2012 monitoring feature for Lite Touch deployments, but first I had to actually describe it. If you haven’t reviewed that post, you might want to check it out first at http://blogs.technet.com/b/mniehaus/archive/2012/03/09/mdt-2012-new-feature-monitoring.aspx.
So now let’s talk about troubleshooting. First, let’s look at the server side. You have to enable monitoring on a computer that has MDT 2012 installed. When you use Deployment Workbench on that computer and check the box to enable monitoring, Workbench will first check to see if the specified monitoring host name is local:
It doesn’t really matter if you specify an IP address, a short host name, or a fully-qualified host name, as long as the clients can resolve whatever you specify. If you specify a name that Workbench doesn’t think is local (because Workbench itself can’t resolve the name back to an IP address assigned to the current machine), it won’t try to install the monitoring component; instead, it will try to contact that server to see if monitoring is running on that computer. If it is, great; if it isn’t, you’ll see an error message:
If you look closely at the error “tip” at the end of the “Monitoring host” line, you’ll see a message like “Unable to connect to the specified server and port”. If you think you specified the local computer name and got that error, then Workbench couldn’t figure out that it was the local computer name (something that’s harder to do than you might think). If you are specifying a different server and see this error, then it’s having problems communicating with that other server.
Tip #1: Make sure the name you specify in Workbench can be resolved to the IP address of the current machine.
You’ve checked the checkbox and can’t see that anything happened. So what was actually done? Two things:
Tip #2: Make sure the “Microsoft Deployment Toolkit Monitor Service” is installed and running. If it’s not installed and it should be, you can uncheck the box, click apply, then check the box again and click apply to reinstall it. If it’s installed but not running, try to start it.
Tip #3: Make sure the entry was added to CustomSettings.ini by looking at the Rules tab. Because of a peculiarity with the way Workbench works, if you make any changes to the Rules tab after you’ve clicked the “Enable monitoring” checkbox but before you’ve clicked OK, it’s possible that the changes made on the Rules tab overlay the EventService entry in CustomSettings.ini, but it’s easy enough to put it back manually.
The service has two dependencies:
So there’s no dependency on IIS or SQL Server. The service uses .NET to host a web server as part of the service process, and it uses a SQL Compact database (basically a set of DLLs, which ship with MDT, that run in the service process) to store the monitoring information. It’s designed to be easy to install and run.
Tip #4: If you try to start the service and it won’t start, that most likely means the ports you chose were already in use. (If you want to know what’s using the ports, use a tool like TCPView, available from http://technet.microsoft.com/en-us/sysinternals/bb897437.) Pick different ports.
While it’s always possible that there could be some other reason the service fails, I haven’t seen any other causes. But if you know the ports are not in use and the service still won’t start, capture a trace using DebugView (http://technet.microsoft.com/en-us/sysinternals/bb896647) to see if it provides any further clues. If not, contact Microsoft Support for assistance.
The monitoring service listens on the two ports that you specified. The first of these ports (9800) is used by computers being deployed to send progress events. The second (9801) is used by Workbench itself to query information about deployments being monitored. To make sure these ports are accessible, we can manually connect to each one using Internet Explorer.
To verify the “event port” from the monitor server itself, you can use a URL such as:
http://localhost:9800/MDTMonitorEvent/
If that works, you should see a response like:
That’s a proper response in this case – the web service doesn’t expect to be called in this way (an HTTP GET request instead of an HTTP POST request), so it’s telling you the proper way to call the service.
To verify the “data port” from the monitor server itself, you can use a URL such as:
http://localhost:9801/MDTMonitorData/
This response (which is an ODATA feed in case you are curious) confirms that the data feed is working as expected.
But those are the easy queries – they are using “localhost”, which is almost never subject to firewall restrictions. Next, you need to try these queries remotely, using the appropriate “remote” URLs:
http://mdt-server:9800/MDTMonitorEvent/
http://mdt-server:9801/MDTMonitorData/
If those work, great. If they don’t, then you need to make sure that whatever firewall is running on the monitoring server allows the ports you specified (e.g. 9800 and 9801) to be accessed from remote hosts.
Tip #5: Make sure you can access the monitor service ports both locally and remotely. Adjust the firewall rules as necessary.
Note that there are other networking “challenges” that can get in the way, e.g. IPSec domain isolation. In this configuration, computers that aren’t domain-joined, e.g. running from Windows PE, can’t talk to domain-joined computers because they aren’t using encrypted IPsec communication. This type of configuration will never work – you would need to set up the monitoring service on a “boundary server” that has been configured to allow non-IPsec traffic on the configured ports. So don’t assume that if a “remote” URL works from a domain-joined machine, it will also work from a workgroup machine (or Windows PE) – know how your network is configured.
When the EventService task sequence variable is set (via the processing of CustomSettings.ini), each MDT script executed in the task sequence will send an event to the monitor service on the “event port” URL. When this succeeds, you’ll see a message like this:
If a script is unable to send an event, you’ll see something different:
That’s a clear sign that something isn’t right. Make sure the service is running, that the firewall ports are open, etc. – the same challenges we already reviewed.
Tip #6: Check the client logs to make sure the clients are able to talk to the monitoring service.
Another way you might notice an issue: If the monitor service isn’t running, the clients will still try to connect to it, eventually timing out. This timeout process will cause a delay at the end of each step in the task sequence, so if you are watching the task sequence progress dialog, you’ll see steps that you never noticed before (because they usually run so fast) now taking a long time.
When you try to look at the monitoring data from Workbench, it calls a PowerShell cmdlet (Get-MDTMonitorData), then that PowerShell cmdlet makes the “data port” query to retrieve the details from the monitoring service. If the service is working as expected, you can see the list of monitored machines in Workbench. If the service isn’t working, you’ll see something like this instead:
Good advice, make sure the service is running
Still having issues? Post them as comments here, or send me an e-mail at mniehaus@microsoft.com and we’ll try to figure out what’s going on.
In MDT 2008, we provided unknown computer support for ConfigMgr 2007, since it didn’t provide that capability – you first had to import new computers into the ConfigMgr database before you could install an OS, so MDT helped automate that process. When ConfigMgr 2007 R2 was released, it included unknown computer functionality, so we have now removed most of that from MDT 2010.
One of the useful parts of this unknown computer process was a pre-execution hook that would run a wizard, leveraging the same wizard framework that we used for MDT 2010 Lite Touch deployments. This was useful because we provided all the pieces to make it work: the TSCONFIG.INI file that tells ConfigMgr what to run, the script that gets executed by ConfigMgr (referenced in TSCONFIG.INI), the rules processing logic to gather information from WMI and other data sources, and the wizard files themselves.
With MDT 2010, we’ve left these pieces in place, but set them up to do something more basic: prompt for a new computer name. This is provided as a sort of general purpose “sample”, showing how to hook this into ConfigMgr. While you might not find the sample particularly useful, you can edit the wizard definition (using Notepad or something like the MDT Wizard Editor, http://mdtwizardeditor.codeplex.com/) to add additional panes.
All the files related to this general purpose sample are located in the “C:\Program Files\Microsoft Deployment Toolkit\SCCM” directory:
So if you wanted too do some customization, the files you would want to change are the “Deploy_SCCM_Definition_ENU.xml” (to add or change wizard panes) and “Deploy_SCCM_Scripts.vbs” (to specify additional initialization or validation logic).
To actually get these pieces added into a boot image, you can check the “Add media hook files to enable the Deployment Wizard for this boot media” checkbox when running either the “Create Boot Image using Microsoft Deployment” wizard (which creates only new boot image which you would then need to configure the task sequence and ISOs to use) or the “Create Microsoft Deployment Task Sequence” wizard (which can create a new boot image as part of the task sequence creation process).
Booting from this boot image, you should see a wizard that looks like this:
You can then specify the computer name you want, which will set the OSDComputerName task sequence variable. Like I said, pretty simple, but provided as a starting point for your own customizations – just edit the XML and VBS files, create a new boot image, and then deploy. (Remember that all of these files are actually embedded in the boot image WIM file, so when you make changes you either need to create a new boot image or mount the existing one to change those files. Update the distribution points after making changes.)
I’ve also attached a short video that shows the startup process (including the initial ConfigMgr wizard screen that can be used for specifying static IP information and for typing in the media password).
MDT 2010 includes a new script called LTISuspend.wsf that isn’t actually part of any of the task sequence templates – by default it’s not used. But if you add it into a task sequence during the “State Restore” phase activities, it will allow you to temporarily suspend the task sequence. This is intended for reference image creation processes where there might be some activities that you just can’t possibly automate – you can make the changes by hand and then when finished restart the task sequence to create your image. (If there is any way to do the automation, you should – this is for those “I have no other choice” situations.)
To use this new script, add a step into the task sequence with the following command line:
cscript.exe %SCRIPTROOT%\LTISuspend.wsf
When this step runs, you’ll see a popup dialog on the screen:
There will be a “Resume Task Sequence” shortcut on the desktop that should be used to eventually restart the task sequence from the next step after the suspend step. You can do anything you want while the task sequence is suspended, including rebooting, but when you are done you should make sure that you are logged in as the local Administrator account again, and that UAC is still disabled for the local Administrator account.
This same capability is used by MDT 2010 in other ways – more on those later.
If you’ve done ConfigMgr OS deployments, you’ve probably seen an error dialog like this before:
Pretty simple, right? Distribute this package to a local DP and then try again. But there are often dozens (or in extreme cases, hundreds) of packages referenced by the package, so you might have to go through this cycle a few times before you get them all. Fortunately, there are some ConfigMgr WMI classes that can help figure out which packages are missing. With a little bit of PowerShell logic, we can automate the whole process, through these basic steps:
This process will take care of boot images, OS images, OS install packages, driver packages, software update packages, or any other type of packages that are referenced (because behind the scenes, packages are packages to ConfigMgr – only the UI differentiates).
The scripts needed to this are attached to this blog. You’ll need to save the SCCM.psm1 file in an appropriate PowerShell module folder (see the comments in the script for more details) so that the CheckTaskSequences.ps1 script can find it (and of course you’ll have to enable PowerShell script execution using something like “Set-ExecutionPolicy RemoteSigned”). Then, just run CheckTaskSequences.ps1 each time you create a new task sequence. It will very quickly identify the packages that need to be distributed and take care of those for you.
Note: Version 1.1 of the script is now attached (using version 1.5 of SCCM.psm1) to address a couple of bugs found in the current version. See the comments in CheckTaskSequences.ps1 for more information.
After you have created a collection, you need to be able to populate it with either direct membership rules or query rules. That’s where the next function comes into play. As with the previous examples, take the following script code and add it to your growing SCCM.PSM1 file:
--- snip --- function Add-SCCMCollectionRule { [CmdletBinding()] PARAM ( [Parameter(ValueFromPipelineByPropertyName=$true)] $collectionID, [Parameter(ValueFromPipeline=$true)] [String[]] $name, [Parameter()] $queryExpression, [Parameter()] $queryRuleName ) Process { # Get the specified collection (to make sure we have the lazy properties) $coll = [wmi]"\\$sccmServer\$($sccmNamespace):SMS_Collection.CollectionID='$collectionID'" # Build the new rule if ($queryExpression -ne $null) { # Create a query rule $ruleClass = [wmiclass]"\\$sccmServer\$($sccmNamespace):SMS_CollectionRuleQuery" $newRule = $ruleClass.CreateInstance() $newRule.RuleName = $queryRuleName $newRule.QueryExpression = $queryExpression $null = $coll.AddMembershipRule($newRule) } else { $ruleClass = [wmiclass]"\\$sccmServer\$($sccmNamespace):SMS_CollectionRuleDirect" # Find each computer foreach ($n in $name) { foreach ($computer in get-SCCMComputer -filter "Name = '$n'") { # See if the computer is already a member $found = $false if ($coll.CollectionRules -ne $null) { foreach ($member in $coll.CollectionRules) { if ($member.ResourceID -eq $computer.ResourceID) { $found = $true } } } if (-not $found) { Write-Verbose "Adding new rule for computer $n" $newRule = $ruleClass.CreateInstance() $newRule.RuleName = $n $newRule.ResourceClassName = "SMS_R_System" $newRule.ResourceID = $computer.ResourceID $null = $coll.AddMembershipRule($newRule) } else { Write-Verbose "Computer $n is already in the collection" } } } } } } --- snip ---
--- snip ---
function Add-SCCMCollectionRule { [CmdletBinding()] PARAM ( [Parameter(ValueFromPipelineByPropertyName=$true)] $collectionID, [Parameter(ValueFromPipeline=$true)] [String[]] $name, [Parameter()] $queryExpression, [Parameter()] $queryRuleName ) Process { # Get the specified collection (to make sure we have the lazy properties) $coll = [wmi]"\\$sccmServer\$($sccmNamespace):SMS_Collection.CollectionID='$collectionID'" # Build the new rule if ($queryExpression -ne $null) { # Create a query rule $ruleClass = [wmiclass]"\\$sccmServer\$($sccmNamespace):SMS_CollectionRuleQuery" $newRule = $ruleClass.CreateInstance() $newRule.RuleName = $queryRuleName $newRule.QueryExpression = $queryExpression $null = $coll.AddMembershipRule($newRule) } else { $ruleClass = [wmiclass]"\\$sccmServer\$($sccmNamespace):SMS_CollectionRuleDirect"
# Find each computer foreach ($n in $name) { foreach ($computer in get-SCCMComputer -filter "Name = '$n'") { # See if the computer is already a member $found = $false if ($coll.CollectionRules -ne $null) { foreach ($member in $coll.CollectionRules) { if ($member.ResourceID -eq $computer.ResourceID) { $found = $true } } } if (-not $found) { Write-Verbose "Adding new rule for computer $n" $newRule = $ruleClass.CreateInstance() $newRule.RuleName = $n $newRule.ResourceClassName = "SMS_R_System" $newRule.ResourceID = $computer.ResourceID $null = $coll.AddMembershipRule($newRule) } else { Write-Verbose "Computer $n is already in the collection" } } } } } }
So now you can add a static collection membership rule or a query rule:
Connect-SCCMServer Add-SCCMCollectionRule -collectionID CEN00001 -name MYSERVER Add-SCCMCollectionRule -collectionID CEN00001 -queryRuleName "My Rule" -queryExpression "select * from SMS_R_System"
Connect-SCCMServer
Add-SCCMCollectionRule -collectionID CEN00001 -name MYSERVER
Add-SCCMCollectionRule -collectionID CEN00001 -queryRuleName "My Rule" -queryExpression "select * from SMS_R_System"
But notice that the function above can actually accept some inputs from the PowerShell pipeline. That means you can get more creative. For example, assume you have a file that has the list of machines you want to add, something like this Computers.txt file that has one computer name per line:
COMPUTER1 COMPUTER2 COMPUTER3
COMPUTER1
COMPUTER2
COMPUTER3
With that, you can do something like this:
Get-Content C:\Computers.txt | add-SCCMCollectionRule -collectionID CEN00001
It will add three new rules to the collection. Or, if you want to create a collection and add a member at the same time, you can pipe the collection into the collection rule function:
New-SCCMCollection -name "My Collection" | Add-SCCMCollectionRule -name MYSERVER
A few comments on the logic above:
Those are all exercises for some other time :-)
fThe task sequencer used in MDT 2010 and ConfigMgr 2007 is designed to show you at all times what’s going on with your task sequence:
As a result, it forces this dialog to be on the top of all other windows, which can be annoying in some situations because you want to get this out of the way. I’ve seen various solutions for this, typically that just move the dialog out of the way (to the edge of the screen, off the screen, etc.). But there’s a much easier way to do this: you can just tell this dialog to close itself using a very simple script:
' Hide the progress dialog Set oTSProgressUI = CreateObject("Microsoft.SMS.TSProgressUI") oTSProgressUI.CloseProgressDialog Set oTSProgressUI = Nothing
' Hide the progress dialog
Set oTSProgressUI = CreateObject("Microsoft.SMS.TSProgressUI") oTSProgressUI.CloseProgressDialog Set oTSProgressUI = Nothing
That causes the progress dialog to close (not surprisingly) – at least until it is told to update the progress, something that would typically happen at the start of the next step. So it’s only gone temporarily, but that’s OK – in most cases, you just want it to be gone for one step anyway, so have that step run a VBScript that hides the dialog, does its work, and then exits.
If you did want the progress dialog to show up again before the next step, you can force a progress update. If you are referencing the MDT ZTIUtility.vbs script, this is pretty simple too because it already has a function to do this. Just include logic like this:
' Report progress to get the dialog to show up again oLogging.ReportProgress "Done", 100
' Report progress to get the dialog to show up again
oLogging.ReportProgress "Done", 100
If you aren’t using ZTIUtility.vbs, you can add some logic like this:
Public Function OpenProgressDialog Dim oProgress Dim uStep Dim uMaxStep ' Try to create the progress UI object On Error Resume Next Set oProgress = CreateObject("Microsoft.SMS.TSProgressUI") If Err then Err.Clear Exit Function End if On Error Goto 0 ' Update the progress On Error Resume Next uStep = CLng(oEnvironment.Item("_SMSTSNextInstructionPointer")) uMaxStep = CLng(oEnvironment.Item("_SMSTSInstructionTableSize")) Call oProgress.ShowTSProgress(oEnvironment.Item("_SMSTSOrgName"), oEnvironment.Item("_SMSTSPackageName"), oEnvironment.Item("_SMSTSCustomProgressDialogMessage"), oEnvironment.Item("_SMSTSCurrentActionName"), (uStep), (uMaxStep)) On Error Goto 0 ' Dispose of the object Set oProgress = Nothing End Function
Public Function OpenProgressDialog
Dim oProgress Dim uStep Dim uMaxStep
' Try to create the progress UI object
On Error Resume Next Set oProgress = CreateObject("Microsoft.SMS.TSProgressUI") If Err then Err.Clear Exit Function End if On Error Goto 0
' Update the progress
On Error Resume Next
uStep = CLng(oEnvironment.Item("_SMSTSNextInstructionPointer")) uMaxStep = CLng(oEnvironment.Item("_SMSTSInstructionTableSize")) Call oProgress.ShowTSProgress(oEnvironment.Item("_SMSTSOrgName"), oEnvironment.Item("_SMSTSPackageName"), oEnvironment.Item("_SMSTSCustomProgressDialogMessage"), oEnvironment.Item("_SMSTSCurrentActionName"), (uStep), (uMaxStep)) On Error Goto 0
' Dispose of the object
Set oProgress = Nothing
End Function
If using ZTIUtility, the complete script could look like this:
<job id="Scripting201"> <script language="VBScript" src="ZTIUtility.vbs"/> <script language="VBScript"> ' Hide the progress dialog Set oTSProgressUI = CreateObject("Microsoft.SMS.TSProgressUI") oTSProgressUI.CloseProgressDialog Set oTSProgressUI = Nothing ' <Do your work here> ' Show the progress dialog (using one of these methods) if you don't want to wait oLogging.ReportProgress "Done", 100 ' <Maybe do some more work> </script> </job>
<job id="Scripting201"> <script language="VBScript" src="ZTIUtility.vbs"/> <script language="VBScript">
' <Do your work here>
' Show the progress dialog (using one of these methods) if you don't want to wait
oLogging.ReportProgress "Done", 100 ' <Maybe do some more work>
</script> </job>
If you aren’t using ZTIUtility.vbs, it gets a little longer:
<job id="Scripting201"> <script language="VBScript"> ' Hide the progress dialog Set oTSProgressUI = CreateObject("Microsoft.SMS.TSProgressUI") oTSProgressUI.CloseProgressDialog Set oTSProgressUI = Nothing ' <Do your work here> ' Show the progress dialog (using one of these methods) if you don't want to wait OpenProgressDialog ' <Maybe do some more work> ' The OpenProgress Dialog method gets the dialog to show up again Public Function OpenProgressDialog Dim oProgress Dim uStep Dim uMaxStep ' Try to create the progress UI object On Error Resume Next Set oProgress = CreateObject("Microsoft.SMS.TSProgressUI") If Err then Err.Clear Exit Function End if On Error Goto 0 ' Update the progress On Error Resume Next uStep = CLng(oEnvironment.Item("_SMSTSNextInstructionPointer")) uMaxStep = CLng(oEnvironment.Item("_SMSTSInstructionTableSize")) Call oProgress.ShowTSProgress(oEnvironment.Item("_SMSTSOrgName"), oEnvironment.Item("_SMSTSPackageName"), oEnvironment.Item("_SMSTSCustomProgressDialogMessage"), oEnvironment.Item("_SMSTSCurrentActionName"), (uStep), (uMaxStep)) On Error Goto 0 ' Dispose of the object Set oProgress = Nothing End Function </script> </job>
<job id="Scripting201"> <script language="VBScript">
OpenProgressDialog
' <Maybe do some more work>
' The OpenProgress Dialog method gets the dialog to show up again
Microsoft Deployment Toolkit contains a script named ZTIWindowsUpdate.wsf that can be enabled to run during Lite Touch OS deployments. By default, it will talk to the Microsoft Update site on the internet to get the latest updates needed for your Windows OS and Microsoft applications like Office. But you might not want all of the machines you deploy doing that. So with MDT 2008, we added the ability to install updates from a WSUS server. The "Toolkit Reference" document describes the basic process:
MDT 2008 can also configure WUA to collect updates from computers on the corporate network that are running WSUS instead of connecting to Microsoft Updates over the Internet. MDT 2008 can optionally configure WUA to use a specific computer running WSUS using the WSUSServer property.
But the actual description of the WSUSServer property, and a sample of how to set it, was accidentally left out of the documentation. This needs to be configured via CustomSettings.ini by adding an entry that looks like this:
WSUSServer=http://mywsusservername
With that set, the ZTIWindowsUpdate.wsf script will automatically configure the Windows Update Agent to talk to this WSUS server instead of using Microsoft Update.
One other note: the new OS being deployed to the machine must be running a supported version of the Windows Update Agent (WUA). Windows XP and Windows Server 2003 don't contain that needed version, so they need to be upgraded. This will be done automatically by the script, downloading the files from the internet if necessary. But it would be more efficient for you to download them in advance and place them where the script can find them. Again from the documentation:
For additional information and for WUA deployment instructions, go to http://technet.microsoft.com/en-us/library/bb932139.aspx. You can obtain the latest version of the WUA stand-alone installer for: x86 versions (WindowsUpdateAgent30-x86.exe) at http://go.microsoft.com/fwlink/?LinkID=100334. x64 version (WindowsUpdateAgent30-x64.exe) at http://go.microsoft.com/fwlink/?LinkID=100335. Windows Vista and Windows Server 2008 include the most recent version of WUA, so no upgrade is necessary for these operating systems. In Windows XP and Windows Server 2003, one of the following will occur: If the WUA 3.0 stand-alone installer files are in the TOOLS\architecture folder (where architecture is either x86 or x64) on the deployment point, MDT 2008 will automatically install WUA on the target computer. When downloading the WUA 3.0 stand-alone installer files, save them in the distribution\TOOLS\architecture folder (where distribution is the folder where the distribution point is created). If the WUA 3.0 stand-alone installer files are not in the TOOLS\architecture folder on the deployment point and if the existing version of WUA is configured for a WSUS server, then WUA will attempt to update itself from a WSUS server. If the existing version of WUA is not configured for a WSUS server, then MDT 2008 will attempt to download and install WUA 3.0 from the Microsoft Update site. In this case, Internet access is required for the target computer.
For additional information and for WUA deployment instructions, go to http://technet.microsoft.com/en-us/library/bb932139.aspx.
You can obtain the latest version of the WUA stand-alone installer for:
Windows Vista and Windows Server 2008 include the most recent version of WUA, so no upgrade is necessary for these operating systems. In Windows XP and Windows Server 2003, one of the following will occur:
So if you set WSUSServer and download the updated stand-alone installers, then the ZTIWindowsUpdate.wsf script will be able to update your computer without access the internet to do so.
In the first part of this series, I talked about the three (well, four with Johan’s) approaches for managing drivers with ConfigMgr 2007. There’s actually another method that I didn’t mention so far (one that we briefly mentioned at the end of the MMS session):
Honestly, I didn’t think anyone did this for all of their drivers, although I did expect to see this done for a few limited drivers (such as the more complicated ones I mentioned before). But I have found a couple of customers who do exactly this: every driver is installed as an application. Still, I expect that to be rare in the “real world”.
So that leads us to the topic of “hybrid scenarios” – you can mix and match these scenarios to address all of your driver needs, something I would strongly encourage. Imagine a task sequence that uses something like this:
See Chris Nacker’s blog posting at http://myitforum.com/cs2/blogs/cnackers/archive/2010/04/09/configuration-manager-2007-r2-sp2-driver-management.aspx for a real-world example of how you might set up something like this.
In the session I also showed a simple script that created driver packages pointing to existing folders in the driver store, to quickly set up “Johan’s Control Freak Method”. That script was basically the same script as attached to the previous blog posting, with all the logic for importing drivers into the driver store stripped out, since that logic isn’t used in Johan’s scenario. That script, named CreateJohanDriverPackages.ps1 (which still depends on the SCCM.psm1 module), is attached to this blog posting.
Johan has also added a new blog posting detailing the MDT 2010 Lite Touch scenarios, which you can find at http://www.deployvista.com/Blog/JohanArwidmark/tabid/78/EntryID/132/language/en-US/Default.aspx.
A few other random driver-related items that may be of interest: