<# This script will create a RAM Disk at either System Startup or via a User initiated Scheduled Task using the Arsenal Image Mounter command line. This was written using Arsenal Image Mounter version 3.11.x. When creating a RAM disk, it automatically takes the next available drive letter after C:, labels it as "RAM disk", and formats it as an NTFS drive. It will fail to create a RAM disk if there is not enough RAM available. The script will set a default size for the RAM disk if the Size parameter is omitted. This is based on the ammount of total physical RAM in the system: - If total physical RAM is greater than or equal to 160GB, it will create a Ram disk of size 32GB - If total physical RAM is greater than or equal to 128GB, it will create a Ram disk of size 24GB - If total physical RAM is greater than or equal to 64GB, it will create a Ram disk of size 16GB - If total physical RAM is greater than or equal to 48GB, it will create a Ram disk of size 8GB - If total physical RAM is less than 48GB, it will create a Ram disk of size 4GB After the default creation of a RAM disk permissions are pretty loose: - Administrators - Full Control - This folder, subfolders and files - SYSTEM - Full Control - This folder, subfolders and files - CREATOR OWNER - Full Control - This folder, subfolders and files - Everyone - Full Control - This folder, subfolders and files - Local Administrator (SID of S-1-5-21-1234567890-1234567890-1234567890-500) is the Owner Will look at locking that down further, specifically for the RDS servers to avoid users modifying and deleting each other's data. However, we do lock down the permissions on the "IMPORTANT Read Me.txt" file so that users cannot delete it. Each time a new RAM disk is created a new "Arsenal Virtual SCSI Disk Device" is added as a Disk drive. These are removed when the associated RAM disk is removed. This script uses the "aim_cli.exe" command line tool. I will look to re-write this using the API should the command line tool be too limiting for what needs to be achieved. I have found that either for new VMs that have not yet created a Write-Cache drive, or if a Write-Cache drive on an existing VM is deleted so it gets recreated, we must wait for the "LIC_BISF_Device_Personalize" Scheduled Task to complete at startup before creating a RAM disk. Even though the RAM disk is correctly created as E:, the BISF process will change the letter to D: and fail to create the Write-Cache drive as intended. This is intentional. So instead of changing BISF behaviour, it's allowed for in this script. Syntax: .\CreateRAMDisk.ps1 -Action: -Size: -RAMDiskStartingFrom: Where... - Action is either Create or Remove. If the action is remove, it deletes all existing RAM disks. - Size the size of the ram disk. Size in bytes, can be suffixed with for example M or G for MB or GB. - RAMDiskStartingFrom is the drive letter you want the RAM Disk to start from. This script will then reserve unused the letters between C and the starting letter. This allows for consistency with the way the RAM Disk is created. Script name: CreateRAMDisk.ps1 Release 1.4 Written by Jeremy Saunders (jeremy@jhouseconsulting.com) 17th February 2024 Modified by Jeremy Saunders (jeremy@jhouseconsulting.com) 7th March 2024 #> #------------------------------------------------------------- [cmdletbinding()] param( [Parameter(Mandatory = $true)] [ValidateSet("Create", "Remove")] [string]$Action, [string]$Size, [string]$RAMDiskStartingFrom="E" ) # Set Powershell Compatibility Mode Set-StrictMode -Version 2.0 # Enable verbose, warning and error mode $VerbosePreference = 'Continue' $WarningPreference = 'Continue' $ErrorPreference = 'Continue' $StartDTM = (Get-Date) #------------------------------------------------------------- $invalidChars = [io.path]::GetInvalidFileNamechars() $datestampforfilename = ((Get-Date -format s).ToString() -replace "[$invalidChars]","-") # Get the script path $ScriptPath = {Split-Path $MyInvocation.ScriptName} $ScriptPath = $(&$ScriptPath) $ScriptName = [System.IO.Path]::GetFilenameWithoutExtension($MyInvocation.MyCommand.Path.ToString()) $Logfile = "$ScriptName-$($datestampforfilename).log" $logPath = "${env:TEMP}" try { Start-Transcript "$logPath\$logFile" } catch { Write-Verbose "This host does not support transcription" } #------------------------------------------------------------- # Set the path for the Arsenal Image Mounter CLI (AIM CLI) $Executable = "${env:ProgramFiles}\Arsenal Image Mounter\aim_cli.exe" $ReadMe=@" IMPORTANT INFORMATION --------------------- - This RAM disk is to be used as a temporary scratch space ONLY. - ALL data on this drive is lost when the system reboots (application is closed) or the RAM disk is deleted. - To reduce risk and avoid data loss please ensure your data is always safely stored on a network file share. "@ #------------------------------------------------------------- Function Wait-For-Scheduled-Task { param( [Parameter(Mandatory = $true)] [string]$TaskName ) $IsComplete = $False write-verbose "Checking the current status of the `"$TaskName`" Scheduled Task..." -verbose try { $task = Get-ScheduledTask -TaskName "$TaskName" -ErrorAction Stop If ($null -ne $task) { write-verbose "- A Scheduled Task with name `"$TaskName`" was found." -verbose $exitwhencomplete = $False $Interval = 1 $LastBootTime = [Management.ManagementDateTimeConverter]::ToDateTime((Get-WmiObject -Class Win32_OperatingSystem).LastBootUpTime) write-verbose "- This host was last booted on $LastBootTime" -verbose Do { $LastRunTime = Get-ScheduledTaskInfo -TaskName "$TaskName" -ErrorAction Continue | Select-Object -ExpandProperty LastRunTime if ($null -ne $LastRunTime) { write-verbose "- The task was last run on $($LastRunTime.DateTime)" -verbose If ($LastRunTime.DateTime -ge $LastBootTime) { Write-Verbose "- Task `"$TaskName`" has run since startup." -verbose $TaskState = Get-ScheduledTask -TaskName "$TaskName" -ErrorAction Continue | Select-Object -ExpandProperty State If ($TaskState -ne $null) { If ($TaskState -eq "Ready") { $exitwhencomplete = $True Write-Verbose "- Task `"$TaskName`" is in a Ready state." -verbose $IsComplete = $True } ElseIf ($TaskState -eq "Running") { Write-Verbose "- Task `"$TaskName`" is still running." -verbose } Else { Write-Verbose "- Task `"$TaskName`" is in a $($TaskState) state" -verbose } } } else { Write-Verbose "- Task `"$TaskName`" has not run since startup." -verbose } } Else { Write-Verbose "- Task `"$TaskName`" has not yet run." -verbose } Start-Sleep -Seconds $Interval } Until ($exitwhencomplete-eq $true) } else { Write-Verbose "- Task `"$TaskName`" not found." -verbose $IsComplete = $True } } catch { if ($_.Exception -like "*No MSFT_ScheduledTask objects found*") { write-verbose "- A Scheduled Task with name `"$TaskName`" was not found." -verbose } Else { write-verbose "- $($_.Exception)" -verbose } $IsComplete = $True } return $IsComplete } Function StartProcess { param ( [string]$FilePath, [array]$Arguments ) write-verbose "Starting the process: `"$FilePath`"" -verbose write-verbose "with the arguments: $Arguments" -verbose $pinfo = New-Object System.Diagnostics.Process $pinfo.StartInfo.FileName = $FilePath # WindowStyle; 1 = hidden, 2 =maximized, 3=minimized, 4=normal $pinfo.StartInfo.WindowStyle = 1 $pinfo.StartInfo.Arguments = $Arguments $pinfo.StartInfo.RedirectStandardError = $True $pinfo.StartInfo.RedirectStandardOutput = $True $pinfo.StartInfo.UseShellExecute = $false Try { $null = $pinfo.start() | Out-Null $result = $pinfo.StandardOutput.ReadToEnd().Trim().Split("`r`n") $result += $pinfo.StandardError.ReadToEnd().Trim().Split("`r`n") } Catch { # } $pinfo.Dispose() return $result } Function IsAccessAllowed { param( [string]$Folder, [switch]$ConsoleOutput ) If (TEST-PATH $Folder) { If ($ConsoleOutput) { write-verbose "Testing access to: $Folder" -verbose } Get-ChildItem -path $Folder -EA SilentlyContinue -ErrorVariable ErrVar is # The -ErrorVariable common parameter creates an ArrayList. This variable # always initialized which means it will never be $null. The proper way # to test if an ArrayList is empty or not is to use the Count property. It # should be empty or equal to 0 if there are no errors. If ($ErrVar.count -eq 0) { If ($ConsoleOutput) { write-verbose "Access is good" -verbose } $return = $True } Else { If ($ConsoleOutput) { write-warning "Access denied" -verbose } $return = $False } } Else { If ($ConsoleOutput) { write-warning "The `"$Folder`" does not exist" -verbose } $return = $False } return $return } Function Get-NextAvailableDriveLetter { param( [parameter(Mandatory=$False)][Switch]$Descending ) # Get all available drive letters, and store in a temporary variable. $UsedDriveLetters = @(Get-Volume | % { "$([char]$_.DriveLetter)"}) + @(Get-WmiObject -Class Win32_MappedLogicalDisk| %{$([char]$_.DeviceID.Trim(':'))}) + @(Get-WmiObject -Class Win32_LogicalDisk| %{$([char]$_.DeviceID.Trim(':'))}) | Select-Object -Unique $TempDriveLetters = @(Compare-Object -DifferenceObject $UsedDriveLetters -ReferenceObject $( 67..90 | % { "$([char]$_)" } ) | ? { $_.SideIndicator -eq '<=' } | % { $_.InputObject }) If (!$Descending) { $AvailableDriveLetter = ($TempDriveLetters | Sort-Object) } Else { $AvailableDriveLetter = ($TempDriveLetters | Sort-Object -Descending) } Return $AvailableDriveLetter[0] } $FirstAvailableDriveLetter = Get-NextAvailableDriveLetter # Create an array of drive letters to reserve $ReservedDriveLetters = @() If ($FirstAvailableDriveLetter -ne $RAMDiskStartingFrom) { # Convert uppercase letters to ASCII codes $FirstAvailableDriveLetterAscii = [int][char]$FirstAvailableDriveLetter.ToUpper() $RAMDiskStartingFromAscii = [int][char]$RAMDiskStartingFrom.ToUpper() # Loop through ASCII values and convert back to letters For ($i = $FirstAvailableDriveLetterAscii; $i -le $RAMDiskStartingFromAscii -1; $i++) { $currentLetter = [char]$i $ReservedDriveLetters += $currentLetter } } #----------------------------------- [int]$TotalRAM = 0 $CanConnect = $false Try { $ComputerInformation = Get-WmiObject -Class Win32_ComputerSystem -ErrorAction Stop | Select-Object ` @{N="TotalPhysicalRam"; E={[math]::round(($_.TotalPhysicalMemory / 1GB),0)}} $CanConnect = $true } Catch { $ErrorDescription = "Error connecting using the Get-WmiObject cmdlet." write-warning "*ERROR*: $ErrorDescription" -verbose } if ($CanConnect) { [int]$TotalRam = $ComputerInformation.totalphysicalram Write-Verbose "Total RAM: $TotalRAM GB" -verbose } $DefaultSize = "4G" If ($TotalRam -ge 48 ) { $DefaultSize = "8G" } If ($TotalRam -ge 64 ) { $DefaultSize = "16G" } If ($TotalRam -ge 128 ) { $DefaultSize = "24G" } If ($TotalRam -ge 160 ) { $DefaultSize = "32G" } if ([String]::IsNullOrEmpty($Size)) { $Size = $DefaultSize } #----------------------------------- $IsDriverInstalled = $false $CanConnect = $false Try { $PnPSignedDriver = Get-WmiObject -Class Win32_PnPSignedDriver -ErrorAction Stop | Where {$_.Description -eq "Arsenal Image Mounter"} $CanConnect = $true } Catch { $ErrorDescription = "Error connecting using the Get-WmiObject cmdlet." write-warning "*ERROR*: $ErrorDescription" -verbose } if ($CanConnect -AND $null -ne $PnPSignedDriver) { $IsDriverInstalled = $true write-verbose "The Arsenal Image Mounter SCSI device is installed" -verbose write-verbose "- Device ID: $($PnPSignedDriver.DeviceID)" -verbose write-verbose "- Device Version: $($PnPSignedDriver.DriverVersion)" -verbose } #----------------------------------- If ($IsDriverInstalled -AND (Test-Path -Path "$Executable")) { # Get all RAM Disks $AllRamDisks = StartProcess -FilePath:"$Executable" -Arguments:"--list" If ($AllRamDisks -Like "*No virtual disks*") { write-verbose "No virtual disks mounted" -verbose } $DriveLetters = @() $DeviceNumbers = @() $DeviceCount = 0 ForEach ($Line in $AllRamDisks) { $DeviceNumber = "" If ($Line -like "*Device number*") { $DeviceCount ++ $DeviceNumber = ($Line.Trim() -Split("Device number"))[1].Trim() } if (!([String]::IsNullOrEmpty($DeviceNumber))) { $DeviceNumbers += $DeviceNumber #$DeviceNumber } $DeviceIs = "" If ($Line -like "*Device is*") { $DeviceIs = ($Line.Trim() -Split("Device is"))[1].Trim() } if (!([String]::IsNullOrEmpty($DeviceIs))) { #$DeviceIs } $ContainsVolume = "" If ($Line -like "*Contains volume*") { $ContainsVolume = ($Line.Trim() -Split("Contains volume"))[1].Trim() } if (!([String]::IsNullOrEmpty($ContainsVolume))) { #$ContainsVolume } $MountedAt = "" If ($Line -like "*Mounted at*") { $MountedAt = ($Line.Trim() -Split("Mounted at"))[1].Trim() } if (!([String]::IsNullOrEmpty($MountedAt))) { $DriveLetters += $MountedAt #$MountedAt } } If ($DeviceCount -gt 0) { If ($DeviceCount -eq 1) { write-verbose "There is $DeviceCount device mounted" -verbose } Else { write-verbose "There are $DeviceCount devices mounted" -verbose } If (($DriveLetters | Measure-Object).Count -gt 0) { write-verbose "The following RAM disks are present..." -verbose ForEach ($DriveLetter in $DriveLetters) { write-verbose "- $DriveLetter" -verbose } } } Else { write-verbose "There are no devices mounted" -verbose } #------------------------------------------------------------- If ($Action -eq "Remove") { ForEach ($DeviceNumber in $DeviceNumbers) { write-verbose "Removing device $DeviceNumber" -verbose $MyArgs = "--dismount=$DeviceNumber --force" $RemoveDevices = StartProcess -FilePath:"$Executable" -Arguments:$MyArgs If ($RemoveDevices -Like "*All devices dismounted*") { write-verbose "All devices have been deleted" -verbose } If ($RemoveDevices -Like "*No mounted devices*") { write-verbose "There are no devices to delete" -verbose } } } #------------------------------------------------------------- If ($Action -eq "Create") { # Waiting for the BISF Personalize scheduled task to complete. $IsTaskComplete = Wait-For-Scheduled-Task -TaskName:"LIC_BISF_Device_Personalize" write-verbose "Proceeding to create RAM disk..." -verbose $RemoveSubstDrives = @() $Reserved = $False ForEach ($ReservedDriveLetter in $ReservedDriveLetters) { If ((IsAccessAllowed -Folder "${ReservedDriveLetter}:\") -eq $False) { write-verbose "Reserving drive letter $ReservedDriveLetter" -verbose subst "${ReservedDriveLetter}:" "${env:TEMP}" $RemoveSubstDrives += $ReservedDriveLetter $Reserved = $True } } If ($Reserved) { Start-Sleep -Seconds 5 } write-verbose "Creating a new Ram disk of size $Size ..." -verbose $UnattendedArgsAdd = "--ramdisk --disksize=`"$Size`"" $CreateDevice = StartProcess -FilePath:"$Executable" -Arguments:$UnattendedArgsAdd $MountedAt = "" ForEach ($Line in $CreateDevice) { If ($Line -like "*Mounted at*") { $MountedAt = ($Line.Trim() -Split("Mounted at"))[1].Trim() } } if (!([String]::IsNullOrEmpty($MountedAt))) { write-verbose "Successfully created the $MountedAt drive as a Ram disk" -verbose write-verbose "Writing the `"${MountedAt}IMPORTANT Read Me.txt`" file" -verbose $ReadMe | Out-File -FilePath "${MountedAt}IMPORTANT Read Me.txt" -Encoding ASCII # Define the access rule to grant read and execute permissions to Users $userAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Users", "ReadAndExecute", "Allow" ) # Define the access rule to grant full control to SYSTEM and Administrators $systemAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "SYSTEM", "FullControl", "Allow" ) # Define the access rule to grant full control to SYSTEM and Administrators $adminAccessRule = New-Object System.Security.AccessControl.FileSystemAccessRule( "Administrators", "FullControl", "Allow" ) # Get the local Administrator account $adminAccount = [System.Security.Principal.NTAccount]::new("Administrators") # Get the current access control list (ACL) for the file $acl = Get-Acl -Path "${MountedAt}IMPORTANT Read Me.txt" # Break inheritance $acl.SetAccessRuleProtection($true, $false) # Remove any inherited access rules foreach ($accessRule in $acl.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])) { if ($accessRule.IsInherited) { $acl.RemoveAccessRule($accessRule) } } # Remove any existing access rules $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } # Add the new access rules to the ACL $acl.SetAccessRule($userAccessRule) $acl.SetAccessRule($systemAccessRule) $acl.SetAccessRule($adminAccessRule) # Set the owner of the file to the local Administrator account $acl.SetOwner($adminAccount) # Set the modified ACL back to the file Set-Acl -Path "${MountedAt}IMPORTANT Read Me.txt" -AclObject $acl } Else { write-warning "Failed to created a Ram disk!" -verbose } ForEach ($RemoveSubstDrive in $RemoveSubstDrives) { subst "${RemoveSubstDrive}:" /D } } #------------------------------------------------------------- } Else { If ($IsDriverInstalled -eq $False) { write-warning "The Arsenal Image Mounter SCSI device is not installed" -verbose } If (-not(Test-Path -Path "$Executable")) { write-warning "The `"$Executable`" tool cannot be found" -verbose } } #------------------------------------------------------------- $EndDTM = (Get-Date) Write-Verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalSeconds) Seconds" -Verbose Write-Verbose "Elapsed Time: $(($EndDTM-$StartDTM).TotalMinutes) Minutes" -Verbose try { Stop-Transcript } catch { Write-Verbose "This host does not support transcription" }