WSLBlobNFS.psm1

# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------

<#
.SYNOPSIS
    WSLBlobNFS module provides a way to mount Azure Blob NFS share in Windows via WSL.
 
.DESCRIPTION
    The native NFS client on Windows is not performant. This module provides a way to mount the Blob NFS share on WSL Linux distro and access the share from Windows.
    This module helps with the following:
    1. Install WSL and WSL distro if they are not installed already.
    2. Initialize WSL environment for Blob NFS usage.
    3. Mount the Blob NFS share in WSL.
    4. Dismount the Blob NFS share in WSL.
 
.LINK
    https://github.com/Azure/BlobNFS-wsl2
 
.NOTES
    Author: Azure Blob NFS
    Website: https://github.com/Azure/BlobNFS-wsl2/
#>



# To-do:
# - Clear error message and actions to fix those errors.
# - Add support for cleanup and finding mountmappings
# - Add tests for the module using Pester
# - Sign the module
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_output_streams?view=powershell-5.1
# - Add Uninitialize module support
# - Add retry logic for each of the commands
# - Detect processor architecture and disallow ARM processors.

# Throw an error if any cmdlet, function, or command fails or a variable is unknown and stop the script execution.
Set-PSDebug -Strict
Set-StrictMode -Version Latest

# WSL distro name and user name
$distroName = "Ubuntu-22.04"

# Most of the commands require admin privileges. Hence, we need to run the script as admin.
$userName = "root"

# Username and password for SMB share.
$smbUserName = "root"

$moduleName = "WSLBlobNFS"

$modulePathForWin = $PSScriptRoot
$modulePathForLinux = ("/mnt/" + ($modulePathForWin.Replace("\", "/").Replace(":", ""))).ToLower()

$wslScriptName = "wsl2_linux_script.sh"
$queryScriptName = "query_quota.sh"

#
# Internal functions
#
function Enable-Verbosity
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][int]$verbosity
    )

    [Environment]::SetEnvironmentVariable("VERBOSE_MODE", $verbosity)
    $wslenvrionment = [Environment]::GetEnvironmentVariable("WSLENV")
    $wslenvrionment += ":VERBOSE_MODE"
    [Environment]::SetEnvironmentVariable("WSLENV",$wslenvrionment)
}

function Get-ModuleVersion
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param()

    # In local dev env, the module is not installed from gallery. Hence, Get-InstalledModule will fail.
    # Hence, suppress the error and return 0.0.0 as the module version.
    $blobNFSModule = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue
    if($null -eq $blobNFSModule)
    {
        return "0.0.0"
    }

    return $blobNFSModule.Version.ToString()
}

function Write-Success
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost',
        '',
        Justification='Need foreground color change for success message')]
    param(
        [Parameter(Mandatory = $true)][string]$message
    )

    Write-Host $message -ForegroundColor DarkGreen
}

function Write-ErrorLog
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSAvoidUsingWriteHost',
        '',
        Justification='Need foreground color change for error message')]
    param(
        [Parameter(Mandatory = $true)][string]$message
    )

    Write-Error -Message $message -ErrorAction SilentlyContinue
    $Host.UI.WriteErrorLine($message)
}

function Invoke-WSL
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$wslcommand
    )

    Write-Verbose "Executing $wslcommand"
    wsl -d $distroName -u $userName -e bash -c $wslcommand
}

function Format-WSLFile
{
    [CmdletBinding()]
    param()

    # Note: Quote the path with '' to preserve space
    # Files saved from windows will have \r\n line endings. Hence, we need to remove \r.
    Invoke-WSL "sed -i -e 's/\r$//' '$modulePathForLinux/$wslScriptName'"
    Invoke-WSL "chmod +x '$modulePathForLinux/$wslScriptName'"

    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Failed to update $wslScriptName in WSL."
        $global:LastExitCode = 1
        return
    }

    # Files saved from windows will have \r\n line endings. Hence, we need to remove \r.
    Invoke-WSL "sed -i -e 's/\r$//' '$modulePathForLinux/$queryScriptName'"
    Invoke-WSL "chmod +x '$modulePathForLinux/$queryScriptName'"

    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Failed to update $modulePathForLinux in WSL."
        $global:LastExitCode = 1
        return
    }
}

