SnapmirrorConvert.psm1

#############################################################################################
# HELPER FUNCTIONS for
# - logging log4net
# - pretty console output
# - load modules
# - connect to cdot cluster
#############################################################################################
function init_log4net{
    write-verbose "Loading log4net from path $PSScriptRoot\log4net.dll"
    $log4NetLocation = "$PSScriptRoot\log4net.dll" 
    [Reflection.Assembly]::LoadFrom($log4NetLocation) | Out-Null
    [log4net.LogManager]::ResetConfiguration();
}
function exit_log4net{
    [log4net.LogManager]::Shutdown();
    [log4net.LogManager]::ResetConfiguration();
}
function get_log4net_level($level){
    switch($level){
        "Debug" {return [log4net.Core.Level]::Debug}
        "Info" {return [log4net.Core.Level]::Info}
        "Warn" {return [log4net.Core.Level]::Warn}
        "Error" {return [log4net.Core.Level]::Error}
        "Off" {return [log4net.Core.Level]::Off}
        "Fatal" {return [log4net.Core.Level]::Fatal}
        "All" {return [log4net.Core.Level]::All}

    }
}
function set_log_level{
    param(
        $loggerInstance,
        [ValidateSet("All","Debug","Info","Warn","Error","Fatal","Off")]
        $level
    )
    $wrapper = [log4net.LogManager]::GetLogger($loggerinstance)
    $l = $wrapper.Logger
    $l.Level = get_log4net_level -level $level
    $l.Repository.Configured=$true

}
function add_appender($loggerinstance,$appender){
    if( $null -ne $appender ){
        $wrapper = [log4net.LogManager]::GetLogger($loggerinstance)
        $l = $wrapper.Logger
        $l.AddAppender($appender)
    }
}
function create_rolling_log_appender($name,$file,$Threshold="Info"){
    $rlf = new-object log4net.Appender.RollingFileAppender
    $rlf.MaximumFileSize = "50MB"
    $rlf.Name = $name
    $rlf.File = $file
    $rlf.RollingStyle = "Size"
    $rlf.StaticLogFileName = $true
    $rlf.MaxSizeRollBackups = "20"
    $rlf.Layout = new-object log4net.Layout.PatternLayout("[%date{yyyy-MM-dd HH:mm:ss.fff}] [%level] %message%n")
    $rlf.Threshold = (get_log4net_level -level $Threshold)
    $rlf.ActivateOptions()
    return $rlf
}
function get_logger($name){
    return [log4net.LogManager]::GetLogger($name)
}
Function clean_and_exit () { 

    Write-Log "[ EXIT ] Finished"
    exit_log4net
    break

}
Function handle_error([object]$object){
    $ErrorMessage = $object.Exception.Message
    $FailedItem = $object.Exception.ItemName
    $Type = $object.Exception.GetType().FullName
    $CategoryInfo = $object.CategoryInfo
    $ErrorDetails = $object.ErrorDetails
    $Exception = $_.Exception
    $FullyQualifiedErrorId = $object.FullyQualifiedErrorId
    $InvocationInfoLine = $object.InvocationInfo.Line
    $InvocationInfoLineNumber = $object.InvocationInfo.ScriptLineNumber
    $PipelineIterationInfo = $object.PipelineIterationInfo
    $ScriptStackTrace = $object.ScriptStackTrace
    $TargetObject = $object.TargetObject
    Write-LogError  "Trap Error: [$ErrorMessage]"
    Write-LogDebug  "Trap Item: [$FailedItem]"
    Write-LogDebug  "Trap Type: [$Type]"
    Write-LogDebug  "Trap CategoryInfo: [$CategoryInfo]"
    Write-LogDebug  "Trap ErrorDetails: [$ErrorDetails]"
    Write-LogDebug  "Trap Exception: [$Exception]"
    Write-LogDebug  "Trap FullyQualifiedErrorId: [$FullyQualifiedErrorId]"
    Write-LogDebug  "Trap InvocationInfo: [$InvocationInfoLineNumber] [$InvocationInfoLine]"
    Write-LogDebug  "Trap PipelineIterationInfo: [$PipelineIterationInfo]"
    Write-LogDebug  "Trap ScriptStackTrace: [$ScriptStackTrace]"
    Write-LogDebug  "Trap TargetObject: [$TargetObject]"
}
function format-colorbrackets {
    Param(
        [Parameter(Position=0, Mandatory=$true)]
        [string] $Format,
        [switch] $FirstIsSpecial,
        [switch] $NoNewLine,
        [string] $ForceColor=""
    )

    $result = Select-String -Pattern '\[([^\]]*)\]' -InputObject $Format -AllMatches
    $i = 0
    $first = $true
    foreach($match in $result.Matches) {
        $group = $match.Groups[1]
        WriteHost $Format.Substring($i, $group.Index - $i) -NoNewline
        if(($first -and $FirstIsSpecial)){
            $color="green"
            if($ForceColor.length -gt 1){$color=$ForceColor}
            $first=$false
        }else{
            $color="cyan"
        }
        
        WriteHost $group.Value -NoNewline -ForegroundColor $color
        $i = $group.Index + $group.Length
    }

    if($i -lt $Format.Length){WriteHost $Format.Substring($i, $Format.Length - $i) -NoNewline }
    WriteHost "" -NoNewline:$NoNewLine

}
function import_module($name){
    if (-not(Get-Module -Name $name)) {
        if (Get-Module -ListAvailable | Where-Object { $_.name -eq $name }) {
            Import-Module $name  
            return $true
        } else {   
            return $false
        }
    } else{
        return $true
    }
}
function load_module($name){
    try {
        if (import_module $name) {
            Write-LogSuccess "Loaded module $name"
        } else {
            Write-LogError "Failed to load module $name"
            Write-Log "Did you install the module ? `n`r"
            Write-Log "[ Tip ] Install-Module $name `n`r" -forceColor Magenta -firstIsSpecial
            clean_and_exit 
        }
    } catch {
            Write-LogError "Failed to load module $name"
            Write-Log "Did you install the module ? `n`r"
            Write-Log "[ Tip ] Install-Module $name `n`r" -forceColor Magenta -firstIsSpecial
            clean_and_exit 
        handle_error -object $_
        clean_and_exit 
    }
}
function connect_controller($name){
    if(-not $connections[$name]){
        $credentials = Get-Credential -Message "Provide credentials for $name"
        try{
            $connections[$name] = Connect-NcController r2d2 -Credential $credentials -HTTPS
            Write-LogSuccess "Connected to cluster $name"
        }catch{
            Write-LogError "Failed to connect to cluster $name"
            handle_error $_
            clean_and_exit 
        }
    }
    return $connections[$name]
}
function WriteHost{
    param(
        [Parameter(Position=0, Mandatory=$true)]
        $Object,
        $ForegroundColor="white",
        [switch]$NoNewLine
    )

    Write-Host $Object -ForegroundColor $ForegroundColor -NoNewline:$NoNewLine

}
function Write-Brackets ([string]$mess,$status,$foregroundcolor) {
    if($mess){
        WriteHost "[" -NoNewLine
        WriteHost $status -ForegroundColor $foregroundcolor -NoNewLine
        WriteHost "] " -NoNewLine
        format-colorbrackets "$mess" -FirstIsSpecial -ForceColor yellow
    }
}
function Write-LogDebug ([string]$mess) {
    if($LogLevel -eq "Debug"){
        Write-Brackets -mess $mess -status " INFO " -foregroundcolor cyan
    }
    $Global:MasterLog.Debug($mess)
}
function Write-LogSuccess ([string]$mess) {
    Write-Brackets -mess $mess -status " OK " -foregroundcolor green
    $Global:MasterLog.Info($mess)
}
function Write-LogError ([string]$mess) {
    Write-Brackets -mess $mess -status " ERR " -foregroundcolor red
    $Global:MasterLog.Error($mess)  
}
function Write-LogWarn ([string]$mess) {
    Write-Brackets -mess $mess -status " WARN " -foregroundcolor yellow
    $Global:MasterLog.Warning($mess)  
}
function Write-Log ([string]$mess,[switch]$firstIsSpecial,[switch]$noNewLine,$forceColor) {

    format-colorbrackets "$mess" -FirstIsSpecial:$firstIsSpecial -NoNewLine:$noNewLine -ForceColor $forceColor
    $Global:MasterLog.Info($mess)
 
}
function Write-LogTitle ([string]$mess) {
    if($mess){
        WriteHost ("-" * $mess.Length)
        WriteHost $mess -ForegroundColor Magenta
        WriteHost ("-" * $mess.Length)
        $Global:MasterLog.Info($mess)
    }
}


