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:
PrePostBackup.ini - This .INI file defines global settings that will be used each time the script is run.
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 ( "" )
}