PowerShell cmdlets invocation through Management ODATA using WCF client

Management ODATA uses the Open Data Protocol (ODATA) to expose and consume data over the Web or Intranet. It is primarily designed to expose resources manipulated by PowerShell cmdlets and scripts as
schematized ODATA entities using the semantics of representational state transfer (REST). The philosophy of REST ODATA limits the verbs that can be supported on resources to only the basic operations: Create, Read, Update and Delete. 
In this topic I will talk about Management ODATA being able to expose resources that model PowerShell pipelines that return unstructured data. This is an optional feature and is called “PowerShell
pipeline invocation”. A single Management ODATA endpoint can expose schematized resources, or the arbitrary cmdlet resources or both.

In this blog I will show how to write a windows client built on WCF that creates a PowerShell pipeline invocation on MODATA endpoint. Any client can be used that supports ODATA. WCF Data Services includes a set of client libraries for general .NET Framework client applications that is used in this example. You can read more about WCF at: https://msdn.microsoft.com/en-us/library/cc668792.aspx. If you are building a WCF client, the only requirement is to use WCF Data Services 5.0 libraries to be compatible. In this topic I will assume you already have a MODATA endpoint configured and up and running. For more information on MODATA in general and how to create an endpoint please refer to msdn documentation at https://msdn.microsoft.com/en-us/library/windows/desktop/hh880865(v=vs.85).aspx

Since “PowerShell pipeline invocation” feature is an optional feature and is disabled by default, you will need to enable it by adding the following configuration to your MODATA endpoint web.config:

 <commandInvocation  enabled="true"/>

Table 1.1 – Enable Command Invocation

To make sure “PowerShell pipeline invocation” is now enabled, you will need to send a GET https://endpoint_service_URI/$metadata query to MODATA endpoint and should see a similar response in return: 

 <Schema>    <EntityType Name="CommandInvocation">       <Key>          <PropertyRef Name="ID"/>       </Key>       <Property Name="ID" Nullable="false"  Type="Edm.Guid"/>       <Property Name="Command" Type="Edm.String"/>       <Property Name="Status" Type="Edm.String"/>       <Property Name="OutputFormat"   Type="Edm.String"/>       <Property Name="Output" Type="Edm.String"/>       <Property Name="Errors" Nullable="false" Type="Collection(PowerShell.ErrorRecord)"/>       <Property Name="ExpirationTime"  Type="Edm.DateTime"/>       <Property Name="WaitMsec" Type="Edm.Int32"/>    </EntityType>    <ComplexType Name="ErrorRecord">       <Property Name="FullyQualifiedErrorId"  Type="Edm.String"/>       <Property Name="CategoryInfo"  Type="PowerShell.ErrorCategoryInfo"/>       <Property Name="ErrorDetails"  Type="PowerShell.ErrorDetails"/>       <Property Name="Exception" Type="Edm.String"/>   </ComplexType>    <ComplexType Name="ErrorCategoryInfo">       <Property Name="Activity"  Type="Edm.String"/>       <Property Name="Category"  Type="Edm.String"/>       <Property Name="Reason" Type="Edm.String"/>       <Property Name="TargetName"  Type="Edm.String"/>       <Property Name="TargetType"  Type="Edm.String"/>    </ComplexType>    <ComplexType Name="ErrorDetails">       <Property Name="Message" Type="Edm.String"/>       <Property Name="RecommendedAction"  Type="Edm.String"/>    </ComplexType></Schema> 

Table 1.2 - Command Invocation Schema Definition

Management ODATA defines two ODATA resource sets related to PowerShell pipeline invocation: CommandDescriptions and CommandInvocations.
The CommandDescriptions resource set represents the collection of commands available on the server.
By enumerating the resource set, a client can discover the commands that it is allowed to execute and their parameters.The client must be authorized to execute Get-Command cmdlet for the CommandDescriptions query to succeed. At a high level, if a client sends the following request: 

 GET https://endpoint_service_URI/CommandDescriptions 

