PowerShell: File Server Cluster Migration Scripts

<# File-Server-Migration-Intra-Domain.ps1

Purposes:
1. Create Volumes with Labels
2. Create Virtual Clustered File Servers
3. Create SMB Shares on Clustered File Servers
4. Disable caching of certain shares
5. Generate Sources and Destination HashTable Variable
6. Perform File Copy Operation

Assumptions:
1. These are to be executed in the context of the System Administrator
2. PowerShell version 5.0 on Windows 2016 is expected

Limitations:
1. On "Create Volumes with Labels," PowerShell cannot reliably set disks as online or offline currently. Thus, prior to starting this script, the admin must set disks as online manually; otherwise, any offlined disks will be skipped by the program
2. I still consider myself a newbie at this time. Hence, please optimize/refactor this script without hesitation.

#>

################################## ↓ Part 1: Create Volumes with Labels ↓ #############################################

# Set global variables
$diskLabelsAndIndexes=@(
    @("APP001",14,"A"),
    @("APP002",15,"B"),
    @("APP003",16,"E"),
    @("APP004",17,"F"),
    @("APP005",18,"G"),
    @("APP006",19,"I"),
    @("APP007",20,"J"),
    @("APP008",21,"K"),
    @("APP009",24,"L"),
    @("APP010",22,"M")
) # Set cluster disk label and disk number
$sectorSize=8192 #8K which translates to 32TB max per volume (https://kimconnect.com/disk-partitioning-formatting-reference)
$driveLetterExclusions="[CDH]" # Scan for next available drive letters, excluding D "CD Rom" and H "Home"

# 1. Format disks as defined on variable $clusterDisks
# 2. Assign a drive letter to each item
# 3. Add those volumes into the connected MS Failover Cluster

function processDisks{
    param($clusterDisks=$diskLabelsAndIndexes)
    #$allAvailableDisks=Get-WmiObject -Class win32_volume | ?{$_.Name -match "\\\\?\\";}
    
    # Checkpoint: requiring use to understand what's happening
    $warningMessage="Warning: this function will wipe out all data on items inside array $diskLabelsAndIndexes`r`n";
    $warningMessage+="This function requires that the disks to be set online using diskmgmt.msc prior to its execution because current PowerShell commands do not consistently set disks online."
    $confirmed=confirmation;
    if(!$confirmed){
        return $false;
        }else{
            function validateDisksArray{
                $proceed=$false
                $diskIndexes=$diskLabelsAndIndexes|%{$_[1]}
                $diskIndexes|%{try{if(get-disk -Number $_ -ErrorAction Stop){$proceed=$true}}catch{$proceed=$false}}
                return $proceed
            }
    
            function createVolume($name,$number,$letter){
                # Set variables
                $diskNumber=$number
                $diskLabel=$name
                $driveLetter=$letter
                $drivePath=$driveLetter+":"
                #$driveLetterExclusions="[ABCDHKLMPZ]"
                #$availableDriveLetters=ls function:[A-Z]: -n|?{!(test-path $_)}|%{$_[0]}|?{!($_ -match $driveLetterExclusions)}
                #$tempDriveLetter=$availableDriveLetters[$($($availableDriveLetters.Length) -1)]
                #$tempDriveLetter="Z"
                #$tempPath=$tempDriveLetter+":"
    
                write-host "Processing disk number $diskNumber`: set drive letter as $driveLetter & label equals $diskLabel.";
        
                # Clean Disk
                Set-Disk $diskNumber -isOffline $false
                Set-Disk $diskNumber -isReadOnly $false
                Clear-Disk -Number $diskNumber -RemoveData -Confirm:$False
                Update-HostStorageCache
    

                # Set partition as GPT
                Initialize-Disk -Number $diskNumber -PartitionStyle GPT -InformationAction SilentlyContinue
        
                # Create a new partition and format it
                #Add-PartitionAccessPath -DiskNumber $diskNumber -PartitionNumber 2 -AccessPath $drivePath
                try{
                    #New-Partition $diskNumber -UseMaximumSize -DriveLetter $tempDriveLetter
                    #Format-Volume -DriveLetter $tempDriveLetter -FileSystem NTFS -AllocationUnitSize $sectorSize -NewFileSystemLabel $diskLabel -Confirm:$false -Force
                    New-Partition $diskNumber -UseMaximumSize -DriveLetter $driveLetter
                    Format-Volume -DriveLetter $driveLetter -FileSystem NTFS -AllocationUnitSize $sectorSize -NewFileSystemLabel $diskLabel -Confirm:$false -Force
                    }catch{
                        write-host "$Error"
                        break;
                        }
                <#
                PS C:\Windows\system32> Format-Volume -DriveLetter $driveLetter -FileSystem NTFS -AllocationUnitSize $sectorSize -NewFil
                eSystemLabel $diskLabel -Confirm:$false -Force

                DriveLetter FileSystemLabel FileSystem DriveType HealthStatus OperationalStatus SizeRemaining  Size
                ----------- --------------- ---------- --------- ------------ ----------------- -------------  ----
                F           SIMONICULA-007   NTFS       Fixed     Healthy      OK                        12 TB 12 TB
                #>

                Set-Disk $diskNumber -isOffline $false
                Set-Disk $diskNumber -isReadOnly $false
    
                # Set-Partition -DriveLetter $driveLetter -IsActive $true
                <#
                Error:
                PS C:\Windows\system32> Set-Partition -DriveLetter $driveLetter -IsActive $true
                Set-Partition : A parameter is not valid for this type of partition.

                Extended information:
                The parameters MbrType and IsActive cannot be used on a GPT disk.

                Activity ID: {9b225291-2b3f-43e5-b62f-74a96d8e5dd6}
                At line:1 char:1
                + Set-Partition -DriveLetter $driveLetter -IsActive $true
                + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                    + CategoryInfo          : InvalidArgument: (StorageWMI:ROOT/Microsoft/..._StorageCmdlets) [Set-Partition], CimExce
                   ption
                    + FullyQualifiedErrorId : StorageWMI 41006,Set-Partition

                Resolution:
                The command only works with MBR disks - cannot issue it against at GTP type.
                #>

                # Use WMI to relabel volumes as native PowerShell currently doesn't have this capability
                try{
                    $disk = Get-WmiObject -Class win32_volume -Filter "Label = '$diskLabel'"
                    }catch{
                        write-host "$Error";
                        break;
                    }
                Set-WmiInstance -input $disk -Arguments @{DriveLetter=$drivePath; Label="$diskLabel"}

                #if (Test-Path $tempPath) {Remove-PartitionAccessPath -DiskNumber $diskNumber -PartitionNumber 2 -Accesspath $tempPath}
                }
    
            function addDiskToCluster($name,$number){
                # Test values
                #$number=2
                #$name="Disk 2";
                Get-Disk -Number $number | Add-ClusterDisk | %{$_.name=$name }
                }

            if(validateDisksArray){
                # Initialize and create volumes
                $i=0;
                foreach ($disk in $clusterDisks){    
                    #createVolume $($disk[0])  $($disk[1]) $availableDriveLetters[$i++];
                    createVolume $($disk[0])  $($disk[1]) $($disk[2]);
                    #pause;
                    }

                # Create report after volumes have been created
                Get-WmiObject -Class win32_volume | select name,blocksize
    
                Write-Host "Please verify that all disks have been online prior to add disks to Clusters."
                pause;
    
                $clusterName=(get-cluster).name    
                $clusterDisks | %{addDiskToCluster -diskObject $_
                    "Please verify the accuracy of this statement and press Enter to accept:`r`n";
                    #pause;
                    $label=$_[0];
                    $diskIndex=$_[1];
                    "Name: $label | Disk Number: $diskIndex | Clustername: $clusterName";
                    try{
                        addDiskToCluster $label $diskIndex;
                        }
                        catch{
                            Write-Host "unable to add disk $label into the cluster $clusterName"
                            }
                    }
            }
    }
}

processDisks;
<################################################################################################
Some troubleshooting documentation:

using diskpart (manual):
    SELECT DISK 2
    ONLINE DISK
    CLEAN
    CREATE PARTITION PRIMARY
    FORMAT FS=NTFS UNIT=16K QUICK
    ASSIGN LETTER=B  #Change B to any other available access-path letter
    ACTIVE
    EXIT

Add to Cluster (manual):
    $numero=7
    $etiqueta="LUN007"
    Get-Disk -Number $numero | Add-ClusterDisk | %{$_.Name=$etiqueta }

