Thursday, July 1, 2010

Charting Drive Usage from Backups


PROBLEM


One of the biggest problems with the free reporting tools that are available for NetBackup is that they don't have visual tools for schedule planning. I've done charts manually by taking the start times and end times and made a spreadsheet, but there should be a way to do this automatically from a given set of data. That's what I'm going to attempt to do here.



ANALYSIS


First, I'll start with some sample data. I know I want my start time and end time and I'll want to know either the name of the policy or the name of the client. I'll include both in my sample data, but I'll just need one in my report.
I'll also exclude backups that have 0 bytes associated to them. This is often the parent job or is a duplicate. Alternatively, I can search to see if the job has a parent. These exclusions/inclusions will be done from the data aggregation side and will not be covered here. To create my sample data, I'll start by creating what I want my chart to look like, then create the data from it.



Date/Time

Drive 1

Drive 2

Drive 3

Drive 4

Drive 5

Drive 6

0000







0030







0100

hagar






0130

hagar

honi

snert




0200

hagar

helga

snert




0230


helga

snert




0300

hamlet


snert




0330

hamlet


snert




0400

hamlet


snert




0430

hamlet






0500

hamlet

kvack





0530

hamlet

kvack





0600







0630

hernia






0700









From this table, I can see that hagar's backup started at 1:00 and ended by 2:30. In between that time, Honi's backup started at 01:30 and ended at 2:00, where helga's backup ran until 3:00. Snert's backup started the same time as honi's, but ended much later, at 4:30. hamlet had a backup start at 3:00 and end at 6:00. Kvack ran from 5:00 until 6:00. After 30 minutes of no backups, hernia's backup started at 6:30. Using the Start Time as my sort key for the data, the following CSV would be an appropriate set of data:


0100, 0230, hagar, hagar-windows-full
0130, 0200, honi, honi-data
0130, 0430, snert, snert-database-only
0200, 0300, helga, helga-system-files
0300, 0600, hamlet, hamlet-windows-full
0500, 0600, kvack, kvack-policy
0630, 0700, hernia, all-linux


So the format I have for my CSV is “Start Time”, “End Time”, “client”, “policy”.


From Table 1, I can see that I want Time to be the major axis in gathering data. Time will increment as I sample the data. Notice, too, that two clients start their backup at the same time, so I will have to factor that in, as well.

Now, let's break down Table 1, comparing to my data set and see what's really going on. I'm starting out with a time set at midnight (0000), and I increment that by 30 minutes for each table row. I'm also going to say that my “current data line” is the first line of my data set. Since I sorted my data by the start time, I compare the start time of the current data line. If I haven't yet reached the start time, I increment my table row time. I do this until I find I've matched. When I've reached the start time of my current line, I “claim” the first available drive.

At the same time, I'm also looking for end times of “claimed” drives. But I realize that I should do this first, so that if a drive is “released” at 0300, that same drive can now be used at 0300. This is only a minor preference and can sometimes be erroneous, as I are rounding to the nearest half-hour. Because of this rounding, I will have to be careful not to omit small backups that ran for less than 15 minutes (0200-0210 would be listed as 0200-0200 and would cancel out), but I'll get to that later. In summary, my pseudo-code would look like this:


Time starts at 0000

Grab current data line

for each drive, if drive is claimed, if end time for claimed drive is current time, release drive.

If current data line's start time is current time, claim next unused drive; mark start time for the unused drive.

If current time has not exceeded current data line's start time yet, increment the time

Otherwise, I'll go to the next data line.


Now, Let's see how this pseudo-code stacks up to my sample data:


Time starts at 0000.

Grab current data line (0100, 0230, hagar, hagar-windows-full)

For each drive, if drive is claimed, ...

is drive 1 claimed? No

is drive 2 claimed? No

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0000 = 0100? No)...

If current time has not exceeded current data line's start time, increment the time:

is 0000 <>


0000









Current data line: 0100, 0230, hagar, hagar-windows-full

Current Time: 0030

For each drive, if drive is claimed, ...

is drive 1 claimed? No

is drive 2 claimed? No

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0030 = 0100? No)...

If current time has not exceeded current data line's start time, increment the time:

is 0030 <>


0000







0030









Current data line: 0100, 0230, hagar, hagar-windows-full

Current Time: 0100

For each drive, if drive is claimed, ...

is drive 1 claimed? No

is drive 2 claimed? No

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0100 = 0100? YES!), Claim drive #1.

If current time has not exceeded current data line's start time, increment the time:

is 0100 <>


0000







0030









Current data line: 0130, 0200, honi, honi-data

Current Time: 0100

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? No

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0100 = 0130? No)...

If current time has not exceeded current data line's start time, increment the time:

is 0100 <>


0000







0030







0100

hagar








Current data line: 0130, 0200, honi, honi-data

Current Time: 0130

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? No

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0130 = 0130? YES!), claim next available drive (#2).

If current time has not exceeded current data line's start time, increment the time:

is 0130 <>


0000







0030







0100

hagar








Current data line: 0130, 0430, snert, snert-database-only

Current Time: 0130

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? YES!

Have I reached my end time (0200) yet? No.

is drive 3 claimed? No

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0130 = 0130? YES!), claim next available drive (#3), with end time of 0430.

If current time has not exceeded current data line's start time, increment the time:

is 0130 <>



0000







0030







0100

hagar








Current data line: 0200, 0300, helga, helga-system-files

Current Time: 0130

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? YES!

Have I reached my end time (0200) yet? No.

is drive 3 claimed? YES!

Have I reached my end time (0430) yet? No.

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0130 = 0200? No)...

If current time has not exceeded current data line's start time, increment the time:

is 0130 <>


0000







0030







0100

hagar






0130

hagar

honi

snert






Current data line: 0200, 0300, helga, helga-system-files

