Programmatically join Windows machine to AD domain

2019-01-25 14:05发布

问题:

This is similar to, but not a dupe of, this question - however, where it sought information on manually joining a server to a domain (and was rightly redirected) I am looking for help with some code that programmatically joins a machine to a domain.

The scenario is that we have a launcher service that instantiates Amazon EC2 Server2008R1 VMs, optionally passing a Machine Name in through the User-Data stream. A process is baked into our images that checks User-Data for a name on bootup - If none is present then the VM remains outside of our Cloud domain, but if the name is present then the machine is renamed as specified and auto-joined to the domain.

Here's the problem - if I run this process manually any time after instance start, it works exactly as described; the machine name is changed, and the VM is joined to the domain (we force a restart to make this happen).

However, when running as a Scheduled Task (triggered on startup) the machine rename happens as expected, but the subsequent call to JoinDomainOrWorkgroup (see below) picks-up the old randomised machine name given to the VM by EC2 instead of the new name it has just been assigned.

This results in a WMI return code of 8525, we get a disconnected misnamed entry in the AD repository (of that randomised name) and the machine is not joined to the domain. The VM then restarts, and a second pass through the startup process (abnormally triggered because there is content in User-Data but the machine is not yet in the domain) executes all the same steps and succeeds.

It looks like the machine name is set in the first pass but not 'finalised', and JoinDomainOrWorkgroup still sees the original name. On the second pass, the machine name is already set properly, and so JoinDomainOrWorkgroup works as expected. Quite why the process behaves this way during startup, but works perfectly when run manually on an already-started VM, is I think the nub of the problem.

I've tried inserting a delay between the rename and join steps in case the call to JoinDomainOrWorkgroup was happening before the rename was finalised behind the scenes, but this hasn't helped - and I didn't really expect it to, since the whole process works perfectly when run manually. So it's probably a combination of a subtle difference in machine state during bootup and something silly in the code.

Maybe using System.Environment.MachineName in the SetDomainMembership method is inadvisable? But it stil fails even if I pass the new name in as a string as I do for SetMachineName. So I'm stumped.

Here's the WMI code that renames the machine:

/// <summary>
/// Set Machine Name
/// </summary>
public static bool SetMachineName(string newName)
{
  _lh.Log(LogHandler.LogType.Debug, string.Format("Setting Machine Name to '{0}'...", newName));

  // Invoke WMI to populate the machine name
  using (ManagementObject wmiObject = new ManagementObject(new ManagementPath("Win32_ComputerSystem.Name='" + System.Environment.MachineName + "'")))
  {
    ManagementBaseObject inputArgs = wmiObject.GetMethodParameters("Rename");
    inputArgs["Name"] = newName;

    // Set the name
    ManagementBaseObject outParams = wmiObject.InvokeMethod("Rename", inputArgs, null);

    // Weird WMI shennanigans to get a return code (is there no better way to do this??)
    uint ret = (uint)(outParams.Properties["ReturnValue"].Value);
    if (ret == 0)
    {
      // It worked
      return true;
    }
    else
    {
      // It didn't work
      _lh.Log(LogHandler.LogType.Fatal, string.Format("Unable to change Machine Name from '{0}' to '{1}'", System.Environment.MachineName, newName));
      return false;
    }
  }
}

And here's the WMI code that joins it to the domain:

/// <summary>
/// Set domain membership
/// </summary>
public static bool SetDomainMembership()
{
  _lh.Log(LogHandler.LogType.Debug, string.Format("Setting domain membership of '{0}' to '{1}'...", System.Environment.MachineName, _targetDomain));

  // Invoke WMI to join the domain
  using (ManagementObject wmiObject = new ManagementObject(new ManagementPath("Win32_ComputerSystem.Name='" + System.Environment.MachineName + "'")))
  {
    try
    {
      // Obtain in-parameters for the method
      ManagementBaseObject inParams = wmiObject.GetMethodParameters("JoinDomainOrWorkgroup");

      inParams["Name"] = "*****";
      inParams["Password"] = "*****";
      inParams["UserName"] = "*****";
      inParams["FJoinOptions"] = 3; // Magic number: 3 = join to domain and create computer account

      // Execute the method and obtain the return values.
      ManagementBaseObject outParams = wmiObject.InvokeMethod("JoinDomainOrWorkgroup", inParams, null);
      _lh.Log(LogHandler.LogType.Debug, string.Format("JoinDomainOrWorkgroup return code: '{0}'", outParams["ReturnValue"]));

      // Did it work?  ** disabled so we restart later even if it fails
      //uint ret = (uint)(outParams.Properties["ReturnValue"].Value);
      //if (ret != 0)
      //{
      //  // Nope
      //  _lh.Log(LogHandler.LogType.Fatal, string.Format("JoinDomainOrWorkgroup failed with return code: '{0}'", outParams["ReturnValue"]));
      //  return false;
      //}

      return true;
    }
    catch (ManagementException e)
    {
      // It didn't work
      _lh.Log(LogHandler.LogType.Fatal, string.Format("Unable to join domain '{0}'", _targetDomain), e);
      return false;
    }
  }
}

