How To Automate Sysprep of an IaaS VM on Microsoft Azure

Goal

To automate sysprep of a remote VM running on Microsoft Azure using .NET c# APIs for Remote Powershell.

Scenarios

A lot of dev-test and build-out scenarios require configuring the machine on cloud, and then generalizing it to make it re-deployable and reusable. There are a few other ways of achieving this, by having some agents running inside the VMs. But WinRM is one of the easiest ways, and probably cheapest.

Requirements

The VM should be provisioned with WinRM or should have WinRM service running and configured. If you create the VM from Azure portal, then it has WinRM configured by default.

Steps and code

To start with, create a VM in Microsoft Azure with WinRM configuration (enabled by default if creating VM from Azure portal).Once the VM is created and running, we can use Remote Powershell APIs in C# to automate sysprep.

But there is a catch.RemotePowershell API, if used directly to start Sysprep.exe dies off without actually running sysprep. If you see sysprep logs, they are not very useful. Seemed like the child process spun off by sysprep cannot communicate to sysprep.exe because of named pipes, and the default authentication being used doesn’t support double hop of credentials, but it is just a theory. If someone knows the cause, please reply in comments.

So, I came up with this workaround. I would write the sysprep command to a .bat file, and spin off a cmd to run the .bat file. And it works!! But I am not sure why. Again, If someone has explanations, please let me know.

Here is the sample code:

You can find the code on GitHub Here

Make a .bat file with sysprep command in the VM, and run it.  Here is how you can do it.

 Var sysDir = InvokeScript(remoteConnection,“gc env:SystemDrive”) //can also try “SystemRoot”

Then make a .bat file with sysprep command in it:

 

  string generatedFileName = Path.GetRandomFileName();
 
 string filename = string.Format("{0}.bat", generatedFileName.Substring(0, generatedFileName.IndexOf('.')));
 
 string filePath = Path.Combine(sysDir, filename);
 
 string cmd = string.Format(@"{0}Windows\system32\sysprep\sysprep.exe /oobe /generalize /shutdown", sysDir);
 

Then write this file to remote VM...

 

  Collection<CommandParameter> param = new Collection<CommandParameter>();
 
 param.Add(new CommandParameter("path", filePath));
 
 param.Add(new CommandParameter("value", writeContent));
 
InvokeCommand(remoteConnection,"Set-Content", param);
  

Now run this bat file...         

  try
 
 {
 InvokeScript(filePath); //this will fail after sometime as machine is being generalized 
 } 
 catch (System.Management.Automation.Remoting.PSRemotingTransportException)
 
 {
 //sysprep started (may not have completed - wait for role’s InstanceStatus to be StoppedVM) 
 }
 

That’s it. This works!! The sysprep.exe will be started. You will get an access denied exception in the last step, which means that your machine is being generalized and passwords are being wiped off.

Now, Instead if you try doing something like

  string cmd = string.Format(@"{0}Windows\system32\sysprep\sysprep.exe /oobe /generalize /shutdown", sysDir);
 
 InvokeScript(cmd)
 

It doesn’t work reliably. You can see the sysprep.exe started but it dies in a few seconds without actually running the sysprep. You can see the
sysprep logs, which say the sysprep started and validated credentials, but rest of the logs were not very helpful to me. Maybe someone with knowledge of
sysprep logs might decipher them.

As you can see, I use a bunch of helper functions, which are explained below.

To Get a PSCredential object

 public PSCredential GetPSCredential(string username, string password)
 
 { 
 
 SecureString sPassword = new SecureString();
 
 char[] pWChars = password.ToCharArray();
 
 foreach (char pWChar in pWChars)
 
 {
 
 sPassword.AppendChar(pWChar);
 
 }
 
 PSCredential credential = new PSCredential(username, sPassword);
 
 return credential;
 
 }
  

To Make a connection object for a remote VM using:

 

  Var remoteConnection = new WSManConnectionInfo(useSSL:true, hostedServiceDnsName, winrmPort, "/wsman", "https://schemas.microsoft.com/powershell/Microsoft.PowerShell", psCredential);
 
 remoteConnection.SkipCNCheck = true; //No need if you have cert installed correctly
 
 remoteConnection.SkipCACheck = true; //No need if you have cert installed correctly
 
 remoteConnection.OperationTimeout = 10 * 60 * 1000; //10 minutes for operation timeout
 

For a VM on Azure, You can get the computerName and Port by doing GetDeployment and GetRole calls. Select the Public Port of WinRM endpoint on the Azure VM for the value of winrmPort above. Also, select the dns name, which will be something like <hostedServiceName>.cloudapp.net, for the hostedServiceDnsName input while making the WSManConnectionInfo object.

Now, To create a runspace and execute the command using­ the following executor functions

 public void InvokeScript ( WSManConnectionInfo remoteConnection, string psScript)
 
 {
 
 using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace(remoteConnection))
 
 {
 
 remoteRunspace.Open();
 
 using (System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create())
 
 {
 
 ps.Runspace = remoteRunspace;
 
 ps.AddScript(psScript);
 
 psResult = ps.Invoke();
 
 }
 
 remoteRunspace.Close();
 
 }
 
 }
  

And

 

  Public void InvokeCommand(WSManConnectionInfo remoteConnection, string cmd, Collection<CommandParameter> param)
 
 {
 
 using (Runspace remoteRunspace = RunspaceFactory.CreateRunspace(remoteConnection))
 
 {
 
 remoteRunspace.Open();
 
 using (System.Management.Automation.PowerShell ps = System.Management.Automation.PowerShell.Create())
 
 {
 
 ps.Runspace = remoteRunspace;
 
 Command Cmd = new Command(cmd);
 
 if (param != null && param.Count > 0)
 
 {
 
 foreach (CommandParameter p in param)
 
 {
 
 Cmd.Parameters.Add(p);
 
 }
 
 }
 
 ps.Commands.AddCommand(Cmd);
 
 psResult = ps.Invoke();
 
 } 
 
 remoteRunspace.Close();
 
 }
 
 }
  

I hope it helps. You might have to toggle with code a bit before you can make it work. But the main idea that I wanted to share was, giving sysprep command from remote powershell APIs directly doesn't work for me, while if I write the same command to a .bat file and run the .bat file, it works for me. I don't know why exactly. If someone knows, please share the wisdom.