Current Time: 0200

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? YES!

Have I reached my end time (0200) yet? Yes! Release this drive

is drive 3 claimed? YES!

Have I reached my end time (0430) yet? No.

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0200 = 0200? YES!), claim next unclaimed drive (#2), with new end time of 0300

If current time has not exceeded current data line's start time, increment the time:

is 0200 <>


0000







0030







0100

hagar






0130

hagar

honi

snert






Current data line: 0300, 0400, hamlet, hamlet-windows-full

Current Time: 0200

For each drive, if drive is claimed, ...

is drive 1 claimed? YES!

Have I reached my end time (0230) yet? No.

is drive 2 claimed? YES!

Have I reached my end time (0300) yet? No.

is drive 3 claimed? YES!

Have I reached my end time (0430) yet? No.

is drive 4 claimed? No

is drive 5 claimed? No

is drive 6 claimed? No

If current data line's start time is current time (Is 0200 = 0300? No)...

If current time has not exceeded current data line's start time, increment the time:

is 0200 <>


0000







0030







0100

hagar






0130

hagar

honi

snert




0200

hagar

helga

snert






Note that I run into a problem when I go to the next day. my clock only goes to 2330. The next day is 0000. What happens if my time is 2200 and the current data's start time is 0100? Because I'm testing with a less than (2200 <>




SOLUTION


The above output is fine, as it's generated logically in my brain and just typed on here. but when are I creating the output? Ill, because each line is a function of the time, I output every time the time is incremented. So, from my pseudo-code, let's write a simple perl program to do what I've been doing by hand:


#!/bin/perl
use strict;


# Global variables
my $TIME=0000;
my $TINC=0030;
my $N_DRIVES=8;
my @DRIVE;
my @DRIVE_START;
my @DRIVE_END;
my $INFILE='NetBackup_Export.csv';
my $CURR_LINE;
my $NEXT=0;


# First, I want to initialize all my drive usages:
# client, start time, and end time.
sub init_drives()
{

local $IDX;

foreach $IDX (0..$N_DRIVES-1)

{

$DRIVE[$IDX] = “”;

$DRIVE_START[$IDX] = -1;

$DRIVE_END[$IDX] = -1;

}
}


sub nextempty
{

local $IDX;

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] eq "" ) { return $IDX; }

}
}


sub print_row()
{

print “$TIME”;

foreach $IDX (0..$N_DRIVES-1)

{

print “,$DRIVE[$IDX]”;

}

print “\n”;

}


sub main()
{

my $IDX;


init_drives;

# Things I need before I can start processing:

# 1. Current Time – got it (above)

# 2. Current Line – Need to start on first line

my $FD=open(FD,”<$INFILE”) or die “Cannot open: $!”;


# I grab the first line of the file and extract the values.

$CURR_LINE=;

my ($DATA_START, $DATA_END, $DATA_CLIENT, $DATA_POLICY) =

split(','$CURR_LINE);

# Now I can start my processing. I do this until the end of the file.

do

{

# First, I'll check to see if each drive is claimed and if it is, I check if

# it's reached its end time. If so, I release that drive.

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] ne “” && $DRIVE_END[$IDX] eq $TIME )

{

# To release the drive, I reset all values.

$DRIVE[$IDX] = “”;

$DRIVE_END[$IDX] = -1;

$DRIVE_START[$IDX] = -1;

}

}

# Second, I need to check if my data line's start time matches

# the current time. If it is, I claim a drive. If it isn't, I increment

# the time.

if ( $TIME == $DATA_START )

{

$NEXT=nextdrive;

$DRIVE[$NEXT] = $DATA_CLIENT;

$DRIVE_START[$NEXT] = $DATA_START;

$DRIVE_END[$NEXT] = $DATA_END;

} else

{

print_row;

$TIME = $TIME + 30;

}


} until (eof(FD));


close(FD);
}



Of course, if you try to run the code, you'll find that it has a few issues with it, but I'll add a few subroutines and clean it up a bit.. Let's break down the subroutines first, though. I initialize the drives with my init_drives subroutine:


#####################################################################################
# init_drives() #
# A routine to find the next empty drive #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# - For each index, blank out the indexed drive (unallocating it), and set start #
# and end times to '-1'. #
#####################################################################################
sub init_drives()
{

foreach $IDX (0..$N_DRIVES-1)

{

$DRIVE[$IDX] = "";

$DRIVE_START[$IDX] = -1;

$DRIVE_END[$IDX] = -1;

}
}



Second, I need a subroutine that finds my next empty drive:


#####################################################################################
# nextempty() #
# A routine to find the next empty drive #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# - For each index, if the indexed drive is blank (i.e., empty), return that #
# that number. #
#####################################################################################
sub nextempty
{

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] eq "" ) { return $IDX; }

}
}



Third, I are incrementing by 30 minutes each time, but I'm using basic math to do it, so 30 + 30 = 60, which isn't what I want. Also, I want to round everything to the closest increment. In this case, I create one subroutine to do both rounding and fixing:


