#!/usr/bin/perl
#
# This tools parses /etc/tgt/targets.conf file and configures tgt
#
# You can find more info on http://wpkg.org/TGT-admin and download the 
# source code by pointing wget to http://wpkg.org/tgt-admin
#
# Author:  Tomasz Chmielewski
# License: GPLv2
#

use strict;
use Config::General qw(ParseConfig);
use Data::Dumper;
use Getopt::Long;

# Our config file
my $configfile = "/etc/tgt/targets.conf";

sub usage {
	print <<EOF;
Usage:
tgt-admin [OPTION]...
This tool configures tgt targets.

  -e, --execute			read $configfile and execute tgtadm commands
      --delete <value>		delete all or selected targets
				(see "--delete help" for more info)
      --offline <value>		put all or selected targets in offline state
				(see "--offline help" for more info)
      --ready <value>		put all or selected targets in ready state
				(see "--ready help" for more info)
  -s, --show			show all the targets
  -c, --conf <conf file>	specify an alternative configuration file
      --ignore-errors		continue even if tgtadm exits with non-zero code
  -f, --force			force some operations even if the target is in use
  -p, --pretend			only print tgtadm options
      --dump			dump current tgtd configuration
  -v, --verbose			increase verbosity (show tgtadm commands)
  -h, --help			show this help

EOF
	exit;
}

my %conf;
my $param = $ARGV[0];
my $execute = 0;
my $delete = 0;
my $offline = 0;
my $ready = 0;
my $show = 0;
my $alternate_conf="0";
my $ignore_errors = 0;
my $force = 0;
my $pretend = 0;
my $dump = 0;
my $verbose = 0;
my $help = 0;
my $result = GetOptions (
	"e|execute"     => \$execute,
	"delete=s"      => \$delete,
	"offline=s"     => \$offline,
	"ready=s"       => \$ready,
	"s|show"        => \$show,
	"c|conf=s"      => \$alternate_conf,
	"ignore-errors" => \$ignore_errors,
	"f|force"       => \$force,
	"p|pretend"     => \$pretend,
	"dump"          => \$dump,
	"v|verbose"     => \$verbose,
	"h|help"        => \$help,
);

if (($help == 1) || ($param eq undef)) {
	&usage
}

# Show all the targets and exit
if ($show == 1) {
	execute("tgtadm --op show --mode target");
	exit;
}

# Some variables/arrays/hashes we will use globally
my %tgtadm_output;
my %tgtadm_output_tid;
my %tgtadm_output_name;
my @largest_tid;
my $next_tid;