function Install-WSL2
{
    [CmdletBinding()]
    param()

    # WSL is supported only on 64 bit PS.
    Write-Verbose "Checking if Powershell is 32 bit or 64 bit."
    $is64bit = [Environment]::Is64BitProcess

    if($is64bit -eq $false)
    {
        Write-ErrorLog "WSL2 installation is not supported on 32 bit PS. Please use 64 bit Powershell."
        $global:LastExitCode = 1
        return
    }

    # PSEdition check. Only Desktop edition is supported.
    Write-Verbose "Checking if Powershell is Desktop edition or not."
    if($PSEdition -ne "Desktop")
    {
        Write-ErrorLog "This module installation is not supported on Powershell Core. Please use Powershell Desktop edition."
        $global:LastExitCode = 1
        return
    }

    # Avoid collecting computer info path if everything is already installed and return from here.
    Write-Verbose "Checking WSL installation status."

    # Check the WSL version
    $wslVersionOp = wsl -v 2>&1
    $wslstatus = $LastExitCode
    Write-Verbose "WSL Version: `n$wslVersionOp"
    Write-Verbose "WSL Version status code: `n$wslstatus"

    # WSL2 is already present.
    if(($wslstatus -eq 0))
    {
        Write-Verbose "WSL2 is already installed. Checking for updates."
        $wslupdate = wsl --update 2>&1

        if($LastExitCode -ne 0)
        {
            # Since WSL2 is already installed, we can continue the script execution. Hence, the exit code is 0.
            Write-Warning "WSL2 update failed: $wslupdate."
            Write-Warning "Continuing with the currently installed WSL2 version."
        }
        else
        {
            Write-Verbose "WSL2 is is upto date!"
        }

        $global:LastExitCode = 0
        return
    }

    Write-Output "WSL2 is not installed. Trying to install WSL2..."

    # Check if the device support virtualization or not.
    Write-Verbose "Checking if the device supports virtualization."
    $compInfo = Get-ComputerInfo

    $windowsVersion = $compInfo.WindowsVersion
    Write-Verbose "Windows version: $windowsVersion"

    $winEdition = $compInfo.OsName
    Write-Verbose "Windows edition: $winEdition"

    # Check the os version number.
    # WSL2 commands are supported only on Windows 10/11 version than 2004.
    # and on Windows Server 2022 on version higher than 2009.
    # To-do: Add support for Server 2019
    # https://learn.microsoft.com/en-us/windows/wsl/install#prerequisites
    $isWSLsuppported = ($winEdition -notmatch "Server" -and $windowsVersion -ge 2004) -or ($winEdition -match "Server" -and $windowsVersion -ge 2009)

    if($isWSLsuppported -eq $false)
    {
        Write-ErrorLog "Your Windows version - $winEdition ($windowsVersion) - does not support WSL2 commands used by this module. Please check Prerequisites section of the module for help."
        $global:LastExitCode = 1
        return
    }

    # Only certain Azure VMs support nested virtualization required for WSL2.
    # Query Azure Instance Metadata Service to check if the VM is an Azure VM or not.
    # https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service
    $azureVmInfo = $(Invoke-RestMethod -Headers @{"Metadata"="true"} -Method GET -Proxy $Null -Uri "http://169.254.169.254/metadata/instance?api-version=2021-01-01" -TimeoutSec 3) 2>&1

    if ($azureVmInfo -match "compute")
    {
        Write-Output "This is an Azure VM. Checking virtualization status..."

        $vmSecurityProfile = [bool]::Parse($azureVmInfo.compute.securityProfile.secureBootEnabled) -or [bool]::Parse($azureVmInfo.compute.securityProfile.virtualTpmEnabled)
        Write-Output "Device protection status: $vmSecurityProfile"

        $vmSize = $azureVmInfo.compute.vmSize
        Write-Output "Azure VM SKU: $vmSize"

        if(($vmSize -match "v[1-4]$") -and ($vmSecurityProfile -eq $true))
        {
            Write-ErrorLog "Sorry, this module will not work on this VM. :( `nCurrently only v5 and above Azure VMs with Trusted Launch support nested virtualization needed for WSL2."
            $global:LastExitCode = 1
            return
        }
    }
    else
    {
        Write-Output "This is not an Azure VM. Skipping virtualization check."
    }

    # WSL version check.
    $wslNotInstalled = ($wslstatus -eq 1)
    $wsl1Installed = ($wslstatus -eq -1)
    $wsl2Installed = ($wslstatus -eq 0)

    # No presence of WSL
    if($wslNotInstalled)
    {
        Write-Output "WSL2 is not installed. Installing WSL2..."

        wsl --install --no-distribution

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "WSL2 installation failed. Try again."
            $global:LastExitCode = 1
            return
        }

        Write-Success "Successfully installed WSL2!"

        # Restart computer to complete WSL installation.
        Write-Success "Restart the machine to complete WSL2 setup."
        Restart-Computer -Confirm

        Write-ErrorLog "Setup not completed. Restart the machine to complete WSL2 setup."

        # Set the exit code to 1 to indicate that the script execution is not completed.
        $global:LastExitCode = 1
        return
    }

    # When WSL1 is present.
    elseif($wsl1Installed)
    {
        Write-Output "WSL1 is installed. Updating WSL1 to WSL2..."
        wsl --update
        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "WSL2 update failed. Try again."
            $global:LastExitCode = 1
            return
        }

        # Wait for 8 secs for the upgrade to finish.
        Write-Output "Waiting 8 secs for the WSL upgrade to finish.."
        Start-Sleep -s 8

        $wslstatus1 = wsl -v 2>&1

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "WSL2 update failed with error: $wslstatus1. Try again."
            $global:LastExitCode = 1
            return
        }

        Write-Success "Successfully updated WSL to WSL2!"

        # Restart computer to complete WSL installation.
        Write-Success "Restart the machine to complete WSL2 setup."
        Restart-Computer -Confirm

        Write-ErrorLog "Setup not completed. Restart the machine to complete WSL2 setup."

        # Set the exit code to 1 to indicate that the script execution is not completed.
        $global:LastExitCode = 1
        return
    }

    # WSL2 installed.
    elseif($wsl2Installed)
    {
        Write-Verbose "WSL2 is already installed. Checking for updates."
        $wslupdate = wsl --update 2>&1
        Write-Verbose "WSL2 update status: $wslupdate"

        if($LastExitCode -ne 0)
        {
            # Since WSL2 is already installed, we can continue the script execution. Hence, the exit code is 0.
            Write-Warning "WSL2 update failed: $wslupdate"
            Write-Warning "Continuing with the currently installed WSL2 version."
        }
        else
        {
            Write-Verbose "WSL2 is already installed and updated!"
        }
    }

    # Print a warning message and proceed. The other part of the script will the distro installation and version support.
    else
    {
        Write-Warning "WSL2 installation status is unknown. `nWSL Version: $wslVersionOp `nWSL Version status code: $wslstatus"
        Write-Warning "Continuing with the currently installed WSL version."
    }

    $global:LastExitCode = 0
    return
}

