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 :-)
I just finished installing Office 2010 on all my home computers, and immediately ran into a “wife acceptance factor” issue: the “Outlook Upcoming Appointments” Sidebar gadget didn’t work:
“Fix it.” Sigh. Fortunately, it wasn’t hard to do. Locate the “C:\Users\<user ID>\AppData\Local\Microsoft\Windows Sidebar\Gadgets\OutlookAppointmentsGadget[1].gadget\en-US\js” folder and in it you’ll see a file called Outlook.vbs. Open it in your favorite text editor and find the two lines that mention Outlook “12” and change them to reference Outlook “14” instead. Change this:
if value = "Outlook.Application.12" then
to:
if value = "Outlook.Application.14" then
and this:
if mid(outlookApplication.Version, 1, 2) <> "12" then
to this:
if mid(outlookApplication.Version, 1, 2) <> "14" then
Save the changes, close the gadget, and open it again. You should then see something that works:
If only all application compatibility issues were that simple…
We have created a new forum on TechNet specifically for MDT-related questions – no question is too simple or too advanced. You can access the forum via http://social.technet.microsoft.com/Forums/en-US/mdt/threads. It’s been around for a couple of months, but we haven’t publicized it too heavily so we could slowly ramp up.
As always, I seem to be keeping too busy to spent a lot of time responding to questions on the forum, but fortunately there are several other experts (including Tim Mintner, Keith Garner, and Johan Arwidmark) who have been helping out with that.
Levi Stevens posted a blog entry today describing a workaround for an issue with ConfigMgr SP2 and USMT 4.0 that causes USMT to not capture all of the user state information. See http://blogs.technet.com/configmgrteam/archive/2009/12/01/known-issue-migrating-from-windows-xp-to-windows-7-with-usmt-when-used-with-configmgr-2007-sp2-os-deployment-may-not-migrate-all-settings.aspx for all the details.
We are still working on a KB article for MDT 2010 that provides a similar fix. That KB article (which will be 977565 once it is finally available) will describe the changes necessary to ZTIUserState.wsf. (They are pretty minor, just adding two lines to set the working directory to the folder containing the USMT binaries before running scanstate.exe or loadstate.exe.)
The Office 2010 Beta has been available for a short while now and should be released sometime next year. I’m already quite attached to it, and not only because of the “People Pane” in Outlook 2010 and its coming ties to various social networks.
To help with the coming migration, some new Office compatibility tools have been released. See http://blogs.technet.com/gray_knowlton/archive/2009/12/04/office-2010-application-compatibility-tools-beta-now-available.aspx for more details.
Also, if you aren’t yet running the Key Management Service (KMS) or using Multiple Activation Keys (MAK) with Windows Vista or Windows 7, you’ll probably want to study up on Volume Activation, as Office 2010 will use it too. There’s also a new beta version of the Volume Activation Management Tool (VAMT) 2.0 available for download here.
12/10/2009 Addendum: There is another new download that might be useful at http://www.microsoft.com/downloads/details.aspx?FamilyID=600c2142-abc3-4fea-9271-0c326c45dc8f&displaylang=en. It’s a poster that describes the deployment and installation options for Office 2010.
On Wednesday, December 2, the Windows Networking User’s Group is having their monthly meeting at the Microsoft office in Bellevue. Chris Avis is the featured speaker this month, and the topic of his presentation is:
Windows 7 New Features: Windows XP Mode & Boot to VHD Windows 7 introduces host of new features and technologies to simplify your work and make you more productive. Two technologies you may not have heard about are Windows XP Mode and the new “Boot to VHD” feature. Windows XP Mode allows administrators to migrate legacy applications from that Windows XP world into Windows 7 regardless of that applications compatibility with Windows 7. Windows XP Mode provides a virtualized instance of Windows XP that allows you to continue to run older applications while still enjoying the benefits of Windows 7. The best part of this deal is that it is FREE! The Boot to VHD feature of Windows 7 allows you to boot a virtualized operating system “natively”. This provides for better resource utilization, access to some hardware components that normally can’t be used in a virtual machine, as well as eliminating boot code issues surrounding dual or multi-boot scenarios. Chris will discuss how to configure and run Boot to VHD features as well as some new options in Windows 7 that make creating and supporting this feature a breeze.
Windows 7 New Features: Windows XP Mode & Boot to VHD
Windows 7 introduces host of new features and technologies to simplify your work and make you more productive. Two technologies you may not have heard about are Windows XP Mode and the new “Boot to VHD” feature.
Windows XP Mode allows administrators to migrate legacy applications from that Windows XP world into Windows 7 regardless of that applications compatibility with Windows 7. Windows XP Mode provides a virtualized instance of Windows XP that allows you to continue to run older applications while still enjoying the benefits of Windows 7. The best part of this deal is that it is FREE!
The Boot to VHD feature of Windows 7 allows you to boot a virtualized operating system “natively”. This provides for better resource utilization, access to some hardware components that normally can’t be used in a virtual machine, as well as eliminating boot code issues surrounding dual or multi-boot scenarios. Chris will discuss how to configure and run Boot to VHD features as well as some new options in Windows 7 that make creating and supporting this feature a breeze.
Pizza and soft drinks will be served at 6pm, with the presentation scheduled for 6:30pm. If you are in the area and would like to attend, please RSVP. For more details, visit the WNUG site at http://www.winnetusergroup.com/default.aspx. (I hope to attend myself.)
Over a year ago, I posted a blog entry at http://blogs.technet.com/mniehaus/archive/2008/09/25/dynamic-application-detection-with-lite-touch.aspx with details on how to automatically detect installed applications during a Lite Touch deployment, enabling those to be pre-selected when the application list is presented. Due to changes in the XML file schemas, this script needs some updates for MDT 2010 to be truly “correct”. The updated script follows, showing how to create a user exit that leverages the ZTIConfigFile script to enumerate all the available (enabled, visible) applications.
Option Explicit Function UserExit(sType, sWhen, sDetail, bSkip) Dim oScriptFile Dim oXMLApps, dAllApps Dim sGUID Dim oNode, oKey Dim sValueName, sKey, sValue Dim oApplications ' Only do this before processing the rule contents If sWhen <> "BEFORE" then UserExit = Success Exit Function End if ' Make sure ZTIConfigFile is loaded On Error Resume Next Set oScriptFile = oFSO.OpenTextFile(oUtility.ScriptDir & "\ZTIConfigFile.vbs", 1, false) ExecuteGlobal oScriptFile.ReadAll On Error Goto 0 ' Load the applications XML File Set oXMLApps = new ConfigFile oXMLApps.sFileType = "Applications" Set dAllApps = oXMLApps.FindItems ' Enumerate the applications For each sGUID in dAllApps Set oNode = dAllApps(sGUID) ' Retrieve the uninstall key node if present Set oKey = oNode.selectSingleNode("UninstallKey") ' If one was specified, look for the key in the registry If not (oKey is nothing) then ' Get the key name sKey = oKey.Text ' Check if the registry key exists by looking for well-known values For each sValueName in Array("DisplayName", "UninstallString", "QuietUninstallString") sValue = empty On Error Resume Next sValue = oShell.RegRead("HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\" & sKey & "\" & sValueName) On Error Goto 0 If IsEmpty(sValue) then On Error Resume Next sValue = oShell.RegRead("HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" & sKey & "\" & sValueName) On Error Goto 0 End if ' If a value was found, add the application's GUID to the list so it gets pre-selected by the wizard If not IsEmpty(sValue) then oLogging.CreateEntry "Uninstall registry key found for application " & oNode.selectSingleNode("Name").Text & ", application is present and will be reinstalled.", LogTypeInfo ' Add the GUID if it doesn't already exist in the list Set oApplications = oEnvironment.ListItem("Applications") If not oApplications.Exists(oNode.Attributes.getNamedItem("guid").value) then oApplications.Add oNode.Attributes.getNamedItem("guid").value, "" oEnvironment.ListItem("Applications") = oApplications End if Exit For End if Next Else oLogging.CreateEntry "UninstallKey not specified for application " & oNode.selectSingleNode("Name").Text, LogTypeInfo End if Next UserExit = Success End Function
Option Explicit
Function UserExit(sType, sWhen, sDetail, bSkip)
Dim oScriptFile Dim oXMLApps, dAllApps Dim sGUID Dim oNode, oKey Dim sValueName, sKey, sValue Dim oApplications
' Only do this before processing the rule contents
If sWhen <> "BEFORE" then UserExit = Success Exit Function End if
' Make sure ZTIConfigFile is loaded
On Error Resume Next Set oScriptFile = oFSO.OpenTextFile(oUtility.ScriptDir & "\ZTIConfigFile.vbs", 1, false) ExecuteGlobal oScriptFile.ReadAll On Error Goto 0
' Load the applications XML File
Set oXMLApps = new ConfigFile oXMLApps.sFileType = "Applications" Set dAllApps = oXMLApps.FindItems
' Enumerate the applications
For each sGUID in dAllApps
Set oNode = dAllApps(sGUID)
' Retrieve the uninstall key node if present
Set oKey = oNode.selectSingleNode("UninstallKey")
' If one was specified, look for the key in the registry
If not (oKey is nothing) then
' Get the key name
sKey = oKey.Text
' Check if the registry key exists by looking for well-known values
For each sValueName in Array("DisplayName", "UninstallString", "QuietUninstallString")
sValue = empty On Error Resume Next sValue = oShell.RegRead("HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\" & sKey & "\" & sValueName) On Error Goto 0
If IsEmpty(sValue) then On Error Resume Next sValue = oShell.RegRead("HKLM\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" & sKey & "\" & sValueName) On Error Goto 0 End if
' If a value was found, add the application's GUID to the list so it gets pre-selected by the wizard
If not IsEmpty(sValue) then
oLogging.CreateEntry "Uninstall registry key found for application " & oNode.selectSingleNode("Name").Text & ", application is present and will be reinstalled.", LogTypeInfo
' Add the GUID if it doesn't already exist in the list
Set oApplications = oEnvironment.ListItem("Applications") If not oApplications.Exists(oNode.Attributes.getNamedItem("guid").value) then oApplications.Add oNode.Attributes.getNamedItem("guid").value, "" oEnvironment.ListItem("Applications") = oApplications End if
Exit For
End if
Next Else
oLogging.CreateEntry "UninstallKey not specified for application " & oNode.selectSingleNode("Name").Text, LogTypeInfo
Next
UserExit = Success
End Function
The code has been updated some, but the concepts haven’t, so if you need more details on what this does check out the original article.