Quantcast
Channel: MSDN Blogs
Viewing all articles
Browse latest Browse all 35736

PowerShell cmdlets invocation through Management ODATA using WCF client

$
0
0

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” or “Invoke”. 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 client to create a PowerShell pipeline invocation.
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: http://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 http://msdn.microsoft.com/en-us/library/windows/desktop/hh880865(v=vs.85).aspx

Since “Invoke” 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 “Invoke” is enabled, you will need to send a GET http://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 execution: 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 http://endpoint_service_URI/CommandDescriptions 

Table 1.3 – Command Invocation Query Sample

 …then the server might reply with the following information: 

 <entry>
<id>http://endpoint_service_URI/CommandDescriptions('Get-Process')</id>
<category scheme="http://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 command.

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 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 “Complete”, 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 “Complete”, 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 need to specify the endpoint service URI as well as pass user name, password and domain
Uri serviceroot = new Uri(“<endpoint_service_URL>”);
NetworkCredential serviceCreds = new NetworkCredential("testuser","testpassword","testdomain");
CredentialCache cache = new CredentialCache();
cache.Add(serviceroot, "Basic", serviceCreds);

//create data service context with protocol version 3 to connect to your endpoint
DataServiceContext context = new DataServiceContext(serviceroot,System.Data.Services.Common.DataServiceProtocolVersion.V3);
context.Credentials = cache;

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

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

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

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

// Ask for the invocation instance we just created
DataServiceContext 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 data
if (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 endpoint
afterInvokeContext.DeleteObject(afterInvokeInstance);
afterInvokeContext.SaveChanges();

Table 2.2 – Client Code Implementation

 

Narine Mossikyan
Software Engineer in Test
Standards Based Management


Viewing all articles
Browse latest Browse all 35736

Trending Articles