Report a list of physical disks available on the host:    
PS C:\Windows\system32> Get-WmiObject -Class win32_volume | select name,blocksize
name                                              blocksize
----                                              ---------
C:\                                                    4096
E:\                                                   16384
F:\                                                   16384
G:\                                                   16384
I:\                                                   16384
J:\                                                   16384
N:\                                                   16384
\\?\Volume{3b1730ae-2559-40e3-a547-ae6a91d2d540}\      4096

Error:
Add-ClusterDisk : The disk with Id {1}\\TestCluster007\root/Microsoft/Windows/Storage/Providers_v2\WSP_Disk.ObjectId="{4ef8972
8-9924-414d-960a-f09d2ab2155a}:DI:\\?\Disk{6fca7305-68a9-5804-84ed-bf26bd6bfc62}" is unable to be clustered.
At line:1 char:22
+ Get-Disk -Number 7 | Add-ClusterDisk | %{$_.name="APP007" }
+                      ~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (MSFT_Disk (Obje...Windows/Sto...):CimInstance) [Add-ClusterDisk], Invalid
    OperationException
    + FullyQualifiedErrorId : Add-ClusterDisk,Microsoft.FailoverClusters.PowerShell.AddClusterDiskCommand

Resolution:
- Delete the LUNs at the SAN Volumes Manager
- Online disk > partition GPT > format disk as NTFS > assign drive letter > add disk to Cluster

Miscellaneous clustering commands:
# $clusterDiskObject=Get-ClusterResource -Cluster $cluster -Name $clusterDiskName
# Put disk in cluster maintenance mode
# $clusterDiskObject | Suspend-ClusterResource
# $clusterDiskObject | Stop-ClusterResource
    <# avoid this error:
Format-Volume : The specified object is managed by the Microsoft Failover Clustering component. The disk must be in
cluster maintenance mode and the cluster resource status must be online to perform this operation.

#Clear-ClusterDiskReservation -Disk $diskNumber
#$clusterDiskObject | Start-ClusterResource | Resume-ClusterResource
# Put clustered disk in maintenance mode
# Get volume
# get-partition -DriveLetter C
#New-FileShare -Name $shareName -FileServerFriendlyName $serverName -SourceVolume $Volume -IsContinuouslyAvailable $True -Protocol SMB

# If only extracting 1 set of variables
$regexServerName="\\\\(.{1,})\\"
$regexShareName="\\\\.+\\(.{1,})"
for ($i=0;$i -lt $smbShares.to.length;$i++){[void]($smbShares.to[$i] -match $regexServerName);$matches[1];}
for ($i=0;$i -lt $smbShares.to.length;$i++){[void]($smbShares.to[$i] -match $regexShareName);$matches[1];}

# Set disk as online via GUI
Run diskmgmt.msc > locate newly created volume > right-click > Online > note the disk number as value for below variable

# Set disk name and number
$diskNumber="SELECT_NUMBER_FROM_ABOVE_RESULT"
$diskName="APPLICATION_NAME"
$driveLetter="Pick_An_Available_Drive_Letter"
$tempDriveLetter="A" #Make sure that this letter is currently not in use
$sectorSize=16384 #16K which translates to 64TB max per volume

# Create volume
function createVolume{
        # Set variables
        $drivePath=$driveLetter+":"
        $tempPath=$tempDriveLetter+":"
    
        "Processing disk number $diskNumber`: set drive letter as $driveLetter & label equals $diskName"    
   
        # Clean disk
        Clear-Disk -Number $diskNumber -RemoveData -Confirm:$False

        # Set partition as GPT
        Initialize-Disk -Number $diskNumber -PartitionStyle GPT -InformationAction SilentlyContinue
        
        # Create a new partition and format it
        New-Partition $diskNumber -UseMaximumSize -DriveLetter $tempDriveLetter
        Format-Volume -DriveLetter $tempDriveLetter -FileSystem NTFS -AllocationUnitSize $sectorSize -NewFileSystemLabel $diskName -Confirm:$false -Force
        
        # Online disk and mark writable
        Set-Disk $diskNumber -isOffline $false
        Set-Disk $diskNumber -isReadOnly $false  
        
        # Use WMI to relabel volumes as PowerShell currently doesn't have this capability
        $disk = Get-WmiObject -Class win32_volume -Filter "Label = '$diskName'"
        Set-WmiInstance -input $disk -Arguments @{DriveLetter=$drivePath; Label="$diskName"}

        Remove-PartitionAccessPath -DiskNumber $diskNumber -PartitionNumber 2 -Accesspath $tempPath
        }
createVolume

# Add disk to cluster
Get-Disk -Number $diskNumber | Add-ClusterDisk | %{$_.name=$diskName }

# Resize volume to its available maximum
$driveLetter="V"
Update-HostStorageCache
$max=(Get-PartitionSupportedSize -DriveLetter $driveLetter).SizeMax
Resize-Partition -DriveLetter $driveLetter -Size $max

Error: Remove-PartitionAccessPath : The access path is not valid
Ignore this error until you know what it means. Or try this
Remove-PsDrive -Name $tempDriveLetter -Force
or this
net use /delete "$tempDriveLetter`:" /y

###############################################################################################
#>

################################## ↑ Part 1: Create Volumes with Labels ↑ #############################################


################################## ↓ Part 2: Create Virtual Clustered File Servers ↓ #############################################

# Specify the servers subnet to scan for available IPs
$serversCidrBlock="500.500.500.1/24"

# Obtain drive letters and labels
$suffix="-n";
$driveLetterExclusions="[ABCDHKLMPZ]"
$maxCharsServernameAllowed=15;
$driveLettersAndLabels = Get-WmiObject Win32_Volume -Filter "DriveType=3" |?{$_.DriveLetter -ne $null -and $_.Label -ne $null -and $_.DriveLetter[0] -notlike $driveLetterExclusions}| select DriveLetter,Label | Sort Label;