#############################################################################################
# LOGIC STARTS HERE
#############################################################################################

<#
.Synopsis
   Convert snapmirror policy
.DESCRIPTION
   SRM and NetApp(c) SRA (currently) have no capability to properly run planned migrations
   or test migrations when the underlying SnapMirror(c) relationsships are Synchronous.
   The SRA was built for Asynchronous relations and certain underlying actions such as
   snapmirror update fail. Also, after migrations, the SRA create async relations.
 
   The cmdlet converts all snapmirrors, found in the volumes matching the config regular expressions,
   to the target snapmirror policy in the config file.
 
   So now you can run this cmdlet to convert sync to async (before srm testing).
   And to convert async to sync (after finishing testing).
     
   The final state, either broken-off or snapmirrored, will remain.
 
   TIPS : Use -WhatIf and -Confirm.
 
   DEPENDENCIES : Requires DataONTAP and Powershell-yaml (use install-module)
 
   CONFIG FILE : What convertion and in what direction on which snapmirrors is defined
                 in the config yaml file you need to pass.
.EXAMPLE
    Invoke-SrmScript -configfile .\srmsync\prod-dr-prep.yaml -LogLevel Debug -Confirm:$false
    ----------------
    Preparing script
    ----------------
    [ OK ] Loaded module dataOntap
    [ OK ] Loaded module powershell-yaml
    [ OK ] Loaded configfile
    [ INFO ] source_cluster : r2d2
    [ INFO ] destination_cluster : c3po
    [ INFO ] snapmirror_volume_regex : srm_sync.*
    [ INFO ] snapmirror_vserver_regex : srm_vserver.*
    [ INFO ] target_snapmirror_policy : MirrorAndVault
    [ OK ] Connected to cluster c3po
    [ OK ] Lookup volumes
    [ OK ] Lookup snapmirrors
    --------------------------------
    Found following matching volumes
    --------------------------------
    [broken-off][Sync][sync_mirror][srm_sync_volume] -> [srm_sync_volume_dest]
    ----------------------------
    Converting to MirrorAndVault
    ----------------------------
    [ INFO ] [already broken-off][srm_sync_volume] -> [srm_sync_volume_dest]
    [ OK ] [removed ][srm_sync_volume] -> [srm_sync_volume_dest]
    [ OK ] [released ][srm_sync_volume] -> [srm_sync_volume_dest]
    [ OK ] [recreated ][srm_sync_volume] -> [srm_sync_volume_dest]
    [ INFO ] [converted to async][srm_sync_volume] -> [srm_sync_volume_dest]
    [ EXIT ] Finished
