Part three of a three part series
On setting up a UPS solution, to enable clean shutdown of vital network components.
In this post, we’ll be reviewing the library that performs the shutting down of our servers.
When I started on the library PowerOffUPSGuests.dll,
my thoughts were, if I’m going to do this, I wanted it to be extensible.
Able to shutdown pretty much any machines, requiring a clean shutdown due to power failure.
What I’ve done is left points to be easily extended in the future, when new requirements present themselves.
Source Code
The PowerOffUPSGuests repository is on bitbucket
I’m assuming you know how to use Mercurial and have it installed on your dev machine.
If you’re not familiar with Mercurial (hg) There’s a little here to get your feet wet.
Besides that, there is plenty of very good documentation on the net.
For starters, you’ll need to create a directory that you want to have as your repository.
For example, I use C:\Scripts.
Set this directory as a repository.
Then from within the directory,
issue an hg pull https://bitbucket.org/LethalDuck/poweroffupsguests
Then update your working directory to the tip of the local repository.
You’ll need a BinaryMist.PowerOffUPSGuests.dll.config in your <repository>\UPS\PowerOffUPSGuests\PowerOffUPSGuests\
which should look something like the following.
At this stage I’ve only been shutting down an ESXi host.
Replace the value at line 22 with the user that has privileges to perform shutdown on the server.
Replace the value at line 23 with the absolute path to the password file you’re about to generate.
Line 28 is the class name of the ServerController.
Line 34 denotes whether or not the Initiator will perform the shutdowns synchronously or asynchronously
The Server[n] and ServerPort[n] values that are commented out, are used when you want to intercept the messages being sent to/from the target server.
This is useful for examination and to help build the appropriate messages that the target server expects.
<?xml version="1.0"?> <configuration> <assemblySettings> <!--As aditional target servers are added to the queue to be shutdown Keep the same nameing convention used below Just increment the suffix number for each target servers key. The first target suffix must start at 0. Additional target suffix's must be sequential. --> <!--Target servers to be shutdown--> <!--FreeNAS--> <!--add key="ServerUser0" value="YourUser"/> <add key="ServerUserPwFile0" value="Absolute directory that your password file resides\FileServerPw"/--> <!--add key="Server0" value="127.0.0.1"/--><!--localhost used for interception--> <!--add key="Server0" value="YourFileServerName"/--> <!--add key="ServerPort0" value="8080"/--><!--port used for interception--> <!--add key="ServerPort0" value="443"/--> <!--add key="Controller0" value="FreeNASController"/--> <!--ESXi--> <add key="ServerUser0" value="YourUser"/> <add key="ServerUserPwFile0" value="Absolute directory that your password file resides\VMHostPw"/> <!--add key="Server1" value="127.0.0.1"/--><!--localhost used for interception--> <add key="Server0" value="YourVSphereHostName"/> <!--add key="ServerPort1" value="8080"/--><!--port used for interception--> <add key="ServerPort0" value="443"/> <add key="Controller0" value="VMServerController"/> <!--Assembly settings--> <add key="LogFilePath" value="Some absolute path\Log.txt"/> <add key="CredentialEntropy" value="A set of comma seperated digits"/><!--"4,2,7,9,1" for example--> <add key="Synchronicity" value="Synchronous"/> <!--Check other values in Initiator.Synchronicity--> <add key="IgnoreSslErrors" value="true"/> <add key="Debug" value="true"/> </assemblySettings> <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
Build the solution
You’ll now need to navigate to <repository>\UPS\PowerOffUPSGuests\
and run the file PowerOffUPSGuests.sln.
Build.
You should now notice a couple of binaries in <repository>\UPS\
along with the libraries config file.
As I mentioned in part two, the PCNS will execute <repository>\UPS\PowerOff.bat which will run PowerOffUPSGuests.ps1.
Which will inturn kick off the BinaryMist.PowerOffUPSGuests.dll which does the work.
Generate the encrypted password file
Just run the BinaryMist.PasswordFileCreator.exe.
This will provide the required user prompts to capture the password for the vSphere host you’re intending to shutdown.
Or if you would like to extend the project and create a specialized ServerController for your needs.
You can use the BinaryMist.PasswordFileCreator to capture any credentials and save to file.
The code that performs the capture and encryption looks like the following:
static void Main() { CreatePasswordFile(); } /// <summary> /// Provides interactive capture for the insertion of an encrypted password, /// based on the ServerUserPwFile0 specified in the BinaryMist.PowerOffUPSGuests.dll.config file. /// </summary> public static void CreatePasswordFile() { bool validPath; string path = null; string RetryMessage = "Please try again."; Console.WriteLine("You must create the password file running under" + Initiator.NewLine + "the same account that will run BinaryMist.PowerOffUPSGuests."); do { Console.WriteLine( "From the BinaryMist.PowerOffUPSGuests.dll.config file." + Initiator.NewLine + "Please specify the ServerUserPwFile[n] value" + Initiator.NewLine + "for the encrypted Password to be stored to." + Initiator.NewLine + "This must be a valid path" + Initiator.NewLine ); try { validPath = true; path = Path.GetFullPath(Console.ReadLine()); if (! ((IEnumerable<string>)ConfigReader.Read.AllKeyVals.Values) .Contains<string>( path, StringComparer.CurrentCultureIgnoreCase ) ) { Console.WriteLine(Initiator.NewLine); Console.WriteLine("The value that was entered" + Initiator.NewLine + "was not one of the specified values for ServerUserPwFile[n]"); Console.WriteLine(RetryMessage + Initiator.NewLine); validPath = false; } } catch (Exception) { Console.WriteLine(Initiator.NewLine + "An invalid path was entered." + Initiator.NewLine + RetryMessage + Initiator.NewLine); validPath = false; } } while (validPath == false); Console.WriteLine( Initiator.NewLine + "The password you are about to enter" + Initiator.NewLine + "will be encrypted to file \"{0}\"" , path ); byte[] encryptedBytes = ProtectedData.Protect( new ASCIIEncoding().GetBytes(pWord), ServerAdminDetails.CredentialEntropy(), DataProtectionScope.CurrentUser ); File.WriteAllBytes(path, encryptedBytes); Console.WriteLine( Initiator.NewLine + Initiator.NewLine + string.Format( "The password you just entered has been encrypted" + Initiator.NewLine + "and saved to {0}" , path ) ); Console.WriteLine(Initiator.NewLine + "Press any key to exit"); Console.ReadKey(true); }
private static string pWord { get { bool passWordsMatch = false; bool firstAttempt = true; string passWord = null; while (!passWordsMatch) { if (!firstAttempt) Console.WriteLine(Initiator.NewLine + "The passwords did not match." + Initiator.NewLine); Console.WriteLine(Initiator.NewLine + "Please enter the password..." + Initiator.NewLine); string passWordFirstAttempt = GetPWordFromUser(); Console.WriteLine(Initiator.NewLine + Initiator.NewLine + "Please confirm by entering the password once again..." + Initiator.NewLine); string passWordSecondAttempt = GetPWordFromUser(); if (string.Compare(passWordFirstAttempt, passWordSecondAttempt) == 0) { passWordsMatch = true; passWord = passWordFirstAttempt; } firstAttempt = false; } Console.WriteLine(Initiator.NewLine + Initiator.NewLine + "Success, the passwords match." + Initiator.NewLine); return passWord; } }
private static string GetPWordFromUser() { string passWord = string.Empty; ConsoleKeyInfo info = Console.ReadKey(true); while (info.Key != ConsoleKey.Enter) { if (info.Key != ConsoleKey.Backspace) { if (info.KeyChar < 0x20 || info.KeyChar > 0x7E) { info = Console.ReadKey(true); continue; } passWord += info.KeyChar; Console.Write("*"); info = Console.ReadKey(true); } else if (info.Key == ConsoleKey.Backspace) { if (!string.IsNullOrEmpty(passWord)) { passWord = passWord.Substring (0, passWord.Length - 1); Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); Console.Write(' '); Console.SetCursorPosition(Console.CursorLeft - 1, Console.CursorTop); } info = Console.ReadKey(true); } } return passWord; }
Once you’ve created the password file, you’re pretty much ready to start testing.
If you’ve followed the directions covered in the first two parts of this series, you should be good to go.
Part one.
Part two.
We’ll quickly go through some of the more interesting parts of the code…
The ConfigReader
The constructor loads the _settings IDictionary by calling the ReadConfig function.
.net libraries don’t usually contain an assembly config file.
This is how we get around it.
We read the name of the assemblies config file (see line 70).
We then load the configuration into an XmlDocument.
Create and populate the XmlNodeList.
Return the populated IDictionary.
/// <summary> /// Reads the libraries configuration into memory. /// Provides convienient readonly access of the configuration from a single instance. /// </summary> public class ConfigReader { #region singleton initialization private static readonly Lazy<ConfigReader> _instance = new Lazy<ConfigReader>(() => new ConfigReader()); /// <summary> /// constructor that sets the value of our "_settings" variable /// </summary> private ConfigReader() { _settings = ReadConfig(Assembly.GetCallingAssembly()); } /// <summary> /// The first time Read is called must be from within this assembly, /// in order to create the instance of this class for the containing assembly. /// </summary> public static ConfigReader Read { get { return _instance.Value; } } #endregion /// <summary> /// settings to be used throughout the class /// </summary> private IDictionary _settings; /// <summary> /// constant name for the node name we're looking for /// </summary> private const string NodeName = "assemblySettings"; /// <summary> /// class Indexer. /// Provides the value of the specified key in the config file. /// If the key doesn't exist, an empty string is returned. /// </summary> public string this[string key] { get { string settingValue = null; if (_settings != null) { settingValue = _settings[key] as string; } return settingValue ?? string.Empty; } } public IDictionary<string, string> AllKeyVals { get { IDictionary<string, string> settings = new Dictionary<string, string>(); foreach(DictionaryEntry item in _settings) { settings.Add((string)item.Key, (string)item.Value); } return settings; } } /// <summary> /// Open and parse the config file for the provided assembly /// </summary> /// <param name="assembly">The assembly that has a config file.</param> /// <returns></returns> private static IDictionary ReadConfig(Assembly assembly) { try { string cfgFile = assembly.CodeBase + ".config"; XmlDocument doc = new XmlDocument(); doc.Load(new XmlTextReader(cfgFile)); XmlNodeList nodes = doc.GetElementsByTagName(NodeName); foreach (XmlNode node in nodes) { if (node.LocalName == NodeName) { DictionarySectionHandler handler = new DictionarySectionHandler(); return (IDictionary)handler.Create(null, null, node); } } } catch (Exception e) { Logger.Instance.Log(e.Message); } return (null); } #region Config related settings /// <summary> /// Readonly value, specifying whether debug is set to true in the assemblySettings of the BinaryMist.PowerOffUPSGuests.dll.config file. /// </summary> public bool Debug { get { if (_debug != null) return _debug == true ? true : false; _debug = Read["Debug"] == "true" ? true : false; return _debug == true ? true : false; } } private bool? _debug; #endregion }
The Initiator
private void ShutdownSynchronously(Queue<ServerController> serverControllers) { foreach (ServerController serverController in serverControllers) { serverController.Shutdown(); } } private void ShutdownAsynchronously(Queue<ServerController> serverControllers) { Action[] shutdownActions = new Action[serverControllers.Count]; ServerController[] serverControllerArray = serverControllers.ToArray(); for (int i = 0; i < serverControllerArray.Length; i++) { shutdownActions[i] = serverControllerArray[i].Shutdown; } try { Parallel.Invoke(shutdownActions); } // No exception is expected in this example, but if one is still thrown from a task, // it will be wrapped in AggregateException and propagated to the main thread. See MSDN example catch (AggregateException e) { Logger.Instance.Log(string.Format("An action has thrown an exception. THIS WAS UNEXPECTED.\n{0}", e.InnerException)); throw new Exception(); } } public string InitShutdownOfServers() { Logger.Instance.LogTrace(); Queue<ServerController> serverControllers = new Queue<ServerController>(); try { foreach (ServerAdminDetails serverAdminDetail in ServerAdminDetails.QueuedDetails) { Type t = Type.GetType(GetType().Namespace + "." + serverAdminDetail.ServerControllerType); serverControllers.Enqueue(Activator.CreateInstance(t, serverAdminDetail) as ServerController); } } catch(Exception e) { Logger.Instance.Log("Exception occured while enqueueing the server controllers. Details follow:" + NewLine + e.ToString() ); throw; } bool ignoreCase = true; Synchronicity synchronicity = (Synchronicity)Enum.Parse(typeof (Synchronicity), ConfigReader.Read["Synchronicity"], ignoreCase); if(synchronicity == Synchronicity.Synchronous) ShutdownSynchronously(serverControllers); else ShutdownAsynchronously(serverControllers); return "InitShutdownOfServers successfully executed."; }
In order to place the ServerController‘s on the serverController‘s Queue (line 33)… via the iterator provided by the Queue of ServerAdminDetails returned from
the static QueuedDetails property of ServerAdminDetails,
We must first instantiate the single instance of ServerAdminDetails,
which is what line 30 does.
In order for ServerAdminDetails to be constructed, the static _queue member (which is a Lazy of Queue of ServerAdminDetails) must be initialized first.
In order for the _queue member to be initialized with a Queue of ServerAdminDetails, the static QueueServersForShutdown procedure must be called.
This is where we pull out the values from the BinaryMist.PowerOffUPSGuests.dll.config shown above with the help of the ConfigReader.
As you can see, we iterate through the config file, building up the Queue of ServerAdminDetails until we’ve read all the appropriate values.
Each pass through the loop instantiates a new ServerAdminDetails (non singleton because of inside class scope) with the values we pulled from the config file.
The ServerAdminDetails
/// <summary> /// Provides the administration details of the servers listed in the BinaryMist.PowerOffUPSGuests.dll.config file. /// </summary> public class ServerAdminDetails { #region singleton initialization private static readonly Lazy<Queue<ServerAdminDetails>> _queue = new Lazy<Queue<ServerAdminDetails>>(QueueServersForShutdown); private ServerAdminDetails(string serverController, string serverName, string serverPort, string userName, byte[] serverCredential) { ServerControllerType = serverController; ServerName = serverName; ServerPort = serverPort; UserName = userName; Password = serverCredential; } /// <summary> /// Provides the process wide single instance queue of each servers admin details. /// </summary> public static Queue<ServerAdminDetails> QueuedDetails { get { return _queue.Value; } } #endregion private static Queue<ServerAdminDetails> ServersQueuedForShutdown { get; set; } private static byte[] GetMyCredential(string pWFileName) { try { return File.ReadAllBytes(pWFileName); } catch(Exception e) { string error = string.Format( "Error occured while instantiating a ServerAdminDetails instance for queueing. Specificaly while reading bytes from the following file: {0}{1}Exception details follow.{1}{2}", pWFileName, Initiator.NewLine, e ); Logger.Instance.Log(error); throw new Exception(error); } } private static Queue<ServerAdminDetails> QueueServersForShutdown() { ServersQueuedForShutdown = new Queue<ServerAdminDetails>(); const int firstServerIndex = 0; int serverCount = firstServerIndex; string empty = string.Empty; do { string controller = ConfigReader.Read["Controller" + serverCount]; string server = ConfigReader.Read["Server" + serverCount]; string serverPort = ConfigReader.Read["ServerPort" + serverCount]; string serverUser = ConfigReader.Read["ServerUser" + serverCount]; string serverUserPwFile = ConfigReader.Read["ServerUserPwFile" + serverCount]; if (controller == empty || server == empty || serverPort == empty || serverUser == empty || serverUserPwFile == empty) break; ServersQueuedForShutdown.Enqueue( new ServerAdminDetails( controller, server, serverPort, serverUser, GetMyCredential(Path.GetFullPath(serverUserPwFile)) ) ); Logger.Instance.Log ( string.Format ( "Server admin details of Controller: {0}, Server: {1}, ServerPort: {2}, ServerUser: {3}, ServerUserPwFile: {4} added to queued element number {5}.", controller, server, serverPort, serverUser, serverUserPwFile, serverCount ) ); serverCount++; } while (true); return ServersQueuedForShutdown; } /// <summary> /// Retreives the entropy found in the BinaryMist.PowerOffUPSGuests.dll.config file, used to encrypt all the passwords. /// </summary> /// <returns>byte[]</returns> public static byte[] CredentialEntropy() { string[] numbers = ConfigReader.Read["CredentialEntropy"].Split(','); byte[] entropy = new byte[numbers.Length]; for (int i = 0; i < numbers.Length; i++) { entropy[i] = Byte.Parse(numbers[i]); } return entropy; } /// <summary> /// The name of the ServerController child type. /// </summary> internal string ServerControllerType { get; private set; } /// <summary> /// The name of the server /// </summary> internal string ServerName { get; private set; } /// <summary> /// The port of the server /// </summary> internal string ServerPort { get; private set; } /// <summary> /// The user name for the server /// </summary> internal string UserName { get; private set; } /// <summary> /// The password for the user /// </summary> internal byte[] Password { get; private set; } }
Back to line 33 of the Initiator.
Now that we can access each ServerAdminDetails within the Queue of ServerAdminDetails via the Queue iterator provided by ServerAdminDetails.QueuedDetails.
We can create the ServerController child types using the late bound Activator.CreateInstance method, based on the ServerAdminDetails.ServerControllerType.
A reference to each ServerController child instance is added to the serverControllers queue.
The Shutdown procedure for each ServerController child is then called.
The ServerController
Notice the constructor which has been called by the child’s constructor, calls back to the child’s AssembleRequests procedure before completing.
/// <summary> /// Controls the process of shutting down the associated server. /// </summary> /// <remarks> /// An instance of this class is created indirectly via the more specific concrete creators for each server that requires shutdown. /// Plays the part of the Creator, in the Factory Method pattern. /// </remarks> internal abstract class ServerController { protected static readonly string NewLine = Initiator.NewLine; protected enum RequestMethod { Get, Post } /// <summary> /// Constructor for the <see cref="ServerController"/> class. /// Called via the more specific children to initialize the less specific members. /// </summary> /// <param name="serverAdminDetails"> /// The details required to create the messages that need to be sent to the server in order to perform the shutdown.</param> public ServerController(ServerAdminDetails serverAdminDetails) { ServerAdminDetails = serverAdminDetails; RequestAssembler = new RequestAssembler(); SoapEnvelopes = new Queue<XmlDocument>(); this.AssembleRequests(); } protected ServerAdminDetails ServerAdminDetails { get; set; } protected RequestAssembler RequestAssembler { get; set; } /// <summary> /// Reference a <see cref="System.Collections.Generic.Queue{System.Xml.XmlDocument}">queue</see> of soap envelopes. /// used by the children of this class to send to the server. /// </summary> public Queue<XmlDocument> SoapEnvelopes { get; protected set; } /// <summary> /// Initial preparation of messages that will be sent to the server to perform shutdown. /// </summary> /// <remarks>Factory method.</remarks> public abstract void AssembleRequests(); /// <summary> /// Completes the compilation of the sequence of messages that need to be sent to the server in order to perform the shutdown. /// Sends the messages. /// </summary> public abstract void Shutdown(); protected void NotifyOfShutdown() { Logger.Instance.Log( string.Format( "{0}.Shutdown on server: {1} has now been executed.", ServerAdminDetails.ServerControllerType, ServerAdminDetails.ServerName ) ); } protected bool SimplePing() { string serverName = ServerAdminDetails.ServerName; Logger.Instance.Log(string.Format("Performing Ping test on server: {0}", serverName)); Ping pingSender = new Ping(); int pingRetry = 3; PingReply reply = null; for (int i = 0; i < pingRetry; i++) { try { //may take a couple of tries, as may time out due to arp delay pingSender.Send(serverName); Logger.Instance.Log(string.Format("Initiating Ping number {0} of {1} on server: {2}. ", i + 1, pingRetry, serverName)); reply = pingSender.Send(serverName); if (reply.Status == IPStatus.Success) break; } catch (Exception e) { Logger.Instance.Log(e.ToString()); } } bool optionsAvailable = reply.Options != null; string noOptions = "No Options available"; Logger.Instance.Log( "Reply status for server: " + serverName + " was " + reply.Status + ". " + NewLine + "Address: " + reply.Address.ToString() + ". " + NewLine + "RoundTrip time: " + reply.RoundtripTime + ". " + NewLine + "Time to live: " + ((optionsAvailable) ? reply.Options.Ttl.ToString() : noOptions) + ". " + NewLine + "Don't fragment: " + ((optionsAvailable) ? reply.Options.DontFragment.ToString() : noOptions) + ". " + NewLine + "Buffer size: " + reply.Buffer.Length + ". " ); return reply.Status == IPStatus.Success; } }
The ServerController children
The AssembleRequests constructs as much of the SOAP envelopes as it can,
without knowing all the information that the target server will provide to be able to complete the messages before being sent.
The RequestAssembler‘s CreateSoapEnvelope does the assembly of the SOAP envelope.
You’ll notice that the RequestAssembler‘s CreateLoginSoapEnvelope takes an extra argument.
The ServerAdminDetails is passed so that the target server’s credentials can be included in the SOAP envelope.
The SOAP envelopes are then queued ready for dispatch.
Now when line 02 or 11 of the Initiator is executed,
line 143 of the VMServerController will be called. That’s Shutdown.
Then we dequeue, send request, and process the response in DequeueSendRequestProcessResponse.
Passing in an optional parameter of Action of HttpWebResponse (the lambda).
From DequeueSendRequestProcessResponse,
we dequeue each SOAP envelope and call CreateWebRequest,
which in-turn, palms the work it knows about to the less specific RequestAssembler‘s CreateWebRequest as a lambda.
Now in DequeueSendRequestProcessResponse, when we get our initialized HttpWebRequest back,
We pass both the SOAP envelope and the HttpWebRequest to the RequestAssembler‘s InsertSoapEnvelopeIntoWebRequest‘s procedure to do the honors.
/// <summary> /// Controls the process of shutting down the associated vSphere server. /// </summary> /// <remarks> /// Plays the part of the Concrete Creator, in the Factory Method pattern. /// </remarks> internal class VMServerController : ServerController { private static string _operationIDEndTag = "</operationID>"; private static string _operationIDTags = "<operationID>" + _operationIDEndTag; private uint _operationIDaVal = 0xAC1CF80C; private uint _operationIDbVal = 0x00000000; private const int EstimatedHeaderSize = 45; private enum RequestType { Hello, HandShake, Login, Shutdown } private string _host; private string _uRL; private readonly string _action = @"""urn:internalvim25/4.1"""; private readonly string _userAgent = @"VMware VI Client/4.0.0"; private KeyValuePair<string, string> _cookie; public VMServerController(ServerAdminDetails serverAdminDetails) : base(serverAdminDetails) { } /// <summary> /// Loads the <see cref="ServerController.SoapEnvelopes">SoapEnvelopes</see> with the sequence of messages /// that need to be sent to the server in order to perform the shutdown. /// </summary> /// <remarks> /// Factory method implementation /// </remarks> public override void AssembleRequests() { Logger.Instance.LogTrace(); _host = "https://" + ServerAdminDetails.ServerName + ":" +ServerAdminDetails.ServerPort; _uRL = "/sdk"; SoapEnvelopes.Enqueue(CreateHelloEnvelope()); SoapEnvelopes.Enqueue(CreateHandshakeEnvelope()); SoapEnvelopes.Enqueue(CreateLoginEnvelope()); SoapEnvelopes.Enqueue(CreateShutdownEnvelope()); } private string InitialHeaderContent() { StringBuilder headerContent = new StringBuilder(_operationIDTags, EstimatedHeaderSize); headerContent.Insert(headerContent.ToString().IndexOf(_operationIDEndTag), _operationIDaVal.ToString("X8") + "-" + (++_operationIDbVal).ToString("X8")); return headerContent.ToString(); } private XmlDocument CreateHelloEnvelope() { Logger.Instance.LogTrace(); string bodyContent = @" <RetrieveServiceContent xmlns=""urn:internalvim25""> <_this xsi:type=""ManagedObjectReference"" type=""ServiceInstance"" serverGuid="""">ServiceInstance</_this> </RetrieveServiceContent>"; return RequestAssembler.CreateSoapEnvelope(InitialHeaderContent(), bodyContent); } private XmlDocument CreateHandshakeEnvelope() { Logger.Instance.LogTrace(); string bodyContent = @" <RetrieveInternalContent xmlns=""urn:internalvim25""> <_this xsi:type=""ManagedObjectReference"" type=""ServiceInstance"" serverGuid="""">ServiceInstance</_this> </RetrieveInternalContent>"; return RequestAssembler.CreateSoapEnvelope(InitialHeaderContent(), bodyContent); } private XmlDocument CreateLoginEnvelope() { Logger.Instance.LogTrace(); string bodyContent = @" <Login xmlns=""urn:internalvim25""> <_this xsi:type=""ManagedObjectReference"" type=""SessionManager"" serverGuid="""">ha-sessionmgr</_this> <userName></userName> <password></password> <locale>en_US</locale> </Login>"; try { // As VMware insist on putting credentials in the SOAP body what else can we do? return RequestAssembler.CreateLoginSoapEnvelope(InitialHeaderContent(), bodyContent, ServerAdminDetails); } catch(InvalidCredentialException e) { string error = string.Format( "Error occured during a call to CreateLoginSoapEnvelope, for server: {0}.{1}Exception details follow.{1}{2}", ServerAdminDetails.ServerName, NewLine, e ); Logger.Instance.Log(error); throw new Exception(error); } } private XmlDocument CreateShutdownEnvelope() { Logger.Instance.LogTrace(); string bodyContent = @" <ShutdownHost_Task xmlns=""urn:internalvim25""> <_this xsi:type=""ManagedObjectReference"" type=""HostSystem"" serverGuid="""">ha-host</_this> <force>true</force> </ShutdownHost_Task>"; return RequestAssembler.CreateSoapEnvelope(InitialHeaderContent(), bodyContent); } private HttpWebRequest CreateWebRequest(string uRL, KeyValuePair<string, string> cookie) { return RequestAssembler.CreateWebRequest( uRL, (request)=>{ request.Method = RequestMethod.Post.ToString(); request.UserAgent = _userAgent; request.ContentType = "text/xml; charset=\"utf-8\""; request.Headers.Add("SOAPAction", _action); request.Accept = "text/xml"; request.KeepAlive = true; if (!string.IsNullOrEmpty(cookie.Key)) request.Headers.Add("Cookie", cookie.Key + "=" + cookie.Value); } ); } private void DequeueSendRequestProcessResponse(RequestType requestType, KeyValuePair<string, string> cookie, Action<HttpWebResponse> additionalResponseProcessing = null) { Logger.Instance.Log(string.Format("Will now attempt sending {0} message to server: {1}", requestType, ServerAdminDetails.ServerName)); XmlDocument soapEnvelope = SoapEnvelopes.Dequeue(); HttpWebRequest httpWebRequest = CreateWebRequest(_host + _uRL, cookie); RequestAssembler.InsertSoapEnvelopeIntoWebRequest(soapEnvelope, httpWebRequest); string soapResult; using (HttpWebResponse response = (HttpWebResponse)httpWebRequest.GetResponse()) using (Stream responseStream = response.GetResponseStream()) using (StreamReader streamReader = new StreamReader(responseStream)) { //pull out the bits we need for the next request. if (additionalResponseProcessing != null) { additionalResponseProcessing(response); } soapResult = streamReader.ReadToEnd(); } } /// <summary> /// Perform the shutdown of the server specified in the <see cref="ServerAdminDetails">server admin details</see>. /// </summary> public override void Shutdown() { bool serverOnline = SimplePing(); Logger.Instance.Log( serverOnline ? string.Format("Initiating sending of {0} to server: {1}", RequestType.Hello, ServerAdminDetails.ServerName) : string.Format("Could not reach server: {0}. Aborting shutdown of server: {0}", ServerAdminDetails.ServerName) ); ServicePointManager.ServerCertificateValidationCallback += ValidateRemoteCertificate; KeyValuePair<string, string> emptyCookie = new KeyValuePair<string, string>(); DequeueSendRequestProcessResponse( RequestType.Hello, emptyCookie, (response)=> { string[] setCookieElementsResponse = response.Headers["Set-Cookie"].Split(new[] { "\"" }, StringSplitOptions.RemoveEmptyEntries); _cookie = new KeyValuePair<string, string>(setCookieElementsResponse[0].TrimEnd('='), "\"" + setCookieElementsResponse[1] + "\""); } ); DequeueSendRequestProcessResponse(RequestType.HandShake, _cookie); DequeueSendRequestProcessResponse(RequestType.Login, _cookie); DequeueSendRequestProcessResponse(RequestType.Shutdown, _cookie); NotifyOfShutdown(); } private static string RemoteCertificateDetails(X509Certificate certificate) { return string.Format( "Details of the certificate provided by the remote party are as follows:" + NewLine + "Subject: {0}" + NewLine + "Issuer: {1}", certificate.Subject, certificate.Issuer ); } }
The RequestAssembler
/// <summary> /// Provides ancillary operations for <see cref="ServerController">server controllers</see> /// that assist in the creation of the requests intended for dispatch to the servers requiring shutdown. /// </summary> internal class RequestAssembler { private static string _soapEnvelope = @"<soap:Envelope xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'> <soap:Header> </soap:Header> <soap:Body> </soap:Body> </soap:Envelope>"; /// <summary> /// Produces a SOAP envelope in XML form with the server admins user credentials /// </summary> /// <param name="headerContent">Content targeted for between the header tags.</param> /// <param name="bodyContent">Content targeted for between the body tags.</param> /// <param name="serverAdminDetails">The <see cref="ServerAdminDetails"/> instance used to access the user name and password.</param> /// <returns>The SOAP envelope.</returns> public XmlDocument CreateLoginSoapEnvelope(string headerContent, string bodyContent, ServerAdminDetails serverAdminDetails) { string failMessage = "The credentials were not correctly set. Username: {0} Password byte count: {1}"; if (string.IsNullOrEmpty(serverAdminDetails.UserName) || serverAdminDetails.Password.Length < 1) throw new InvalidCredentialException(string.Format(failMessage, serverAdminDetails.UserName, serverAdminDetails.Password.Length)); StringBuilder bodyContentBuilder= new StringBuilder(bodyContent, bodyContent.Length + 10); bodyContentBuilder.Insert( bodyContentBuilder.ToString().IndexOf("</userName>"), serverAdminDetails.UserName ); bodyContentBuilder.Insert( bodyContentBuilder.ToString().IndexOf("</password>"), decryptedCredential(serverAdminDetails) ); return CreateSoapEnvelope(headerContent, bodyContentBuilder.ToString()); } private string decryptedCredential(ServerAdminDetails serverAdminDetails) { try { return new ASCIIEncoding().GetString( ProtectedData.Unprotect( serverAdminDetails.Password, ServerAdminDetails.CredentialEntropy(), DataProtectionScope.CurrentUser ) ); } catch(CryptographicException e) { // Retrieve the exception that caused the current // CryptographicException exception. Exception innerException = e.InnerException; string innerExceptionMessage = ""; if (innerException != null) { innerExceptionMessage = innerException.ToString(); } // Retrieve the message that describes the exception. string message = e.Message; // Retrieve the name of the application that caused the exception. string exceptionSource = e.Source; // Retrieve the call stack at the time the exception occured. string stackTrace = e.StackTrace; // Retrieve the method that threw the exception. System.Reflection.MethodBase targetSite = e.TargetSite; string siteName = targetSite.Name; // Retrieve the entire exception as a single string. string entireException = e.ToString(); // Get the root exception that caused the current // CryptographicException exception. Exception baseException = e.GetBaseException(); string baseExceptionMessage = ""; if (baseException != null) { baseExceptionMessage = baseException.Message; } Logger.Instance.Log( "Caught an unexpected exception:" + Initiator.NewLine + entireException + Initiator.NewLine + Initiator.NewLine + "Properties of the exception are as follows:" + Initiator.NewLine + "Message: " + message + Initiator.NewLine + "Source: " + exceptionSource + Initiator.NewLine + "Stack trace: " + stackTrace + Initiator.NewLine + "Target site's name: " + siteName + Initiator.NewLine + "Base exception message: " + baseExceptionMessage + Initiator.NewLine + "Inner exception message: " + innerExceptionMessage + Initiator.NewLine ); throw; } } /// <summary> /// Produces a SOAP envelope in XML form. /// </summary> /// <param name="headerContent">Content targeted for between the header tags.</param> /// <param name="bodyContent">Content targeted for between the body tags.</param> /// <returns><see cref="System.Xml.XmlDocument">The SOAP envelope</see>.</returns> public XmlDocument CreateSoapEnvelope(string headerContent, string bodyContent) { StringBuilder sb = new StringBuilder(_soapEnvelope); try { sb.Insert(sb.ToString().IndexOf(Initiator.NewLine + " " + "</soap:Header>"), headerContent); sb.Insert(sb.ToString().IndexOf(Initiator.NewLine + " " + "</soap:Body>"), bodyContent); } catch(Exception e) { Logger.Instance.Log(e.ToString()); throw; } XmlDocument soapEnvelopeXml = new XmlDocument(); soapEnvelopeXml.LoadXml(sb.ToString()); return soapEnvelopeXml; } /// <summary> /// Creates a web request based on the url passed in. /// </summary> /// <param name="url">The target URL of the server that will be shutdown.</param> /// <param name="additionalWebRequestManipulation">delegate of type /// <see cref="System.Action{HttpWebRequest}">Action{HttpWebRequest}</see>. /// This is used to take additional tasking defined in the calling procedure. /// This procedure creates the web request, /// and passes it into this parameter for the additional initialization work to be performed on the web request. /// </param> /// <returns> /// An initialized <see cref="System.Net.HttpWebRequest">web request</see>, /// ready to have a <see cref="System.Xml.XmlDocument">soap envelope</see> inserted.</returns> public HttpWebRequest CreateWebRequest(string url, Action<HttpWebRequest> additionalWebRequestManipulation = null) { HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url); if(additionalWebRequestManipulation != null) { additionalWebRequestManipulation(webRequest); } return webRequest; } /// <summary> /// Insert the <see cref="System.Xml.XmlDocument">soap envelope</see> into the <see cref="System.Net.HttpWebRequest">web request</see>. /// </summary> /// <param name="soapEnvelopeXml"> /// The <see cref="System.Xml.XmlDocument">soap envelope</see> to be inserted into the /// <see cref="System.Net.HttpWebRequest">web request</see>. /// </param> /// <param name="webRequest">The <see cref="System.Net.HttpWebRequest">web request</see> that the /// <see cref="System.Xml.XmlDocument"/>soap envelope</see> is inserted into. /// </param> public void InsertSoapEnvelopeIntoWebRequest(XmlDocument soapEnvelopeXml, HttpWebRequest webRequest) { using (Stream stream = webRequest.GetRequestStream()) { soapEnvelopeXml.Save(stream); } } private static void InsertByteArrayIntoWebRequest(byte[] postData, HttpWebRequest webRequest) { using (Stream stream = webRequest.GetRequestStream()) { stream.Write(postData, 0, postData.Length); } webRequest.ContentLength = postData.Length; } /// <summary> /// Inserts the credentials from the <see cref="ServerAdminDetails">server admmin details</see> into the /// <see cref="System.Net.HttpWebRequest">web request</see> supplied. /// </summary> /// <param name="serverAdminDetails"> /// The <see cref="ServerAdminDetails">server admin details</see> /// containing the information for the server that the request will be sent to. /// </param> /// <param name="webRequest"> /// The <see cref="System.Net.HttpWebRequest">web request</see> that will have the server administration credentials inserted. /// </param> public void InsertCredentialsIntoWebRequest(ServerAdminDetails serverAdminDetails, HttpWebRequest webRequest) { InsertByteArrayIntoWebRequest( new ASCIIEncoding().GetBytes("username=" + serverAdminDetails.UserName + "&password=" + decryptedCredential(serverAdminDetails)), webRequest ); } }
–
–
Let me know if any of this is unclear, and requires additional explanation.
Once again, the full source code can be found here.
Tags: C#, Microsoft, Networking, UPS, Virtualisation
Leave a Reply