Homemade remote CLI for NetApp

Security is one of those things that everyone knows they need to do, but it rarely gets done to the level that it should be.  This, at least in my experience, is primarily because security makes general, day-to-day tasks more difficult.  Take, for instance, rsh.  Rsh by itself is a great time saver…admit it…it’s great to just be able to execute commands from your admin host and have the results returned back.  You can parse them however you like using standard operating system tools like grep, awk, and sed, and best of all (or perhaps worst…) you don’t have to type the password repeatedly.

However, all of the benefits of rsh can be realized using ssh, it just takes a little more setup.  But, I’m not going to get into that today.  What if you just want a way to securely execute commands against your NetApp without consuming the sole connection to your your filer via ssh (you have telnet and rsh disabled, right?).  What if you don’t want to enable ssh, telnet, or rsh but still want to have a pseudo command line?  Assuming you have SSL (https) access enabled, you can use the Perl SDK to access, and execute commands against, your filer almost like you were telnet/ssh’d into it.

The magic comes from the undocumented system-cli SDK command.  It allows you to execute almost any command just as though you were sitting at the console.

The great part is that with this, you can accomplish probably 99% or more of all tasks having only one access method enabled to your NetApp: the https/ssl option.  SSH, RSH, telnet and HTTP can all be disabled.

I say almost because there are two types of commands that do not work using the below Perl script.  The first type is non-terminating commands.  These, at least off the top of my head, are primarily the stats show commands with the –i option specified.  With the –i option, the stats command repeats every number of seconds specified.  Now, the caveat to this is that you can also specify a –c option that limits the number of occurrences to the number specified.  The downside to this is that if you issue a command like stats show –i 5 –c 5 volume:*:read_ops then the command will take 25 seconds, at which point the results, as a whole, will be returned.

This also applies to issuing man commands.  Man will not return (at least with the simulator) to STDOUT, so system-cli doesn’t capture the output.

So, without any more pontificating by me, here is some sample output and the script.  If you would like to see additional examples, let me know in the comments.

        Welcome to the NetApp rCLI!
Connected to netapp1, OnTAP 7.3.1, using HTTPS
  Type "exit" to leave the rCLI.

not_root@netapp1> hostname
netapp1
not_root@netapp1> vol status
         Volume State           Status            Options
           vol0 online          raid0, flex       root, no_atime_update=on,
                                                  create_ucode=on,
                                                  convert_ucode=on
not_root@netapp1> stats list counters volume
Counters for object name: volume
        avg_latency
        total_ops
        read_data
        read_latency
        read_ops
        write_data
        write_latency
        write_ops
        other_latency
        other_ops

not_root@netapp1> priv set advanced
Warning: These advanced commands are potentially dangerous; use
         them only when directed to do so by Network Appliance
         personnel.
not_root@netapp1*> stats list counters volume
Counters for object name: volume
        avg_latency
        total_ops
        read_data
        read_latency
        read_ops
        write_data
        write_latency
        write_ops
        other_latency
        other_ops
        nfs_read_data
        nfs_read_latency
        nfs_read_ops
        nfs_write_latency
        nfs_write_ops
        nfs_other_latency
        nfs_other_ops
        cifs_read_data
        cifs_read_latency
        cifs_read_ops
        cifs_write_data
        cifs_write_latency
        cifs_write_ops
        cifs_other_latency
        cifs_other_ops

not_root@netapp1*> priv set
not_root@netapp1> exit
#!/usr/bin/perl -w
#
# na-rcli.pl - written by Andrew Sullivan, 2010-02-08
#
# Please report bugs and request improvements at http://get-admin.com/blog/?p=947
#
# The NetApp SDK can be found here: http://communities.netapp.com/docs/DOC-1110
#
# Options are:
#   --hostname|-H = (mandatory) hostname or IP of NetApp to connect to
#   --username|-u = (mandatory) username to connect with
#   --password|-p = (optional)  password for user, will be prompted if not supplied
#   --protocol|-P = (optional)  Currently only HTTP and HTTPS are available, HTTPS
#                                 is the default
#
# Examples:
#   na-rcli.pl --hostname my_filer --username not_root --password some_secret
#     This will result in you connecting to the host using the HTTPS protocol.
#
#   na-rcli.pl -H my_filer -u not_root -P HTTP
#     Short method of connecting, will prompt for password and use HTTP connection
#
# TODO:
#   Add support for batch mode
#   Add support for SSH as the transport method
#
# You may need to uncomment this line and correct the path if the NetApp Perl SDK
# is not available in your default Perl library path.
#use lib "./NetApp";
 
use strict;
use Getopt::Long qw(:config no_ignore_case);
 
use NaServer;
use NaElement;
 
main( );
 