#####################################################################################
# round_to_incr() #
# Rounds the argument to the next '$TINC' minute mark off the hour (up or down) #
# #
# Args: #
# $time - This is the time that needs to be rounded in HHMM format. #
# #
# Pseudocode: #
# - Grab the minutes by getting the modulus of $time and 100 #
# e.g. $time = 1422 --> 1400 = (1422 % 100) = 22 #
# - Grab the hour by subtracting the minutes from the time. #
# 1400 = 1422 - (22) #
# - Find out how close I are to the $TINC minute mark by creating my $rem #
# variable: my minutes modulus $TINC (e.g., above, 22 %30 = 22). #
# - If my remainder is less than 15, I round down, by subtracting the remainder #
# of my modulus from the actual minutes. #
# e.g. $minutes = 44 --> 44 - (44 % $TINC) = 44 - 14 = $TINC. #
# - Otherwise, I round up by adding the difference of ther remainder and $TINC. #
# $minutes = 22 --> 22 + 30 - (22 % 30) = 22 + 30 - 22 = 30 #
# $minutes = 48 --> 48 + 30 - (48 % 30) = 48 + 30 - 18 = 60 #
# - If my minutes component is less than 60 (less than 1 hour), I add that to #
# my hour component. Otherwise, I just ad 100 to the hour component to get #
# the rounded time. #
# - If my time has exceeded or is at 2400, I subtract 2400. #
# #
#####################################################################################
sub round_to_incr
{

my ( $time ) = @_;


my $minutes = $time % 100;

my $hour = $time - $minutes;


my $rem = $minutes % $TINC;


if ( $rem < minutes =" $minutes">

else { $minutes = $minutes + $TINC - $rem; }


if ( $minutes < time =" $hour">

else { $time = $hour + 100; }


if ($time >= 2400) { $time = $time - 2400 ;}


return $time;
}



We also need a subroutine that outputs everything I have in my basic CSV format. Let's call this “print_row”, since I'm printing one row of CSV every time.



#####################################################################################
# print_row() #
# The main printing function of my program; prints the time and drive allocations #
# for each drive. #
# #
# Args: none #
# #
# Pseudocode: #
# - Print the time in HHMM format #
# - Starting with the lowest index (0), #
# For each index, print a comma, then the indexed drive allocation. #
# - Print carriage return. #
#####################################################################################
sub print_row()
{

print "$TIME";

foreach $IDX (0..$N_DRIVES-1)

{

print ",$DRIVE[$IDX]";

}

print "\n";
}



Finally, I'll need to find out what the latest end time is of each allocated drive. That is, what's the latest time that all drives will be released?


#####################################################################################
# get_last_end_time() #
# Finds the latest end time for the current drive allocations. At this point, #
# does not factor if the end time is the next day. #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# For each index, if the drive end time is greater than the last one I checked, #
# store that value to return. #
# - Return value stored. #
#####################################################################################
sub get_last_end_time()
{

my $RETURNVAL=0;


foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE_END[$IDX] > $RETURNVAL ) { $RETURNVAL=$DRIVE_END[$IDX]; }

}

return $RETURNVAL;
}



Now, I've added a debug function to help when things get really hairy. This isn't necessary, but it helps me figure out if my variables are getting updated when they're supposed to, or not getting updated when they're not supposed to.


#####################################################################################
# print_debug() #
# A simple subroutine to print out the current variables. #
# #
# Args: none #
# #
#####################################################################################
sub print_debug()
{

print "TIME: $TIME\n";

print "DATA_START: $DATA_START\n";

print "DATA_END: $DATA_END\n";

print "DATA_CLIENT: $DATA_CLIENT\n";

print "LAST END TIME: $LAST_END_TIME\n";

print "CURR_LINE: $CURR_LINE\n";
}


And finally, my main program, as seen above, but tweaked with the added functions. Note that I've had to add checks for the do..while loop to see if I'm at the end of the file, and my current line is blank. This is because if I reach the end of the file, it will skip over that last line, which is something I definitely don't want.


#####################################################################################
# main() #
# The main body of the program. #
# #
# Args: none #
# #
# Pseudocode: #
# - Initialize the drive variables (call sub init_drives). #
# - Open the $INFILE for reading. #
# - Read in the first line. #
# - Split that line into my fields: #
# $DATA_START $DATA_END, $DATA_CLIENT, $DATA_POLICY #
# - Start processing within a do-while-loop: #
# - For each index of drives, if the drive is claimed and the drive end time #
# is at the current time, then I release the drive (reset indexed variables). #
# - If the current time is the same as the start time from the line I just got, #
# then claim the drive, by storing the client name, start, and end times. #
# I also grab the next line from my input file $INFILE. #
# Otherwise, I print out my row and increment the time by $TINC minutes. #
# - I find out when my latest end time for allocation is and I store it. #
# - processing ends when (1) I've reached the end of the file, and (2) the time #
# has passed the latest end time. #
# - I close my input file. #
# #
#####################################################################################
sub main()
{


init_drives;


# Things I need before I can start processing:

# 1. Current Time "" got it (above)

# 2. Current Line "" Need to start on first line


my $FD=open(FD,"<$INFILE") or die "Cannot open: $!";


# I grab the first line of the file and extract the values.

$CURR_LINE=;

($DATA_START, $DATA_END, $DATA_CLIENT, $DATA_POLICY) = split(',',$CURR_LINE);


# Now I can start my processing. I do this until the end of the file.

do

{

# First, I'll check to see if each drive is claimed and if it is, I check if

# it's reached its end time. If so, I release that drive.

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] ne "" && $DRIVE_END[$IDX] == $TIME )

{

# To release the drive, I reset all values.

$DRIVE[$IDX] = "";

$DRIVE_END[$IDX] = -1;

$DRIVE_START[$IDX] = -1;

}

}


# Second, I need to check if my data line's start time matches

# the current time.

# If it is, I claim a drive, and go to the next line.

# If it isn't, I print the output and increment the time.

if ( $TIME == $DATA_START )

{

$NEXT=nextempty;

$DRIVE[$NEXT] = $DATA_CLIENT;

$DRIVE_START[$NEXT] = $DATA_START;

$DRIVE_END[$NEXT] = $DATA_END;

$CURR_LINE=;

($DATA_START, $DATA_END, $DATA_CLIENT, $DATA_POLICY) = split(',',$CURR_LINE);

} else

{

print_row();

$TIME = round_to_incr($TIME + $TINC);

}


