This is a script I wrote to automatically mount a volume’s last snapshot from our LeftHand P4300 SAN in order for us to do our backups.

The script communicates to the LeftHand SAN via the LeftHand CLI and XML to locate the latest snapshot, grants permission to the IQN, and then mounts it. Once mounted the backup servers do their business and the script is then run again to unmount the snapshot. Please know that when it unmounts a snapshot it removes ALL permissions to that snapshot and not just to the server that was given permissions prior. This is due to limitation in the LeftHand CLI “unassignVolume” command.

PLEASE TEST THIS SCRIPT BEFORE RUNNING IT IN A PRODUCTION ENVIRONMENT. I TAKE ABSOLUTELY NO RESPONSIBILITY FOR WHAT YOU DO WITH THIS SCRIPT!

Requirements: PowerShell v1.0 or higher HP LeftHand CLI MS iSCSI Initiator

This script requires TWO .INI files:

  1. PrePostBackup.ini - This .INI file defines global settings that will be used each time the script is run.
  2. VolumeSettings.ini - This .INI file defines the Volume that has the latest snapshot. It also defines the drive letter to mount the snapshot to. You may have multiple of these and the name can vary.

An example PrePostBackup.ini:

SanIP, SanPort, KeyFile, IQN
10.0.1.2, 3260, C:\HPSAN.key, iqn.1991-05.com.microsoft:bigbertha.example.com

The PrePostBackup.INI file above MUST exist in the same directory as the PrePostBackup.ps1 script and MUST be named PrePostBackup.ini!

SanIP - The IP address of the LeftHand SAN SanPort - The port of the LeftHand SAN KeyFile - An absolute path to the LeftHand encrypted credentials file. (See below on how to create it.) IQN - The IQN of the server that will be granted permissions to the snapshots.

An example VolumeSettings.ini:

Volume,         DriveLetter
Server1_System,	Y
Server1_Data,   Z

The VolumeSettings.ini can be named anything. This allows you to store multiple configurations. I like to separate these into servers and name them [SERVERNAME].ini.

Volume - The name of the volume that you wish to mount the latest snapshot of. DriveLetter - The drive letter to mount the latest snapshot to.

To generate a keyfile:

cliq createKey login=10.0.1.2 username=admin password=secret keyfile=HPSAN.key

When running from PowerShell:

./PrePostBackup.ps1 VolumeSettings.INI [MOUNT / UNMOUNT]"

When scheduling:

C:\Windows\System32\WindowsPowerShell\v1.0\PowerShell.exe -command "& 'C:\Utilities\PrePostBackup.ps1' VolumeSettings.INI [MOUNT / UNMOUNT]"

The Script:

#  Script name:    MountLatestSnapshots.ps1
#  Created on:     2010-01-22
#  Author:         Gregory Strike
#     URL:         //www.gregorystrike.com/2010/05/19/powershell-script-to-mount-the-latest-lefthand-san-snapshots/
#  Purpose:        Given an INI file this script will communicate with the LeftHand SAN P4300 using
#                  HP/CLIQ and find all the volumes for that server and mount the latest snapshots.
#
#  Update:         2010-12-21
#                  Fixed an intermittent issue where some volumes are not completely disconnected.

CLS
$Error.Clear()

#Settings
$RunningDirectory = (Get-Item $MyInvocation.MyCommand.Path).Directory.ToString() + "\"
$Settings = Import-CSV ($RunningDirectory + "PrePostBackup.ini")
$SANIP = $Settings.SanIP
$SANPort = $Settings.SANPort
$KeyFile = $Settings.KeyFile
$IQN = $Settings.IQN

If ( !($SANIP) -or !($SANPort) -or !($KeyFile) -or !($IQN) ){
    Write-Error("Could not load SAN IP, SAN Port, Key File, or IQN from PrePostBackup.ini.  Check the file and try again.")
    IncorrectSyntax
}