Table 1.3 – Command Invocation Query Sample

 …then the server might reply with the following information: 

 <entry><id>https://endpoint_service_URI/CommandDescriptions('Get-Process')</id><category scheme="https://schemas.microsoft.com/ado/2007/08/dataservices/scheme" term="PowerShell.CommandDescription"/><link title="CommandDescription" href="CommandDescriptions('Get-Process')" rel="edit"/><title/><updated>2012-09-10T23:14:52Z</updated>-<author>  <name/></author>-<content type="application/xml">  -<m:properties>    <d:Name>Get-Process</d:Name><d:HelpUrl m:null="true"/><d:AliasedCommand m:null="true"/>-<d:Parameters m:type="Collection(PowerShell.CommandParameter)">      -<d:element>        <d:Name>Name</d:Name>        <d:ParameterType>System.String[]</d:ParameterType>      </d:element>-<d:element>        <d:Name>Id</d:Name>        <d:ParameterType>System.Int32[]</d:ParameterType>      </d:element>-<d:element>        <d:Name>ComputerName</d:Name>        <d:ParameterType>System.String[]</d:ParameterType>      </d:element>-<d:element>        <d:Name>Module</d:Name>        <d:ParameterType>System.Management.Automation.SwitchParameter</d:ParameterType>      </d:element>-<d:element>        <d:Name>FileVersionInfo</d:Name>        <d:ParameterType>System.Management.Automation.SwitchParameter</d:ParameterType>      </d:element>-<d:element>        <d:Name>InputObject</d:Name>        <d:ParameterType>System.Diagnostics.Process[]</d:ParameterType>      </d:element>-<d:element>        <d:Name>Verbose</d:Name>        <d:ParameterType>System.Management.Automation.SwitchParameter</d:ParameterType>      </d:element>-<d:element>        <d:Name>Debug</d:Name>        <d:ParameterType>System.Management.Automation.SwitchParameter</d:ParameterType>      </d:element>-<d:element>        <d:Name>ErrorAction</d:Name>        <d:ParameterType>System.Management.Automation.ActionPreference</d:ParameterType>      </d:element>-<d:element>        <d:Name>WarningAction</d:Name>        <d:ParameterType>System.Management.Automation.ActionPreference</d:ParameterType>      </d:element>-<d:element>        <d:Name>ErrorVariable</d:Name>        <d:ParameterType>System.String</d:ParameterType>      </d:element>-<d:element>        <d:Name>WarningVariable</d:Name>        <d:ParameterType>System.String</d:ParameterType>      </d:element>    </d:Parameters>  </m:properties></content></entry>

Table 1.4 – Command Response Sample

 This indicates that the client is allowed to execute the Get-Process cmdlet.

The CommandInvocations resource set represents the collection of commands or pipelines that have been invoked on the server.  Each entity in the collection represents a single invocation of some PowerShell pipeline.  To invoke a pipeline, the client sends a POST request containing a new entity.  The contents of the entity include the PowerShell pipeline itself (as a string), the desired output format (typically “xml” or “json”), and the length of time to wait synchronously for the command to complete.  A pipeline string is a sequence of one or more commands, optionally with parameters and delimited by a vertical bar character. For example, if the server receives the pipeline string “Get-Process –Name iexplore”,with output type specified as “xml” then it will execute the Get-Process command (with optional parameter Name set to “iexplore”), and send its output to “ConvertTo-XML”.

The server begins executing the pipeline when it receives the request.  If the pipeline completes quickly (within the synchronous-wait time) then the server stores the output in the entity’s Output property, marks the invocation status as “Completed”, and returns the completed entity to the client.

If the synchronous-wait time expires while the command is executing, then the server marks the entity as “Executing” and returns it to the client.  In this case, the client must periodically request the updated entity from the server; once the retrieved entity’s status is “Completed”, then the pipeline has completed and the client can inspect its output. The client should then send an ODATA DeleteEntity request, allowing the server to delete resources associated with the pipeline.

There are some important restrictions on the types of commands that can be executed. Specifically, requests that use the following features will not execute successfully: 

1. script blocks
2. parameters using environment variables such as "Get-Item -path $env:HOMEDRIVE\\Temp"
3. interactive parameters such as –Paging (Get-Process | Out-Host –Paging )

Authorization and PowerShell initial session state are handled by the same CLR interfaces as for other Management ODATA resources. Note that every invocation calls some ConvertTo-XX cmdlet, controlled by the OutputFormat property of the invocation. The client must be authorized to execute this cmdlet in order for the invocation to succeed.