$LAST_END_TIME = round_to_incr(get_last_end_time());


} until ( eof(FD) && $TIME == $LAST_END_TIME && $CURR_LINE eq "" );

# I want to stop processing after (a) I've reached the end of the file, and (b)

# and (b) I've gone past the last end time.

print_row();


close(FD);
}



And finally, I need my variables:


# Global variables
my $TIME=0000;
my $TINC=30;
my $N_DRIVES=6;
my @DRIVE;
my @DRIVE_START;
my @DRIVE_END;
my $INFILE='NetBackup_Export.csv';
my $CURR_LINE;
my $NEXT=0;
my $IDX;
my $DATA_CLIENT;
my $DATA_START;
my $DATA_END;
my $DATA_POLICY;
my $LAST_END_TIME=-1;



Piecing it all together, I have the following code:


#!/bin/perl
#####################################################################################
# #
# generate_drive_usage.pl() #
# #
# Written By: Alan T. Landucci-Ruiz #
# http://solarisdeveloper.blogspot.com #
# #
# Abstract: This program generates CSV output of Tape drive usage, based on CSV #
# input. It is designed to help facilitate scheduling of tape drive #
# allocations when creating and moving backup schedules. #
# #
# #
# Args: none #
# #
# Variables: #
# $TIME - The time component of my output CSV. #
# $TINC - The increment component of my CSV. #
# $N_DRIVES - The number of drives I have. #
# @DRIVE - My "DRIVE" array: holds the string of allocation. #
# @DRIVE_START - Time that the drive is allocated. #
# @DRIVE_END - Time that the drive is unallocated. #
# $INFILE - The input file csv. #
# $CURR_LINE - The line being processed from the input CSV. #
# $NEXT - Index of my next empty drive. #
# $IDX - Index counter. #
# $DATA_CLIENT - Client that is allocating the drive. #
# $DATA_START - Start time for the client. #
# $DATA_END - End time for the client. #
# $DATA_POLICY - Policy of the client that is allocating the drive. #
# $LAST_END_TIME - The latest end time of all drives. #
# #
# Known Issues: #
# Currently, if a last end time is the next day's time, but earlier than the #
# currently known last end time, then it will use the currently known last end #
# time instead of the earlier one the next day. #
# e.g., 0200 tomorrow will be considered earlier than 1400 today. #
# #
#####################################################################################
use strict;


# Global variables
my $TIME=0000;
my $TINC=30;
my $N_DRIVES=6;
my @DRIVE;
my @DRIVE_START;
my @DRIVE_END;
my $INFILE='NetBackup_Export.csv';
my $CURR_LINE;
my $NEXT=0;
my $IDX;
my $DATA_CLIENT;
my $DATA_START;
my $DATA_END;
my $DATA_POLICY;
my $LAST_END_TIME=-1;


#####################################################################################
# init_drives() #
# A routine to find the next empty drive #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# - For each index, blank out the indexed drive (unallocating it), and set start #
# and end times to '-1'. #
#####################################################################################
sub init_drives()
{

foreach $IDX (0..$N_DRIVES-1)

{

$DRIVE[$IDX] = "";

$DRIVE_START[$IDX] = -1;

$DRIVE_END[$IDX] = -1;

}
}


#####################################################################################
# nextempty() #
# A routine to find the next empty drive #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# - For each index, if the indexed drive is blank (i.e., empty), return that #
# that number. #
#####################################################################################
sub nextempty
{

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] eq "" ) { return $IDX; }

}
}


#####################################################################################
# round_to_incr() #
# Rounds the argument to the next '$TINC' minute mark off the hour (up or down) #
# #
# Args: #
# $time - This is the time that needs to be rounded in HHMM format. #
# #
# Pseudocode: #
# - Grab the minutes by getting the modulus of $time and 100 #
# e.g. $time = 1422 --> 1400 = (1422 % 100) = 22 #
# - Grab the hour by subtracting the minutes from the time. #
# 1400 = 1422 - (22) #
# - Find out how close I are to the $TINC minute mark by creating my $rem #
# variable: my minutes modulus $TINC (e.g., above, 22 %30 = 22). #
# - If my remainder is less than 15, I round down, by subtracting the remainder #
# of my modulus from the actual minutes. #
# e.g. $minutes = 44 --> 44 - (44 % $TINC) = 44 - 14 = $TINC. #
# - Otherwise, I round up by adding the difference of ther remainder and $TINC. #
# $minutes = 22 --> 22 + 30 - (22 % 30) = 22 + 30 - 22 = 30 #
# $minutes = 48 --> 48 + 30 - (48 % 30) = 48 + 30 - 18 = 60 #
# - If my minutes component is less than 60 (less than 1 hour), I add that to #
# my hour component. Otherwise, I just ad 100 to the hour component to get #
# the rounded time. #
# - If my time has exceeded or is at 2400, I subtract 2400. #
# #
#####################################################################################
sub round_to_incr
{

my ( $time ) = @_;


my $minutes = $time % 100;

my $hour = $time - $minutes;


my $rem = $minutes % $TINC;


if ( $rem < minutes =" $minutes">

else { $minutes = $minutes + $TINC - $rem; }


if ( $minutes < time =" $hour">

else { $time = $hour + 100; }


if ($time >= 2400) { $time = $time - 2400 ;}


return $time;
}



#####################################################################################
# print_row() #
# The main printing function of my program; prints the time and drive allocations #
# for each drive. #
# #
# Args: none #
# #
# Pseudocode: #
# - Print the time in HHMM format #
# - Starting with the lowest index (0), #
# For each index, print a comma, then the indexed drive allocation. #
# - Print carriage return. #
#####################################################################################
sub print_row()
{

print "$TIME";

foreach $IDX (0..$N_DRIVES-1)

{

print ",$DRIVE[$IDX]";

}

print "\n";
}


#####################################################################################
# get_last_end_time() #
# Finds the latest end time for the current drive allocations. At this point, #
# does not factor if the end time is the next day. #
# #
# Args: none #
# #
# Pseudocode: #
# - Starting with the lowest index (0), #
# For each index, if the drive end time is greater than the last one I checked, #
# store that value to return. #
# - Return value stored. #
#####################################################################################
sub get_last_end_time()
{

my $RETURNVAL=0;


foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE_END[$IDX] > $RETURNVAL ) { $RETURNVAL=$DRIVE_END[$IDX]; }

}

return $RETURNVAL;
}