# Look up which targets are configured
sub process_targets {
	# We need to run as root
	if ( $> ) {
		die("You must be root to run this program.\n");
	}

	my @show_target = `tgtadm --op show --mode target`;
	my $tid;
	my $targetname;

	# Here, we create hashes of target names (all target data) and target tids
	foreach my $show_target_line (@show_target) {
		if ( $show_target_line =~ m/^Target (\d*): (.+)/ ) {
			$tid = $1;
			$targetname = $2;
			$tgtadm_output{$targetname} = $show_target_line;
			$tgtadm_output_tid{$targetname} = $tid;
			$tgtadm_output_name{$tid} = $targetname;
		} else {
			$tgtadm_output{$targetname} .= $show_target_line;
		}
	}
	# What is the largest tid?
	my @tids = values %tgtadm_output_tid;
	@largest_tid = sort { $a <=> $b } @tids;
	$next_tid = $largest_tid[$#largest_tid];
}

# Parse config file(s)
sub parse_configs {
	# Parse the config
	if ($alternate_conf ne 0) {
		# Check if alternative configuration file exist
		if (-e "$alternate_conf") {
			execute("# Using $alternate_conf as configuration file\n");
			%conf = ParseConfig(-ConfigFile => "$alternate_conf", -UseApacheInclude => 1, -IncludeGlob => 1,);
		}
		else {
			die("file $alternate_conf not found. Exiting...\n");
		}
	} else {
		# Parse the config file with Config::General
		if (-e "$configfile") {
			%conf = ParseConfig(-ConfigFile => "$configfile", -UseApacheInclude => 1, -IncludeGlob => 1,);
		} else {
			die("Config file $configfile not found. Exiting...\n");
		}
	}
}

# Add targets, if they are not configured already
my $default_driver;
my $target;
my $option;
my $value;

sub add_targets {

	foreach my $k (sort keys %conf) {

		if ( $k eq "default-driver" ) {
			if ( not length ref($conf{$k}) ) {
				$default_driver = $conf{$k};
			} else {
				print "Multiple default-driver definitions are not allowed!\n";
				print "Check your config file for errors.\n";
				exit 1;
			}
		}
	}

	# If $default_driver is empty, default to iscsi
	if ( not defined $default_driver ) {
		execute("# default-driver not defined, defaulting to iscsi.\n");
		$default_driver = "iscsi";
	}

	foreach my $k (sort keys %conf) {
		if ( $k eq "target" ) {
			foreach my $k2 (sort keys %{$conf{$k}}) {
				$target = $k2;
				my $allowall = 1;
				if ( not defined $tgtadm_output{$k2} ) {
					# We have to find available tid
					$next_tid = $next_tid + 1;
				}
				else {
					execute("# Target $target already exist!");
					execute("# Updating Target $target");
					execute("tgtadm --op update --mode target --tid=$next_tid -n state -v offline");
					execute("tgtadm --mode target --op delete --tid=$next_tid");
				}

				# Before we add a target, we need to know its type
				my $driver;
				foreach my $k3 (sort keys %{$conf{$k}{$k2}}) {
					$option = $k3;
					$value = $conf{$k}{$k2}{$k3};
					&check($value);
					if ( $option eq "driver" ) {
						if (ref($value) eq "ARRAY") {
						print "Multiple driver definitions not allowed!\n";
						print "Check your config file for errors (target: $target).\n";
						exit 1;
						}
					$driver = $value;
					}
				}

				if ( not defined $driver ) {
					$driver = $default_driver;
				}
				execute("# Adding target: $target");
				execute("tgtadm --lld $driver --op new --mode target --tid $next_tid -T $target");
				foreach my $k3 (sort keys %{$conf{$k}{$k2}}) {
					$option = $k3;
					$value = $conf{$k}{$k2}{$k3};
					&check($value);
					&process_options($driver);
					# If there was no option called "initiator-address", it means
					# we want to allow ALL initiators for this target
					if ( $option eq "initiator-address" ) {
						$allowall = 0;
					}
				}

				if ( $allowall == 1 ) {
					execute("tgtadm --lld $driver --op bind --mode target --tid $next_tid -I ALL");
				}
				execute();
			}
		}
	}
}

# Process options from the config file
sub process_options {
	my $driver = $_[0];
	if ( $option eq "backing-store" ) {
        # if we have one command, force it to be an array anyway
		unless (ref($value) eq 'ARRAY') {
			$value = [ $value ];
		}
		my @value_arr = @$value;
		my $i = 1;

		foreach my $backing_store (@value_arr) {
			# Check if device exists
			if ( -e $backing_store) {
				execute("tgtadm --lld $driver --op new --mode logicalunit --tid $next_tid --lun $i -b $backing_store");
				$i += 1;
			}
			else {
				print("skipping device $backing_store\n");
				print("$backing_store does not exist - please check the configuration file\n");
			}
		}
	}

	if ( $option eq "direct-store" ) {
		my $inq;
		my $vendor_id="";
		my $prod_id="";
		my $prod_rev="";
		my $scsi_serial="";
	        # if we have one command, force it to be an array anyway
		unless (ref($value) eq 'ARRAY') {
			$value = [ $value ];
		}
		my @value_arr = @$value;
		my $i = 1;
		foreach my $direct_store (@value_arr) {
			$inq=`sg_inq $direct_store`;
			if ($inq=~/Vendor identification:\s*(\w+)\s*\n*Product identification:\s*([\w\s\/\-]+)\n\s*\n*Product revision level:\s*(\w*)\s*\n*Unit serial number:\s*(\w+)/)
			{
				$vendor_id="$1";
				$prod_id="$2";
				$prod_rev="$3";
				$scsi_serial="$4";
			}
			$vendor_id =~ s/\s+$//;
			$prod_id =~ s/\s+$//;
			$prod_rev =~ s/\s+$//;
			$scsi_serial =~ s/\s+$//;

			execute("tgtadm --lld $driver --op new --mode logicalunit --tid $next_tid --lun 1 -b $direct_store");
			execute("tgtadm --lld $driver --op update --mode logicalunit --tid  $next_tid --lun 1 --params vendor_id=\"$vendor_id\",product_id=\"$prod_id\",product_rev=\"$prod_rev\",scsi_sn=\"$scsi_serial\"");
			$i += 1;
		}
	}

	if ( $option eq "incominguser" ) {
	        # if we have one command, force it to be an array anyway
		unless (ref($value) eq 'ARRAY') {
			$value = [ $value ];
		}
		my @value_arr = @$value;
		foreach my $incominguser (@value_arr) {
			my @userpass = split(/ /, $incominguser);
			&check($userpass[1]);
			execute("tgtadm --lld $driver --mode account --op delete --user=$userpass[0]");
			execute("tgtadm --lld $driver --mode account --op new --user=$userpass[0] --password=$userpass[1]");
			execute("tgtadm --lld $driver --mode account --op bind --tid=$next_tid --user=$userpass[0]");
		}
	}

	if ( $option eq "outgoinguser" ) {
	        # if we have one command, force it to be an array anyway
		unless (ref($value) eq 'ARRAY') {
			$value = [ $value ];
		}
		execute("# Warning: only one outgoinguser is allowed. Will only use the first one.");
		my @userpass = split(/ /, @$value[0]);
		&check($userpass[1]);
		execute("tgtadm --lld $driver --mode account --op delete --user=$userpass[0]");
		execute("tgtadm --lld $driver --mode account --op new --user=$userpass[0] --password=$userpass[1]");
		execute("tgtadm --lld $driver --mode account --op bind --tid=$next_tid --user=$userpass[0] --outgoing");
	}

	if ( $option eq "initiator-address" ) {
	        # if we have one command, force it to be an array anyway
		unless (ref($value) eq 'ARRAY') {
			$value = [ $value ];
		}
		my @value_arr = @$value;
		foreach my $initiator_address (@value_arr) {
			execute("tgtadm --lld $driver --op bind --mode target --tid $next_tid -I $initiator_address");
		}
	}
}

# If the target is configured, but not present in the config file,
# offline it and try to remove it
sub remove_targets {

	&process_targets;
	my @all_targets = keys %tgtadm_output_tid;

	foreach my $existing_target (@all_targets) {
		my $dontremove = 0;
		my $k2;
		foreach my $k (sort keys %conf) {
			if ( $k eq "target" ) {
				foreach $k2 (sort keys %{$conf{$k}}) {
					if ( $k2 eq $existing_target ) {
						$dontremove = 1;
					}
				}

				if ( $dontremove == 0 ) {
					# Right now, it is not possible to remove a target if any initiators
					# are connected to it. We'll do our best - offline the target first
					# (so it won't accept any new connections), and remove.
					# Note that remove will only work if no initiator is connected.
					execute("# Removing target: $existing_target");
					execute("tgtadm --op update --mode target --tid=$tgtadm_output_tid{$existing_target} -n state -v offline");
					execute("tgtadm --mode target --op delete --tid=$tgtadm_output_tid{$existing_target}");
				}
			}
		}
	}
}

# Dump current tgtd configuration
sub dump_config {

	&process_targets;

	my @all_targets = keys %tgtadm_output_tid;

	foreach my $target (@all_targets) {
		foreach my $show_target_line ($tgtadm_output{$target}) {
		    if ( $show_target_line =~ m/^Target (\d*): (.+)/ ) {
			print "<target $2>\n";
		    }

		    if ( $show_target_line =~ m/\s+Driver: (.+)/ ) {
			print "\tdriver $1\n";
		    }

		    if ( $show_target_line =~ m/\s+Backing store: (?!No backing store)(.+)/ ) {
			print "\tbacking-store $1\n";
		    }
		}

		# Process account and ACL information
		my $account_acl;

		foreach my $show_target_line ($tgtadm_output{$target}) {
		    $account_acl .= $show_target_line
		}

		# start with account information...
		while ($account_acl =~ m{
			\s+Account\ information:\n(.*)ACL\ information:
			     }xmgs
		      ) {

			my @account = split(/\n/, $1);

			foreach my $user (@account) {
				my @var = split(/^\s+/, $user);
				@var = split(/\s/, $var[1]);

				if ( $var[1] eq "(outgoing)" ) {
					print "\toutgoinguser $var[0] PLEASE_CORRECT_THE_PASSWORD\n";
				} elsif ( ($var[0] ne "") && ($var[1] eq "") ) {
					print "\tincominguser $var[0] PLEASE_CORRECT_THE_PASSWORD\n";
				}
			}
		}

		#...and finish with ACL information
		while ($account_acl =~ m{
			\s+ACL\ information:\n(.*)
			     }xmgs
		      ) {
		    my @ini_addresses = split(/\n/, $1);
		    foreach my $ini_address (@ini_addresses) {
			my @var = split(/^\s+/, $ini_address);
			print "\tinitiator-address $var[1]\n";
			}
		}
		print "</target>\n\n";
	}
}

# Offline or ready targets
sub ready_offline_targets {
	my $var = $_[0]; # This variable is either "offline" or "ready"
	my $off_ready;
	if ($ready eq 0) {
		$off_ready = $offline
	} elsif ($offline eq 0) {
		$off_ready = $ready
	} else {
		print "Invalid value (you can't use both ready and offline)!\n";
		exit 1;
	}
	if ($off_ready eq "help") {
		print <<EOF;
      --$var <value>		$var all or selected targets

Example usage:
      --$var help	      - display this help
      --$var ALL	      - $var all targets
      --$var tid=4	      - $var target 4 (target with tid 4)
      --$var iqn.2008-08.com.example:some.target - $var this target

EOF
	} elsif ($off_ready eq "ALL") {
		&process_targets;
		# Run over all targets and offline/ready them
		my @all_targets = keys %tgtadm_output_tid;
		foreach my $existing_target (@all_targets) {
			execute("tgtadm --op update --mode target --tid=$tgtadm_output_tid{$existing_target} -n state -v $var");
		}
	} elsif ($off_ready =~ m/tid=(.+)/) {
		&process_targets;
		execute("tgtadm --op update --mode target --tid=$1 -n state -v $var");
	} else {
		&process_targets;
		if (length $tgtadm_output_tid{$off_ready}) {
			execute("tgtadm --op update --mode target --tid=$tgtadm_output_tid{$off_ready} --name=\"$off_ready\" -n state -v $var");
		} else {
			print "There is no target with name \"$off_ready\", can't $var it!\n";
			exit 1;
		}
	}
}

# Show info for a given target
sub show_target_info {
	my $existing_target = $_[0];
	my $task = $_[1];
	# Returns driver information
	if ($task eq "driver") {
		if ( $tgtadm_output{$existing_target} =~ m/\s+Driver: (.+)/ ) {
			print $1;
			return $1;
		}
	# Returns ACL information
	} elsif ($task eq "acl_information") {
		while ($tgtadm_output{$existing_target} =~ m{
			\s+ACL\ information:\n(.*)
				}xmgs
			) {
			my @ini_addresses = split(/\n/, $1);
			my @acls;
			foreach my $ini_address (@ini_addresses) {
				my @var = split(/^\s+/, $ini_address);
				push(@acls, $var[1]);
			}
			return @acls;
		}
	# Returns sessions
	} elsif ($task eq "sessions") {
		my @var = split(/\n/, $tgtadm_output{$existing_target});
		my @sids;
		foreach my $sid (@var) {
			if ( $sid =~ m/\s+I_T nexus: (.+)/ ) {
				push(@sids, $1);
			}
		}
		return @sids;
	}
}

# Delete the targets which are not in use
sub delete_targets {

	# Check if the target is used by an initiator
	sub check_in_use {
		my $existing_target = $_[0];
		my $cur_option = $_[1];
		my $cur_tid = $_[2];
		if ($tgtadm_output{$existing_target} =~ m/\s+Connection:/) {
			if ($force == 1) {
				# Remove ACLs first
				my @acl_info = &show_target_info($existing_target, "acl_information");
				foreach my $acl (@acl_info) {
					execute("tgtadm --op unbind --mode target --tid $tgtadm_output_tid{$existing_target} -I $acl");
				}
				# Now, remove all sessions / connections from that tid
				my @sessions = &show_target_info($existing_target, "sessions");
				foreach my $session (@sessions) {
					execute("tgtadm --op delete --mode conn --tid $tgtadm_output_tid{$existing_target} --sid $session --cid 0");
				}
				execute("tgtadm --mode target --op delete --tid=$tgtadm_output_tid{$existing_target}");
			} else {
				execute("# Target with tid $tgtadm_output_tid{$existing_target} ($existing_target) is in use, it won't be deleted.");
			}
		} elsif (length $tgtadm_output_tid{$existing_target}) {
			execute("tgtadm --mode target --op delete --tid=$tgtadm_output_tid{$existing_target}");
		} else {
			if ($cur_option eq "tid") {
				execute("# Target with tid $cur_tid does not exist!");
			} else {
				execute("# Target $existing_target does not exist!");
			}
		}
	}

	if ($delete eq "help") {
		print <<EOF;
      --delete <value>		delete all or selected targets
				The target will be deleted only if it's not used
				(no initiator is connected to it).
				If you want to delete targets which are in use,
				you have to add "--force" flag

Example usage:
      --delete help	      - display this help
      --delete ALL	      - delete all targets
      --delete tid=4	      - delete target 4 (target with tid 4)
      --delete iqn.2008-08.com.example:some.target - delete this target

EOF
		exit;
	} elsif ($delete eq "ALL") {
		&process_targets;
		# Run over all targets and delete them if they are not in use
		my @all_targets = keys %tgtadm_output_tid;
		foreach my $existing_target (@all_targets) {
		&check_in_use($existing_target);
		}
	} elsif ($delete =~ m/tid=(.+)/) {
		# Delete by tid
		&process_targets;
		my $existing_target = $1;
		&check_in_use($tgtadm_output_name{$existing_target}, "tid", $existing_target);
	} else {
		# Delete by name
		&process_targets;
		my $existing_target = $delete;
		&check_in_use($existing_target);
	}
}

# Some checks
sub check {
	if ( not defined $_[0] or not length $_[0] ) {
		print "\nOption $option has a missing value!\n";
		print "Check your config file for errors (target: $target)\n";
		exit 1;
	}
}


# Execute or just print (or both) everything we start or would start
sub execute {
	if ($pretend == 0) {

		my $args = "@_";
		if ($verbose == 1) {
			print "$args\n";
		}
		# Don't try to execute if it's a comment
		my @execargs = split(/#/, $args);
		if ( $execargs[0] ne undef  ) {
			system($args);

			# If non-zero exit code was return, exit
			my $exit_value  = $? >> 8;
			if (($exit_value != 0) && ($ignore_errors == 0)) {
				print "Command:\n\t$args\nexited with code: $exit_value.\n";
				exit $exit_value;
			}
		}

	} elsif ( $pretend == 1 ) {
		print "@_\n";
	}
}

if ($execute == 1) {
	&process_targets;
	&parse_configs;
	&add_targets;
	&remove_targets;
} elsif ($delete ne 0) {
	&delete_targets;
} elsif ($dump == 1) {
	&dump_config;
} elsif ($offline ne 0) {
	&ready_offline_targets("offline");
} elsif ($ready ne 0) {
	&ready_offline_targets("ready");
} else {
	print "No action specified.\n";
}

