Thursday, December 8, 2016

Invoke workflow with Perl and REST

Ever heard of REST ?  In a nutshell : REST is a way of talking to a remote 'system' using existing standards.  http(s) as the protocol and json or xml as the carrier.  Reading is a GET, Writing is a POST.

Oh, and did you know WFA has a REST api ?  That's right, you can talk to WFA with about any programming language there is.

I'm a powershell / .NET guy, so I'll post a powershell sample ASAP.  But I just saw this nice piece of PERL code from my colleague Todd and couldn't wait sharing.  I guess Todd won't mind :), and hopefully he'll add some more comment to this article.

As far as I can read the perl code, he has built in the capture of CLI parameters, he's validating them and basing the workflow-userinput and workflow-name on the CLI input.  Obviously you don't just want to copy-paste this, you would need change the logic of the CLI input to your needs, for after all, a nice piece of code, that allows you to study how the workflow config can be passed by cli-input.

#!/usr/bin/perl

# If we primarily run the workflow from outside WFA, then we should
# use no-op Unix scripts that return 'false' so that the workflow
# fails.

use strict;
use MIME::Base64;
use REST::Client;
use XML::Simple;
#use XML::SAX::Expat; # This is just here for pp to work right when packaging.
use Getopt::Long;

use Data::Dumper;

$ENV{'PERL_LWP_SSL_VERIFY_HOSTNAME'} = 0;

my $WFA_SERVER          = '';
my $WFA_USER            = 'admin';
my $WFA_USER_PW         = '';
my $EOD_WF_NAME_cDOT    = 'Cdot workflowname';
my $EOD_WF_NAME_7MODE   = '7Mode workflowname';


my $WF_EXEC_TIMEOUT_MINS = 30;

my $TRUE = 1;
my $FALSE = 0;

my $global_error; # global error variable

my $workflow_name;

my $cfg = XMLin(undef,ForceArray => ['ENV'], KeyAttr => [])
   or die "Can't open cfg file: $!";
die $global_error unless cfg_is_valid($cfg);

my %cli_params;
#$cli_params{'environment'} = 'SUP';
$cli_params{'timeout'} = $WF_EXEC_TIMEOUT_MINS;
$cli_params{'na_mode'} = 'cDOT';

GetOptions(
   'environment=s'  => \$cli_params{'environment'},
   'na_mode=s'      => \$cli_params{'na_mode'},
   'no-op'         => \$cli_params{'no-op'},
   'timeout=i'      => \$cli_params{'timeout'},
   'clone-set=s'    => \$cli_params{'clone-set'},
   'help'           => \$cli_params{'help'},
);
usage($cfg) if $cli_params{'help'};
validate_params(\%cli_params, $cfg);

#$workflow_name = $cfg->{'WF_cDOT'}{'name'};

if ( $cli_params{'no-op'} ) {
   $workflow_name = $cfg->{'WF_NOOP'}{'name'};
}
elsif ( $cli_params{'na_mode'} =~ m/cDOT/i ) {
  $workflow_name = $cfg->{'WF_cDOT'}{'name'};
}
elsif( $cli_params{'na_mode'} =~ m/7Mode/i ){
  $workflow_name = $cfg->{'WF_7MODE'}{'name'};
}

my $headers = {
        Authorization => 'Basic '
          . encode_base64($cfg->{'WFA_SERVER'}{'user'}
          . ':'
          . $cfg->{'WFA_SERVER'}{'password'}),
        'Content-type' => 'application/xml',
};

my $wfa_handle = REST::Client->new( {host => "https://" . $cfg->{'WFA_SERVER'}{'host'} ."/rest",} );

my $workflow = get_workflow_xml($wfa_handle, $headers, $workflow_name);
if ( ! $workflow ) {
   print STDERR "ERROR: unable to update storage for " . $cli_params{'environment'} . "\n";
   print STDERR "WFA server returned response code: " . $wfa_handle->responseCode() . "\n";
   print STDERR $wfa_handle->responseContent() . "\n";
   exit (1);
}