sub main {
	my $opts = parse_options();
	$opts->{ 'privState' } = "admin";
 
	my $server = getFiler( $opts->{ 'hostname' }, $opts->{ 'username' }, $opts->{ 'password' }, $opts->{ 'protocol' } );
 
	# some nice data to print for the user
	my $ontap = ( getOnTAP( $server ) =~ /NetApp Release (.*?):/ ? $1 : "unknown" ); 
 
	my $hostname = executeCli( $server, $opts, "hostname" );
	chomp( $hostname );
	$opts->{ 'hostname' } = $hostname;
 
	print "\n\tWelcome to the NetApp rCLI!\n";
	print "Connected to " . $hostname . ", OnTAP " . $ontap . ", using " . $opts->{ 'protocol' } . "\n";
	print "\tType \"exit\" to leave the rCLI." . "\n\n";
	print prompt( $opts );
 
	# loop until we break on purpose, accepting input from the user.  Ctrl-C will exit the program.
	# Fortunately, there is no state...each command is a seperate entity which reconnects and 
	# reauthenticates against the NetApp each time.  This means we don't have to trap interrupts
	# in order to cleanup after ourselves and prevent lingering connections.
	while ( my $line  = <STDIN> ) {
		chomp($line);
 
		# exit if requested
		if ( $line =~ /[Ee][Xx][Ii][Tt]/ ) {
			last;
		}
 
		# passthrough if empty line
		if ( $line eq "" || ! $line ) {
			next;
		}
 
		# execute our request
		my $result = executeCli( $server, $opts, $line );
 
		# show the goods
		print $result;
 
	} continue {
		# check for one of the priv elevation states
		#print "  Continue: " . $line . "\n";
		if ( $line =~ /^priv set\s*(.*)$/ ) {
			my $state = $1;
			chomp($state);
 
			if ( $state ne "admin" && $state ne "diag" && $state ne "advanced" ) {
				$state = "admin";
			}
 
			$opts->{ 'privState' } = $state;
 
		}
 
		# show the prompt each time
		print prompt( $opts );
	}
}
 
sub prompt {
	my ($opts) = @_;
 
	my $priv = ($opts->{ 'privState' } eq "advanced" || $opts->{ 'privState' } eq "diag") ? '*' : '';
 
	# create the default prompt
	return $opts->{ 'username' } . "@" . $opts->{ 'hostname' } . $priv . "> ";
}
 
sub getFiler {
	my ($hostname, $username, $password, $protocol) = @_;
 
	my $s = NaServer->new($hostname, 1, 3);
	$s->set_style('LOGIN');
	$s->set_admin_user($username, $password);
	$s->set_transport_type($protocol);
 
	return $s;
}
 
sub executeCli {
	my ($server, $opts, $command) = @_;
 
	# form our request
	my $request = NaElement->new('system-cli');
	my $args = NaElement->new('args');
 
	my $executable = split_string( $command );
 
	for my $arg ( @{ $executable } ) {
		$args->child_add_string('arg', $arg);
	}
 
	$request->child_add($args);
 
	# elevate our priv level, if needed
	if ( $opts->{ 'privState' } ne "admin" ) {
		$request->child_add_string('priv', $opts->{ 'privState' });
	}
 
	# execute and print an error or the result from the NetApp
	my $result = $server->invoke_elem($request);
 
	if ($result->results_errno != 0) {
		print STDERR 'Invoke failed! Reason: ' . $result->results_reason() . "\n";
		print STDERR 'Exiting rCLI...';
		exit(1);
	} else {
		return $result->child_get_string('cli-output');
	}
}
 
# extracting out the passed command, taking into account quoted strings that should
# be passed as a single argument
sub split_string {
	# loosely based on http://www.perlmonks.org/?node_id=552969
    my $text = shift;
    my @new = ();
 
	push(@new, $+) while $text =~ /(
		# groups the phrase inside double quotes
		"([^\"\\]*(?:\\.[^\"\\]*)*)"\s?
		# groups the phrase inside single quotes
		| '([^\'\\]*(?:\\.[^\'\\]*)*)'\s?
		# unquoted strings
		| ([^\s]+)\s?
		)/gx;
 
	return \@new;
}
 
sub getOnTAP {
	my ($server) = @_;
 
	my $request = NaElement->new('system-get-version');
	my $result = $server->invoke_elem($request);
 
	if ($result->results_errno != 0) {
		print STDERR 'Invoke failed! Reason: ' . $result->results_reason() . "\n";
		exit(1);
	} else {
		return $result->child_get_string('version');
	}
 
}
 
sub parse_options {
 
	my %options = (
		'hostname'  => '',
		'username'  => '',
		'password'  => '',
		'protocol'  => 'HTTPS',
		'help'      => 0
	);
 
	GetOptions( \%options,
		'hostname|H=s',
		'username|u=s',
		'password|p:s',
		'protocol|P:s',
		'help|h'
	);
 
	if (! $options{ 'hostname' } || ! $options{ 'username' } || $options{ 'help' }) {
		print_usage();
		exit(1);
	}
 
	$options{'protocol'} = uc( $options{'protocol'} );
 
	# default to HTTP protocol
	if ( $options{'protocol'} ne "HTTP" || $options{'protocol'} ne "HTTPS" ) {
		$options{'protocol'} = "HTTP";
	}
 
	if (! $options{ 'password' }) {
		print "Enter password: ";
		if ( $^O eq "MSWin32" ) {
			require Term::ReadKey;
 
			Term::ReadKey->import( qw(ReadMode) );
			Term::ReadKey->import( qw(ReadLine) );
			ReadMode('noecho');
 
			chomp( $options{ 'password' } = ReadLine(0) );
 
			ReadMode('normal');
 
		} else {
			system("stty -echo") and die "ERROR: stty failed\n";
			chomp ( $options{ 'password' } = <STDIN> );
			system("stty echo") and die "ERROR: stty failed\n";
		}
 
		print "\n";
	}
 
	return \%options;
}
 
sub print_usage {
	print <<EOU
  Missing or incorrect arguments!
 
  na-rcli.pl --hostname|-H <hostname> 
             --username|-u <username> 
           [ --password|-p <password> ]
		   [ --protocol|-P HTTP|HTTPS ]
 
  na-rcli.pl --help|-h
 
EOU
 
}