#####################################################################################
# print_debug() #
# A simple subroutine to print out the current variables. #
# #
# Args: none #
# #
#####################################################################################
sub print_debug()
{

print "TIME: $TIME\n";

print "DATA_START: $DATA_START\n";

print "DATA_END: $DATA_END\n";

print "DATA_CLIENT: $DATA_CLIENT\n";

print "LAST END TIME: $LAST_END_TIME\n";

print "CURR_LINE: $CURR_LINE\n";
}


#####################################################################################
# main() #
# The main body of the program. #
# #
# Args: none #
# #
# Pseudocode: #
# - Initialize the drive variables (call sub init_drives). #
# - Open the $INFILE for reading. #
# - Read in the first line. #
# - Split that line into my fields: #
# $DATA_START $DATA_END, $DATA_CLIENT, $DATA_POLICY #
# - Start processing within a do-while-loop: #
# - For each index of drives, if the drive is claimed and the drive end time #
# is at the current time, then I release the drive (reset indexed variables). #
# - If the current time is the same as the start time from the line I just got, #
# then claim the drive, by storing the client name, start, and end times. #
# I also grab the next line from my input file $INFILE. #
# Otherwise, I print out my row and increment the time by $TINC minutes. #
# - I find out when my latest end time for allocation is and I store it. #
# - processing ends when (1) I've reached the end of the file, and (2) the time #
# has passed the latest end time. #
# - I close my input file. #
# #
#####################################################################################
sub main()
{


init_drives;


# Things I need before I can start processing:

# 1. Current Time "" got it (above)

# 2. Current Line "" Need to start on first line


my $FD=open(FD,"<$INFILE") or die "Cannot open: $!";


# I grab the first line of the file and extract the values.

$CURR_LINE=;

($DATA_START, $DATA_END, $DATA_CLIENT, $DATA_POLICY) = split(',',$CURR_LINE);


# Now I can start my processing. I do this until the end of the file.

do

{

# First, I'll check to see if each drive is claimed and if it is, I check if

# it's reached its end time. If so, I release that drive.

foreach $IDX (0..$N_DRIVES-1)

{

if ( $DRIVE[$IDX] ne "" && $DRIVE_END[$IDX] == $TIME )

{

# To release the drive, I reset all values.

$DRIVE[$IDX] = "";

$DRIVE_END[$IDX] = -1;

$DRIVE_START[$IDX] = -1;

}

}


# Second, I need to check if my data line's start time matches

# the current time.

# If it is, I claim a drive, and go to the next line.

# If it isn't, I print the output and increment the time.

if ( $TIME == $DATA_START )

{

$NEXT=nextempty;

$DRIVE[$NEXT] = $DATA_CLIENT;

$DRIVE_START[$NEXT] = $DATA_START;

$DRIVE_END[$NEXT] = $DATA_END;

$CURR_LINE=;

($DATA_START, $DATA_END, $DATA_CLIENT, $DATA_POLICY) = split(',',$CURR_LINE);

} else

{

print_row();

$TIME = round_to_incr($TIME + $TINC);

}


$LAST_END_TIME = round_to_incr(get_last_end_time());


} until ( eof(FD) && $TIME == $LAST_END_TIME && $CURR_LINE eq "" );

# I want to stop processing after (a) I've reached the end of the file, and (b)

# and (b) I've gone past the last end time.

print_row();


close(FD);
}


main;




So, let's see how this program stacks up on our sample data:


~ $ cat NetBackup_Export.csv
0100, 0230, hagar, hagar-windows-full
0130, 0200, honi, honi-data
0130, 0430, snert, snert-database-only
0200, 0300, helga, helga-system-files
0300, 0600, hamlet, hamlet-windows-full
0500, 0600, kvack, kvack-policy
0630, 0700, hernia, all-linux
~ $ ./generate_drive_usage2.pl
0,,,,,,
30,,,,,,
100, hagar,,,,,
130, hagar, honi, snert,,,
200, hagar, helga, snert,,,
230,, helga, snert,,,
300, hamlet,, snert,,,
330, hamlet,, snert,,,
400, hamlet,, snert,,,
430, hamlet,,,,,
500, hamlet, kvack,,,,
530, hamlet, kvack,,,,
600,,,,,,
630, hernia,,,,,
~ $



Well, that looks pretty good so far. Let's double the data (i.e., add the same data for next day):


