# 
# © Copyright 2009 Hewlett-Packard Development Company, L.P.
# EPL license http://www.eclipse.org/legal/epl-v10.html
#
## API for talking to HP Onboard Administrator
## NOTE:
## All parameters are passed by name!
## For example:
## 	hpoa->new(oaAddress => '16.129.49.209');
package xCAT::hpoa;
use strict;
use SOAP::Lite;
use vars qw(@ISA);
@ISA = qw(SOAP::Lite);
# Constructor
# Input: oaAddress, the IP address of the OA
# Output: SOAP::SOM object (SOAP response)
sub new {
  my $class = shift;
  return $class if ref $class;
  my $self = $class->SUPER::new();
  my %args = @_;
  die "oaAddress is a required parameter"
    unless defined $args{oaAddress};
  # Some info we'll need
  $self->{HPOA_HOST} 		= $args{oaAddress}; # OA IP address
  $self->{HPOA_KEY} 		= undef; # oaSessionKey returned by userLogIn
  $self->{HPOA_SECURITY_XML} 	= undef; # key placed in proper XML
  $self->{HPOA_SECURITY_HEADER} = undef; # XML translated to SOAP::Header obj
  bless($self, $class);
  # We contact the OA via this URL:
  my $proxy = "https://". $self->{HPOA_HOST} . ":443/hpoa";
  # One of the cool things about SOAP::Lite is that almost every
  # method returns $self.  This allows you to string together
  # as many calls as you need, like this:
  $self
    # keep the XML formatted for human readability, in case
    # we ever have to look at it (unlikely)
    -> readable(1)
    # Need to tell SOAP about some namespaces.  I don't know if they
    # are all necessary or not, but I got them from the hpoa.wsdl
    -> ns("http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", "wsu")
    -> ns('http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', "wsse")
    -> ns('http://www.w3.org/2001/XMLSchema-instance', 'xsi')
    -> ns('http://www.w3.org/2003/05/soap-encoding', 'SOAP-ENC')
    -> ns('http://www.w3.org/2003/05/soap-envelope', 'SOAP-ENV')
    -> ns('http://www.w3.org/2001/XMLSchema', 'xsd')
    -> default_ns("hpoa.xsd", "hpoa")
    # Inform SOAP of the OA URL
    -> proxy($proxy);
  return $self;
}
# Method: call
# Input: method and a hash of method's input params (see below)
# Output: SOAP::SOM object (SOAP response)
#
# All methods in the OA API end up getting called by this routine,
# even though the user invokes them directly using the method name.
# For example, code that looks like this:
# 	$hpoa->userLogIn(username=>$name, password=>$pass)
# results in this call:
#	$hpoa->call('userLogIn', username=>$name, password=>$pass)
sub call {
  my ($self, $method, %args) = @_;
  #
  # Each item of %args is of the form:
  #    ($name => $value).
  #
  # $value is usually a scalar and SOAP::Lite infers a type.
  #
  # If the value needs to be explicitly typed, the $value should be a
  # reference to an array of the form:
  #    [ $scalar, $type ]
  # This should work for any parameter that you want to explicitly
  # type, but for some reason the OA was not having any of it the
  # last time I tried.
  #
  # If the method calls for an array of values, the $value should be
  # a reference to an array of the form:
  #    [ $itemName, $itemArrayRef, $itemType ]
  #
  # If the method calls for more complicated structure, the $value
  # should be a reference to a hash of the form:
  #    { name1 => value1, name2 => value2 ... }
  # The values can themselves be scalars, array refs or hash refs,
  # which will themselves be processed recursively.
  #
  # Put the params in a form SOAP likes.
  my @soapargs = ();
  while (my ($k, $v) = each %args) {
    push @soapargs, $self->process_args($k, $v);
  }
  # This is required if there are no params, otherwise SOAP::Lite
  # makes an XML construct that the OA doesn't like.
  @soapargs = SOAP::Data->type('xml'=> undef)
    unless @soapargs;
  # Add the security header if it's not the login method.
  # I'm hoping that the header will be ignored by the few methods
  # that don't require security.
  push (@soapargs, $self->{HPOA_SECURITY_HEADER})
    unless ($method eq 'userLogIn') || !defined $self->{HPOA_SECURITY_HEADER};
  # Make sure we're using the correct version of SOAP, but
  # don't mess up packages that use a different version.
  my $version = xCAT::hpoa->soapversion();
  xCAT::hpoa->soapversion('1.2');
  # Call the method and put the response in $r
  my $r = $self->SUPER::call($method, @soapargs);
  # Reset the SOAP version
  xCAT::hpoa->soapversion($version);
  # If this was the login method and it was successful, then extract
  # the session key and remember it for subsequent calls.
  if ($method eq 'userLogIn' && !$r->fault) {
    my $key = $r->result()->{oaSessionKey};
    # Got this XML code from the HP Insight Onboard Administrator SOAP
    # Interface Guide 0.9.7
    my $xml = '
  
     '
       . $key .
    '
  
';
    $self->{HPOA_KEY} 		  = $key;
    $self->{HPOA_SECURITY_XML} 	  = $xml;
    $self->{HPOA_SECURITY_HEADER} = SOAP::Header->type('xml' => $xml);
  }
  # Return the response
  return $r;
}
## Create the correct SOAP::Data structure for the given args
## $n is the argument name
## $v is the value and can be of the following 4 forms:
##   $scalar
##	- A scalar value.  No further processing takes place.
##	  Produces: value
##   [ $scalar, $type ]
##	- An array ref containing a scalar value and type.  No further
##	  will take place.
##	  Produces: value
##   [ $itemName, $aref, $type ]
##	- An array ref containing the name for the elements, the elements
##	  themselves in an array ref, and the type for the elements.  The
##	  elements themselves can be processed.
##	  Produces: 
##		      - value1##
- value2##		    
##   { $n1 => $v1, $n2 => $v2 ... }
##	- A hash ref containing name value pairs that can themselves
##	  be processed.
##	  Produces: 
##		      v1
##		      v2
##	 	    
sub process_args {
  my ($self, $n, $v, $t) = @_;
  print "process args: $n => $v\n"					if 0;
  if (!ref $v) {		# untyped scalar
    print "\nUNTYPED SCALAR: $n => $v\n"				if 0;
    return SOAP::Data->new(name => $n, value => $v, type => '');
  }
  if (ref $v eq 'HASH') {	# structure
    my ($nn, $vv, @ar);
    while (($nn, $vv) = each %$v) {
      print "\nSTRUCTURE $n: $nn => $vv\n"				if 0;
      unshift @ar, $self->process_args($nn, $vv);
    }
    return SOAP::Data->name($n => \SOAP::Data->value(@ar));
  }
  if (ref $v eq 'ARRAY') {
    if (scalar @$v == 2) {	# typed scalar
      my ($value, $type) = @$v;
      print "\nTYPED SCALAR: $n => $value ($type)\n"			if 0;
      return SOAP::Data->new(name => $n, value => $value, type => $type);
    }
    # Else an array of values
    my ($itemName, $aref, $type) = @$v;
    my (@ar, $item);
    foreach $item (@$aref) {
      if (ref $item eq 'HASH') {
	print "\nSUB STRUCTURE $n: $itemName => $item ($type)\n"	if 0;
	unshift @ar, $self->process_args("$itemName", $item);
      } else {
	print "\nARRAY $n: $itemName => $item ($type)\n"		if 0;
	unshift @ar, $self->process_args($itemName, [$item, $type]);
      }
    }
    return SOAP::Data->name($n => \SOAP::Data->value(@ar));
  }
  die "Unexpected input parameter value: $n => $v\n";
}
###
### Special fault info for OAs
###
# The OA uses it's own fault data structures.  The simple
# fault methods provided by SOAP::Lite are usually undef.
# The OA's fault data looks like this:
# {
#   'Detail' => {
#      'faultInfo' => {
#         'operationName' => 'userLogIn',
#         'errorText' => 'The user could not be authenticated.',
#         'errorCode' => '150',
#         'errorType' => 'USER_REQUEST'
#      }
#   },
#   'Reason' => {
#      'Text' => 'User Request Error'
#   },
#   'Code' => {
#      'Value' => 'SOAP-ENV:Sender'
#   }
#}
#
# In your code, you should generally check that $response->fault
# is defined, then print $response->oaErrorMessage.
# If you know the codes, you can act on $response->oaErrorCode
#
# The OA's fault structure
sub SOAP::SOM::oaFaultInfo {
  my ($self, @args) = @_;
  return $self->fault->{Detail}->{faultInfo}
    if (defined $self->fault &&
	defined $self->fault->{Detail} &&
	defined $self->fault->{Detail}->{faultInfo});
  return undef;
}
# The name of the method producing the fault
sub SOAP::SOM::oaOperationName {
  my ($self, @args) = @_;
  my $oafi = $self->oaFaultInfo;
  return $oafi->{operationName}
    if defined $oafi &&
      defined $oafi->{operationName};
  return undef;
}
# Text of the OA fault
sub SOAP::SOM::oaErrorText {
  my ($self, @args) = @_;
  my $oafi = $self->oaFaultInfo;
  return $oafi->{errorText}
    if defined $oafi &&
      defined $oafi->{errorText};
  return undef;
}
# Numeric code of the OA fault
sub SOAP::SOM::oaErrorCode {
  my ($self, @args) = @_;
  my $oafi = $self->oaFaultInfo;
  if (defined $oafi) {
    return $oafi->{errorCode}
      if defined $oafi->{errorCode};
    return $oafi->{internalErrorCode}
      if defined $oafi->{internalErrorCode};
  }
  return undef;
}
# Bay Number of the OA fault
sub SOAP::SOM::oaOperationBayNumber {
  my ($self, @args) = @_;
  my $oafi = $self->oaFaultInfo;
  return $oafi->{operationBayNumber}
    if defined $oafi &&
      defined $oafi->{operationBayNumber};
  return undef;
}
# Sometimes there's extra fault information
# (Haven't seen any yet!)
sub SOAP::SOM::oaExtraFaultData {
  my ($self, @args) = @_;
  my $oafi = $self->oaFaultInfo;
  return $oafi->{extraData}
    if defined $oafi &&
      defined $oafi->{extraData};
  return undef;
}
# Nicely formatted error message for human consumption.
# Tries to use the oaErrorText and oaErrorCode, if defined,
# else uses the reason text.
sub SOAP::SOM::oaErrorMessage {
  my ($self, @args) = @_;
  my $errorText  = $self->oaErrorText;
  # Reason text is either an error message from SOAP (as when
  # the method or argument doesn't exist), or it's a formatted
  # form of the faultInfo->errorType enumeration.
  my $reasonText = $self->fault->{Reason}->{Text};
  return $reasonText
    unless defined $errorText;
  my $operationName = $self->oaOperationName;
  my $operationBay  = $self->oaOperationBayNumber;
  my $errorCode     = $self->oaErrorCode;
  my $extraData     = $self->oaExtraFaultData;
  my $operation = "'$operationName' call";
  $operation .= " on bay $operationBay"
    if $operationBay;
  my $completeText =
    "$reasonText $errorCode during $operation: $errorText";
  $completeText .= "\n\t$extraData" if $extraData;
  return $completeText;
}
1;