function createClusteredFileServers{
    param(
        $newFileServers=($driveLettersAndLabels|%{ $lettersCount=($_.Label+$suffix).length;
                                        if($lettersCount -lt $maxCharsServernameAllowed){
                                            "$($_.Label)$suffix";
                                            }else{
                                            #($_.Label).length - ($lettersCount-15)
                                            $sliceIndex=($_.Label).length - ($lettersCount-$maxCharsServernameAllowed)                                            
                                            $truncatedLabel=$_.Label.SubString(0,$sliceIndex)
                                            "$truncatedLabel$suffix";
                                            }})
        )

    $existingFileServers=get-clusterresource|?{$_.ResourceType -eq "File Server"}|%{[void]($_.Name -match "\\\\(.*)\)");$matches[1]}
    
    function checkIfServerExists{
        param($nameToCheck)     
        if($nameToCheck -in $existingFileServers){return $true}else{return $false}
    }

    function scanForAvailableIPs{
        param(
            $cidrBlock=$(
                    $interfaceIndex=(Get-WmiObject -Class Win32_IP4RouteTable | where { $_.destination -eq '0.0.0.0' -and $_.mask -eq '0.0.0.0'} |  Sort-Object metric1).interfaceindex;
                    $interfaceObject=(Get-NetIPAddress -InterfaceIndex $interfaceIndex|select IPAddress,PrefixLength)[0];
                    "$($interfaceObject.IPAddress)/$($interfaceObject.PrefixLength)";
                    )          
            )

        function Get-IPrange{
          <# This Get-IPrange function has been obtained at https://gallery.technet.microsoft.com/scriptcenter/List-the-IP-addresses-in-a-60c5bb6b
            Snippet Author: BarryCWT
          .SYNOPSIS  
            Get the IP addresses in a range 
          .EXAMPLE 
           Get-IPrange -start 192.168.8.2 -end 192.168.8.20 
          .EXAMPLE 
           Get-IPrange -ip 192.168.8.2 -mask 255.255.255.0 
          .EXAMPLE 
           Get-IPrange -ip 192.168.8.3 -cidr 24 
            #> 
 
            param ( 
          [string]$start, 
          [string]$end, 
          [string]$ip, 
          [string]$mask, 
          [int]$cidr 
            ) 
 
            function IP-toINT64 () { 
              param ($ip) 
 
              $octets = $ip.split(".") 
              return [int64]([int64]$octets[0]*16777216 +[int64]$octets[1]*65536 +[int64]$octets[2]*256 +[int64]$octets[3]) 
            } 
 
            function INT64-toIP() { 
              param ([int64]$int) 

              return (([math]::truncate($int/16777216)).tostring()+"."+([math]::truncate(($int%16777216)/65536)).tostring()+"."+([math]::truncate(($int%65536)/256)).tostring()+"."+([math]::truncate($int%256)).tostring() )
            } 
 
            if ($ip) {$ipaddr = [Net.IPAddress]::Parse($ip)} 
            if ($cidr) {$maskaddr = [Net.IPAddress]::Parse((INT64-toIP -int ([convert]::ToInt64(("1"*$cidr+"0"*(32-$cidr)),2)))) } 
            if ($mask) {$maskaddr = [Net.IPAddress]::Parse($mask)} 
            if ($ip) {$networkaddr = new-object net.ipaddress ($maskaddr.address -band $ipaddr.address)} 
            if ($ip) {$broadcastaddr = new-object net.ipaddress (([system.net.ipaddress]::parse("255.255.255.255").address -bxor $maskaddr.address -bor $networkaddr.address))} 
 
            if ($ip) { 
              $startaddr = IP-toINT64 -ip $networkaddr.ipaddresstostring 
              $endaddr = IP-toINT64 -ip $broadcastaddr.ipaddresstostring 
            } else { 
              $startaddr = IP-toINT64 -ip $start 
              $endaddr = IP-toINT64 -ip $end 
            } 
 
 
            for ($i = $startaddr; $i -le $endaddr; $i++) 
            { 
              INT64-toIP -int $i 
            }

        }

        # Regex values
        $regexIP = [regex] "\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
        $regexCidr=[regex] "\/(.*)"
        $regexFourthOctetValue=[regex] ".+\..+\..+\.(.+)"

        # Value Extractions
        $ip=$regexIP.Matches($cidrBlock).Value
        $cidr=$regexCidr.Matches($cidrBlock).Groups[1].Value
        $allIPs=Get-IPrange -ip $ip -cidr $cidr

        # Remove fourth octet values matching 0,1, and 255
        if($regexFourthOctetValue.Matches($allIPs[0]).Groups[1].Value -eq 0){$first, $rest= $allIPs; $allIPs=$rest;}
        if($regexFourthOctetValue.Matches($allIPs[0]).Groups[1].Value -eq 1){$first, $rest= $allIPs; $allIPs=$rest;}    
        if($regexFourthOctetValue.Matches($allIPs[$allIPs.length-1]).Groups[1].Value -eq 255){$allIPs = $allIPs | ? {$_ -ne $allIPs[$allIPs.count-1]}}

        # Display sweep scanning output
        #$allIPs | ForEach-Object {if(!(Get-WmiObject Win32_PingStatus -Filter "Address='$_' and Timeout=200 and ResolveAddressNames='true' and StatusCode=0" | select ProtocolAddress*)){$_}}

        # Collect unpingable IPs
        "Collecting available IPs. Please wait awhile..."
        $GLOBAL:availableIPs=$allIPs | ForEach-Object { if(!(Get-WmiObject Win32_PingStatus -Filter "Address='$_' and Timeout=200 and ResolveAddressNames='true' and StatusCode=0" | select ProtocolAddress*)){
                                                            write-host "$_ is available.";
                                                            $_;}else{write-host "$_ is NOT available.";}
                                                        }
        
        # Also, export unavailableIPs just because I can
        $GLOBAL:unavailableIPs=Compare-Object $allIPs $availableIPs -PassThru
        }
    if(!($availableIPs)){scanForAvailableIPs};
    #$availableIPs;    

    # Proceed to create virtual file servers
    $i=0;
    for ($i=0;$i -lt $newFileServers.count;$i++){
        $serverName=$newFileServers[$i];
        $availableIP=[string]$availableIPs[$i];        
        $vServerLabel=$driveLettersAndLabels[$i].Label;
        if(!(checkIfServerExists $serverName)){ 
            Write-Host "Verify the accuracy of this statement. Press any key to commit.";
            Write-Host "Add-ClusterFileServerRole -Name $serverName -StaticAddress $availableIP -Storage $vServerLabel";
            pause;
            Add-ClusterFileServerRole -Name $serverName -StaticAddress $availableIP -Storage $vServerLabel
            "$serverName processed.";
            }else{
                Write-Host "$vServerLabel already exists. Skipping this item."
                }
    }
}

if($driveLettersAndLabels){createClusteredFileServers};

################################## ↑ Part 2: Create Virtual Clustered File Servers ↑ #############################################

################################## ↓ Part 3: Create SMB Shares on Clustered File Servers ↓ #############################################

# This section allows three types of inputs:
# (a) Manual entries of file shares
# (b) Specifying standalone file server(s) and 
# (c) Identifying the clustername to retrieve SMB Shares

# Set SMB Shares manually:
$manualShares=@();
$manualShares+=[PSCustomObject]@{ShareFolder="PUBLIC";ServerName="SHERVER007";ShareName="PUBLIC";Description=""};
$manualShares+=[PSCustomObject]@{ShareFolder="NOTPUBLIC";ServerName="SHERVER700";ShareName="NOTPUBLIC";Description=""};

# Retrieve shares from standalone servers
# Automatically populate lists of all volumes, sharenames, and descriptions of the cluster(s) to be migrated from
$standaloneFileServers="RAMBO01"
$standaloneFileServerShares=@()
if ($standaloneFileServers){
$standaloneFileServerShares=$standaloneFileServers|%{Get-WmiObject Win32_Share -computername $_ |?{$_.Name -notlike "*$" -and $_.Path -match "^\w{1}\:\\.*"}|`
                            select @{name="ShareFolder";Expression={$_.Name}},`
                            @{name="ServerName";Expression={$_.PSComputerName}},`
                            @{name="ShareName";Expression={$_.Name}},`
                            Description}
}