~ $ cat NetBackup_Export.csv
0100, 0230, hagar, hagar-windows-full
0130, 0200, honi, honi-data
0130, 0430, snert, snert-database-only
0200, 0300, helga, helga-system-files
0300, 0600, hamlet, hamlet-windows-full
0500, 0600, kvack, kvack-policy
0630, 0700, hernia, all-linux
0100, 0230, hagar, hagar-windows-full
0130, 0200, honi, honi-data
0130, 0430, snert, snert-database-only
0200, 0300, helga, helga-system-files
0300, 0600, hamlet, hamlet-windows-full
0500, 0600, kvack, kvack-policy
0630, 0700, hernia, all-linux
~ $ ./generate_drive_usage2.pl
0,,,,,,
30,,,,,,
100, hagar,,,,,
130, hagar, honi, snert,,,
200, hagar, helga, snert,,,
230,, helga, snert,,,
300, hamlet,, snert,,,
330, hamlet,, snert,,,
400, hamlet,, snert,,,
430, hamlet,,,,,
500, hamlet, kvack,,,,
530, hamlet, kvack,,,,
600,,,,,,
630, hernia,,,,,
700,,,,,,
730,,,,,,
800,,,,,,
830,,,,,,
900,,,,,,
930,,,,,,
1000,,,,,,
1030,,,,,,
1100,,,,,,
1130,,,,,,
1200,,,,,,
1230,,,,,,
1300,,,,,,
1330,,,,,,
1400,,,,,,
1430,,,,,,
1500,,,,,,
1530,,,,,,
1600,,,,,,
1630,,,,,,
1700,,,,,,
1730,,,,,,
1800,,,,,,
1830,,,,,,
1900,,,,,,
1930,,,,,,
2000,,,,,,
2030,,,,,,
2100,,,,,,
2130,,,,,,
2200,,,,,,
2230,,,,,,
2300,,,,,,
2330,,,,,,
0,,,,,,
30,,,,,,
100, hagar,,,,,
130, hagar, honi, snert,,,
200, hagar, helga, snert,,,
230,, helga, snert,,,
300, hamlet,, snert,,,
330, hamlet,, snert,,,
400, hamlet,, snert,,,
430, hamlet,,,,,
500, hamlet, kvack,,,,
530, hamlet, kvack,,,,
600,,,,,,
630, hernia,,,,,
~ $




SUMMARY


To conclude, given a set of data, we can plot our drive usage per client (and with some modification, even per policy, probably), if we're given the start time, end time, and the clients. This should be pretty easy to get with any reporting software, such as NetBackup 7 OpsCenter, or an export from the NetBackup Administration Console report. Because these give the output in different time formats (hh:mm non-24-hour), there will be some additional scripting that you will have to do to convert it to this format, either externally (from another program), or internally (added to this program).


Wednesday, March 24, 2010

SNORACLE: Sun and Oracle (soapbox)

I'm going to digress for a minute and get on my soapbox.


Now, when mergers or buyouts happen, you really don't expect much to change initially. When HP acquired Compaq, there wasn't much that changed in the look and feel of the web site. In fact, when you go to Compaq's web site, it still looks like Compaq--not the old compaq, but a new, better-looking compaq. This is what I really expected from the Oracle-Sun buyout: a few months of everything being the same then gradual changes. I quickly realized this was not the case as I increased my work load.

At first, I noticed that all the Sun logos were gone and everything was now "Oracle." Not only did they do this to the web pages, but all the PDFs were re-written to take out any Sun references, up to and including the OS. And the pages were they weren't taken out, everything is owned by Oracle. For example, the PDF on the Sun Blade T6340 (I'm surprised they kept the "Sun" portion) was re-written to say "Oracle's Sun Blade T6340," "Oracle's Solaris 10 Operating System," and "T2 Plus processor from Oracle." This is a minor inconvenience which easily goes unnoticed. But you really start to notice when they remove content that you used to be able to find.

In our industry, we test things like Opensolaris. We have one Opensolaris machine that has difficulty upgrading, so I thought, why not just downloaded the updated CD from Sun? They used to have the Developer's and Community Editions available for download. It took me an hour and discussion with an online "expert" to find out that it was removed. Couldn't it at least have been cached or archived?
Now, my final straw, which has ultimately led me into posting this here, is Sun's ASR. We have acquired more servers and as we get more servers, we haven't acquired any more admins. I don't like having to handle more than I can manage, which is where ASR can come in handy. ASR can make service calls that I don't have to. If the ASR system detects a fault, it can send a service request, a part can be ordered, and we're back and running the next day. Sun was really good about supporting and pushing ASR and they even had a bundle that would help you analyze and set up ASR.

Now, with this new Oracle page (and its search engine), I can no longer find the 'asrassetbundle', nor can I get the version of ASR that worked for me. This new version uses Java 6, of which it doesn't say specifically if it needs the JRE or the JDK. Trial and error found that I needed to download the JDK version 1.6 non-sparcv9 version, but I shouldn't have to go through all that trial and error. Even after I got it running, there are still problems with it, as it tells me it can't communicate with its database!


I'm getting off my soapbox now and getting back to finding out why ASR isn't working properly.

Wednesday, September 23, 2009

MogileFS HowTo Link

MogileFS is one of the possible ingredients to creating a Cloud.  This "server" software allows you to scale storage horizonally, even across servers.  The interesting thing is that it's written in perl.

Erik Osterman has written a very nice howto for setting up this service:  erik osterman - MogileFS HOWTO

Wednesday, March 25, 2009

SAR Visual Data Reports

One of the more important tools that an Administrator can use is sar to generate history on its system's performance. Sar ships natively with Solaris, but needs to be enabled and configured before being able to use it.

Problem: Sar data is usable, but not visually pleasing. We want to create a solution that works well with reports and allows us to show quickly and effortlessly to upper management how well (or poorly) our systems are responding. How do we create reports that are visual and informative?

Analysis: First, we need to look at our goal. Do we want something that we can run on-the-fly or that's regularly scheduled? For this case, I want to have something that's regularly scheduled and archived; we'll create graphs that we can put behind our intranet to view progression. While doing this, we'll keep in mind that we may want to run a report from within Webmin.

For our graphs, we can do one of two things: we can reformat the data to be imported to an Exchell spreadsheet, where we can generate the graph, and possibly use pivot tables (if we had enough data); or we can download and install gnuplot. We want automation at its best, and because the Domain Administrator controls whether or not we can run macros, it's best to stay native to the OS we love: Solaris. Unfortunately, Solaris doesn't ship with gnuplot, but we can easily get a stable version through our blastwave resources.