my $uuid = $workflow->{'workflow'}{'uuid'};

my %wf_inputs = (
   'environment_type'    => $cli_params{'environment'},
   'AutoSelectSnap'      => 'true',
   'called_from_rest'    => 'true',
   'CloneSetName'       => $cli_params{'clone-set'},
);

my $job_id = run_workflow($wfa_handle, $headers, $uuid, \%wf_inputs);
if ( $job_id ) {
  if ( get_workflow_status( $wfa_handle, $job_id, $headers ) ){
    exit(0);
  }
  else{
    print STDERR $global_error;
    exit(1);
  }
}
else{
  print STDERR "ERROR: Unable to update storage for " . $cli_params{'environment'}  . "\n";
  print STDERR "WFA server returned response code: "  . $wfa_handle->responseCode() . "\n";
  print STDERR $wfa_handle->responseContent()                                       . "\n";
  exit(1);
}
exit;

############################## FUNCTIONS #################################

sub get_workflow_xml {
 my ($wfa_handle,$headers,$workflow) = (@_);
 my ($wfa,$uuid);

 $workflow =~ s/\s/%20/g;
 $wfa_handle->GET("/workflows?name=${workflow}", $headers);

 if ($wfa_handle->responseCode() > 201) {
  my $error = $wfa_handle->responseContent();
         print "Error connecting to WFA: $error\n";
         return undef;
 } 
 $wfa  = XMLin($wfa_handle->responseContent(), ForceArray => ['userInput']);

 return $wfa;
}

sub run_workflow {
 my ($wfa_handle,$headers,$uuid,$inputs) = (@_);
 my ($postdata,$key,$value,$sleep,$date);

 # Setup workflow input in XML format.
 $postdata = qq{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workflowInput>
  <userInputValues>};

 # Loop through our user inputs and add them in the XML.
 for $key (keys %{$inputs}) {
  $postdata .= "\n";
  $value = $inputs->{$key};
  $postdata .= qq{    <userInputEntry key="$key" value="$value"/>};
 } 

 # Finish the XML.
 $postdata .= qq{
  </userInputValues>
</workflowInput>
}; 

 # Submit the workflow.
 $wfa_handle->POST( "/workflows/$uuid/jobs" , $postdata , $headers);

 # Get results of workflow submission, sleeping between each time we check,
 # until it is finished.
 if ($wfa_handle->responseCode() > 201) {
  $date = localtime;
      return undef;
 }
   else {
  my $result = XMLin($wfa_handle->responseContent());
  my $jobId  = $result->{'jobId'};
  $date = localtime;
  print "$date: JobId $jobId submitted.\n";
      return $jobId;
 }
}

sub get_workflow_status{
   my ( $wfa_handle, $jobId, $headers ) = @_;
   
   my $sleep = 1;
   my $date;
   
   my $wf_start_time = time();
   
   my $jobfinished = $FALSE;
   my $timeout     = $FALSE;
   my $fail        = $FALSE;
   while (! $jobfinished && ! $timeout ) {
      $date = localtime;
      $wfa_handle->GET("/workflows/${uuid}/jobs/$jobId", $headers);
      my $jobs      = XMLin($wfa_handle->responseContent());
      my $jobStatus = $jobs->{'jobStatus'}{'jobStatus'};
      my $startTime = $jobs->{'jobStatus'}{'startTime'};
      my $endTime   = $jobs->{'jobStatus'}{'endTime'};
      my $error     = $jobs->{'jobStatus'}{'errorMessage'} if ($jobs->{'jobStatus'}{'errorMessage'});
      if ($jobStatus =~ /completed/i) {
         print "$date: Job $jobId finished successfully.  Started at $startTime and ended at $endTime.\n";
         $jobfinished++;
      } elsif ($jobStatus =~ /failed/i) {
         #print "$date: Job $jobId failed.  Started at $startTime and ended at $endTime.  Error message was: $error\n";
         $fail = $TRUE;
         $global_error = $error;
         $jobfinished++;
      } else {
         #print "$date: Job $jobId still running.  Status is $jobStatus.\n";
         sleep $sleep;
      }      
      $timeout = (time() - $wf_start_time) > $cli_params{'timeout'}*60;
   }

  if ( ! $fail ) {
    return 1;
  }
  elsif ( $timeout ){
    $global_error = 'Workflow timed out';
    return undef;
  }
  else{
    return undef;
  }
}