function GetSnapshotWithLargestSerialNumber($VolumeName){
	#GetSnapshotWithLargestSerialNumber takes a Node set and compares the serialNumber value.
	#it will return the <snapshot> with the largest serialNum

	$VolumeSnapshots = $XML.SelectNodes("/gauche/response/group/cluster/volume [@name='"+($VolumeName)+"']/snapshot")

	$Temp = ""
    $Temp = $VolumeSnapshots.Item(0)

	if ($Temp -ne "") {
		Write-Host("Latest Snapshot for volume named '" + $VolumeName + "' is '" + $Temp.name + "'.")
		Return $Temp.iscsiIqn
	} else {
		Write-Error("No Snapshot found for volume named '" + $VolumeName + "'.")
		Return $False
	}
}

function GetPriorSnapshot($IQN){
	#GetPriorSnapshot takes a is given an IQN of a snapshot and finds the snapshot just before.

	Write-Host("Finding the snapshot prior to " + $IQN + "...")

    $MySnapshots = $XML.SelectNodes("/gauche/response/group/cluster/volume[snapshot[@iscsiIqn='"+($IQN)+"']]/snapshot")

    For ($x = 0; $x -lt $MySnapshots.Count; $x++){
        If($IQN -eq $MySnapshots.Item($x).iscsiIqn){
            $PriorSnapshot = $x + 1;
        }
    }

    If ($PriorSnapshot -le $MySnapshots.Count){
        Return $MySnapshots.Item($PriorSnapshot).iscsiIqn;
    } else {
        Write-Error("A prior snapshot doesn't exist.  Is this the first snapshot?")
        Return $False
    }
}

function VolumePermissions($VolumeIQN, $Access){
	#Grant/Remove permissions to a volume.  $Access = $True to add, $Access = $False to remove.
	#Note: When removing permissions CLIQ will remove ALL permissions to the volume.

	$Volumes = $XML.SelectNodes("/gauche/response/group/cluster/volume/snapshot [@iscsiIqn='"+($VolumeIQN)+"']")
	ForEach($Volume in $Volumes){
	}	

	#Are we granting or removing access?
	if ($Access) {
		Write-Host("Granting permissions for " + $IQN + " to " + $Volume.Name + "...")
		$Result = [XML](cliq assignVolume login=$SANIP keyfile="$KeyFile" accessRights="rw" initiator=$IQN volumeName=($Volume.Name) output=XML)
		If (!(CheckCliqResult($Result))){
			Write-Error("There was a problem assigning permissions to the volume.")
			Return $False
		}
	} else {
		Write-Host("Revoking permissions for " + $IQN + " to " + $Volume.Name + "...")
		$Result = [XML](cliq unassignVolume login=$SANIP keyfile="$KeyFile" volumeName=($Volume.Name) output=XML)
		If (!(CheckCliqResult($Result))){
			Write-Error("There was a problem removing permissions from the volume.")
			Return $False
		}
	}
}