# /opt/csw/bin/pkgutil -i gnuplot

Now that we have our graphing program installed, we need to configure sar:


# svcadm enable sar
# svcs -xv sar
svc:/system/sar:default (system activity reporting package)
State: online since Mon Mar 23 10:03:06 2009
See: man -M /usr/share/man -s 1M sar
See: /var/svc/log/system-sar:default.log
Impact: None.


Running "sar" should give us data, but we haven't populated it yet. User "sys" already has templates in its crontab for sar, but we'll schedule our own for every 10 minutes:


# EDITOR=vi;export EDITOR; crontab -e sys
#ident "@(#)sys 1.5 92/07/14 SMI" /* SVr4.0 1.2 */
#
# The sys crontab should be used to do performance collection. See cron
# and performance manual pages for details on startup.
#
1,11,21,31,41,51 * * * 1-6 /usr/lib/sa/sa1
# 0 * * * 0-6 /usr/lib/sa/sa1
# 20,40 8-17 * * 1-5 /usr/lib/sa/sa1
# 5 18 * * 1-5 /usr/lib/sa/sa2 -s 8:00 -e 18:01 -i 1200 -A


Now, we'll let that populate at its regular intervals. We don't collect data on Sunday because there's no batch processes running on that day. This will make our scripting a little difficult, but we have ways of getting around it. When working on our script, we're going to want to run a report on the previous [sar] day's data--not today's. Now that we've got everything in place, let's work on the solution.

Solution: If it we didn't skip a day, we could just use gnu's date command with yesterday's date. First, let's create a timestamp script that takes care of this:

#!/bin/ksh
#########################################################
# sarstamp.ksh #
# A program to create a timestamp based on the last #
# (previous day) sar file generated #
#########################################################

YEAR=`date +%Y`
MODAY=`ls -ltr /var/adm/sa|tail -2 |head -1| sed -e 's/Jan/01/g;s/Feb/02/g;s/Mar/03/g;s/Apr/04/g;s/May/05/g;s/Jun/06/g;s/Jul/07/g;s/Aug/08/g;s/Sep/09
/g;s/Oct/10/g;s/Nov/11/g;s/Dec/12/g' | awk '{print $6$7}'`

echo ${YEAR}${MODAY}

If we didn't want each sar graph to have the timestamp match its sar-file creation date, we wouldn't have needed the timestamp script. We'll create a gnuplot script called "sar.gpl," and save our files as png files. We'll also want a grid, titles, and times for our x-axis. Using the sar data, we'll plot in this example, the %USR, %IDLE, Freemem, and Freeswap data as an 800x600 png file.

#!/opt/csw/bin/gnuplot -persist
set terminal png size 800,600

set grid

# Set time formats
set xdata time
set timefmt "%H:%M:%S"
set xrange ["00:00:00":"24:00:00"]
set xlabel "Time"

set key left box


set yrange [0:20]
set ylabel "% Usr"
set output "/tmp/sar-`hostname`-`/root/bin/sarstamp.ksh`-usr.png"
plot "/tmp/sar-cpu.dat" using 1:2 title 'Usr' with lines

set yrange [50:100]
set ylabel "% Idle"
set output "/tmp/sar-`hostname`-`/root/bin/sarstamp.ksh`-idle.png"
plot "/tmp/sar-cpu.dat" using 1:5 title 'Idle' with lines

unset yrange
set yrange [*:*]

set ylabel "FreeMem"
set output "/tmp/sar-`hostname`-`/root/bin/sarstamp.ksh`-mem.png"
plot "/tmp/sar-mem.dat" using 1:2 title 'Freemem' with lines

set ylabel "Swap"
set output "/tmp/sar-`hostname`-`/root/bin/sarstamp.ksh`-swap.png"
plot "/tmp/sar-mem.dat" using 1:3 title 'Freeswap' with lines

Now that we've got our sar script created, we'll create a nice wrapper around the whole thing to re-format the sar data to usable output (columned output with data and no text).

#!/bin/ksh
########################################################################
# #
# sar_graphs.ksh #
# #
# Author: Alan T. Landucci-Ruiz #
# #
# Program to generate graphical representation of the previous #
# day's sar data #
# #
########################################################################
HOST=`/bin/hostname`
MAILTO="myname@host.com"

SARFILE=`ls -1tr /var/adm/sa | tail -2 | head -1`

sar -f /var/adm/sa/${SARFILE} | egrep -v '[a-z]|^$' > /tmp/sar-cpu.dat
sar -r -f /var/adm/sa/${SARFILE} | egrep -v '[a-z]|^$'> /tmp/sar-mem.dat

/opt/csw/bin/gnuplot sar.gpl

rm /tmp/sar-cpu.dat
rm /tmp/sar-mem.dat

Easy enough. Now we can copy our sar data from /tmp to any location on our web server. Alternatively, we can have our web-server's location NFS-mounted and save directly. Later on, we'll use this same tool to plot drive usage.

Friday, March 13, 2009

Patch Reporting Tool for Solaris

Problem: Our recent security assessment found that no patch monitoring or patch notification system was in place. The Security Team is pressuring us to come up with a solution. The solution must include the following: (1) patch notification, (2) patch monitoring, and, if possible (3) patch deployment. Sun's xVM Ops Center would be perfect for this, so I asked for budget restrictions. Our manager said it would be best to fit this just below $0.*

Analysis: We already have a Sun support contract, which is good because it gives us access to patches from sunsolve.sun.com. The Linux support contract was not renewed, which is bad, because it disallows us access to the up2date repositories. We'll concentrate on the Sun area for now. Let's look into our current software implementations and see if there's anything available that can help us.