function Install-WSLBlobNFS-Internal
{
    [CmdletBinding()]
    param()

    Install-WSL2
    if($LastExitCode -ne 0)
    {
        return
    }

    # Check if the distro is installed or not.
    # If WSL2 installed but the distro is not installed, then wsl -l -v will return -1.
    $wslDistros = wsl -l -v

    if( -not (($LastExitCode -eq 0) -or ($LastExitCode -eq -1)))
    {
        Write-ErrorLog "WSL distro check failed."
        $global:LastExitCode = 1
        return
    }

    $wslDistros = $wslDistros -replace '\0', ''
    $distroStatus = $wslDistros -match $distroName
    Write-Verbose "WSL distro status: $distroStatus"

    if(-not $distroStatus)
    {
        Write-Output "Installing WSL distro $distroName..."
        $wslListOnline = wsl -l -o 2>&1

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "Unable to fetch the list of WSL distros: $wslListOnline"
            $global:LastExitCode = 1
            return
        }

        Write-Output "WSL distro $distroName is not installed but MUST be installed to proceed with the Blob NFS setup. This is only one time installation."
        Write-Output "Installing WSL distro $distroName..."
        Write-Warning "!!!!! After the distro is installed, setup a new user and exit the distro (using 'exit') to continue the setup !!!!!" -WarningAction Inquire

        wsl --install -d $distroName

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "WSL distro $distroName installation failed. Try again."
            $global:LastExitCode = 1
            return
        }

        # Check if the distro is installed successfully or not.
        wsl -d $distroName -u $userName -e bash -c "whoami" | Out-Null

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "WSL distro $distroName installation failed. Check Prerequisites section of the module for help."
            $global:LastExitCode = 1
            return
        }

        Write-Success "Installed WSL distro $distroName."

        # Since the distro can now be used, we can continue the script execution. Hence, the exit code is 0.
        $global:LastExitCode = 0
        return
    }
    else
    {
        Write-Verbose "WSL distro $distroName is already installed. This distro will be used for Blob NFS usage."
    }
}