#>

function Convert-SnapmirrorPolicy{
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
    param(
        # The path of the yaml file that contains the configuration
        # - determines the snapmirror direction (source, dest)
        # - determines the volume selection (volume & vserver regex)
        # - determines the target snapmirror policy
        #
        # Example :
        # source_cluster : "r2d2"
        # destination_cluster : "c3po"
        # snapmirror_volume_regex : "srm_sync.*"
        # snapmirror_vserver_regex : "srm_vserver.*"
        # target_snapmirror_policy : "MirrorAndVault"
        #
        [Parameter(Mandatory = $true)]
        [string]$ConfigFile,

        [ValidateSet("Debug", "Info", "Warn", "Error", "Fatal", "Off")]
        [string]$LogLevel = "Debug",

        [Parameter(Mandatory = $false)]
        [string]$LogFile = ".\SnapmirrorConvert.log",

        # Forces the resync in case the snapmirror is broken
        [switch]$ForceResync
    )

    $ErrorActionPreference = "stop"
    $connections = @{}
    $config = @{}
    $found_snapmirrors = @()

    ###########
    # init log
    ###########
    try{
        init_log4net
        $masterLogFileAppender = create_rolling_log_appender -name "srmsync.log" -file $LogFile -threshold $LogLevel
        add_appender -loggerinstance "srmsynclog" $masterLogFileAppender
        set_log_level -loggerinstance "srmsynclog" -level "All"
        $Global:MasterLog = get_logger -name "srmsynclog" 
    }catch{
        Write-Error "Failed to initialize log4net"
        clean_and_exit
    }

    ###########
    # PREP
    ###########
    Write-LogTitle "Preparing script"

    # import modules
    load_module dataOntap
    load_module powershell-yaml

    # load config file
    try{
        $config = Get-Content $ConfigFile | ConvertFrom-Yaml
    }catch{
        Write-LogError "Failed to load ConfigFile $ConfigFile"
        handle_error $_
        clean_and_exit 
    }
    Write-LogSuccess "Loaded ConfigFile"
    Write-LogDebug ("source_cluster : {0}" -f $config.source_cluster)
    Write-LogDebug ("destination_cluster : {0}" -f $config.destination_cluster)
    Write-LogDebug ("snapmirror_volume_regex : {0}" -f $config.snapmirror_volume_regex)
    Write-LogDebug ("snapmirror_vserver_regex : {0}" -f $config.snapmirror_vserver_regex)
    Write-LogDebug ("target_snapmirror_policy : {0}" -f $config.target_snapmirror_policy)

    ##################
    # connect to both clusters
    ##################
    $source_controller = connect_controller $config.source_cluster
    $destination_controller = connect_controller $config.source_cluster

    ###################
    # policy check
    ###################
    $targetSnapmirrorPolicy = $config.target_snapmirror_policy
    $checkpolicy = Get-NcSnapmirrorPolicy -Name $targetSnapmirrorPolicy

    if(-not $checkpolicy){
        Write-LogError "$targetSnapmirrorPolicy does not exist on $destination_controller"
        clean_and_exit
    }else{
        Write-LogSuccess "$targetSnapmirrorPolicy is a valid snapmirror policy"
    }

    ##################
    # get information
    ##################
    try{
        $source_volumes = Get-NcVol -Controller $source_controller
        Write-LogSuccess "Lookup volumes"
        $destination_snapmirrors = Get-NcSnapmirror -Controller $destination_controller -DestinationCluster $destination_controller.Name -SourceCluster $source_controller.Name
        Write-LogSuccess "Lookup snapmirrors"
    }catch{
        handle_error $_
        clean_and_exit 
    }

    # search maching volumes based on regex's
    foreach($v in $source_volumes){
        if($v.name -match $config.snapmirror_volume_regex -and $v.Vserver -match $config.snapmirror_vserver_regex){
            $found_snapmirrors +=[PSCustomObject]@{
                volume = $v.name
                mirror = ""
                policy = ""
                type = ""
            }
        }
    }

    # find mirrors on found volumes and complete info with snapmirror policy
    foreach($s in $found_snapmirrors){
        foreach($sm in $destination_snapmirrors){
            try{
                $policy = Get-NcSnapmirrorPolicy -Name $sm.policy
            }catch{
                Write-LogError "Failed to get snapmirror policy for $s"
                handle_error
                clean_and_exit 
            }
            if($s.volume -eq $sm.sourceVolume -and $source_controller.name -eq $sm.sourcecluster){
                $s.mirror = $sm
                $s.policy = $policy.Name
                $s.type = $policy.Type
            }

        }
    }
    
    ##################
    # output to console
    ##################
    Write-LogTitle "Found following matching volumes"
    foreach($s in $found_snapmirrors){
        if($s.mirror){
            if($s.type -eq "snapmirrored"){
                $color = "green"
            }else{
                $color = "yellow"
            }
            Write-Log ("[{0}][{4}][{3}][{1}] -> [{2}]" -f $s.mirror.mirrorstate,$s.volume,$s.mirror.destinationvolume,$s.type,$s.policy) -firstIsSpecial -forceColor $color
        }else{
            Write-Log ("[missing][{0}] -> []" -f $s.volume) -firstIsSpecial -forceColor red
        }
    }

    ##################
    # start conversion
    ##################
    Write-LogTitle "Converting to $targetSnapmirrorPolicy"
    foreach($s in ($found_snapmirrors | ?{$_.mirror})){

        try{

            # implement whatif and confirm
            if($pscmdlet.ShouldProcess(("[{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume), "Convert Snapmirror to $targetSnapmirrorPolicy")){
    
                # if not broken, break
                if($s.mirror.MirrorState -ne "broken-off"){

                    # quiesce before break
                    try{
                        Invoke-NcSnapmirrorQuiesce -Controller $destination_controller `
                            -DestinationCluster $s.mirror.DestinationCluster `
                            -DestinationVserver $s.mirror.DestinationVserver `
                            -DestinationVolume $s.mirror.DestinationVolume `
                            -SourceCluster $s.mirror.sourceCluster `
                            -SourceVserver $s.mirror.sourceVserver `
                            -SourceVolume $s.mirror.sourceVolume `
                            -Confirm:$false 
                        Write-LogSuccess ("[queisced ][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)

                    }catch{
                        Write-LogError ("Failed to quiesce relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                        handle_error $_
                        clean_and_exit 
                    }

                    # break
                    try{
                        Invoke-NcSnapmirrorBreak -Controller $destination_controller `
                            -DestinationCluster $s.mirror.DestinationCluster `
                            -DestinationVserver $s.mirror.DestinationVserver `
                            -DestinationVolume $s.mirror.DestinationVolume `
                            -SourceCluster $s.mirror.sourceCluster `
                            -SourceVserver $s.mirror.sourceVserver `
                            -SourceVolume $s.mirror.sourceVolume `
                            -Confirm:$false
                    
            
                        Write-LogSuccess ("[broken-off][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)

                    }catch{
                        Write-LogError ("Failed to break relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                        handle_error $_
                        clean_and_exit 
                    }
                }elseif($s.mirror.MirrorState -eq "broken-off"){
                    Write-LogDebug ("[already broken-off][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                }

                # remove
                try{
                    Remove-NcSnapmirror -Controller $source_controller `
                        -DestinationCluster $s.mirror.DestinationCluster `
                        -DestinationVserver $s.mirror.DestinationVserver `
                        -DestinationVolume $s.mirror.DestinationVolume `
                        -SourceCluster $s.mirror.sourceCluster `
                        -SourceVserver $s.mirror.sourceVserver `
                        -SourceVolume $s.mirror.sourceVolume `
                        -Confirm:$false
            
                    Write-LogSuccess ("[removed ][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)

                }catch{
                    Write-LogError ("Failed to remove relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                    handle_error $_
                    clean_and_exit 
                }

                # release on source
                try{
                    Invoke-NcSnapmirrorRelease -Controller $source_controller `
                        -DestinationCluster $s.mirror.DestinationCluster `
                        -DestinationVserver $s.mirror.DestinationVserver `
                        -DestinationVolume $s.mirror.DestinationVolume `
                        -SourceCluster $s.mirror.sourceCluster `
                        -SourceVserver $s.mirror.sourceVserver `
                        -SourceVolume $s.mirror.sourceVolume `
                        -RelationshipInfoOnly `
                        -Confirm:$false
            
                    Write-LogSuccess ("[released ][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)

                }catch{
                    if($_.Exception.Message -match "not found"){
                        Write-LogDebug ("[no relation to release][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                    }else{
                        Write-LogError ("Failed to release relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                        handle_error $_
                        clean_and_exit 
                    }
                }

                # recreate with new policy
                try{
                    $out = New-NcSnapmirror -Controller $source_controller `
                        -DestinationCluster $s.mirror.DestinationCluster `
                        -DestinationVserver $s.mirror.DestinationVserver `
                        -DestinationVolume $s.mirror.DestinationVolume `
                        -SourceCluster $s.mirror.sourceCluster `
                        -SourceVserver $s.mirror.sourceVserver `
                        -SourceVolume $s.mirror.sourceVolume `
                        -Policy $targetSnapmirrorPolicy

                    Write-LogSuccess ("[recreated ][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
   
                }catch{
                    Write-LogError ("Failed to recreate relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                    handle_error $_
                    clean_and_exit 
                }

                if($s.mirror.MirrorState -ne "broken-off" -or $ForceResync){

                    if($s.mirror.MirrorState -eq "broken-off" -and $ForceResync){
                            Write-LogDebug ("[force resync][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                        }


                    try{

                        $out = Invoke-NcSnapmirrorResync -Controller $source_controller `
                            -DestinationCluster $s.mirror.DestinationCluster `
                            -DestinationVserver $s.mirror.DestinationVserver `
                            -DestinationVolume $s.mirror.DestinationVolume `
                            -SourceCluster $s.mirror.sourceCluster `
                            -SourceVserver $s.mirror.sourceVserver `
                            -SourceVolume $s.mirror.sourceVolume `
                            -Confirm:$false
            
                        Write-LogSuccess ("[resynced ][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                    }catch{
                        Write-LogError ("Failed to resync relation [{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
                        handle_error $_
                        clean_and_exit 
                    }
                }

                Write-LogDebug ("[converted to $targetSnapmirrorPolicy][{0}] -> [{1}]" -f $s.volume,$s.mirror.destinationvolume)
            }
        }catch{
            handle_error $_
            clean_and_exit 
        }finally{
        }
    }
    clean_and_exit

}