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 :-)
How do i acctually implement this. I have problems with values not beeing set. All values remain blank except the UUID.
Hi Michael - thanks for the code, this web service will be really useful for us. We have the service returning a machine name to MDT, as can be seen in our BDD.log file. The web service is creating the corresponding account successfully in AD. However, the domain join is failing. If I attempt a manual domain join after MDT is finished, I get an error saying the account already exists. Is there a particular attribute that needs to be set to make it a "managed" account that's missing from your code perhaps? Or is this a security issue?
I work with RayDiack and we've finally got this working with an amendment to Michael's initial code.
The account that was created by the webservice was created as disabled and didn't allow the domain join. By adding the following line to change the attributes of the computer account: newUser.Properties["userAccountControl"].Value = 4128; the problem was resolved.
See the following article for other userAccountControl options:
msdn.microsoft.com/.../ms680832(VS.85).aspx
The other issue we ran into was running the web service as an account with permission to join computers to the domain.
For this we set up an application pool and set the identity to an account with permissions. Add this account to the group "IIS_WPG" and then give the account permissions to C:\winnt\temp folder. You can copy the permission of the network service account.
If you encounter this error "Request format is unrecognized for URL unexpectedly ending in '/???'" check out this URL: www.rikware.com/.../URL-Unexpectedly-Ending-Error-with-ASPNET-20-Web-Services.aspx and make the relevent changes to the webconfig file.
Thanks again for sharing the web service. We finally have fully automated builds again with no pre-staging!!!
Hi,
there is a bug in this code.
The part existingNames.Add(name, netbootGuid); need to be in the loop instead of under the trace.
Hey ,
Correct me if im wrong but this solution will not work with Windows 7 and autoname deploymebt ? I have tested it and 7 just doestn like object already existing in domain. It claims that it cannot reuse the computer account
Hi, Rick
Sorry I am very new to subject, can you please put some step by step instructions.
Regards,