Using WS-Man to invoke a Powershell Cmdlet
First, let me apologize for the lack of posts to this blog. Out original team goal was to have a new post every month, but holidays/vacations/work got in the way. I’ll try to restart this rhythm. For this first post of the New Year, I’m going to cover a more technical topic.
Most Windows administrators should already be aware that Powershell is the preferred ITPro scripting and command-line experience. Many products from Microsoft and also third-parties expose their management interface as Powershell cmdlets. Since Powershell is currently only available on Windows, how can you manage Windows from a non-Windows box?
Although Powershell remoting is built on top of WS-Management, it’s really a protocol within a protocol and trying to interop with PSRP (Powershell Remoting Protocol) directly would essentially require replicating Powershell on the client. The alternative is to make use of a lesser known remote command-line tool called WinRS. WinRS is a simple utility allows remote cmd.exe and is also built on top of WS-Management. The difference is that WinRS reuses Create and Delete from WS-Transfer and introduces a few new custom SOAP web-methods. For this blog post, I’ll be focusing on the WinRS “protocol” and won’t be discussing WS-Transfer, SOAP, nor HTTP in detail. The complete details of the WinRS WS-Management Protocol extensions is documented here: http://msdn.microsoft.com/en-us/library/cc251526(v=PROT.10).aspx
WinRS has a relatively simple protocol, the workflow is:
1. WS-Transfer Create to create a shell, an EPR (End-Point Reference) to the created Shell is returned which is used for the next set of operations
2. Invoke the Command custom SOAP action to start a new command
3. Invoke the Receive custom SOAP action to receive output from the command (there is a corresponding Send for sending input, but it’s not needed for this scenario)
4. repeat step 3 until CommandState is Done
5. WS-Transfer Delete on the shell EPR
Let’s go through each step in a bit more detail.
For the WS-Transfer Create SOAP message, the body should contain the streams you want to send and receive. The resource URI should be: http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd. We are essentially creating a cmd.exe shell which we use to run powershell.exe.
|<Shell xmlns='http://schemas.microsoft.com/wbem/wsman/1/windows/shell’> <InputStreams>stdin</InputStreams> <OutputStreams>stdout stderr</OutputStreams> </Shell>|
If the request was successful, you’ll receive a standard WS-Transfer Create SOAP response which contains the newly created Shell EPR which resembles:
<w:SelectorSet> <w:Selector Name="ShellId">AFCFB065-F551-4604-BFDFD9B706798B5D</w:Selector> </w:SelectorSet>
This EPR should be cached for all subsequent operations. The first custom SOAP action Command uses the Action uri: http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command. WinRS supports two console modes: interactive and batch. For an interactive session, WinRS will wait for input (even if the command completes) until the client indicates there is no more. For a batch session, WinRS will expect input to be sent only during the lifetime of the running command. For this scenario, it is important to specify the WS-Management option WINRS_CONSOLEMODE_STDIN with a value of true which means to use batch mode. The command-line is split into separate Command and Arguments. The SOAP fragment would look like:
… <w:OptionSet> <w:Option Name='WINRS_CONSOLEMODE_STDIN'>TRUE</w:Option> </w:OptionSet> </s:Header> <s:Body> <CommandLine xmlns='http://schemas.microsoft.com/wbem/wsman/1/windows/shell'> <Command>powershell</Command> <Arguments>get-service | format-csv </Arguments> </CommandLine> </s:Body>
If this request was successful, the response will contain a CommandId element in the body that should be cached and used for subsequent operations for receiving the output. Although the protocol was defined to allow one shell to host multiple commands, WinRS is limited to a single command per shell. An example body of this response would resemble:
<rsp:CommandResponse> <rsp:CommandId>772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E</rsp:CommandId> </rsp:CommandResponse>
Once the Command response is received, the command is running on the server. WinRS will block output (and therefore the command) once the maximum envelope size is reached. The custom SOAP action Receive uses the action uri: . Since the resulting output may exceed a single SOAP envelope, the client needs to specify an incrementing SequenceId in case any packets were lost. WinRS will only cache the last sent packet. The request should contain the streams you want to read from and the CommandId associated to the stream in the body:
<Receive SequenceId='0' xmlns='http://schemas.microsoft.com/wbem/wsman/1/windows/shell'> <DesiredStream CommandId='772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E'> stdout stderr </DesiredStream> </Receive>
The response will contain the text output of the streams base64 encoded (to keep the SOAP xml well-formed and valid). The client should check the state of the command to know whether to continue to invoke Receive for more output.
<rsp:ReceiveResponse> <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E">DQo=</rsp:Stream> <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E"> U3RhdHVzICAgTmFtZSAgICAgICAgICAgICAgIERpc3BsYXlOYW1lICAgICAgICAgICAgICAgICAgICAgICAgICAg</rsp:Stream> <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E"> DQotLS0tLS0gICAtLS0tICAgICAgICAgICAgICAgLS0tLS0tLS0tLS0gICAgICAgICAgICAgICAgICAgICAgICAgICANClJ1bm5pbmcgIH dpbm1nbXQgICAgICAgICAgICBXaW5kb3dzIE1hbmFnZW1lbnQgSW5zdHJ1bWVudGF0aW9uICAgIA0KDQoNCg==</rsp:Stream> <rsp:Stream Name="stdout" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E" End="true"></rsp:Stream> <rsp:Stream Name="stderr" CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E" End="true"></rsp:Stream> <rsp:CommandState CommandId="772B44DF-2EA2-4AA5-87D1-A07E1FAE7A4E" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"> <rsp:ExitCode>0</rsp:ExitCode> </rsp:CommandState> </rsp:ReceiveResponse>
Once the CommandState is “Done”, there is no more output and WS-Transfer Delete should be called on the shell EPR. This will clean-up any resources being used on the server.
The example code shows how to invoke a Powershell cmdlet. It does not make use of any WinRM api’s, but instead creates the necessary SOAP messages from templates and uses System.Net.HttpWebRequest to send it over the wire. To enable using the sample code in Windows, you’ll need to enable Basic authentication in the WinRM service config (which only works for local acocunts), you can run this Powershell command as admin: Set-Item WSMan:\localhost\Service\Auth\Basic $true. You will need to deploy a server cert to use HTTPS or for testing purposes, set the WinRM service config to allow unencrypted HTTP. Here's an example command line to use with the sample code:
WinRSPsh http://server:5985/wsman user password "get-service"
If you want the output in a application consumable form, you can convert to XML (.Net serialization):
WinRSPsh http://server:5985/wsman user password "(get-service ^| convertto-xml).OuterXml"
Note that in the above example, you have to escape the pipe so that cmd.exe doesn't try to interpret it.