function Initialize-WSLBlobNFS-Internal
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]$Force=$false
    )
    Write-Verbose "Checking if WSL environment is initialized for WSLBlobNFS usage..."
    # Check if WSL is installed or not.
    Install-WSLBlobNFS-Internal
    if($LastExitCode -ne 0)
    {
        # Set the exit code to 1 to indicate that the script execution is not completed.
        $global:LastExitCode = 1
        return
    }

    $initialized = $false
    if($Force)
    {
        Write-Warning "Force initializing WSL environment for WSLBlobNFS usage. WSL $distroName will be shutdown and existing mounts may be lost."
    }
    else
    {
        Write-Verbose "Initializing WSL environment for your WSLBlobNFS usage."
    }

    Format-WSLFile

    # Install systemd
    Invoke-WSL "systemctl list-unit-files --type=service | grep -q ^systemd-"
    if($LastExitCode -eq 0 -and !$Force)
    {
        Write-Verbose "Systemd is already installed. Skipping systemd installation."
    }
    else
    {
        $initialized = $true
        if($Force)
        {
            Write-Output "Force parameter is provided. Installing systemd again..."
        }
        else
        {
            Write-Output "Systemd is not installed. Installing systemd..."
        }

        Invoke-WSL "'$modulePathForLinux/$wslScriptName' installsystemd"
        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "Installing systemd failed."

            $global:LastExitCode = 1
            return
        }

        # Shutdown WSL and it will restart with systemd on next WSL command execution
        # Confirm from user if we can shudown WSL
        if(!$Force)
        {
            $confirmation = Read-Host -Prompt "WSL $distroName has to be shutdown to install and run systemd. Press y/Y to shutdown WSL or press any key to abort. Default is 'y'."
            if(-not ($confirmation -eq "y" -or $confirmation -eq "Y" -or $confirmation -eq ""))
            {
                Write-ErrorLog "Setup not completed. Allow WSL shutdown to continue the setup."

                $global:LastExitCode = 1
                return
            }
        }

        Write-Verbose "Shutting down WSL $distroName..."

        # Note: Since we shutdown WSL, we need to run dbus-launch again, otherwise WSL will shutdown after 8 secs of inactivity.
        wsl -d $distroName --shutdown

        Write-Verbose "Starting WSL $distroName."
        Invoke-WSL "ls > /dev/null 2>&1"
        Write-Verbose "Started WSL $distroName."

        # Check if systemd is properly installed or not.
        Invoke-WSL "systemctl list-unit-files --type=service | grep -q ^systemd-"

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "Systemd installation failed."

            $global:LastExitCode = 1
            return
        }

        Write-Success "Installed systemd sucessfully!"
    }

    # WSL shutsdown after 8 secs of inactivity. Hence, we need to run dbus-launch to keep it running.
    # Check the issue here:
    # https://github.com/microsoft/WSL/issues/10138
    wsl -d $distroName --exec dbus-launch true

    # Install NFS, AZNFS, & Samba
    Invoke-WSL "dpkg -s nfs-common samba aznfs > /dev/null 2>&1"
    if($LastExitCode -eq 0 -and !$Force)
    {
        Write-Verbose "NFS, AZNFS, & Samba are already installed. Skipping their installation."
    }
    else
    {
        $initialized = $true
        if($Force)
        {
            Write-Output "Force parameter is provided. Installing NFS, AZNFS, & Samba again..."
        }
        else
        {
            Write-Output "NFS, AZNFS, & Samba are not installed. Installing NFS, AZNFS, & Samba..."
        }
        Invoke-WSL "'$modulePathForLinux/$wslScriptName' installnfssmb $smbUserName"
        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "Installing NFS, AZNFS, & Samba failed."

            $global:LastExitCode = 1
            return
        }
        Write-Success "Installed NFS, AZNFS, & Samba successfully!"
    }

    if($initialized)
    {
        Write-Verbose "WSL environment for WSLBlobNFS usage is initialized."
    }
    else
    {
        Write-Verbose "WSL environment for WSLBlobNFS usage is already initialized. Skipping initialization."
    }
}