We already implement Webmin, but there doesn't look to be any Patch Monitoring or Notification modules available. Checking Google doesn't help; I just find patches for Webmin. We also have Altiris, but it's bulky and has a very steep learning curve. As a last step, we'll try Daddy Google.

The first page that comes up is from Sun. Under the section "Intelligent Patch Management," we see some reference to scripting.


Three options for patches:
1. Individual download from SunSolve - login required
2. Automatic system updates - Activate the "Patch Update Manager" feature in Solaris 10 with a valid Solaris Subscription
3. Life Cycle Management - Sun xVM Ops Center has an intelligent patch management tool for Solaris and Linux.

Sun Update Tools
Knowledge-based software update services for Solaris and Linux
Free Scripted Patch Tools

Auxiliary Files

Great! It says there are three options. Option 1 is a decent option for downloading patches, but it doesn't really help me know which patches I need. Option 2 isn't a viable option for our servers, as we don't want to automatically update our production [Oracle] servers, but it may be a good solution for our workstations. Option 3 is ideal because it also updates Linux, but it's not affordable within our budget.

Looking on... "Enterprise" usually implies $$$, so we're not going to look at that. We register our inventory, so I'll spend a little time looking at that. The GUI is java-based, which seems slow and buggy. I can run individual reports, but I can't find a way to automate this or really tell me what patches I need.

"Free Scripted Patch Tools" sounds like the perfect place for me. The link sends me to a page that only demonstrates how to use wget to download patches, not what patches I need. Oddly, it tells you how to use blastwave's wget, but if you have Solaris 10, you already have wget in /usr/sfw/bin.

Patchdiag.xref shows me what patches I need, but I still need a tool that will cross-reference this with my running systems and let me know which patches I need. Well, because Patchdiag.xref is a cross-reference file for patchdiag, let's see if Sun's Patchdiag tool is still available. Patchdiag is sitll available and still free, but its report is ugly. Despite its flaws, this looks like the place to start.

Testing: To automate our process, we need to check if the patchdiag.xref is publicly available and accessible from our systems. Using what we learned about wget, we use wget to download the patchdiag.xref file.

Also, we want to have a cached copy of a patchdiag.xref. I created an /opt/SUNWpatchdiag directory and subdirectories bin/, doc/, man/ and etc/ for the patchdiag.xref. We install our Patchdiag files appropriately.

A simple run of 'patchdiag -h' to look at the options shows that we can use a different copy of the patchdiag.xref file using the -x option, and -l for a more detailed (long) listing; however, both seem to give me the extent of detail that I want. We want the -x option so that we can download the latest patchdiag.xref from Sunsolve and use it. We can also do this on a remote host, but have to specify the file containing its "showrev -p" output. We'll avoid this option for now, but keep it in mind for later. The drawback for this is that you can't get the long output.

Conclusion: Using this and what I know of awk, I can hack together a simple script that allows me to reformat the output to either csv, text, or html. Now that I have this script, I can put it into cron and have it mail me the server reports periodically so I can know if they are up to date. The next step is to add it to Webmin so that the Security Team has access to run a report on-the-fly. Webmin uses perl, and I suddenly realize that I should have written the script in perl to begin with! I have a new project.

*Story of my life.

Wednesday, March 4, 2009

Oracle I/O Tuning

Problem: Oracle I/O is complaining of low performance and recommends tuning the vol_maxio parameters.


Analysis: BigAdmin has a good article on tuning your I/O parameters. One that I'm concerned with is the vol_maxio. According to the article, "VERITAS recommends that this tunable not exceed 20 percent of kernel memory or physical memory (whichever is smaller), and that you match this tunable to the size of your widest stripe." We are currently using concatenated filesystems instead of stripes (I'm unsure why; Symantec helped us set it up that way).

This forum article from sunmanagers.org also has reference to the stripe width, but says that Oracle has a limit of 1M the maximum IO size for release 8i.

If I check the maxio with adb:

# adb -k /dev/ksyms /dev/mem
maxphys/D
physmem 1fb99f
maxphys/D
maxphys:
maxphys: 131072
vol_maxio/D
vol_maxio:
vol_maxio: 2048

We find that it's already set to 1M (2048).

Due to the limits of the maxio specified by Veritas and Oracle, it doesn't really look like playing with the maxio number will buy us much, if we've already optimized the volume. So, let's take a look at the volume.

Almost all the articles I've researched stated benefits with using Direct I/O, but Veritas uses, by default, Quick I/O (ref manpage for mount_vxfs). Miracle Benelux has a good article when describing other limitations when specifying direct I/O, but there's no clarity as to the differences between direct I/O, Quick I/O, or Concurrent I/O (cio). Blog O' Matty's article references that direct I/O was enabled on his system by the following parameters: "mincache=direct” and “convosync=direct," but there is no reference to CIO or QIO.

Let's recap: I started out looking at maxio because Oracle said we're not getting the I/O they expected. From there, I was limited by Oracle and Veritas, so I checked the volume; many found benefits with using Direct I/O, and we're using Quick I/O. In addition to changing this, I also need to check the Oracle Buffer Cache.


Testing:
Now, let's check with the DBAs. According to them, Oracle allows larger I/O sizes than 1M and that the max, 32M should be sufficient enough for Oracle. We set this appropriately on one of our test machines' /etc/system and rebooted:

* Increase the maximum value of IO for Oracle tuning
set vxio:vol_maxio=65535

We moved a service group over to the node after rebooting and the DBAs tested it successfully with positive results.

I tried to remove the QIO by adding "noqio" to the mount options, but Oracle didn't like it. He did, however, enjoy it when I added the "mincache=direct” and “convosync=direct" options.


Conclusion: Oracle I/O was optimized on a VXFS by setting vxio:vol_maxio to 65535 in /etc/system; it was further tuned by adding the "mincache=direct,convosync=direct" option to the mounts.