VirtualMemory: $pageFileName in Set-Resource block ends up null via Join-Path when setting new page file
Details of the scenario you tried and the problem that is occurring
Hello, please forgive any missteps of mine in common practices around contributing to open source projects. I read through the contributing guidelines, but this is still my first time.
I'm trying to put together a short DSC config to be able to change the drive letter of the temporary storage drive attached to Azure VMs when they're first provisioned. The drive is set as the D: drive letter and has the page file assigned to it by default. So in order to change it, I must clear the page file, change the drive letter, and re-assign the page file. I'm performing this via the Azure DSC VM extension.
During testing, I encountered an error stating the parameter PageFileName could not be processed as it was a null or empty string.
After some further testing with editing my local copy of the VirtualMemory resource code, I found that it is the Join-Path cmdlet that populates the $pageFileName variable in the Set-TargetResource block which is returning a null value for some reason. When I tested a replacement line:
$pageFileName = $driveInfo.Name + 'pagefile.sys'
The resource functioned as expected.
Verbose logs showing the problem
Suggested solution to the issue
The replacement line I mentioned does function, but I do understand the value in using Join-Path. I'm at a loss as to why it would be returning a null value the way it's written. When I tested Join-Path outside of DSC, it functioned as expected using the driveInfo.Name value.
The DSC configuration that is used to reproduce the issue (as detailed as possible)
Configuration AzureDiskPagingConfig
{
Import-DscResource -ModuleName ComputerManagementDsc
Import-DscResource -ModuleName StorageDsc
Import-DscResource -ModuleName PSDscResources
Node $AllNodes.NodeName
{
LocalConfigurationManager
{
RebootNodeIfNeeded = $true
ActionAfterReboot = 'ContinueConfiguration'
}
WaitForDisk Disk0
{
DiskId = 0
RetryIntervalSec = 10
RetryCount = 10
}
Disk OSVolume
{
DiskId = 0
DriveLetter = 'C'
PartitionStyle = 'MBR'
AllowDestructive = $true
DependsOn = '[WaitForDisk]Disk0'
}
WaitForDisk Disk1
{
DiskId = 1
RetryIntervalSec = 10
RetryCount = 10
}
VirtualMemory ClearPagingSettings
{
Type = 'NoPagingFile'
Drive = $Node.OldDriveLetter
DependsOn = '[WaitForDisk]Disk1'
}
Script TempVolume
{
GetScript = {
@{
GetScript = $GetScript
SetScript = $SetScript
TestScript = $TestScript
Result = (Get-Partition -DiskNumber 1).DriveLetter
}
}
SetScript = {
$partition = Get-WmiObject -Class Win32_Volume -Filter "Label = 'Temporary Storage'"
Set-WmiInstance -InputObject $partition -Arguments @{DriveLetter = $using:Node.NewDriveLetter; Label = 'Temporary Storage'}
$partition = Get-WmiObject -Class Win32_Volume -Filter "Label = 'Temporary Storage'"
Write-Verbose "Set Temporary Storage drive to $($partition.DriveLetter)"
}
TestScript = {
$partition = Get-WmiObject -Class Win32_Volume -Filter "Label = 'Temporary Storage'"
if (!$partition) { Return $true }
$testPath = "$($partition.DriveLetter)\DATALOSS_WARNING_README.txt"
if (!(Test-Path -Path $testPath)) { Return $true }
Return $false
}
}
WaitForDisk Disk1PostChange
{
DiskId = 1
RetryIntervalSec = 10
RetryCount = 10
DependsOn = '[Script]TempVolume'
}
VirtualMemory PagingSettings
{
Type = 'SystemManagedSize'
Drive = $Node.NewDriveLetter
DependsOn = '[WaitForDisk]Disk1PostChange'
}
}
}
@{
AllNodes = @(
@{
NodeName = "*"
OldDriveLetter = "D:"
NewDriveLetter = "T:"
},
@{
NodeName = 'localhost'
}
)
}
The operating system the target node is running
OsName : Microsoft Windows Server 2016 Datacenter OsOperatingSystemSKU : DatacenterServerEdition OsArchitecture : 64-bit WindowsBuildLabEx : 14393.3085.amd64fre.rs1_release.190703-1816 OsLanguage : en-US OsMuiLanguages : {en-US}
Version and build of PowerShell the target node is running
PSVersion 5.1.14393.3053 PSEdition Desktop PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...} BuildVersion 10.0.14393.3053 CLRVersion 4.0.30319.42000 WSManStackVersion 3.0 PSRemotingProtocolVersion 2.3 SerializationVersion 1.1.0.1
Version of the DSC module that was used ('dev' if using current dev branch)
6.4.0.0 - recently installed via install-module
Hi @Get-Ryan - thank you for raising this! That is really odd that the Join-Path isn't working where the string concatenation is. Is the $driveInfo object null as this would explain the behavior: You can concatenate a string to a null, but Join-Path doesn't allow that.
No, the $driveInfo object populates normally, as that was my initial thought as well. And that would have continued to be an issue even with string concatenation, as I have found the New-PageFile line is critical to the overall success. Without it, the VM enters a strange reboot-loop at the end.
This is why I'm at a loss. All the elements of the Join-Path call seem proper. I even validated the $driveInfo.Name is of type String, so there isn't even the possibility of an object type issue.
That is really strange. We could make a change to use the concatenation method for the path, but I'm always a bit uncomfortable working around a problem like this- e.g. we risk introducing other bugs/issues.
What is also odd, is that I'd expect this line to fail if $driveInfo was $null as well: https://github.com/PowerShell/ComputerManagementDsc/blob/dev/DSCResources/MSFT_VirtualMemory/MSFT_VirtualMemory.psm1#L151
with a You cannot call a method on a null-valued expression. exception.
Can you do a Get-Member on the $driveInfo and confirm the type and that the name property exists?
When I was initially testing, I was inserting numerous verbose messages in the code to test various variables and their properties. The $driveInfo does populate with the correct type and the name property contains the proper drive letter. If it didn't, my proposed concatenation line would also not have worked. Again, hence why I was confused. There is no obvious reason I can see why the Join-Path should return null because all of its required parameters are proper.
The only thing I haven't done yet that I've thought to try is to fully test Join-Path's functionality in a DSC config, i.e. verifying it performs as expected through various types of input styles. That's the only possibility I could think of (albeit being a remote one)
That does sound like a pretty remote possibility. Still scratching my head over this one.
What I'd normally suggest is we create a failing unit test for this scenario/condition, and then modify the code so the test passes. However, it sounds like we might not be able to create a failing unit test for this. In your experience with this issue, were you able to replicate the problem outside of DSC? If so then we've probably got a good shot at creating a failing unit test.
No, the Join-Path worked normally outside of DSC using the same inputs via the [System.IO.DriveInfo] object. This is why I was scratching my head as well. My only remaining thoughts in terms of testing is to first test Join-Path functionality within a simple custom DSC module and local config to verify it works in general in a DSC environment (I don't see why it wouldn't, but I haven't personally tested it), and if that succeeds, then see if the same test fails while using it via the DSC extension on an Azure VM. Might be something specific with that technology. I just haven't found the time as yet.