function CheckCliqResult($InResult){
	#Receives the XML output from a CLIQ command and looks for a result of 0.  If not, returns false.
	If ($InResult.gauche.response.result -ne 0){
		Return $False
	}	

	Return $True
}
function MountVolume($IQN, $DriveLetter){
	#Mounts a Volume/Snapshot to a specific $DriveLetter
	Write-Host("Mounting '" + $IQN + "' to " + $DriveLetter + ":\...");

	#Find the currently connected LeftHand connections to compare with after we connect
	#the new iSCSI disk.  This will allow us to determine which volume we are currently
	#working with.

	$PreMountISCSI = Get-WMIObject -Class Win32_DiskDrive -Filter "Caption LIKE '%LEFTHAND iSCSI%'"

	#Add the target to the iSCSI service.
	$Null = iscsicli addtargetportal $SANIP $SANPort
	$Null = iscsicli refreshtargetportal $SANIP $SANPort

    Write-Host("...Connecting to target.")
	#Tell iSCSICLI to connect and store the output in $Logon
	$Logon = iscsicli logintarget $IQN T * * * * * * * * * * * * * * * 0

	#If there are devices stored in $PreMountISCSI get the currently connected LeftHand connections
	#and compare the two.
    Sleep 10
	$PostMountISCSI = Get-WMIObject -Class Win32_DiskDrive -Filter "Caption LIKE '%LEFTHAND iSCSI%'"
	$iSCSIDeviceID = ""

	If (!$PreMountISCSI){
		Write-Host("...No prior iSCSI connections detected.")
        $iSCSIDeviceID = $PostMountISCSI.DeviceID
	} else {
		Write-Host("...There were prior iSCSI connections detected.  Comparing lists.")
		ForEach ($PostDevice in $PostMountISCSI){
			$Found = $False
			ForEach($PreDevice in $PreMountISCSI) {
				If ($PostDevice.DeviceID -eq $PreDevice.DeviceID) { $Found = $True }
			}
			If ($Found -eq $False) { $iSCSIDeviceID = $PostDevice.DeviceID }
		}
	}

	#If this variable is empty, we did not detect the device.
	If ($iSCSIDeviceID -eq "") {
		Write-Error("There was a problem detecting the newly connected iSCSI device.  Was the target already connected?")
		Return $False
	}

	Write-Host("...New iSCSI connection detected at " + $iSCSIDeviceID)

	#Remove \\.\PHYSICALDRIVE from $iSCSIDeviceID string to be left with only the drive number
	$PhysicalDrive = $iSCSIDeviceID -Replace "\\\\\.\\PHYSICALDRIVE", ""

	#Determine drive letter of the newly found iSCSI connection
	$DiskToPartition = Get-WMIObject -Class Win32_LogicalDiskToPartition

	ForEach ($Partition in $DiskToPartition) {
		If ($Partition.Antecedent -Match "Disk #" + $PhysicalDrive) {
			$FoundDriveLetter = $Partition.Dependent.Substring($Partition.Dependent.Length - 3, 1)
			Write-Host ("...Found disk " + $PhysicalDrive + " mounted at " + $FoundDriveLetter + ":\")
		}
	}

	If ($FoundDriveLetter -ne $DriveLetter){
		If (!(AssignDriveLetter $PhysicalDrive 1 $DriveLetter)){
			Write-Error("Problem assigning drive letter.")
			Exit
		}
	}

	Return $True
}

function GetIQNSession($IQN){
	#Parses the output from iscsi reportttargetmappings and searches for the session ID
	$CurrentSessions = (iscsicli reporttargetmappings)

	Write-Host("Finding session for " + $IQN + "...")

	ForEach($Line in $CurrentSessions){
		$Session = [RegEx]::Matches($Line, "([A-Fa-f0-9]{16}-[A-Fa-f0-9]{16})")
		If($Session[0]){
			$CurrentSessionFound = $Session
		}

		If($Line -Like "*"+$IQN+"*"){
			Return $CurrentSessionFound
		}
	}

	#Could not find the session
	Write-Host("Could not find the session for the IQN provided.  May not currently be signed in.")
	Return $False
}

function UnmountVolume($IQN){
	Write-Host("Unmounting '" + $IQN + "'...")

	$SessionToLogout = GetIQNSession $IQN

	If (!$SessionToLogout){
		Return $False
	}

	#Write-Host("Logging out iSCSI Session: " + $SessionToLogout)
	#Send the LogoutTarget command to the iSCSI Initiator.  This is done
    #in a loop because to ensure it is disconnected.
    $MaxAttempts = 600
    $Attempt = 0
	Do {
        $Attempt = $Attempt + 1
        Write-Host("Attempt " + $Attempt + " of " + $MaxAttempts + ": Logging out iSCSI Session: " + $SessionToLogout)
        $Null = iscsicli LogoutTarget $SessionToLogout
        Start-Sleep -s 1

        $Continue = $True
        If ((GetIQNSession $IQN) -eq $False) {
            $Continue = $False
        }

        If ($Attempt -eq $MaxAttempts){
            $Continue = $False
        }

    } while ($Continue)

    If ($Attempt -eq $MaxAttempts){
        Write-Error("Could not logout iSCSI Session within " + $Attempt + " attempts.");
    }

	Return $True
}

function DriveExist($Letter){
	$Drive = New-Object System.IO.DriveInfo($Letter)

	if ($Drive.DriveType -eq "NoRootDirectory"){
		Return $False
	} else {
		Return $True
	}
}

function AssignDriveLetter($Disk, $Partition, $Letter){
	#$NewDrive = New-Object System.IO.DriveInfo($Letter)
	if (!(DriveExist($Letter))){
		Write-Host("Assigning Disk " + $Disk + "\Partition " + $Partition + " to " + $Letter + ":\...")
		"select disk " + $Disk + [char]13 + [char]10 + "select partition " + $Partition + [char]13 + [char]10 +"assign letter " + $Letter | diskpart > $Null
	} else {
		Write-Error("Can not assign drive letter. " + $Letter + ":\ is already in use.")
		Return $False
	}

	#Now that we've apparently assigned the drive letter.  Make sure it worked.
	if(DriveExist($Letter)){
		Return $True
	} else {
		Write-Error("Diskpart to assign drive letter may not have worked.");
		Return $False
	}
}

function RemoveDriveLetter($Letter) {
    If (DriveExist($Letter)) {
        Write-Host(&ldquo;Removing drive letter &rdquo; + $Letter + &ldquo;:&rdquo;)
        &ldquo;select volume=&rdquo; + $Letter + [char]13 + [char]10 + &ldquo;remove letter=&rdquo; + $Letter | diskpart > $Null
        Return $True
    } else {
        Write-Error($Letter + &ldquo;: does not exist.&rdquo;)
        Return $False
    }
}

function ChangeDriveLetter($Current, $New){
	if (!(DriveExist($New)) -and (DriveExist($Current))){
		Write-Host("Changing drive letter from " + $Current + " to " + $New + "...")
		"select volume " + $Current + [char]13 + [char]10 + "assign letter " + $New | diskpart
		Return $True
	} else {
		Write-Error("Can not change drive letter.  Either " + $Current + ":\ doesn't exist or " + $New + ":\ already exists.")
		Return $False
	}
}

Function IncorrectSyntax(){
    Write-Host("Syntax: ./PrePostBackup.ps1 [VolumeFile] [Mount/Unmount]")
    Exit 1
}

#####################################################################################################
#####################################################################################################

#Check if a config file was passed:
Switch ($Args.Length) {
	2{	$INIFile = $RunningDirectory + $Args[0]
		$Task = $Args[1].ToUpper()
	}
	default {
		Write-Error("Incorrect number of arguments.")
        IncorrectSyntax
	}
}

$INI = Import-CSV $INIFile

If (!$INI) {
    Write-Error("There was a problem opening " + $INIFile + ". Please check your syntax and try again.")
    IncorrectSyntax
}

If ( ($Task -ne "MOUNT") -and ($Task -ne "UNMOUNT") ){
    Write-Error("Mount or Unmount has not been defined.")
    IncorrectSyntax
}

Write-Host("Getting initial state of the SAN...")
$XML = New-Object XML
$XML = [XML] (cliq getGroupInfo login="$SANIP" keyfile="$KeyFile" output="XML")
#$XML = [XML]Get-Content getGroupInfo.xml

If (!(CheckCliqResult($XML))){
	Write-Error("There was a problem running the Cliq getGroupInfo command.")
	Exit
}

#For each volume find the latest snapshot and mount it.
ForEach ($Volume in $INI){
	$LatestSnapshotIQN = GetSnapshotWithLargestSerialNumber($Volume.Volume)

	#Determine whether or not there is a snapshot
	If (!$LatestSnapshotIQN) { Exit }

	If ($Task -eq "MOUNT"){
		#Grant Permissions to the volume
		If (!(VolumePermissions $LatestSnapshotIQN $True) -ne $True){ Exit }

		#Mount the volume
		If ((MountVolume $LatestSnapshotIQN $Volume.DriveLetter) -ne $True){ Exit }
	}

	If ($Task -eq "UNMOUNT"){
        $Null = RemoveDriveLetter $Volume.DriveLetter

		#Unmount the volume
		If ((UnmountVolume $LatestSnapshotIQN) -ne $True){
            #If unable to unmount, try the prior snapshot because another snapshot could have happend
            #while we had the the volume mounted.
            $LatestSnapshotIQN = GetPriorSnapshot $LatestSnapshotIQN
            If ((UnmountVolume $LatestSnapshotIQN) -ne $True){
                Write-Error("Tried the latest snapshot and the snapshot prior and could not unmount the snapshot.")
                Exit
            }
        }

		#Remove permissions to the volume
		If (!(VolumePermissions $LatestSnapshotIQN $False) -ne $True){ Exit }
	}

	Write-Host("")
}


Gregory Strike

Husband, father, IT dude & blogger wrapped up into one good looking package.