# Retrieve shares from clusters
$sourceClusters="CLUSTER08.KIMCONNECT.local"
$clusterShares=@()
if ($sourceClusters){
    #$nodes=$sourceClusters|%{$_;get-clusternode -cluster $_}
    $nodes=$sourceClusters|%{get-clusternode -cluster $_}
    $computerNames=$nodes.Name
    #$computerNames=$nodes|group|Select -ExpandProperty Name
    #$smbShares=$computerNames|%{Get-WmiObject Win32_Share -computername $_ | FT  -Property Path,Name,Caption}
    $regexServerName="\\\\(.{1,})\\";
    $regexShareName="\\\\(.{1,})\\(.{1,})";
    $clusterShares=$computerNames|%{try{
                                        write-host "scanning $_`...";
                                        Get-WmiObject Win32_Share -computername $_ |?{$_.Name -notlike "*$"}|`
                                        select `
                                        @{name="ShareFolder";Expression={$folderName=$_.Path.SubString(3,$_.Path.Length-3);
                                                                                if($folderName){$folderName}else{[void]($_.Name -match $regexShareName);$matches[2]}}},`                                        @{name="ServerName";Expression={[void]($_.Name -match $regexServerName);$matches[1]}},`
                                        @{name="ShareName";Expression={[void]($_.Name -match $regexShareName);$matches[2]}},`
                                        Description
                                        }catch{
                                            write-host "$_ has errors";
                                            write-host $Error;
                                            }
                                    }
    }

if($clusterShares -and $standaloneFileServerShares -and $manualShares){
    $smbShares=$standaloneFileServerShares+$clusterShares+$manualShares;
    }else{
        if($clusterShares){$smbShares=$clusterShares}
        if($standaloneFileServerShares){$smbShares=$standaloneFileServerShares}
        }

#$existingFileServerRoles=get-clusterresource|?{$_.ResourceType -eq "File Server"}|%{[void]($_.Name -match "\\\\(.*)\)$");$matches[1]}

Function listEmptyNodes{
    $sourceClusters|%{$_;get-clusternode -cluster $_}
    $emptyNodes=$computernames|%{if(!(Get-WmiObject Win32_Share -computername $_ |?{$_.Name -notlike "*$"})){$_}}
    write-host "These nodes appear to have zero SMB shares:`r`n$emptyNodes"
}


# These variables may already have been generated from "Part 2: Create Virtual Clustered File Servers"
$suffix="-n";
$maxCharsServernameAllowed=15;
$driveLetterExclusions="[CHKLMPZ]"

Function gatherPaths{

    $driveLettersAndLabels = Get-WmiObject Win32_Volume -Filter "DriveType=3" |`
        ?{$_.DriveLetter -ne $null -and $_.Label -ne $null -and $_.DriveLetter[0] -notlike $driveLetterExclusions}|`
        select @{Name="DriveLetter";Expression={$_.DriveLetter[0]}},Name,Label | Sort DriveLetter

    # Create a map for all file server roles in the standardized format of DriveLetter + ClientAccessPoint Label + Default Shares folder name (e.g. E:\SHERVER01\Shares)
    # Note this is dependent on successful completion of previous steps, where all sub-folder names with ClientAccessPoint labels have been created
    $paths=$driveLettersAndLabels|%{$defaultPath="$($_.Name)$($_.Label)";
                                    $folders=get-childitem $_.Name;
                                        if($folders){
                                            $folders|%{
                                                $subFolderPath= "$($_.FullName)"
                                                mkdir $subFolderPath -ea SilentlyContinue|out-null
                                                $subFolderPath
                                                }
                                            }else{
                                                mkdir $defaultPath -ea SilentlyContinue|out-null;
                                                $defaultPath;
                                                };
                                        }
    return $paths;
}

Function createSharesToMatchClientAccessPoints{
    param(
        $shares=$smbShares
        )                                

    # Generate Domain Admins label
    $subdomain=(net config workstation) -match 'Workstation domain\s+\S+$' -replace '.+?(\S+)$','$1'
    $domainadmins="$subdomain`\Domain Admins";

    function includePrerequisites{
        # Set PowerShell Gallery as Trusted to bypass prompts
        $trustPSGallery=(Get-psrepository -Name 'PSGallery' -ErrorAction SilentlyContinue).InstallationPolicy
        If($trustPSGallery -ne 'Trusted'){
            Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted
            }

        # Add the required NTFS security module
        if (!(Get-InstalledModule -Name NTFSSecurity -ErrorAction SilentlyContinue)) {
	        Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction SilentlyContinue;
	        Install-Module -Name NTFSSecurity -Force -ErrorAction SilentlyContinue;
	        }
    
        # Add the required Microsoft Clustering PowerShell module
        if (!(get-module -Name "FailoverClusters" -ErrorAction SilentlyContinue)){
            #Install-WindowsFeature Failover-Clustering | out-null;
            Import-Module servermanager
            Add-WindowsFeature RSAT-Clustering
            Install-WindowsFeature RSAT-Clustering-MGMT | out-null;
            Install-WindowsFeature RSAT-Clustering-PowerShell | out-null;
            Import-Module FailoverClusters | out-null;
            }
        }
    includePrerequisites;

    $paths=gatherPaths

    if(!($paths)){
        write-host "No clustered share volumes are detected. Program will move all roles to this $($env:computername).";
        get-clustergroup|move-clustergroup -node $env:computername;
        $paths=gatherPaths;
        }

    # Set the number of pauses for checking
    $checkPause=3;
    foreach ($share in $shares){    
        # Generate variables 
        $maxCharsServernameAllowed=15;      
        $smbServer=$( $lettersCount=($share.ServerName+$suffix).length;
                        if($lettersCount -lt $maxCharsServernameAllowed){
                            "$($share.ServerName)$suffix";
                            }else{
                            #($_.Label).length - ($lettersCount-15)
                            $sliceIndex=($share.ServerName).length - ($lettersCount-$maxCharsServernameAllowed)                                            
                            $truncatedLabel=$share.ServerName.SubString(0,$sliceIndex)
                            "$truncatedLabel$suffix";
                            })
        $shareFolder=$share.ShareFolder;
        $shareName=$share.ShareName;
        $smbServerName=$share.ServerName;
        $sharePath=$paths[($paths|%{[void]($_ -match ":\\(.*)");$matches[1]}).IndexOf($smbServerName)]+"\"+$shareFolder
        #$sharePathDriveLetter=$driveLettersAndLabels.DriveLetter[$driveLettersAndLabels.Label.IndexOf($share.ServerName)]
        #$sharePathLabel=$driveLettersAndLabels.Label[$driveLettersAndLabels.Label.IndexOf($share.ServerName)]
        #$sharePath="$sharePathDriveLetter`:\$sharePathLabel\$($share.SharePath)";
        $regexNonAlphaNumeric = '[^a-zA-Z0-9]';
        $description=$share.Description -replace $regexNonAlphaNumeric, ' ';
        $permission = '$domainadmins,Administrators' , 'Full', 'ContainerInherit, ObjectInherit', 'None', 'Allow';
        $rule=New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission

        $statement="
            # Create directory and its parents if they do not exists
            New-Item -ItemType Directory -Force -Path '$sharePath';

            # Set NTFS Permissions on folder
            Add-NTFSAccess –Path '$sharePath' –Account '$domainadmins',Administrators –AccessRights Full -ErrorAction SilentlyContinue;

            # Create the share with associated share permissions
            New-SmbShare -ScopeName $smbServer -Name '$shareName' -Path '$sharePath' -FullAccess '$domainadmins',Administrators -FolderEnumerationMode AccessBased -CachingMode Manual -Description '$description' -Confirm:$('$False');
            Grant-SmbShareAccess -Name '$shareName' -ScopeName $smbServer -AccountName 'Authenticated Users' -AccessRight Full -Force;
            "        
        try{            
            Write-Host "Executing...`r`n $statement";
            if($checkPause -ne 0){pause;$checkPause=$checkPause-1};  
            invoke-expression $statement -ErrorAction Stop | Out-Null;
            Write-Host "$shareName on $smbServer has been created.";

            #pause;
        }catch{
            write-host "$Error";
            break;                       
            }        

        }
}

do{
    $exitLoop=$False
    $userInput=(Read-Host -Prompt "Type 'Yes' or 'y' to trigger 'createSharesToMatchClientAccessPoints' function").toLower()
    if($userInput -match "^(yes|y)$"){createSharesToMatchClientAccessPoints;$exitLoop=$True;}
    }while ($exitLoop -eq $False)
    
<#

# Setting shares manually
New-Item -ItemType Directory -Force -Path 'S:\SHARE005\SCANS';
Add-NTFSAccess –Path 'S:\SHARE005\SCANS' –Account 'INTRANET\Domain Admins',Administrators –AccessRights Full -ErrorAction SilentlyContinue;
New-SmbShare -ScopeName SHARE005-n -Name 'SCANS' -Path 'S:\INTRANET\SCANS' -FullAccess 'INTRANET\Domain Admins',Administrators -FolderEnumerationMode AccessBased -CachingMode Manual -Description '' -Confirm:$False;
Grant-SmbShareAccess -Name 'SCANS' -ScopeName SHARE005-n -AccountName 'Authenticated Users' -AccessRight Full -Force;

# To turn OFF caching
$rolesWithNoCaching="SHARE005-n","SHARE006-n"
$rolesWithNoCaching | %{$shares=Get-SMBShare -scopename "$_"; foreach ($share in $shares){if(!($share.Name -like "*$")){"Turning caching OFF for share: $share.Name..."; Set-SMBShare -Name $share.Name -CachingMode None -Confirm:$False;}};}

# To turn ON caching
$rolesWithCaching="SHARE007-n"
$rolesWithCaching | %{$shares=Get-SMBShare -scopename "$_"; foreach ($share in $shares){if(!($share.Name -like "*$")){"Turning caching ON for share: $share.Name..."; Set-SMBShare -Name $share.Name -CachingMode Manual -Confirm:$False;}};}
        
# Changing caching mode for a single share
$shareName="SHARE005"
Set-SMBShare -Name $shareName -CachingMode None -Confirm:$False;
Set-SMBShare -Name $shareName -CachingMode Manual -Confirm:$False;

# Legend:
-- None: Prevents users from storing documents and programs offline.
-- Manual: Allows users to identify the documents and programs that they want to store offline. Equivalent to "Allow Caching of Shares"
-- Programs: Automatically stores documents and programs offline.
-- Documents: Automatically stores documents offline.
-- BranchCache: Enables BranchCache and manual caching of documents on the

# Changing cache settings for Windows 2008R2 or older:
    
# All Files and Programs that users open from the shared folder are automatically available offline. Also, Optimized for performance is enabled.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Programs")
    
# Only the files and programs that users specify are available offline.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Manual")
    
# All Files and Programs that users open from the shared folder are automatically available offline. Optimized for performance is not checked.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Documents")

# No files or programs from the shared folder are available offline.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:None")
#>

<#
# Get caching mode in 2016
get-smbshare -name "Users" -scopename SHARE005 | select CachingMode

# To turn OFF caching
$rolesWithNoCaching="SHARE005-n","SHARE006-n"
$rolesWithNoCaching | %{$shares=Get-SMBShare -scopename "$_"; foreach ($share in $shares){if(!($share.Name -like "*$")){"Turning caching OFF for share: $share.Name..."; Set-SMBShare -Name $share.Name -CachingMode None -Confirm:$False;}};}

# To turn ON caching
$rolesWithCaching="SHARE007-n","SHARE008-n"
$rolesWithCaching | %{$shares=Get-SMBShare -scopename "$_"; foreach ($share in $shares){if(!($share.Name -like "*$")){"Turning caching ON for share: $share.Name..."; Set-SMBShare -Name $share.Name -CachingMode Manual -Confirm:$False;}};}
        
# Changing caching mode for a single share
$shareName="SHARE005"
Set-SMBShare -Name $shareName -CachingMode None -Confirm:$False;
Set-SMBShare -Name $shareName -CachingMode Manual -Confirm:$False;

# Legend:
-- None: Prevents users from storing documents and programs offline.
-- Manual: Allows users to identify the documents and programs that they want to store offline. Equivalent to "Allow Caching of Shares"
-- Programs: Automatically stores documents and programs offline.
-- Documents: Automatically stores documents offline.
-- BranchCache: Enables BranchCache and manual caching of documents on the

# Changing cache settings for Windows 2008R2 or older:
    
# All Files and Programs that users open from the shared folder are automatically available offline. Also, Optimized for performance is enabled.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Programs")
    
# Only the files and programs that users specify are available offline.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Manual")
    
# All Files and Programs that users open from the shared folder are automatically available offline. Optimized for performance is not checked.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:Documents")

# No files or programs from the shared folder are available offline.
([WMIClass]"\\dc1\root\cimv2:win32_Process").Create("cmd /C net share PS /Cache:None")

# How to manually create shares
$shareNames="19"
$smbServer="SHARE005-n"
$description=""
$sharesFolder="S:\Shares";
$subdomain=(net config workstation) -match 'Workstation domain\s+\S+$' -replace '.+?(\S+)$','$1';
$domainadmins="$subdomain`\Domain Admins";
$shareNames|%{
    New-Item -ItemType Directory -Force -Path $($sharesFolder+"\"+$_)
    New-SmbShare -ScopeName $smbServer -Name $_ -Path "$sharesFolder\$_" -FullAccess $domainadmins,Administrators,"Authenticated Users" -FolderEnumerationMode AccessBased -CachingMode Manual -Description $description -Confirm:$False;
    }
#>

################################## ↑ Part 3: Create SMB Shares on Clustered File Servers ↑ #############################################

################################## ↓ Part 4: Disable caching of certain shares ↓ #############################################
# Specify the exceptions of shares with caching disabled
$sharesWithCachingDisabled=@(
    "SHARE005",
    "SHARE006"
)

function disableCaching($smbSharesayOfShares){
    $smbSharesayOfShares|%{set-smbshare -name $_ -CachingMode None -Confirm:$False}
}

disableCaching $sharesWithCachingDisabled;
################################## ↑ Part 4: Disable caching of certain shares ↑ #############################################

################################## ↓ Part 5: Generate Sources and Destination Array Variable ↓ #############################################

# Supporting variables
$regexServerNameAndShareName="\\\\(.{1,})\\(.{1,})";
$suffix="-n";
$maxCharsServernameAllowed=15;

# Set Sources and Destinations manually:
$manualSourcesAndDestinations=@();
$manualSourcesAndDestinations+=[PSCustomObject]@{Clustername="CLUSTER007";From="P:\PUBLIC";To="\\SHARE007-n\DUDE";SourceSmb="\\SHARE007\DUDE"};
$manualSourcesAndDestinations+=[PSCustomObject]@{Clustername="CLUSTER007";From="N:\NOTPUBLIC";To="\\SHARE008-n\DUDE";SourceSmb="\\SHARE008\DUDE"};

# Setting server names
$standaloneFileServers="STANDALONEFILESERVER3"
$sourceClusters="CLUSTER08","CLUSTER09"
$nodes=$sourceClusters|%{write-host "Scanning $_...";get-clusternode -cluster $_}
$computerNames=$nodes|group|Select -ExpandProperty Name

# Generate Sources and Destinations from clusters
$clusteredSourcesAndDestinations=$computerNames|ForEach-Object{
                write-host "Scanning $_..."; Get-WmiObject Win32_Share -computername $_ |?{$_.Name -notlike "*$"}|`
                select `
                @{name="Clustername";Expression={get-wmiobject -class "MSCluster_Cluster" -namespace "root\mscluster" -ComputerName $($_.PSComputerName)|select -ExpandProperty Name}},`
                @{name="SourceSmb";Expression={$_.Name}},`
                @{name="From";Expression={$_.Path}},`
                @{name="To";Expression={$([void]($_.Name -match $regexServerNameAndShareName);
                                            $serverName=$matches[1];
                                            $smbShare=$matches[2];
                                            $lettersCount=($serverName+$suffix).length;
                                            if($lettersCount -lt $maxCharsServernameAllowed){
                                                "\\$serverName$suffix\$smbShare";
                                                }else{                                                
                                                $sliceIndex=($serverName).length - ($lettersCount-$maxCharsServernameAllowed)                                            
                                                $truncatedLabel=$serverName.SubString(0,$sliceIndex)
                                                "\\$truncatedLabel$suffix\$smbShare";
                                                })
                                            }}
                                            }|?{$_.To -ne $null}

#  Generate Sources and Destinations from standalone file servers
$standaloneSourcesAndDestinations=$standaloneFileServers|%{Get-WmiObject Win32_Share -computername $_ |?{$_.Name -notlike "*$" -and $_.Path -match "^\w{1}\:\\.*"}|`
                select `
                @{name="Clustername";Expression={get-wmiobject -class "MSCluster_Cluster" -namespace "root\mscluster" -ComputerName $($_.PSComputerName)|select -ExpandProperty Name}},`
                @{name="SourceSmb";Expression={"\\$($_.PSComputerName)\$($_.Name)"}},`
                @{name="From";Expression={$_.Path}},`
                @{name="To";Expression={$( $serverName=$_.PSComputerName;
                                            $smbShare=$_.Name;
                                            $lettersCount=($serverName+$suffix).length;
                                            if($lettersCount -lt $maxCharsServernameAllowed){
                                                "\\$serverName$suffix\$smbShare";
                                                }else{                                                
                                                $sliceIndex=($serverName).length - ($lettersCount-$maxCharsServernameAllowed)                                            
                                                $truncatedLabel=$serverName.SubString(0,$sliceIndex)
                                                "\\$truncatedLabel$suffix\$smbShare";
                                                })
                                            }}}

$sourcesAndDestinations=$clusteredSourcesAndDestinations+$standaloneSourcesAndDestinations+$manualSourcesAndDestinations

################################## ↑ Part 5: Generate Sources and Destination Array Variable ↑ #############################################

################################## ↓ Part 6: Perform File Copy Operation ↓ #############################################

# Retrieve the $sourcesAndDestinations Object from "Part 5" prior to proceeding further
function validateDestinations{
    param($inputData=$sourcesAndDestinations)
    $objectLength=$inputData.from.length 
    for ($i=0;$i -lt $objectLength; $i++){        
        #$from=$inputData.from[$i]
        $to=$inputData.to[$i]
        #"Checking $from and $to ..."
        #if(!(test-path $from) -OR !(test-path $to) ){
        if(!(test-path $to) ){
            write-host "Record corresponding to $to is not reachable."    
            # Remove row if any path doesn't resolve. Overcome limitations of Powershell's immutable array "fixed size" using this workaround
            $castedArrayList=[System.Collections.ArrayList]$inputData;
            $castedArrayList.RemoveAt($i);
            $inputData=[Array]$castedArrayList; #reverse the casting and reassign to original Array
            $objectLength--;
            $i--;
            } #else{write-host "$to is reachable."}
    }
    return $inputData
}

$validatedData=validateDestinations -inputData $sourcesAndDestinations

Function FindDuplicates{
    param(
        $originalArray,
        $subArrayWithDuplicates
        )

    function FindDuplicatesInArray{
	    param(
		    $array
		    )	
	    $hash = @{ }
	    $duplicates = @()
	    for ($i=0;$i -lt $array.Count;$i++){
		    $item=$array[$i];
            try{
		        $hash.add($item, 0)
		        }
		    catch [System.Management.Automation.MethodInvocationException]{           
                $duplicates += $array[$i]
		        }
		    }	 
        return $duplicates
        }

    function returnDuplicateRecords{
        param(
            $duplicates,
            $sourceArray
            )
        $duplicateRecords=@();
        for ($i=0;$i -lt $duplicates.count;$i++){
            $x=$duplicates[$i]
            for ($j=0;$j -lt $sourceArray.Count;$j++){            
                if ($x -like $sourceArray[$j].From){$duplicateRecords+=$sourceArray[$j]}
            }
        }
        return $duplicateRecords
    }
    $duplicates=FindDuplicatesInArray $subArrayWithDuplicates
    $duplicateRecords=returnDuplicateRecords -duplicates $duplicates -sourceArray $originalArray
    return $duplicateRecords
}

for ($col=0;$col -le $validatedData.length;$col++){	
	for ($row=0;$row -le $array[$col].length;$row++){
		echo $validatedData[$col][$row]
	}
}
# I currently don't know how to retrieve the number of columns to set the number of $columns-1 as the variable for $passes
$duplicatesPass1=FindDuplicates -originalArray $validatedData -subArrayWithDuplicates $validatedData.From
$duplicatesPass2=FindDuplicates -originalArray $duplicatesPass1 -subArrayWithDuplicates $duplicatesPass1.Clustername
if($duplicatesPass2){"These records have duplicated sources:`r`n$duplicatesPass2"}

function constructHashTable($array){
    $hash="`$arr=@{};
`$arr['from']=@{}; `$arr['to']=@{}; 
`$arr['from']=@('$($array[0].from)'); `$arr['to']=@('$($array[0].to)');
    "
    for ($i=1;$i -lt $array.length;$i++){
        $hash+="`r`n`$arr['from']+='$($array[$i].from)'; `$arr['to']+='$($array[$i].to)';";
    }
    return $hash
}

$hashTableConstructor=constructHashTable $validatedData
# $hashTableConstructor


function createArrayConstructor($array){
    $arrayConstructor="`$arr=@();"
    for ($i=0;$i -lt $array.length;$i++){
        $arrayConstructor+="`r`n`$arr+=[PSCustomObject]@{Clustername='$($array[$i].Clustername)';From='$($array[$i].From)';To='$($array[$i].To)'};";
    }
    return $arrayConstructor
}

$systemArrayConstructor=createArrayConstructor $validatedData
$systemArrayConstructor

# Proceed to plugin the hashTableConstructor into the file-copy operations (a different script) 

################################## ↑ Part 6: Perform File Copy Operation  ↑ #############################################

################################## ↓ Part 7: Copy SMB Share Permissions ↓ #############################################

# Please run "Part 5: Generate Sources and Destination Array Variable" prior to proceeding
$dateStamp = Get-Date -Format "yyyy-MM-dd-hhmmss"
$logFile="C:\copySmbPermissions-Log-$dateStamp.txt"
$GLOBAL:logMessages="";

function copySmbPermissions{
    param(
        [string]$sourceSmbSharePath,
        [string]$destinationSmbSharePath
        )

    # Set some variables
    $sourceSmbServerName = $sourceSmbSharePath.split("\")[2];
    $sourceSmbShareName = $sourceSmbSharePath.split("\")[3];
    $destinationSmbServerName = $destinationSmbSharePath.split("\")[2];
    $destinationSmbShareName = $destinationSmbSharePath.split("\")[3];
    $GLOBAL:messages="";

    # Legacy method to obtain SMB share permissions that would work with PowerShell 2.0 (Windows 2008)
    function getSmbPermmissions{
        param(
            [string]$smbServerName,
            [string]$smbPath
            )
        $smbShareName = $smbPath.split("\")[3]
        $smbList = Get-WmiObject win32_LogicalShareSecuritySetting -ComputerName $smbServerName|?{$_.Name -notlike "*$"}
        if($smbList){
            $sample=$smbList.Name[0];
            if($sample -match "^\\\\(.*)"){
                # This accounts for Clustered File Server Role
                $smbObject=$smbList|?{$_.Name -eq $smbPath};
                }else{
                    # This accounts for Standalone File Server Role
                    $smbObject=$smbList|?{$_.Name -eq $smbShareName};
                    }
            }else{
                $message="Failed to retrieve SMB Permissions from $smbServerName";
                $GLOBAL:messages+=$message+"`r`n";
                write-host $message -ForegroundColor Yellow;
                $smbObject=$false;
                }

        if($smbObject){
            $message="Collecting share permissions for $smbPath...";
            $GLOBAL:messages+=$message+"`r`n";
            Write-Host $message;
            $smbPermissions = @()
            $acls = $smbObject.GetSecurityDescriptor().Descriptor.DACL
            foreach($acl in $acls){
                $user = $acl.Trustee.Name
                if(!($user)){$user = $acl.Trustee.SID}
                $domain = $acl.Trustee.Domain
                switch($acl.AccessMask){
                    2032127 {$accessRight = "Full"}
                    1245631 {$accessRight = "Change"}
                    1179817 {$accessRight = "Read"}
                    }          
                $smbPermissions+=[PSCustomObject]@{IdentityReference="$domain\$user";AccessRight=$accessRight};
                }
            return $smbPermissions;
            }else{
                return $false;
                }
    }

    # This function requires PowerShell 4.0 and higher
    function setSmbPermissions{
        param(
            $smbName=$destinationSmbShareName,
            $fileServerRole=$destinationSmbServerName,
            $smbPermissions=$sourceSmbPermissions
            )
        # Locate the host for the file server role
        $roleOwner=(Get-ClusterResource -Name $fileServerRole -ErrorAction SilentlyContinue).OwnerNode.Name
        
        if ($roleOwner){
            # Connect to host
            $message="Connecting to remote computer $roleOwner...";
            $GLOBAL:messages+=$message;
            write-host $message;
            $maxTime=10; $duration=0;
            do{
                $session = New-PSSession -ComputerName $roleOwner -ErrorAction SilentlyContinue;
                if (!($session)){
                    write-host "." -NoNewline;
                    $duration++;
                    sleep -seconds 1;
                    }
                if ($session){
                    $message=".. Connected in $duration seconds.";
                    $GLOBAL:messages+=$message+"`r`n";
                    write-host $message -NoNewline;
                    }
                } until ($session.state -match "Opened" -or $duration -eq $maxTime)

            if($session){
                $message="Setting share permissions for $smbName of $fileServerRole, currently owned by $roleOwner...";
                $GLOBAL:messages+=$message+"`r`n";
                Write-Host $message;
                $invokeCommandResult=Invoke-Command -Session $session -ScriptBlock{
                            param($smbName,$fileServerRole,$smbPermissions)

                            # Declare some variables;
                            [int]$successCount=0;
                            [int]$failuresCount=0;
                            [bool]$singleItem=$smbPermissions.Count -eq $null -or $smbPermissions.Count -eq 1;
                            [string]$sessionMessages="";

                            foreach ($permission in $smbPermissions){
                                $identityReference=$permission.IdentityReference;
                                # This accounts for non-domain accounts
                                if($identityReference -match "^\\[^\\]+$"){$identityReference=$identityReference.SubString(1,$identityReference.Length-1)}
                                $accessRight=$permission.AccessRight;
                                $expression="Grant-SmbShareAccess -Name $smbName -ScopeName $fileServerRole -AccountName '$identityReference' -AccessRight $accessRight -Force";

                                # Using self executing anonymous function to exit try-loop upon failures
                                .{
                                    try{
                                        $success=Invoke-Expression $expression;
                                        if(!($success)) {
                                            $failuresCount++;                                       
                                            $success=$false;
                                            return; # Exit try-loop
                                            }
                                        $successCount++;                                     
                                        $success=$true;                              
                                        }
                                    catch{
                                        write-host $Error;
                                        }                      
                                }
                            if($success){
                                $message="$expression => Success";
                                $sessionMessages+=$message+"`r`n";
                                write-host $message;
                                }else{
                                    $message="$expression => Failure"
                                    $sessionMessages+=$message+"`r`n";
                                    write-host $message -ForegroundColor Yellow;
                                    }
                            }

                            if ($singleItem){
                                $message="$successCount of 1 permission set is successful.";
                                $sessionMessages+=$message+"`r`n";
                                write-host $message;
                                }else{
                                    $message="$successCount of $($smbPermissions.Count) permission settings are successful.";
                                    $sessionMessages+=$message+"`r`n";
                                    write-host $message;                                    
                                    }
                            $overallResult=$failuresCount -eq 0;
                            $outputArray=@($overallResult,$sessionMessages)
                            return $outputArray;
                        } -Args $smbName,$fileServerRole,$smbPermissions
                Remove-PSSession $session;

                $GLOBAL:messages+=$invokeCommandResult[1]+"`r`n";                
                $result=$invokeCommandResult[0];                
                }else{
                    $message="Unable to connect to $roleOwner.";
                    $GLOBAL:messages+=$message+"`r`n";
                    write-host $message -ForegroundColor Red;
                    $result=$false;
                    }
            }else{
                    $message="Unable to get owner node for $fileServerRole.";
                    $GLOBAL:messages+=$message+"`r`n";
                    write-host $message -ForegroundColor Red;
                    $result=$false;
                    }
        return $result;
        }

    $sourceSmbPermissions=getSmbPermmissions -smbServerName $sourceSmbServerName -smbPath $sourceSmbSharePath
    if ($sourceSmbPermissions){
        $success=setSmbPermissions -smbName $destinationSmbShareName -fileServerRole $destinationSmbServerName -smbPermissions $sourceSmbPermissions
        if ($success){
            $message="Permissions from $sourceSmbSharePath have been successfully copied to $destinationSmbSharePath.";
            $GLOBAL:messages+=$message+"`r`n";
            write-host $message;
            return $true;
                }else{
                $message="Unable to *SET* SMB Permissions to destination $destinationSmbSharePath.";
                $GLOBAL:messages+=$message+"`r`n";
                write-host $message -ForegroundColor Red;
                return $false;
                }
        }else{
            $message="Unable to *GET* SMB Permissions from $sourceSmbSharePath.";
            $GLOBAL:messages+=$message+"`r`n";
            write-host $message -ForegroundColor Yellow;
            return $false;
            }
}

foreach ($item in $sourcesAndDestinations){
    # This part could be further optimized by grouping all SMB paths by role/servername and copying permissions of the whole group in one batch
    $sourceSmbPath=$item.SourceSmb
    $destinationSmbPath=$item.To
    $expression="copySmbPermissions -sourceSmbSharePath $sourceSmbPath -destinationSmbSharePath $destinationSmbPath";
    $successfulPass=Invoke-Expression $expression;
    if($successfulPass){
        write-host "$expression => succeeded!`r`n";
        }else{
            write-host "$expression => failed!`r`n" -ForegroundColor Red
            }
    $logMessages+=$messages;
    Add-Content $logFile $messages;
}

<#
# Manually revoking SMB Share access of an account (after moving all roles onto 'this' node)
$currentSmbShares=get-smbshare|?{$_.Name[$_.Name.Length-1] -ne '$' -and $_.Path[0] -ne 'c'}
$currentSmbShares|%{Revoke-SmbShareAccess -Name $_.Name -ScopeName $_.ScopeName -AccountName 'Authenticated Users' -Force}
#>
################################## ↑ Part 7: Copy SMB Share Permissions  ↑ #############################################


################################## ↓ Part 8: Remove the 260 Characters Path Limitation ↓ #############################################

$newClusters="CLUSTER08","CLUSTER09"
$newNodes=$newClusters|%{get-clusternode -cluster $_}
$newComputerNames=$newNodes|group|Select -ExpandProperty Name

# This function increases the default windows 260 characters path length limit to 1024
Function remove260CharsPathLimit{
    param(
        [string]$computerName=$env:computername
        )    
    # Declare static variables
    $registryHive= "SYSTEM\CurrentControlSet\Control\FileSystem"
    $keyName = "LongPathsEnabled"
    $value= 1
    
    # The legacy command-line method
    #$result=REG ADD "\\$computerName\HKLM\$registryHive" /v $keyName /t REG_DWORD /d $value /f
    #if($result){"\\$computerName\HKLM\$registryHive\$keyName has been added successfully."}

    #The PowerShell Method to set remote registry key
    $registryHive="SYSTEM\CurrentControlSet\Control\FileSystem"
	$keyName="LongPathsEnabled"
	$value=1
    $remoteRegistry = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine",$computerName)
    $remoteHive = $remoteRegistry.OpenSubKey($registryHive,$True)
    $remoteHive.CreateSubKey($keyName)|out-null
    $remoteKeyValue = $remoteRegistry.OpenSubKey("$registryHive\$keyName",$True)
    $remoteKeyValue.SetValue($keyName,$value)

    # Validation
    $resultKey=$remoteRegistry.OpenSubKey("$registryHive\$keyName")    
    if($resultKey.GetValue($keyName) -eq $value){"\\$computerName\HKLM\$registryHive\$keyName has been added successfully."}
	}

# This reverses the effects of that previous function
Function restore260CharsPathLimit{
    param([string]$computerName=$env:computername)

    $registryHive= "SYSTEM\CurrentControlSet\Control\FileSystem"
    $keyName = "LongPathsEnabled"
    $result=REG DELETE "\\$computerName\HKLM\$registryHive" /v $keyName /f
    if($result){"\\$computerName\HKLM\$registryHive\$keyName has been removed successfully."}
	}

$newComputerNames|%{remove260CharsPathLimit $_}

    #This method only works locally
	#$registryHive="REGISTRY::HKLM\SYSTEM\CurrentControlSet\Control\FileSystem"
	#$registryKey="LongPathsEnabled"
	#$value=1
	#New-ItemProperty -Path $registryPath -Name $registryHive -Value $value -PropertyType DWORD -Force -ea SilentlyContinue
    #Verify result
    #Get-Item "REGISTRY::HKLM\$registryHive"
	
    # WMI method
    #$HKEY_LOCAL_MACHINE = 2147483650 #Set unique ID for Registry hive of local machine
    #$registryClass = [WMIClass]"ROOT\DEFAULT:StdRegProv"
    #$keyName = "LongPathsEnabled"
    #$value     = 1
    #$registryHive       = "SYSTEM\CurrentControlSet\Control\FileSystem"
    #$createRegistryKey   = $registryClass.SetExpandedStringValue($HKEY_LOCAL_MACHINE, $registryHive, $keyName, $value)
    #If ($createRegistryKey.Returnvalue -eq 0) {"REGISTRY::HKLM\$registryHive\$keyName has been created with the value of $value"}

################################## ↑ Part 8: Remove the 260 Characters Path Limitation  ↑ #############################################


################################## ↓ Part 9: Create Scheduled Tasks on Source Servers ↓ ###############################################
# Scheduled-tasks-remote.ps1
# Purpose: to add a schedule task onto remote Windows system(s)
# Requires: PowerShell 3.0 or higher at the Jump box. Powershell 2.0 or higher at the targets.

# Set variables:
$scriptFile="\\snapshots\FileServerClusters\File_Copy_Script.ps1";
$description="File Copy Operations";
$taskName="File_Copy_Operations";
[DateTime]$timeToStartTasks = '6:00pm';
#$standaloneFileServers="STANDALONEFILESERVER3";
$sourceClusters="CLUSTER01.kimconnect.local";

$clusterNodes=($sourceClusters|%{get-clusternode -cluster $_})|group|Select -ExpandProperty Name
$allNodes=$clusterNodes+$(if($standaloneFileServers){$standaloneFileServers})

# Credentials
#$user=Read-Host -Prompt "Input $env:USERDOMAIN administrator's username"
$user="$env:UserName"
$domainAccount="$env:UserDomain`\$user"
$plainTextPassword=Read-Host -Prompt "Input the password for $domainAccount"
$password=ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $domainAccount,$password

function enableRemoteWinRM{
  Param([string]$computername)

  Write-Host "checking $computername..."

  function pingTest{
      Param([string]$node)
      try{
        Return Test-Connection $node -Count 1 -Quiet -ea Stop;
      }
      catch{Return $False}
    }

  if (pingTest $computername){
      if (!(Test-WSMan $computername -ea SilentlyContinue)){
        if(!(get-command psexec)){
            if (!(Get-Command choco.exe -ErrorAction SilentlyContinue)) {
                Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
                }
            choco install sysinternals -y
            }
        psexec.exe \\$computername -s C:\Windows\system32\winrm.cmd qc -quiet
        }else{Write-Host "WinRM has been already enabled. No changes to WinRM have been made."}
    }
  Else{Write-Host "Unable to determine if WinRM is enabled on $computername`.`n Ping test has failed. Check if this computer is online and whether there's a firewall blocking of ICMP";}

  $winRMEnabled=test-netconnection $computername -CommonTCPPort WINRM -InformationLevel Quiet
  if ($winRMEnabled){
    write-host "WinRM has been abled on $computerName - Passed!";
    }else{
        write-host "WinRM was NOT reachable via port 5985 of $computerName - Failed!";
        }
}

$allNodes|%{if ($_){enableRemoteWinRM $_}} ;

function setScheduledTasks{
    param($servers=$allNodes)

    function isUnc($path){
        $GLOBAL:uncServer=$scriptFile | select-string -pattern "(?<=\\\\)(.*?)(?=\\)" | Select -ExpandProperty Matches | Select -ExpandProperty Value
        if ($uncServer){return $True}else{return $False}
    }

    function isDomainAdmin($account){
        if (!(get-module activedirectory)){Install-WindowsFeature RSAT-AD-PowerShell -Confirm:$false}
        if((Get-ADUser $account -Properties MemberOf).MemberOf -match 'Domain Admins'){return $True;}else{return $false;}
    }

    function isValidCred($u,$p){
        # Get current domain using logged-on user's credentials
        $domain = "LDAP://" + ([ADSI]"").distinguishedName
        $domainCred = New-Object System.DirectoryServices.DirectoryEntry($domain,$u,$p)
        if ($domainCred){return $True}else{return $False}
    }
                                                                                                                                                                                                                                                                           if ((isDomainAdmin $user)-AND (isValidCred $user $password)){
    if (isUnc $scriptFile){
        foreach ($computer in $servers){
            "Processing $computer...";
            $result=Invoke-Command -ComputerName $computer -Credential $cred -ScriptBlock{
                param($scriptFile,$taskName,$description,$user,$password,$uncServer,$timeToStart)
                # Debug
                "Running with these variables:`n"
                "`nUserID: $user"
                "`nScript: $scriptFile"
                "`nTask name: $taskName"
                "`nDescription: $description"
                "`nUNC server: $uncServer"
                #
                $username="$user"

                # Execute this sequence if the detected PowerShell version is 3.0 or greater
                #$powershellVersion=$PSVersionTable.PSVersion.Major
                $osName=(Get-WmiObject -class Win32_OperatingSystem).Caption
                $windowsVersionNumber=[System.Environment]::OSVersion.Version.Major
                switch ($windowsVersionNumber){
                    5{
                        "$osName is detected.`r`nNow running commands basing on features available in this version...";
                        #$Moss_backupjob_filename = 'd:\MOSS_Backup.bat';
                        $HostName = $env:computername;
                        $TaskRun = "C:\WINDOWS\system32\windowspowershell\v1.0\powershell.exe -noprofile -ExecutionPolicy Bypass $scriptFile";
                        $startIn=split-path $scriptFile -Parent;
                        schtasks /create /s $hostname /ru $username /rp $password /tn $taskName /tr $TaskRun /sc daily /st $timeToStart.ToString('HH:mm')
                        break;
                        }
                    6{
                        "$osName is detected.`r`nNow running commands basing on features available in this version...";                   
                        $TaskDescription = $description
                        $TaskCommand = "powershell.exe"
                        $TaskScript = $scriptFile
                        $TaskArg = "-noprofile -ExecutionPolicy Bypass -file $TaskScript";
                        $startIn=split-path $TaskScript -Parent;
                        #$TaskStartTime = [datetime]::Now.AddMinutes(60)
                        $TaskStartTime = $timeToStart
                        $service = new-object -ComObject("Schedule.Service")
                        $service.Connect()
                        $rootFolder = $service.GetFolder("\")
                        $TaskDefinition = $service.NewTask(0)
                        $TaskDefinition.RegistrationInfo.Description = $TaskDescription
                        $TaskDefinition.RegistrationInfo.Author = $taskRunAsuser
                        $TaskDefinition.Settings.Enabled = $true
                        $TaskDefinition.Settings.AllowDemandStart = $true
                        $TaskDefinition.Settings.ExecutionTimeLimit = "PT0S"
                        $TaskDefinition.Principal.RunLevel = 1
                        $triggers = $TaskDefinition.Triggers                    
                        $trigger = $triggers.Create(2)
                        $trigger.StartBoundary = $TaskStartTime.ToString("yyyy-MM-dd'T'HH:mm:ss")
                        $trigger.Enabled = $true
                        $action = $TaskDefinition.Actions.Create(0)
                        $action.Path = $TaskCommand
                        $action.Arguments = $TaskArg
                        $action.WorkingDirectory = $startIn
                        $rootFolder.RegisterTaskDefinition($TaskName,$TaskDefinition,6,$username,$password,1)
                        break;
                        }
                   default{
                        "$osName is detected.`r`nNow running commands basing on features available in this version...";
                        $settingsCommand = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit 0
                        $callPowerShell = New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-noprofile -ExecutionPolicy Bypass $scriptFile"
                        $startIn=split-path $scriptFile -Parent;
                        $dailyTrigger =  New-ScheduledTaskTrigger -Daily -At $timeToStart

                        # Unrestrict this Domain Administrator from security prompts
                        Set-Executionpolicy -Scope CurrentUser -ExecutionPolicy UnRestricted -Force                   

                        # Unblock file & Ensure that script exists
                        <# Overcome error caused by double hop issue:
                        Cannot find path '\\snapshots\FileServerClusters\Daily-VSS-Snapshot.ps1' because it does not exist.
                        + CategoryInfo          : ObjectNotFound: (\\snapshots\Fil...SS-Snapshot.ps1:String) [Unblock-File], ItemNotFoundException
                        + FullyQualifiedErrorId : FileNotFound,Microsoft.PowerShell.Commands.UnblockFileCommand
                        + PSComputerName        : SHERVER007

                        1. Run scheduled task as: New-ScheduledTaskAction -Execute "Powershell.exe" -Argument "-ExecutionPolicy Bypass $scriptFile"
                        2. Unblock-File -Path $scriptFile
                        #>       
                        if ((Invoke-Command -computername $uncServer -Credential $Using:cred -ScriptBlock{Unblock-File -Path $Args[0];Test-Path $Args[0] -ErrorAction SilentlyContinue}-Args $scriptFile) -eq $False) {"Errors locating $scriptFile... Skipping";break;}

                        # Unregister the Scheduled task if it already exists
                        Get-ScheduledTask $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false;

                        # Create new scheduled task
                        Register-ScheduledTask -Action $callPowerShell -WorkingDirectory $startIn -Trigger $dailyTrigger -TaskName $taskName -Description $description -User $username -Password $password -Settings $settingsCommand -RunLevel Highest;
                        break;
                        }
                    }                     
                } -ArgumentList $scriptFile,$taskName,$description,$domainAccount,$plainTextPassword,$uncServer,$timeToStartTasks
            
            if($result){write-host "$computer scheduled task setup - Success!"}else{write-host "$computer scheduled task setup: Failed!"}
            #pause;
            $timeToStartTasks = $timeToStartTasks.AddMinutes(1)
        }
    }
    }else{
        "Need to run this program with a valid Domain Administrator account."
        }
}

setScheduledTasks $allNodes

################################## ↑ Part 9: Create Scheduled Tasks on Source Servers ↑ #############################################


################################## ↓ Part 10: Cutover Functions ↓ ###################################################################

###############################################################################################
# Granting Authenticated Users full SMB access to non-admin shares
$nonAdminShares=(get-smbshare).name |?{$_[$_.length-1] -ne "$" }
$nonAdminShares|%{Grant-SmbShareAccess -Name "$_" -AccountName "Authenticated Users" -AccessRight Full -Force}

# Copying SMB access permission from a share to another
$sourceSmb="SHARE006"
$sourceServerName="NODE12"
$destinationSmb="SHARE006-N"
$destinationServerName="NODE008"
$sourcePermissions=Get-SmbShareAccess -Name $sourceSmb -ScopeName $sourceServerName
foreach ($permission in $sourcePermissions){
    $account=$permission.AccountName
    $access=$permission.AccessRight
    Grant-SmbShareAccess -Name $destinationSmb -ScopeName $destinationServerName -AccountName "$account" -AccessRight $access -Force
    }

# Using emcopy to copy all files from DirectoryA to DirectoryB, while excluding some folders
$fromDirectory="X:\"
$toDirectory="\\node008\b$\app007"
$excludeFolders="X:\nocopy","X:\necopo"
emcopy '$fromDirectory' '$toDirectory' *.*  /s /o /a /i /d /c /th 32 /r:0 /w:0 /xd $($excludeFolders -join ' ')


# Get Cluster Nodes
$clusterName="SOMECLUSTER"
$roleName="SOMEROLE"
$nodes=get-clusternode -cluster $clusterName

# Discover connected computers to port 445 (SMB)
invoke-command -computername 'NODE008' -ScriptBlock {
    $connectedComputers=get-nettcpconnection -LocalPort 139,445|select-object @{Name="ComputerName";Expression={[System.Net.dns]::GetHostbyAddress($_.RemoteAddress).HostName;}};
    return $connectedComputers;
	}

invoke-command -computername $server -ScriptBlock {
    param($checkTcpConnection,$port)
#$connectedComputers=get-nettcpconnection -LocalPort 139,445 | ?{$_.RemoteAddress -notmatch "(0.0.0.0)|(::)|(127.0.0.1)"};
[ScriptBlock]::Create($checkTcpConnection).invoke($port); 
$connectedComputers|%{[System.Net.dns]::GetHostbyAddress($_.RemoteAddress).Hostname}
} -arg ${function:Check-TcpConnection},$port

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

Disable-NetAdapterBinding -Name "Your network adapter name" -DisplayName "File and Printer Sharing for Microsoft Networks"
Enable-NetAdapterBinding -Name "Your network adapter name" -DisplayName "File and Printer Sharing for Microsoft Networks"

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

# Get list of nodes in this cluster
$clusterNodes=get-clusternode|group|Select -ExpandProperty Name

# Disable Controlled Folder Access
$clusterNodes=get-clusternode|group|Select -ExpandProperty Name
$clusterNodes|%{invoke-command -computername $_ -scriptBlock {Set-MpPreference -EnableControlledFolderAccess Disabled}}

Leave a Reply

Your email address will not be published. Required fields are marked *