function Dismount-MountInsideWSL
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string]$ShareName
    )

    # Don't unmount the share if SMB share is mounted in windows.
    $smbmapping = Get-SmbMapping -RemotePath "$ShareName" -ErrorAction SilentlyContinue
    if($null -ne $smbmapping)
    {
        Write-ErrorLog "SMB share is mounted in windows on $($smbmapping.LocalPath). Use Dismount-WSLBlobNFS $($smbmapping.LocalPath) to unmount the share."
        return
    }

    Write-Verbose "Unmounting $ShareName."

    Invoke-WSL "'$modulePathForLinux/$wslScriptName' unmountshare '$ShareName'"
    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Unmounting $ShareName failed."
        return
    }

    Write-Success "Unmounting $ShareName done."
}

function Assert-PipelineWSLBlobNFS-Internal
{
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][CimInstance]$smbmapping
    )

    Write-Output "Checking WSLBlobNFS pipeline status..."

    $mountDrive = $smbmapping.LocalPath
    # Sample remote path: \\172.17.47.111\mntnfsv3share
    $remotePathTokens = $smbmapping.RemotePath.Split("\")
    $smbexportname = $remotePathTokens[-1].Trim(" ")
    $smbremotehost = $remotePathTokens[2].Trim(" ")

    # Remote host should be the current WSL's ip address
    $ipaddress = Invoke-WSL "hostname -I"
    $ipaddress = $ipaddress.Trim()
    if($smbremotehost -ne $ipaddress)
    {
        Write-Warning "The $mountDrive is not mounted from the current WSL $distroName."
    }
    else
    {
        Write-Output "Checking the mount status of $mountDrive in WSL $distroName..."
        Invoke-WSL "'$modulePathForLinux/$wslScriptName' checkmount '$smbexportname'"

        if($LastExitCode -ne 0)
        {
            Write-ErrorLog "Unable to mount $mountDrive in WSL $distroName."
        }
        else
        {
            Write-Output "Removing $mountDrive from Windows."
            # Remove the SMB mapping and add again to avoid having to authenticate again in the explorer.
            net use $mountDrive /delete /yes | Out-Null
            $mnt = Get-SmbMapping -LocalPath $mountDrive -ErrorAction SilentlyContinue

            if(($LastExitCode -ne 0) -or ($null -ne $mnt))
            {
                Write-ErrorLog "Unable to mount $mountDrive in Windows."
                return
            }

            Write-Output "Mounting $mountDrive in Windows."
            net use $mountDrive "\\$ipaddress\$smbexportname" /persistent:yes /user:$smbUserName $smbUserName | Out-Null
            $mnt = Get-SmbMapping -LocalPath $mountDrive -ErrorAction SilentlyContinue

            if(($LastExitCode -ne 0) -or ($null -eq $mnt))
            {
                Write-ErrorLog "Unable to mount $mountDrive in Windows."
                return
            }

            Get-SmbMapping -LocalPath $mountDrive

            Write-Success "Mounting SMB share done. Now, you can access the share from $mountDrive."
        }
    }
}

#
# Public functions
#
function Install-WSLBlobNFS
{
    <#
    .SYNOPSIS
        Install WSL and WSL distro for WSLBlobNFS usage.
 
    .DESCRIPTION
        This command installs WSL and WSL distro if they are not installed already.
        Note: You may need to restart the machine to complete WSL installation if WSL is not installed already.
 
    .EXAMPLE
        PS> Install-WSLBlobNFS
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding()]
    param()

    # Set the verbosity preference for WSL based on the current preferences.
    $verbosity = 0
    if($VerbosePreference -eq "Continue")
    {
        $verbosity = 1
    }
    else
    {
        $verbosity = 0
    }
    Enable-Verbosity -verbosity $verbosity


    Install-WSLBlobNFS-Internal

    if($LastExitCode -eq 0)
    {
        Write-Success "WSL2 is already installed. Run Initialize-WSLBlobNFS to setup WSL environment for WSLBlobNFS usage."
    }
}