sub validate_params{
  my ($params_r, $cfg_r ) = @_;
  
  my $valid_envs;
  my @valid_envs;
  foreach my $env ( @{$cfg_r->{'ENVS'}{'ENV'}} ){
    push(@valid_envs, $env->{'name'});
  }
  $valid_envs = join('|', @valid_envs);
  
  $params_r->{'environment'} =~ tr/a-z/A-Z/;
  
  usage($cfg_r) unless ($params_r->{'environment'}
                  && ($params_r->{'environment'} =~ m/${valid_envs}/)
                  && $cli_params{'clone-set'}
                 );

  
  if ( $params_r->{'na_mode'} ) {
   usage($cfg_r) unless ($params_r->{'na_mode'} =~ m/cDOT|7Mode/i );
  }
}

sub usage{
  my ( $cfg_r ) = @_;
  
  my $valid_envs;
  my @valid_envs;
  foreach my $env ( @{$cfg_r->{'ENVS'}{'ENV'}} ){
    push(@valid_envs, $env->{'name'});
  }
  $valid_envs = join('|', @valid_envs);
  
    print <<EOF;
the-name-of-this-script.pl --environment=<${valid_envs}>
EOF

  exit(0);
}

sub cfg_is_valid{
  my ( $cfg_r ) = @_; 

  if ( ! (      exists $cfg_r->{'WFA_SERVER'}
             && exists $cfg_r->{'WF_cDOT'}
             && exists $cfg_r->{'WF_7MODE'}
             && exists $cfg_r->{'NA_MODE'}
             && exists $cfg_r->{'ENVS'}
             ) ){
    $global_error =  "Invalid config file, does not contain required entries";
    return $FALSE;
  }
  elsif( ! ( exists $cfg_r->{'WFA_SERVER'}{'host'}
            && exists $cfg_r->{'WFA_SERVER'}{'user'}
            && exists $cfg_r->{'WFA_SERVER'}{'password'})
        ){
    $global_error =  "Invalid config file: WFA_SERVER must contain host, user, & password attributes.";
    return $FALSE;
  }
  elsif( ! exists $cfg_r->{'WF_cDOT'}{'name'} ){
    $global_error = "Invalid config file: WF_cDOT must contain name entry";
    return $FALSE;
  }
  elsif( ! exists $cfg_r->{'WF_7MODE'}{'name'} ){
    $global_error =  "Invalid config file: WF_7MODE must contain name entry";
    return $FALSE;
  }
  elsif( ! exists $cfg_r->{'NA_MODE'}{'mode'} ){
    $global_error = "Invalid config file: NA_MODE or mode name not present";
    return $FALSE;
  }
  
  if (! exists $cfg_r->{'WFA_SERVER'}{'timeout_mins'} ) {
   $cfg_r->{'WFA_SERVER'}{'timeout_mins'} = $WF_EXEC_TIMEOUT_MINS;
  }
  
  foreach my $env ( @{$cfg_r->{'ENVS'}{'ENV'}} ){
    $env->{'name'} =~ tr/a-z/A-Z/;    
  }
  
  return $TRUE;
  
}

4 comments :

  1. WFA Guy, Not seeing your powershell implementation of using REST API. Was that posted ?

    ReplyDelete
    Replies
    1. hmmm... seems like I didn't. The concept of using "invoke-restmethod" is so simple, it never occurred to me to actually add it. I'll add some something this afternoon.

      Delete
  2. Hi,

    Thanks for this script, it's exactly what I'm looking for.
    With Oncommand Unified Manager now running more and more on appliances the need for Perl scripts is becoming much bigger.
    Thanks again!

    ReplyDelete