#============================================================#
# #
# $ID:$ #
# #
# NaServer.pm #
# #
# Client-side interface to ONTAPI APIs #
# #
# Copyright (c) 2010 NetApp, Inc. All rights reserved. #
# Specifications subject to change without notice. #
# #
# This SDK sample code is provided AS IS, with no support or #
# warranties of any kind, including but not limited to #
# warranties of merchantability or fitness of any kind, #
# expressed or implied. This code is subject to the license #
# agreement that accompanies the SDK. #
# #
# tab size = 8 #
# #
# Note: This library has been modified to include the zapi #
# logging feature. Updation to this library should be #
# done with atmost care. Please e-mail dl-nacl-dev #
# before any modification or updation to this file is #
# done. #
# #
# AUTHOR: dl-nacl-dev #
#============================================================#
package NaServer;
$VERSION = '1.0'; # work with all versions
use Socket;
use LWP::UserAgent;
use XML::Parser;
eval "require Net::SSLeay";
eval "require IO::Select";
use NaElement;
use IO::Socket;
use NATE::Time qw(timestamp);
use NATE::ParamSet qw( param_global);
# For Zapi_logging
use log_zapi;
use Encode qw(encode_utf8);
# use vars ('@ISA', '@EXPORT');
# use Exporter;
# @ISA = qw(Exporter);
# @EXPORT = qw(invoke);
# @EXPORT = qw(invoke_elem);
my $chk_ssl_init = 0;
my $add_sha_256_digest = 1;
#============================================================#
=head1 NAME
NaServer - class for managing Network Appliance(r)
filers using ONTAPI(tm) APIs.
=cut
=head1 DESCRIPTION
An NaServer encapsulates an administrative connection to
a NetApp filer running ONTAP 6.4 or later. You construct
NaElement objects that represent queries or commands, and
use invoke_elem() to send them to the filer (a convenience
routine called invoke() can be used to bypass the element
construction step. The return from the call is another
NaElement which either has children containing the command
results, or an error indication.
The following routines are available for setting up
administrative connections to a filer.
=cut
#============================================================#
use strict;
$::ZAPI_xmlns = "http://www.netapp.com/filer/admin";
$::ZAPI_dtd = "file:/etc/netapp_filer.dtd";
my $FILER_dtd = "file:/etc/netapp_filer.dtd";
my $DFM_ZAPI_dtd = "file:/etc/netapp_dfm.dtd";
my $AGENT_ZAPI_dtd = "file:/etc/netapp_agent.dtd";
$::ZAPI_snoop = 0;
#============================================================#
=head2 new($filer, $majorversion, $minorversion)
Create a new connection to filer $filer. Before
use, you either need to set the style to "hosts.equiv"
or set the username (always "root" at present) and
password with set_admin_user().
=cut
sub new {
my ($class) = shift;
my ($server) = shift;
my ($major_version) = shift;
my ($minor_version) = shift;
my ($port) = 80;
my ($user) = "root";
my ($password) = "";
my ($style) = "LOGIN"; # LOGIN or HOSTS
my ($vfiler) = "";
my ($server_type) = "";
my ($debug_style) = "";
my ($xml) = "";
my ($timeout) = 0;
my ($prev_resv_port) = 0;
my ($originator_id) = "";
my ($target_cluster_uuid) = "";
my ($target_vserver_name) = "";
my ($target_vserver_uuid) = "";
my ($app_header) = "";
my ($zapi_header) = "";
my ($use_cba) = 0;
my ($enable_server_cert_verification) = 0;
my ($enable_hostname_verification) = 0;
my ($ctx) = undef;
my ($trace_threshold) = -1;
my ($complete_xml_output) = undef;
my $self = {
server => $server,
user => $user,
password => $password,
style => $style,
major_version => $major_version,
minor_version => $minor_version,
transport_type => "HTTP",
port => $port,
debug_style =>$debug_style,
vfiler => $vfiler,
server_type => $server_type,
xml => $xml,
timeout => $timeout,
prev_resv_port => $prev_resv_port,
originator_id => $originator_id,
target_cluster_uuid => $target_cluster_uuid,
target_vserver_name => $target_vserver_name,
target_vserver_uuid => $target_vserver_uuid,
app_header => $app_header,
zapi_header => $zapi_header,
use_cba => $use_cba,
enable_server_cert_verification => $enable_server_cert_verification,
enable_hostname_verification => $enable_hostname_verification,
ctx => $ctx,
trace_threshold => $trace_threshold,
complete_xml_output => $complete_xml_output
};
bless $self, $class;
$self->set_server_type("FILER");
return $self;
}
#============================================================#
=head2 set_style($style)
Pass in "LOGIN" to cause the server to use HTTP
simple authentication with a username and
password. Pass in "HOSTS" to use the hosts.equiv
file on the filer to determine access rights (the
username must be root in that case). Pass in "CERTIFICATE"
to use certificate based authentication with the
DataFabric Manager server.
If $style = CERTIFICATE, you can use certificates to
authenticate clients who attempt to connect to a server
without the need of username and password. This style will
internally set the transport type to HTTPS. Verification of
the server's certificate is required in order to properly
authenticate the identity of the server. Server certificate
(with hostname) verification will be enabled by default using
this style. You can disable server certificate (with hostname)
verification using set_server_cert_verification() and you can
disable only hostname verification using set_hostname_verification().
=cut
sub set_style ($$) {
my $self = shift;
my $style = shift;
if ($style ne "HOSTS" && $style ne "LOGIN" && $style ne "CERTIFICATE") {
return $self->fail_response(13001,
"in NaServer::set_style: bad style \"$style\"");
}
if ($style eq "CERTIFICATE") {
my $ret = $self->set_transport_type("HTTPS");
return $ret if ($ret);
$ret = $self->set_server_cert_verification(1);
return $ret if ($ret);
} else {
$self->{enable_server_cert_verification} = 0;
$self->{enable_hostname_verification} = 0;
}
if (($self->{style} eq "CERTIFICATE" && $style ne "CERTIFICATE") &&
$self->{transport_type} eq "HTTPS") {
my $ret = $self->init_ssl_context();
return $ret if ($ret);
}
$self->{style} = $style;
return undef;
}
#============================================================#
=head2 get_style()
Get the authentication style
=cut
sub get_style () {
my $self = shift;
return $self->{style};
}
#============================================================#
=head2 set_admin_user($user, $password)
Set the admin username and password. At present
$user must always be "root".
=cut
sub set_admin_user ($$) {
my $self = shift;
$self->{user} = shift;
$self->{password} = shift;
}
#============================================================#
=head2 set_server_type($type)
Pass in one of these keywords: "FILER" or "NETCACHE"
to indicate whether the server is a filer or a NetCache
appliance.
If you also use set_port(), call set_port() AFTER calling
this routine.
The default is "FILER".
=cut
#
# Note that "AGENT" and "DFM" are also valid values. We
# don't expose those to customers yet.
#
sub set_server_type ($$) {
my $self = shift;
my $type = shift;
my $port = $self->{port};
if ($type !~ /^(Filer|NetCache|Agent|DFM)/i) {
return $self->fail_response(13001,
"in NaServer::set_server_type: bad type \"$type\"");
}
($type =~ /Filer/i) && do {
$self->{url} = "/servlets/netapp.servlets.admin.XMLrequest_filer";
};
($type =~ /NetCache/i) && do {
$self->{url} = "/servlets/netapp.servlets.admin.XMLrequest";
$self->{port} = 80;
};
($type =~ /Agent/i) && do {
$self->{url} = "/apis/XMLrequest";
$self->{port} = 4092;
};
($type =~ /DFM/i) && do {
$self->{url} = "/apis/XMLrequest";
$self->{port} = 8088;
if($self->{transport_type} eq "HTTPS") {
$self->{port} = 8488;
}
};
$self->{servertype} = $type;
return undef;
}
#============================================================#
=head2 get_server_type()
Get the type of server this server connection applies to.
=cut
sub get_server_type () {
my $self = shift;
return $self->{servertype};
}
#============================================================#
=head2 set_transport_type($scheme)
Override the default transport type. The valid transport
type are currently "HTTP", "HTTPS".
=cut
sub set_transport_type ($$) {
my $self = shift;
my $scheme = shift;
if ($scheme ne "HTTP" && $scheme ne "HTTPS") {
return $self->fail_response(13001,
"in NaServer::set_transport_type: bad type \"$scheme\"");
}
if ($scheme eq "HTTP") {
$self->{transport_type} = "HTTP";
$self->{port} = 80;
}
if ($scheme eq "HTTPS") {
$self->{transport_type} = "HTTPS";
$self->{port} = 443;
if($self->{servertype} =~ /DFM/i){
$self->{port} = 8488;
}
#One time SSL initialization
if (!$chk_ssl_init) {
Net::SSLeay::load_error_strings();
Net::SSLeay::SSLeay_add_ssl_algorithms();
#Random seed.
Net::SSLeay::randomize("", time ^ $$);
$chk_ssl_init = 1;
}
return $self->init_ssl_context();
}
return undef;
}
#============================================================#
=head2 get_transport_type()
Retrieve the transport used for this connection.
=cut
sub get_transport_type () {
my $self = shift;
return $self->{transport_type};
}
#============================================================#
=head2 set_debug_style($style)
Set the style of debug.
=cut
sub set_debug_style ($$) {
my $self = shift;
my $debug_style = shift;
if ($debug_style ne "NA_PRINT_DONT_PARSE") {
return $self->fail_response(13001,
"in NaServer::set_debug_style: bad style \"$debug_style\"");
}
else {
$self->{debug_style} = $debug_style;
return;
}
}
#============================================================#
=head2 set_port($port)
Override the default port for this server. If you
also call set_server_type(), you must call it before
calling set_port().
=cut
sub set_port ($$) {
my $self = shift;
my $port = shift;
$self->{port} = $port;
}
#============================================================#
=head2 get_port()
Retrieve the port used for the remote server.
=cut
sub get_port () {
my $self = shift;
return $self->{port};
}
#============================================================#
=head2 is_debugging()
Check the type of debug style and return the
value for different needs.
Return 1 if debug style is NA_PRINT_DONT_PARSE
else return 0.
=cut
sub is_debugging()
{
my $self = shift;
my $style = $self->{debug_style};
if ($style ne "NA_NO_DEBUG") {
return 1;
}
else {
return 0;
}
}
#============================================================#
=head2 get_raw_xml_output()
Return the raw XML output.
=cut
sub get_raw_xml_output()
{
my $self = shift;
return $self->{xml};
}
#============================================================#
=head2 set_raw_xml_output($$)
Save the raw XML output.
=cut
sub set_raw_xml_output($$)
{
my $self = shift;
my $xml = shift;
$self->{xml} = $xml;
}
#============================================================#
=head2 use_https()
Determines whether https is enabled.
=cut
sub use_https () {
my $self = shift;
if ($self->{transport_type} eq "HTTPS" ) {
return 1;
} else {
return 0;
}
}
#============================================================#
=head2 invoke_elem($elt)
Submit an XML request already encapsulated as
an NaElement and return the result in another
NaElement.
=cut
sub invoke_elem ($) {
my ($self, $req) = @_;
my $obj = $self->invoke_xmlin_naelementout($req->toEncodedString());
return $obj;
}
=head2 invoke_xmlin_naelementout
my $naelem_obj = $naserver_obj->invoke_xmlin_naelementout($xml_input);
Submit a raw XML request and return the result in an NaElement object.
=cut
sub invoke_xmlin_naelementout {
my ($self, $xmlrequest) = @_;
my ($output_xml, $input_xml) = $self->invoke_xmlin_xmlout($xmlrequest);
my $obj;
if (ref $output_xml) {
# invoke_xmlin_xmlout has returned an NaElement object, since something
# has failed. In this case $output_xml is actually an NaElement object,
# so return it
$obj = $output_xml;
} else {
$obj = $self->parse_xml($output_xml, $input_xml);
}
return $obj;
}
=head2 invoke_xmlin_xmlout
my ($output_xml, $input_xml) = $naserver_obj->invoke_xmlin_naelementout($xmlrequest);
Submit a raw XML as input and return the output XML along with a UTF-8 encoded
form of the input XML.
=over
=item C<< HIDE_AUTHORIZATION_HEADER= 1|0 >>
This parameter is used to determine whether to include the user/password in authorization header or not.
If HIDE_AUTHORIZATION_HEADER is '1', then it will not set user/password in authorization header.
If HIDE_AUTHORIZATION_HEADER is '0' (default), then it will set user/password in authorization header.
=back
=cut
sub invoke_xmlin_xmlout {
my $self = shift;
my $xmlrequest = shift;
my $server = $self->{server};
my $user = $self->{user};
my $password = $self->{password};
my $debug_style = $self->{debug_style};
my $vfiler = $self->{vfiler};
my $originator_id = $self->{originator_id};
my $ctx = $self->{ctx};
my $server_type = $self->get_server_type();
my $target_cluster_uuid = $self->{target_cluster_uuid};
my $target_vserver_name = $self->{target_vserver_name};
my $target_vserver_uuid = $self->{target_vserver_uuid};
my $app_header = $self->{app_header};
my $zapi_header = $self->{zapi_header};
$self->add_diag_msg("#"x80);
$self->add_diag_msg("ADDITIONAL DIAGNOSTIC INFO");
$self->add_diag_msg("#"x80);
$self->add_diag_msg("Authentication style used: " . $self->get_style);
$self->add_diag_msg("Transport type used: " . $self->get_transport_type);
$self->add_diag_msg("Port used on Destination host: " . $self->get_port);
$self->add_diag_msg("Source host OS: " . $^O);
$xmlrequest = encode_utf8( $xmlrequest );
# This is the filer url, in a form acceptable
# to the method line of an HTTP transaction.
my $url = $self->{url};
my $lowport = 0;
my ($non_blocking);
my $using_ssl = $self->use_https();
my $ssl;
my $timeout = $self->get_timeout();
my $sock = undef;
my $need_server_cert_verification = $self->is_server_cert_verification_enabled();
$self->{complete_xml_output} = undef;
#
# Establish socket connection
#
my (undef, undef, $proto) = getprotobyname("tcp");
my $socket_package;
foreach my $package (qw(NaSocket4 NaSocket6)) {
my $filename = $package;
$filename =~ s@::@/@g;
$filename .= ".pm";
require $filename;
if ($package->can_handle_address($server)) {
$socket_package=$package;
last;
}
}
my ($that_sockaddr, $family) = $socket_package->make_sockaddr($server,$self->{port});
$lowport = 1023 if ( $self->get_style() eq "HOSTS" );
while($lowport >= 0) {
$sock = IO::Socket->new();
if (!socket($sock,$family,&SOCK_STREAM,$proto) ) {
return $self->fail_response(13001,
"in Zapi::invoke, cannot create socket");
}
#
# If we are being asked to use a reserved port (we
# are doing hosts.equiv authentication), then we search to
# find an available port number below 1024.
#
my $this_sockaddr;
do{
# do not bind to a reserved port if it is used in previous invoke
if($lowport != 0 && $lowport == $self->{prev_resv_port}) {
$lowport--;
}
$this_sockaddr=$socket_package->make_local_sockaddr($that_sockaddr, $lowport);
$lowport--;
} while (!bind($sock,$this_sockaddr) && $lowport > 0);
if ($lowport == 0) {
close($sock);
return $self->fail_response(13001,
"in Zapi::invoke, unable to bind "
."to reserved port, you must be "
."executing as root");
}
$self->{prev_resv_port} = $lowport + 1;
$self->add_diag_msg("Port used on Source host: " . $self->{prev_resv_port});
#handle connection time out.
if ($timeout > 0) {
$sock->timeout($timeout);
#In Perl 5.8.8 On windows, IO::Socket::connect call doesn't work
#because it dosn't check for EINPROGRESS after connect call
if($^O eq "MSWin32" && ($] * 1000000 < 5008009)){
$non_blocking = 1;
# On windows, the value of FIONBIO is 0x8004667E.
ioctl($sock, 0x8004667e, pack("L!", $non_blocking));
}
}
$self->add_diag_msg("Connection to destination host attempted at: " . timestamp());
if (!$sock->connect($that_sockaddr)) {
if($^O eq "MSWin32") {
# On windows, sometimes the bind will succeed but connect fails
# with EADDRINUSE on a reserved port. So, try to recreate the
# socket, bind it to the next available reserve port and attempt
# to connect it again.
if($self->get_style() eq "HOSTS" and $!{EADDRINUSE}){
close($sock);
$sock = undef;
next;
}
if($timeout > 0 && ($] * 1000000 < 5008009)){
if (($!{EINPROGRESS} || $!{EWOULDBLOCK})) {
$non_blocking = 0;
if (!IO::Select->new(\*$sock)->can_write($timeout)) {
ioctl($sock, 0x8004667e, pack("L!",$non_blocking));
close($sock);
return $self->fail_response(13001,
"in Zapi::invoke, cannot connect to socket");
}
ioctl($sock, 0x8004667e, pack("L!",$non_blocking));
last;
}
}
}
close ($sock);
$self->add_diag_msg("Socket connection to destination host failed at: " . timestamp());
return $self->fail_response(13001,
"in Zapi::invoke, cannot connect to socket: $!");
} else {
$self->add_diag_msg("connection to destination host succeeded at: " . timestamp());
last;
}
}
select($sock); $| = 1; # Turn on autoflushing
select(STDOUT); $| = 1; # Select STDOUT as default output
#
# Create an HTTP request.
#
my $request = HTTP::Request->new('POST',"$url");
my $authorization_header = param_global->get('HIDE_AUTHORIZATION_HEADER') || 0;
if (( $self->get_style() ne "HOSTS" ) && ( $authorization_header != 1 )){
$request->authorization_basic($user,$password);
}
my $content = "";
my $vfiler_req = "";
my $originator_id_req = "";
my $remote_peer_req = "";
my $trace_threshold = $self->{trace_threshold};
if($vfiler ne "") {
$vfiler_req = " vfiler=\"$vfiler\" ";
}
if($originator_id ne "") {
$originator_id_req = " originator_id=\"$originator_id\" ";
}
if($target_cluster_uuid ne "") {
$remote_peer_req = $remote_peer_req . " target-cluster-uuid=\"$target_cluster_uuid\" ";
}
if($target_vserver_name ne "") {
$remote_peer_req = $remote_peer_req . " target-vserver-name=\"$target_vserver_name\" ";
}
if($target_vserver_uuid ne "") {
$remote_peer_req = $remote_peer_req . " target-vserver-uuid=\"$target_vserver_uuid\" ";
}
if ($trace_threshold >= 0) {
$request->header("X-Trace-Threshold" => $trace_threshold);
}
if ($app_header ne "") {
$request->header("X-Dot-Client-App" => $app_header);
}
if ($zapi_header ne "") {
my %hash_zapi_header = ();
if(ref($zapi_header ) eq 'HASH') {
%hash_zapi_header = %$zapi_header;
} else {
%hash_zapi_header = (split /,/,$zapi_header);
}
$request->header(%hash_zapi_header);
}
my $xml_print = "";
my $FILE = "";
if ($server_type eq "FILER") {
$FILE = $FILER_dtd;
}
elsif ($server_type eq "DFM") {
$FILE = $DFM_ZAPI_dtd;
}
elsif ($server_type eq "AGENT") {
$FILE = $AGENT_ZAPI_dtd;
}
$content = ""
.""
.""
.$xmlrequest
."";
if ($debug_style eq "NA_PRINT_DONT_PARSE") {
$xml_print = "\n"
."\n"
."{major_version}.".".$self->{minor_version}."\">"
.$xmlrequest
."";
print "INPUT:\n$xml_print\n";
}
$request->content($content);
$request->content_length(length($content));
# Adding \r before \n , for burt 1059207
my $methline = $request->method()." ".$request->uri()." HTTP/1.0\r\n";
my $headers = $request->headers_as_string("\r\n");
my $bytes_written = 0;
if ($using_ssl) {
$self->add_diag_msg("This ZAPI is using SSL authentication");
$ssl = Net::SSLeay::new($ctx) or return $self->fail_response(13001,
"in Zapi::invoke, failed to create SSL $!");
Net::SSLeay::set_fd($ssl, fileno($sock)); #Must use fileno
if ($need_server_cert_verification) {
Net::SSLeay::set_verify($ssl,
&Net::SSLeay::VERIFY_PEER | &Net::SSLeay::VERIFY_FAIL_IF_NO_PEER_CERT, \&verify);
}
$self->add_diag_msg("Connection to SSL attempted at: " . timestamp());
if(!Net::SSLeay::connect($ssl)) {
$self->add_diag_msg("Connection to SSL failed at: " . timestamp());
return $self->fail_response(13001,
"in Zapi::invoke failed to connect SSL $!");
}
$self->add_diag_msg("Connection to SSL succeeded at: " . timestamp());
if ($need_server_cert_verification) {
my $ret = $self->verify_server_certificate($ssl, $server);
if ($ret) {
Net::SSLeay::free($ssl);
close($sock);
return $ret;
}
}
$bytes_written += Net::SSLeay::ssl_write_all($ssl, $methline);
$bytes_written += Net::SSLeay::ssl_write_all($ssl, $headers);
$bytes_written += Net::SSLeay::ssl_write_all($ssl, "\r\n");
$bytes_written += Net::SSLeay::ssl_write_all($ssl, $request->content());
} else {
print $sock $methline;
$bytes_written += length($methline);
print $sock $headers;
$bytes_written += length($headers);
print $sock "\r\n";
$bytes_written += length("\r\n");
print $sock $request->content();
$bytes_written += length($request->content());
}
$self->add_diag_msg("No of bytes written to the socket to destination host: " . $bytes_written);
my $xml = "";
my $response;
# Inside this loop we will read the response line and all headers
# found in the response.
my $n;
my $state = 0; # 1 means we're in headers, 2 means we're in content
my ($key, $val);
my $line;
my $sock_err = undef;
my $bytes_received = 0;
## Perl socket timeout has no effect during socket read.
## alarm is used (in eval block) to ensure that the control
## returns to the caller after the timeout period.
eval {
local $SIG{ALRM} = sub { die "Timed Out" };
# Setting the alarm with $timeout value
alarm $timeout;
while (1) {
if ($using_ssl) {
$line = Net::SSLeay::ssl_read_CRLF($ssl);
} else {
$line = <$sock>;
}
if ( !defined($line) || $line eq "" ) {
$sock_err = $!;
last;
}
if ( $state == 0 ) {
if ($line =~ s/^(HTTP\/\d+\.\d+)[ \t]+(\d+)[ \t]*([^\012]*)\012//) {
# HTTP/1.0 response or better
my($ver,$code,$msg) = ($1, $2, $3);
$msg =~ s/\015$//;
$response = HTTP::Response->new($code, $msg);
$response->protocol($ver);
$state = 1;
next;
} else {
$sock_err = $!;
if ($using_ssl) {
Net::SSLeay::free ($ssl);
}
close($sock);
return $self->fail_response(13001,
"in Zapi::invoke, unable to parse "
."status response line - $line");
}
} elsif ( $state == 1 ) {
# ensure that we have read all headers.
# The headers will be terminated by two blank lines
if ( $line =~ /^\r*\n*$/ ) {
$state = 2;
} else {
if ($line =~ /^([a-zA-Z0-9_\-.]+)\s*:\s*(.*)/) {
$response->push_header($key, $val) if $key;
($key, $val) = ($1, $2);
if ($key =~ "Content-Length") {
$bytes_received+= $val;
}
}
elsif ($line =~ /^\s+(.*)/ && $key) {
$val .= " $1";
} else {
$response->push_header(
"Client-Bad-Header-Line" => $line);
}
}
} elsif ( $state == 2 ) {
$xml .= $line;
} else {
$sock_err = $!;
if ($using_ssl) {
Net::SSLeay::free ($ssl);
}
close($sock);
return $self->fail_response(13001,
"in Zapi::invoke, bad state value "
."while parsing response - $state\n");
}
}
# Reset the alarm to 0 (i.e. no alarm)
alarm 0;
}; # end of eval
# Check if the 'die' was executed in the previous eval
if($@ and $@ =~ /Timed Out/) {
if ($using_ssl) {
Net::SSLeay::free ($ssl);
}
close($sock);
return $self->fail_response(13001,
"Timeout. Could not read API response. Bytes received are $bytes_received.");
}
if ($using_ssl) {
Net::SSLeay::free ($ssl); # Tear down connection
}
close($sock);
if (!defined($response)) {
return $self->fail_response(13001,"No response received: " . $sock_err . " Bytes received are $bytes_received.");
}
my $code = $response->code();
if ($code != 200)
{
my $http_error_smg = "Server returned HTTP Error";
if ( $code == 401 ) {
return $self->fail_response(13002,"$http_error_smg: Authorization failed. Bytes received are $bytes_received.");
}
return $self->fail_response(13001, "$http_error_smg: $code " . $response->message(). " Bytes received are $bytes_received.");
}
if($sock_err)
{
return $self->fail_response(13001,"Unable to receive API response: " . $sock_err . " Bytes received are $bytes_received.");
}
if ($self->is_debugging() > 0) {
if ($debug_style eq "NA_PRINT_DONT_PARSE") {
$self->set_raw_xml_output($xml);
print "\nOUTPUT:\n$xml\n";
return $self->fail_response(13001,"debugging bypassed xml parsing");
}
}
# For Zapi_logging:
# Log all the zapi details in NATE db
# as per burt#924696 modifying
# the output to write_in_db as raw xml
log_zapi::write_in_db( input => $xmlrequest, output => $xml);
return ($xml, $xmlrequest);
}
#============================================================#
=head2 invoke($api, [$argname, $argval] ...)
A convenience routine which wraps invoke_elem().
It constructs an NaElement with name $api, and
for each argument name/value pair, adds a child
element to it. It's an error to have an even
number of arguments to this function.
Example: $myserver->invoke("snapshot-create",
"snapshot", "mysnapshot",
"volume", "vol0");
=cut
sub invoke (@) {
my $self = shift;
my $api = shift;
my $num_parms = @_;
my $i;
my $key;
my $value;
if ( ($num_parms & 1) != 0 ) {
return $self->fail_response(13001,
"in Zapi::invoke, invalid number of parameters");
}
my $xi = new NaElement($api);
for ($i = 0; $i < $num_parms; $i += 2) {
$key = shift;
$value = shift;
$xi->child_add(new NaElement($key, $value));
}
return $self->invoke_elem($xi);
}
1;
=head1 COPYRIGHT
Copyright 2002-2003 Network Appliance, Inc. All rights
reserved. Specifications subject to change without notice.
This SDK sample code is provided AS IS, with no support or
warranties of any kind, including but not limited to
warranties of merchantability or fitness of any kind,
expressed or implied. This code is subject to the license
agreement that accompanies the SDK.
=cut
###############################################################################
# "private" subroutines for use by the public routines
#
# This is used when the transmission path fails, and we don't actually
# get back any XML from the server.
#
sub fail_response {
my $self = shift;
my $errno = shift;
my $reason = shift;
my $n = new NaElement("results");
$n->attr_set("status","failed");
$n->attr_set("reason","$reason");
$n->attr_set("errno","$errno");
return $n;
}
sub server_start_handler ($$@) {
my $xp = shift;
my $el = shift;
my $n = new NaElement("$el");
push(@$::ZAPI_stack,$n);
my $sz = $#$::ZAPI_stack;
%::ZAPI_atts = ();
while ( @_ ) {
my $att = shift;
my $val = shift;
$::ZAPI_atts{$att} = $val;
$n->attr_set($att,$val);
}
}
sub server_char_handler {
my $xp = shift;
my $data = shift;
my $i = $#$::ZAPI_stack;
$::ZAPI_stack->[$i]->add_content($data);
}
sub server_end_handler {
my $xp = shift;
my $el = shift;
# We leave the last element on the stack.
if ( $#$::ZAPI_stack > 0 ) {
my $sz = $#$::ZAPI_stack;
# Pop the element and add it as a child
# to its parent.
my $n = pop(@$::ZAPI_stack);
my $i = $#$::ZAPI_stack;
$::ZAPI_stack->[$i]->child_add($n);
}
}
# this is a helper routine for invoke_elem
sub parse_raw_xml($$) {
my $self = shift;
my $xml = shift;
$::ZAPI_stack = [];
my $p = new XML::Parser(ErrorContext => 2);
$p->setHandlers(
Start => \&server_start_handler,
Char => \&server_char_handler,
End => \&server_end_handler
);
eval {
$p->parse($xml);
};
if($@) {
return $self->fail_response(13001,
"NaServer::parse_raw_xml - Error in parsing xml: " . $@);
}
if ( $#$::ZAPI_stack < 0 ) {
return $self->fail_response(13001,
"Zapi::parse_xml - no elements on stack");
}
my $r = pop(@$::ZAPI_stack);
return $r;
}
sub parse_xml {
my $self = shift;
my $xml = shift;
my $xmlrequest = shift;
$::ZAPI_stack = [];
my $p = new XML::Parser(ErrorContext => 2);
$p->setHandlers(
Start => \&server_start_handler,
Char => \&server_char_handler,
End => \&server_end_handler
);
eval {
$p->parse($xml);
};
if($@) {
return $self->fail_response(13001,
"NaServer::parse_xml - Error in parsing xml: " . $@);
}
if ( $#$::ZAPI_stack < 0 ) {
return $self->fail_response(13001,
"Zapi::parse_xml - no elements on stack");
}
my $r = pop(@$::ZAPI_stack);
if ( $r->{name} ne "netapp" ) {
return $self->fail_response(13001,
"Zapi::parse_xml - Expected element, "
."but got ".$r->{name});
}
$self->{complete_xml_output} = $r;
my $results = $r->child_get("results");
if (! defined($results)) {
return $self->fail_response(13001,
"Zapi::parse_xml - No results element in output!");
}
return $results;
}
#============================================================#
=head2 set_vfiler($vfiler)
sets the vfiler name. This function is added for vfiler-tunneling.
=cut
sub set_vfiler ($$) {
my $self = shift;
my $vfname = shift;
if($self->{major_version} >= 1) {
if($self->{minor_version} >= 7) {
$self->{vfiler} = $vfname;
return 1;
}
}
return 0;
}
=head2 set_vserver($vserver)
Sets the vserver name. This function is added for vserver-tunneling.
However, vserver tunneling actually uses vfiler-tunneling. Hence this
function internally sets the vfiler name.
=cut
sub set_vserver ($$) {
my $self = shift;
my $vserver = shift;
if($self->{major_version} >= 1 && $self->{minor_version} >= 15) {
$self->{vfiler} = $vserver;
return 1;
}
return 0;
}
=head2 get_vserver()
Gets the vserver name. This function is added for vserver-tunneling.
However, vserver tunneling actually uses vfiler-tunneling. Hence this
function actually returns the vfiler name.
=cut
sub get_vserver () {
my $self = shift;
return $self->{vfiler};
}
=head2 set_originator_id($originator_id)
Function to set the originator_id before executing any ONTAP API.
=cut
sub set_originator_id ($$) {
my $self = shift;
my $originator_id = shift;
$self->{originator_id} = $originator_id;
return 0;
}
=head2 get_originator_id()
Gets the originator_id for the given server context on which the
ONTAP API commands get invoked.
=cut
sub get_originator_id () {
my $self = shift;
return $self->{originator_id};
}
=head2 set_target_cluster_uuid($target_cluster_uuid)
Sets the UUID of a remote peered cluster to which the ONTAP APIs are to be
redirected from current cluster (identified by this NaServer instance).
=cut
sub set_target_cluster_uuid ($$) {
my $self = shift;
my $target_cluster_uuid = shift;
$self->{target_cluster_uuid} = $target_cluster_uuid;
return 1;
}
=head2 set_app_header($app_header)
Sets the value of the X-Dot-Client-App HTTP header
=cut
sub set_app_header ($$) {
my $self = shift;
my $app_header = shift;
$self->{app_header} = $app_header;
return 1;
}
=head2 set_zapi_header()
Sets the zapi_header from which the ONTAP APIs are generated
=cut
sub set_zapi_header ($$) {
my $self = shift;
my $zapi_header = shift;
$self->{zapi_header} = $zapi_header;
return 1;
}
=head2 get_target_cluster_uuid()
Gets the UUID of the remote peered cluster to which the ONTAP APIs are
redirected from current cluster (identified by this NaServer instance).
=cut
sub get_target_cluster_uuid () {
my $self = shift;
return $self->{target_cluster_uuid};
}
=head2 get_app_header()
Gets the app_header from which the ONTAP APIs are generated
=cut
sub get_app_header () {
my $self = shift;
return $self->{app_header};
}
=head2 get_zapi_header()
Gets the zapi_header from which the ONTAP APIs are generated
=cut
sub get_zapi_header () {
my $self = shift;
return $self->{zapi_header};
}
=head2 set_target_vserver_name($target_vserver_name)
Sets the name of a remote peered vserver to which the ONTAP APIs are to be
redirected from current cluster (identified by this NaServer instance).
Note: vserver tunneling must be enabled on the current NaServer instance
using set_vserver() to set the target vserver name for redirecting the APIs.
=cut
sub set_target_vserver_name ($$) {
my $self = shift;
my $target_vserver_name = shift;
if($self->{vfiler} eq "") {
return 0;
}
$self->{target_vserver_name} = $target_vserver_name;
return 1;
}
=head2 get_target_vserver_name()
Gets the name of the remote peered vserver to which the ONTAP APIs are
redirected from current cluster (identified by this NaServer instance).
=cut
sub get_target_vserver_name () {
my $self = shift;
return $self->{target_vserver_name};
}
=head2 set_target_vserver_uuid($target_vserver_uuid)
Sets the uuid of a remote peered vserver to which the ONTAP APIs are to be
redirected from current cluster (identified by this NaServer instance).
=cut
sub set_target_vserver_uuid ($$) {
my $self = shift;
my $target_vserver_uuid = shift;
if($self->{vfiler} eq "") {
return 0;
}
$self->{target_vserver_uuid} = $target_vserver_uuid;
return 1;
}
=head2 get_target_vserver_uuid()
Gets the uuid of the remote peered vserver to which the ONTAP APIs are
redirected from current cluster (identified by this NaServer instance).
=cut
sub get_target_vserver_uuid () {
my $self = shift;
return $self->{target_vserver_uuid};
}
sub set_timeout ($$) {
my $self = shift;
my $timeout = shift;
$self->{timeout} = $timeout;
}
sub get_timeout () {
my $self = shift;
return $self->{timeout};
}
=head2 get_complete_xml_output()
Gets the complete XML output in NaElement format that is being
# returned by the server.
=cut
sub get_complete_xml_output() {
my $self = shift;
return $self->{complete_xml_output};
}
#============================================================#
=head2 set_server_cert_verification()
Enables or disables server certificate verification by the client.
Server certificate verification is enabled by default
when style is set to CERTIFICATE. Hostname verification is enabled
by default during server certificate verification.
=cut
sub set_server_cert_verification ($$) {
my $self = shift;
my $enable = shift;
if ($enable != 0 and $enable != 1) {
return $self->fail_response(13001,
"in NaServer::set_server_cert_verification:
invalid argument $enable specified");
}
if (!$self->use_https()) {
return $self->fail_response(13001,
"in NaServer::set_server_cert_verification: server certificate verification can only be enabled or disabled for HTTPS transport type");
}
# To verify the DFM Server certificate signature, we need to
# add SHA-256 digest. This digest is available in Net::SSLeay 1.36 library
# compiled with OPENSSL_VERSION_NUMBER >= 0x0090800fL.
if ($add_sha_256_digest && $enable && $Net::SSLeay::VERSION >= 1.36 &&
Net::SSLeay::OPENSSL_VERSION_NUMBER() >= 0x0090800f) {
eval Net::SSLeay::EVP_add_digest(Net::SSLeay::EVP_sha256());
$add_sha_256_digest = 0;
}
$self->{enable_server_cert_verification} = $enable;
$self->{enable_hostname_verification} = $enable;
return undef;
}
#============================================================#
=head2 is_server_cert_verification_enabled()
Determines whether server certificate verification is enabled or not.
Returns 1 if it is enabled, else returns 0.
=cut
sub is_server_cert_verification_enabled($) {
my $self = shift;
return ($self->{enable_server_cert_verification});
}
#============================================================#
=head2 set_client_cert_and_key()
Sets the client certificate and key files that are required for
client authentication by the server using certificates.
If key file is not defined, then the certificate file will be
used as the key file.
=cut
sub set_client_cert_and_key ($$$$) {
my $self = shift;
my $cert_file = shift;
my $key_file = shift;
my $key_passwd = shift;
my $err_reason;
my $ctx = $self->{ctx};
if ($ctx eq "") {
return $self->fail_response(13001,
"in NaServer::set_client_cert_and_key: SSL context not initialized");
}
if (!$cert_file) {
return $self->fail_response(13001,
"in NaServer::set_client_cert_and_key: certificate file not specified");
}
if (!(Net::SSLeay::CTX_use_certificate_chain_file($ctx, $cert_file))) {
$err_reason = $self->get_cert_err_reason(&Net::SSLeay::ERR_get_error);
return $self->fail_response(13001,
"in NaServer::set_client_cert_and_key: failed to load certificate file: " . $err_reason);
}
if (!$key_file) {
$key_file = $cert_file;
}
if (!$key_passwd) {
$key_passwd = "";
}
Net::SSLeay::CTX_set_default_passwd_cb($ctx, sub {$key_passwd});
if (!(Net::SSLeay::CTX_use_PrivateKey_file($ctx, $key_file, &Net::SSLeay::FILETYPE_PEM))) {
$err_reason = $self->get_cert_err_reason(&Net::SSLeay::ERR_get_error);
return $self->fail_response(13001,
"in NaServer::set_client_cert_and_key: failed to load key file: " . $err_reason);
}
return undef;
}
#============================================================#
=head2 set_ca_certs()
Specifies the certificates of the Certificate Authorities (CAs) that are
trusted by this application and that will be used to verify the remote server
certificate.
=cut
sub set_ca_certs ($$$) {
my $self = shift;
my $ca_file = shift;
my $err_reason;
my $ctx = $self->{ctx};
if (!$ctx) {
return $self->fail_response(13001,
"in NaServer::set_ca_certs: SSL context not initialized");
}
if ($ca_file) {
if(!(Net::SSLeay::CTX_load_verify_locations($ctx, $ca_file, undef))) {
$err_reason = $self->get_cert_err_reason(&Net::SSLeay::ERR_get_error);
return $self->fail_response(13001,
"in NaServer::set_ca_certs: failed to load trusted CA certificates: " . $err_reason);
}
}
else {
return $self->fail_response(13001,
"in NaServer::set_ca_certs: missing CA certificate file");
}
return undef;
}
#============================================================#
=head2 set_hostname_verification()
Enables or disables hostname verification by the client during server certificate the
server certificate.
=cut
sub set_hostname_verification ($$) {
my $self = shift;
my $enable = shift;
if ($enable != 0 and $enable != 1) {
return $self->fail_response(13001,
"in NaServer::set_hostname_verification:
invalid argument $enable specified");
}
if ($self->{enable_server_cert_verification} == 0) {
return $self->fail_response(13001,
"in NaServer::set_hostname_verification: server certificate verification is not enabled");
}
$self->{enable_hostname_verification} = $enable;
return undef;
}
#============================================================#
=head2 is_hostname_verification_enabled()
Determines whether hostname verification is enabled or not.
Returns 1 if it is enabled, else returns 0
=cut
sub is_hostname_verification_enabled ($) {
my $self = shift;
return ($self->{enable_hostname_verification});
}
#============================================================#
=head2 verify_server_certificate()
Subroutine which verifies the common name in the server certificate against
the given hostname. This subroutine returns "undef" on success.
=cut
sub verify_server_certificate ($$$) {
my $self = shift;
my $ssl = shift;
my $host_name = shift;
my $err_str;
my $res = Net::SSLeay::get_verify_result($ssl);
if ($res != 0) {
$err_str = $self->get_cert_verify_error_string($res);
return $self->fail_response(13001,
"in NaServer::verify_server_certificate: server certificate verification failed: " . $err_str);
}
if ($self->{enable_hostname_verification} == 0) {
return undef;
}
my $cert = Net::SSLeay::get_peer_certificate($ssl);
if (!$cert) {
return $self->fail_response(13001,
"in NaServer::verify_server_certificate: server certificate not present.");
}
my $x509_name = Net::SSLeay::X509_get_subject_name($cert);
if ($x509_name) {
my $cert_name = Net::SSLeay::X509_NAME_get_text_by_NID ($x509_name, 13);
chop($cert_name) if substr( $cert_name, -1, 1 ) eq "\0";
if (lc($host_name) ne lc($cert_name)) {
$err_str = sprintf("in NaServer::verify_server_certificate: server certificate name (CN=%s), hostname (%s) mismatch.",
$cert_name, $host_name);
return $self->fail_response(13001, $err_str);
}
} else {
return $self->fail_response
(13001, "in NaServer::verify_server_certificate: unable to obtain certificate name");
}
return undef;
}
#============================================================#
sub verify {
my ($ok, $subj_cert, $issuer_cert, $depth, $err_code, $arg, $chain) = @_;
return $ok;
}
sub get_cert_err_reason ($$) {
my $self = shift;
my $err_no = shift;
my $err_str;
my $err_reason;
$err_str = Net::SSLeay::ERR_error_string($err_no);
$err_reason = substr ($err_str, rindex($err_str, ':') + 1);
return $err_reason;
}
sub get_cert_verify_error_string ($$) {
my $self = shift;
my $err_no = shift;
my $err_str;
# Return proper error string for X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT
# and X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN errors
if ($err_no == 18) {
$err_str = "the passed certificate cannot be found in the list of trusted certificates";
} elsif ($err_no == 19) {
$err_str = "the certificate chain could be built up using the untrusted certificates but the root could not be found locally";
} else {
$err_str = Net::SSLeay::X509_verify_cert_error_string($err_no);
}
return $err_str;
}
sub init_ssl_context($) {
my $self = shift;
if ($self->{ctx}) {
Net::SSLeay::CTX_free($self->{ctx});
$self->{ctx} = undef;
}
$self->{ctx} = Net::SSLeay::CTX_new() or return $self->fail_response(13001,
"in NaServer::init_ssl_context - failed to create SSL_CTX ");
Net::SSLeay::ERR_clear_error();
Net::SSLeay::CTX_set_options($self->{ctx}, &Net::SSLeay::OP_ALL) and
Net::SSLeay::die_if_ssl_error("ssl ctx set options. Error is ($!)");
}
sub DESTROY ($) {
my $self = shift;
if($self->{ctx}) {
Net::SSLeay::CTX_free($self->{ctx});
}
}
#============================================================#
=head2
set_trace_threshold($threshold_value)
Enables tracing for the API request. This subroutine is
currently used internally. 0 or any positive integer is a
valid threshold value.
Once you enable this, the trace output will be returned in
"trace_output" child element under the root (netapp) element
in the API response.
invoke_elem() will return the entire root element instead of
results element and the caller has to do a child_get() on root
element to get the "results" and "trace_output" values.
=cut
sub set_trace_threshold ($$) {
my $self = shift;
my $threshold = shift;
$self->{trace_threshold} = $threshold;
}
#============================================================#
=head2
Returns the trace threshold value used for the API request.
This subroutine is currently used internally.
=cut
sub get_trace_threshold () {
my $self = shift;
return $self->{trace_threshold};
}
sub get_diag_msgs {
my ($self) = @_;
$self->add_diag_msg("#"x80);
my $msgs = join("\n", @{$self->{triage_info}});
return $msgs;
}
sub add_diag_msg {
my ($self, $msg) = @_;
push(@{$self->{triage_info}}, $msg);
}