function Initialize-WSLBlobNFS
{
    <#
    .SYNOPSIS
        Setup WSL environment with systemd, NFS, and Samba for WSL Blob NFS usage.
 
    .DESCRIPTION
        systemd is required to run NFS server in WSL. Then, NFS and Samba are installed to mount the Blob NFS share in WSL and access the share from Windows via SMB respectively.
 
    .EXAMPLE
        PS> Initialize-WSLBlobNFS
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding()]
    param(
        # Force initialization of WSL environment. This will unmount all the existing NFS mounts.
        # This is useful when you want to re-initialize the WSL environment.
        [Parameter(Mandatory = $false)][switch]$Force
    )

    # Set the verbosity preference for WSL based on the current preferences.
    $verbosity = 0
    if($VerbosePreference -eq "Continue")
    {
        $verbosity = 1
    }
    else
    {
        $verbosity = 0
    }
    Enable-Verbosity -verbosity $verbosity

    # To-do: Show progress of the script execution.

    Initialize-WSLBlobNFS-Internal -Force:$Force
    if($LastExitCode -ne 0)
    {
        return
    }

    Write-Success "WSL environment for WSLBlobNFS usage is initialized. Now, you can mount the SMB share using Mount-WSLBlobNFS."
}

function Mount-WSLBlobNFS
{
    <#
    .SYNOPSIS
        Mount Blob NFS share in Windows via WSL.
 
    .DESCRIPTION
        This command mounts the provided Blob NFS share in WSL, exports the share via SMB, and mounts the SMB share in Windows.
 
    .EXAMPLE
        PS> Mount-WSLBlobNFS -RemoteMount "account.blob.core.windows.net:account/container"
        You can just provide the NFSv3 share address as below.
        Note: MountDrive parameter is optional. If not provided, the drive will be automatically assigned.
 
    .EXAMPLE
        PS> Mount-WSLBlobNFS -RemoteMount "mount -t aznfs -o nolock,vers=3,proto=tcp account.blob.preprod.core.windows.net:/account/container /mnt/nfsv3share"
        You can also provide the NFS mount command if you want to provide extra mount parameters.
        Note: MountDrive parameter is optional. If not provided, the drive will be automatically assigned.
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding(PositionalBinding=$true)]
    param(
        # RemoteMount is your Blob NFS share address or the NFS mount command.
        # If the RemoteMount is the NFS share address, then the share will be mounted with default mount parameters.
        # If the RemoteMount is the NFS mount command, then the share will be mounted with the provided mount parameters.
        # Check the examples for more details.
        [Parameter(Mandatory = $true, Position=0)][string]$RemoteMount,

        # MountDrive is the drive letter to mount the SMB share in Windows.
        [Parameter(Position=1)][string]$MountDrive
    )

    # Set the verbosity preference for WSL based on the current preferences.
    $verbosity = 0
    if($VerbosePreference -eq "Continue")
    {
        $verbosity = 1
    }
    else
    {
        $verbosity = 0
    }
    Enable-Verbosity -verbosity $verbosity

    Initialize-WSLBlobNFS-Internal
    if($LastExitCode -ne 0)
    {
        return
    }

    # Create a new MountDrive if not provided
    if([string]::IsNullOrWhiteSpace($MountDrive))
    {
        Write-Output "MountDrive parameter is not provided. Finding a free drive letter to mount the share."
        # Get the first free drive letter, starting from Z: upto A:
        # 65 is the ASCII value of 'A' and 90 is the ASCII value of 'Z'
        (90..(65)).ForEach({
            if((-not (Get-PSDrive ([char]$_) -ErrorAction SilentlyContinue) -and [string]::IsNullOrWhiteSpace($MountDrive)))
            {
                $MountDrive = [char]$_ + ":"
                Write-Success "Using $MountDrive to mount the share."
            }})

        if([string]::IsNullOrWhiteSpace($MountDrive))
        {
            Write-ErrorLog "No free drive letter found to mount the share."
            return
        }
    }

    # Check if the MountDrive is already in use or not.
    $pathExists = Test-Path "$MountDrive"
    if($pathExists)
    {
        Write-ErrorLog "$MountDrive is in use already."
        return
    }

    # To-do:
    # - Validate the RemoteMount and MountDrive even further.
    # - Check if the RemoteMount is a valid mount command or not.
    # - Check if the RemoteMount is already mounted or not.
    # - Check the exit code of net use command.

    $mountParameterType = ""

    $mountPattern = "mount -t"
    if($RemoteMount.Contains($mountPattern) -and $RemoteMount.IndexOf($mountPattern) -eq 0)
    {
        $mountParameterType = "command"
        if($RemoteMount.Contains("mount -t nfs"))
        {
            Write-ErrorLog "NFS mounts '-t nfs' are not supported. Please use AZNFS mounts with '-t aznfs'."
            return
        }
    }
    else
    {
        $mountParameterType = "remotehost"
    }

    # Temp file to store the share name in WSL
    # To-do:
    # - Can we use env variables to store the share name instead of temp file?
    # - Blocker: Windows spans a new shell and the env variables are forked and changes made in WSL are not available in windows.
    # - Check how to not span a new shell for wsl command execution.
    # - https://stackoverflow.com/questions/66150671/wsl-running-linux-commands-with-wsl-exec-cmd-or-wsl-cmd
    $winTempFilePath = New-TemporaryFile
    $wslTempFilePath = ("/mnt/" + ($winTempFilePath.FullName.Replace("\", "/").Replace(":", ""))).ToLower()

    Write-Verbose "Mounting $RemoteMount."
    Invoke-WSL "'$modulePathForLinux/$wslScriptName' mountshare '$mountParameterType' '$RemoteMount' '$wslTempFilePath'"

    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Mounting $RemoteMount failed in WSL."
        return
    }

    # Get the remote host ip address and the share name
    $ipaddress = Invoke-WSL "hostname -I"
    $ipaddress = $ipaddress.Trim()

    # Read the share name from the temp file
    $smbShareName = Get-Content $winTempFilePath -ErrorAction SilentlyContinue
    Remove-Item $winTempFilePath -ErrorAction SilentlyContinue

    Write-Output "Mounting SMB share \\$ipaddress\$smbShareName onto drive $MountDrive"

    $password = $smbUserName
    net use $MountDrive "\\$ipaddress\$smbShareName" /persistent:yes /user:$smbUserName $password | Out-Null

    # Rollback the changes in WSL if net use fails.
    if($LastExitCode -ne 0)
    {
        Dismount-MountInsideWSL $smbShareName
        Write-ErrorLog "Mounting '$RemoteMount' failed."
        return
    }

    # Print the mount mappings
    Get-SmbMapping -LocalPath "$MountDrive"

    Write-Success "Mounting SMB share done. Now, you can access the share from $MountDrive."
}

function Register-AutoMountWSLBlobNFS
{
    <#
    .SYNOPSIS
        Resiter a scheduled job to auto mount WSL Blob NFS on startup.
 
    .DESCRIPTION
        This command requires admin privileges to register a scheduled job to auto mount WSL Blob NFS on startup.
 
    .EXAMPLE
        PS> Register-AutoMountWSLBlobNFS
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding()]
    param()

    # Requires admin privileges.
    if(-not ([bool](([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([System.Security.Principal.WindowsBuiltInRole] "Administrator"))))
    {
        Write-ErrorLog "This command requires admin privileges. Run the command again with admin privileges."
        $global:LastExitCode = 1
        return
    }

    Write-Output "Initializing WSL environment for WSLBlobNFS usage on startup."
    Import-Module PSScheduledJob -Force -Verbose:$false | Out-Null

    # Remove the existing scheduled job if it exists.
    Write-Output "Removing existing scheduled job to auto mount WSL Blob NFS on startup."
    Unregister-ScheduledJob -Name "AutoMountWSLBlobNFS" -ErrorAction SilentlyContinue

    Register-ScheduledJob -Name AutoMountWSLBlobNFS -ScriptBlock {
                    Import-Module WSLBlobNFS -Force
                    Assert-PipelineWSLBlobNFS -Verbose

                    if($LastExitCode -ne 0)
                    {
                        Write-ErrorLog 'Error while mounting Blob NFS share on startup.'
                        return
                    }
                } -Trigger (New-JobTrigger -AtStartup) -ScheduledJobOption (New-ScheduledJobOption -ContinueIfGoingOnBattery -StartIfOnBattery)

    $automnt = Get-ScheduledJob -Name AutoMountWSLBlobNFS -ErrorAction SilentlyContinue
    if($null -eq $automnt)
    {
        Write-ErrorLog "Unable to register a scheduled job to auto mount WSL Blob NFS on startup. Try again with admin privileges."
        return
    }

    Write-Success "Successfully registered a scheduled job to auto mount WSL Blob NFS on startup."
}

function Assert-PipelineWSLBlobNFS
{
    <#
    .SYNOPSIS
        Checks and sets the WSL Blob NFS environment in Windows on startup.
 
    .DESCRIPTION
        This command initializes WSL on startup and mounts the Blob NFS share for the persistent SMB mappings present in Windows.
 
    .EXAMPLE
        PS> Assert-PipelineWSLBlobNFS
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding()]
    param()

    # Set the verbosity preference for WSL based on the current preferences.
    $verbosity = 0
    if($VerbosePreference -eq "Continue")
    {
        $verbosity = 1
    }
    else
    {
        $verbosity = 0
    }
    Enable-Verbosity -verbosity $verbosity

    Initialize-WSLBlobNFS-Internal
    if($LastExitCode -ne 0)
    {
        return
    }

    $smbmappings = Get-SmbMapping

    if($null -eq $smbmappings)
    {
        Write-Success "No WSL Blob NFS share is mounted."
        return
    }

    if($smbmappings.GetType().Name -eq "CimInstance")
    {
        Assert-PipelineWSLBlobNFS-Internal -smbmapping $smbmappings
    }
    else
    {
        $smbmappings.ForEach({
            $smbmapping = $_
            Assert-PipelineWSLBlobNFS-Internal -smbmapping $smbmapping
        })
    }
}

function Dismount-WSLBlobNFS
{
    <#
    .SYNOPSIS
        Dismount your Blob NFS share in Windows.
 
    .DESCRIPTION
        You can only dismount the Blob NFS share that is mounted using Mount-WSLBlobNFS.
 
    .EXAMPLE
        PS> DisMount-WSLBlobNFS -MountDrive "Z:"
 
    .LINK
        https://github.com/Azure/BlobNFS-wsl2
 
    .NOTES
        Author: Azure Blob NFS
        Website: https://github.com/Azure/BlobNFS-wsl2
    #>


    [CmdletBinding()]
    param(
        # Mountdrive that has previously mounted Blob NFS share.
        [Parameter(Mandatory = $true)][string]$MountDrive
    )

    # Set the verbosity preference for WSL based on the current preferences.
    $verbosity = 0
    if($VerbosePreference -eq "Continue")
    {
        $verbosity = 1
    }
    else
    {
        $verbosity = 0
    }
    Enable-Verbosity -verbosity $verbosity

    Initialize-WSLBlobNFS-Internal
    if($LastExitCode -ne 0)
    {
        return
    }

    if([string]::IsNullOrWhiteSpace($MountDrive))
    {
        Write-ErrorLog "Empty or Null mounted drive received."
        return
    }

    $smbmapping = Get-SmbMapping -LocalPath "$MountDrive" -ErrorAction SilentlyContinue

    if($null -eq $smbmapping)
    {
        Write-ErrorLog "No SMB share is mounted on $MountDrive."
        return
    }

    # Sample remote path: \\172.17.47.111\mntnfsv3share
    $remotePathTokens = $smbmapping.RemotePath.Split("\")
    $smbexportname = $remotePathTokens[-1].Trim(" ")
    $smbremotehost = $remotePathTokens[2].Trim(" ")

    # Remote host should be the current WSL's ip address
    $ipaddress = Invoke-WSL "hostname -I"
    $ipaddress = $ipaddress.Trim()
    if($smbremotehost -ne $ipaddress)
    {
        Write-ErrorLog "The $MountDrive is not mounted from the current WSL $distroName."
        return
    }

    Invoke-WSL "'$modulePathForLinux/$wslScriptName' unmountshare '$smbexportname'"

    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Unmounting $MountDrive failed in WSL."
        return
    }

    # Force delete when we have open files in the share.
    net use $MountDrive /delete /yes | Out-Null
    if($LastExitCode -ne 0)
    {
        Write-ErrorLog "Unmounting $MountDrive failed in Windows."
        return
    }
    Write-Success "Unmounting $MountDrive done."
}

# This list overrides the list provided in the module manifest (.psd1) file.
Export-ModuleMember -Function Install-WSLBlobNFS, Initialize-WSLBlobNFS, Mount-WSLBlobNFS, Dismount-WSLBlobNFS, Assert-PipelineWSLBlobNFS, Register-AutoMountWSLBlobNFS