Here is the code snippet that shows how to send a request to create a PowerShell pipeline invocation and how to get the cmdlet execution result: 

   public class CommandInvocationResource    {        public Guid ID { get; set; }

        public string Command { get; set; }

        public string OutputFormat { get; set; }

        public int WaitMsec { get; set; }

        public string Status { get; set; }

        public string Output { get; set; }

        public List<ErrorRecordResource> Errors { get; set; }

        public DateTime ExpirationTime { get; set; }

        public CommandInvocationResource()        {            this.Errors = new List<ErrorRecordResource>();        }    }

    public class ErrorRecordResource    {        public string FullyQualifiedErrorId { get; set; }

        public ErrorCategoryInfoResource CategoryInfo { get; set; }

        public ErrorDetailsResource ErrorDetails { get; set; }

        public string Exception { get; set; }    }

    public class ErrorCategoryInfoResource    {        public string Activity { get; set; }

        public string Category { get; set; }

        public string Reason { get; set; }

        public string TargetName { get; set; }

        public string TargetType { get; set; }    }

    public class ErrorDetailsResource    {        public string Message { get; set; }

        public string RecommendedAction { get; set; }    }    

Table 2.1 – Helper class definitions

 

 // user needs to specify the endpoint service URI as well as provide user name, password and domainUri serviceroot = new Uri(“<endpoint_service_URL>”);NetworkCredential serviceCreds = new NetworkCredential("testuser","testpassword","testdomain");CredentialCache cache = new CredentialCache();cache.Add(serviceroot, "Basic", serviceCreds);

//Powershell pipeline invocation command samplestring strCommand ="Get-Process -Name iexplore";

// Expect returned data to be xml formatted. You can set it to “json” for the returned data to be in jsonstring outputType = "xml";

//create data service context with protocol version 3 to connect to your endpoint. V3 supports all new ODATA fetures like property bags that is required for "PowerShell cmdlet invocation" featureDataServiceContext context = new DataServiceContext(serviceroot,System.Data.Services.Common.DataServiceProtocolVersion.V3);context.Credentials = cache;

//Create an invocation instance on the endpointCommandInvocationResource instance = new CommandInvocationResource(){Command = strCommand,OutputFormat = outputType

};context.AddObject("CommandInvocations", instance);DataServiceResponse data = context.SaveChanges();

// Ask for the invocation instance we just createdDataServiceContext afterInvokeContext    = new DataServiceContext(serviceroot, System.Data.Services.Common.DataServiceProtocolVersion.V3);afterInvokeContext.Credentials = cache;afterInvokeContext.MergeOption = System.Data.Services.Client.MergeOption.OverwriteChanges;CommandInvocationResource afterInvokeInstance = afterInvokeContext.CreateQuery<CommandInvocationResource>("CommandInvocations").Where(it => it.ID == instance.ID).First();

Assert.IsNotNull(afterInvokeInstance, "instance was not found!");while (afterInvokeInstance.Status == "Executing"){    //Wait for the invocation to be completed   Thread.Sleep(100);   afterInvokeInstance = afterInvokeContext.CreateQuery<CommandInvocationResource>   ("CommandInvocations").Where(it => it.ID == instance.ID).First();}

if (afterInvokeInstance.Status == "Completed"){   //Results is returned as a string in afterInvokeInstance.Output variable in xml format}   // In case the command execution has errors you can analyze the dataif (afterInvokeInstance.Status == "Error"){    string errorOutput;    List<ErrorRecordResource> errors = afterInvokeInstance.Errors;    foreach (ErrorRecordResource error in errors)    {        errorOutput += "CategoryInfo:Category " + error.CategoryInfo.Category + "\r\n";        errorOutput += "CategoryInfo:Reason " + error.CategoryInfo.Reason + "\r\n";        errorOutput += "CategoryInfo:TargetName " + error.CategoryInfo.TargetName + "\r\n";        errorOutput += "Exception " + error.Exception + "\r\n";        errorOutput += "FullyQualifiedErrorId " + error.FullyQualifiedErrorId + "\r\n";        errorOutput += "ErrorDetails" + error.ErrorDetails + "\r\n"; }}

//Delete the invocation instance on the endpointafterInvokeContext.DeleteObject(afterInvokeInstance);afterInvokeContext.SaveChanges();

Table 2.2 – Client Code Implementation

 

Narine Mossikyan
Software Engineer
Standards Based Management