Apologies if this code looks mind-numbingly stupid - I'm new to WMI, and this is largely cribbed from examples I've found on the interwebs; if there's a smarter/neater way to do this then by all means demonstrate. If you can cure the problem at the same time, bonus points!

回答1:

OK, here it is.

Firstly, the order of the fields in System Properties is a little misleading - you see Machine Name first, and Domain/Workgroup below that. This subconsciously affected my thinking, and meant my code copied that ordering by trying to set the name first, and then join the machine to the domain. Whilst this does work under some circumstances, it's not consistent or reliable. So the biggest lesson learned here is...

Join the domain first - then change the machine name.

Yep, that's actually all there is to it. After numerous test iterations, it finally dawned on me that it might work better if I tried it this way around. I tripped-up on the change of name on my first pass, but quickly realised that it was still using the local system credentials - but now that the machine was joined to the domain at this point, it needed the same domain credentials as were used to join the domain itself. A fast bit of code-tweaking later, and we now have a consistently-reliable WMI routine that joins the domain and then changes the name.

It might not be the neatest implementation (feel free to comment on improvements) but it works. Enjoy.

/// <summary>
/// Join domain and set Machine Name
/// </summary>
public static bool JoinAndSetName(string newName)
{
  _lh.Log(LogHandler.LogType.Debug, string.Format("Joining domain and changing Machine Name from '{0}' to '{1}'...", Environment.MachineName, newName));

  // Get WMI object for this machine
  using (ManagementObject wmiObject = new ManagementObject(new ManagementPath("Win32_ComputerSystem.Name='" + Environment.MachineName + "'")))
  {
    try
    {
      // Obtain in-parameters for the method
      ManagementBaseObject inParams = wmiObject.GetMethodParameters("JoinDomainOrWorkgroup");
      inParams["Name"] = "domain_name";
      inParams["Password"] = "domain_account_password";
      inParams["UserName"] = "domain_account";
      inParams["FJoinOptions"] = 3; // Magic number: 3 = join to domain and create computer account

      _lh.Log(LogHandler.LogType.Debug, string.Format("Joining machine to domain under name '{0}'...", inParams["Name"]));

      // Execute the method and obtain the return values.
      ManagementBaseObject joinParams = wmiObject.InvokeMethod("JoinDomainOrWorkgroup", inParams, null);

      _lh.Log(LogHandler.LogType.Debug, string.Format("JoinDomainOrWorkgroup return code: '{0}'", joinParams["ReturnValue"]));

      // Did it work?
      if ((uint)(joinParams.Properties["ReturnValue"].Value) != 0)
      {
        // Join to domain didn't work
        _lh.Log(LogHandler.LogType.Fatal, string.Format("JoinDomainOrWorkgroup failed with return code: '{0}'", joinParams["ReturnValue"]));
        return false;
      }
    }
    catch (ManagementException e)
    {
      // Join to domain didn't work
      _lh.Log(LogHandler.LogType.Fatal, string.Format("Unable to join domain '{0}'", _targetDomain), e);
      return false;
    }

    // Join to domain worked - now change name
    ManagementBaseObject inputArgs = wmiObject.GetMethodParameters("Rename");
    inputArgs["Name"] = newName;
    inputArgs["Password"] = "domain_account_password";
    inputArgs["UserName"] = "domain_account";

    // Set the name
    ManagementBaseObject nameParams = wmiObject.InvokeMethod("Rename", inputArgs, null);
    _lh.Log(LogHandler.LogType.Debug, string.Format("Machine Rename return code: '{0}'", nameParams["ReturnValue"]));

    if ((uint)(nameParams.Properties["ReturnValue"].Value) != 0)
    {
      // Name change didn't work
      _lh.Log(LogHandler.LogType.Fatal, string.Format("Unable to change Machine Name from '{0}' to '{1}'", Environment.MachineName, newName));
      return false;
    }

    // All ok
    return true;
  }
}