Migrating on-premise file servers with network drives managed by GPO to Azure Files and make this transparent to the end-user can be a challenge.
The process can be almost transparent to the end-user, we can:
- make an Intune script that can deploy a Scheduled Task and mount the drives at user logon, or
- deploy a Company Portal app where users can select which drives they want to mount (assuming permissions are granted)
ACME references to a fictional company for demo purpose.
1. Intune Platform Script that deploys a Scheduled Task and the PowerShell script
Script variables per your enviroment:
C:\Program Files\Intune scripts - location to keep logs and scripts, feel free to adapt to your enviroment or needs
ACME-Azure-file-mount - name of the Scheduled Task, update to your needs
azure-mnt.ps1 - filename of the PowerShell script that will deal with the actual mounting
azure_mnt_log.txt - filename of the log file
The script will run as SYSTEM from Intune to create the scheduled task and create the PowerShell script. The Scheduled Task will run the script at the user level. Logs will be saved in the user profile.
We will hardcode the shares in the script using the below format:
$shares = @{
"Y" = @{ # drive letter
"storageEndpoint" = "yourstorageaccount.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "share-01" # share name, ex: yourshare or yourshare\folder
}
"X" = @{ # drive letter
"storageEndpoint" = "yourstorageaccount.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "share-01\child folder 01" # share name, ex: yourshare or yourshare\folder
}
}
Let's check for the script and logs location:
if (-not(Test-Path -Path 'C:\Program Files\Intune scripts'))
{
New-Item -Path "C:\Program Files\" -Name "Intune scripts" -ItemType "directory"
}
Logg-ing the current time:
Set-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value (Get-Date)
Enumerating the configured shares and mount / unmount the drives based on the permissions check:
foreach ($share in $shares.GetEnumerator())
{
$storageEndpoint = $share.value.storageEndpoint
$shareName = $share.value.shareName
$driveLetter = $share.key
net use ($driveLetter + ":") /delete
$fullURL = Join-Path $storageEndpoint -ChildPath $shareName
if (Test-Path -Path "\\$fullURL")
{
try
{
$result = New-PSDrive -Name "$driveLetter" -PSProvider "FileSystem" -Root "\\$fullURL" -Persist -Scope Global -ErrorAction SilentlyContinue
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName connected ok"
}
catch
{
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName drive failed"
}
}
else
{
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName access check failed"
}
}
If access to the path fails we remove the drive to avoid having Explorer hanging in a loop trying to access the resource.
We need to save this as the script that will be run by the Scheduled task so we have the below section:
$scriptCode = '
$shares = @{
"Y" = @{ # drive letter
"storageEndpoint" = "azuremntintunescript.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "it-share" # share name, ex: yourshare or yourshare\folder
}
"X" = @{ # drive letter
"storageEndpoint" = "azuremntintunescript.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "it-share\child folder 01" # share name, ex: yourshare or yourshare\folder
}
"W" = @{ # drive letter
"storageEndpoint" = "azuremntintunescript.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "it-share\child folder 02" # share name, ex: yourshare or yourshare\folder
}
"V" = @{ # drive letter
"storageEndpoint" = "azuremntintunescript.file.core.windows.net" # endpoint, ex: yourstorageaccount.file.core.windows.net
"shareName" = "hr-share" # share name, ex: yourshare or yourshare\folder
}
}
# Mount the drive
Set-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value (Get-Date)
foreach ($share in $shares.GetEnumerator())
{
$storageEndpoint = $share.value.storageEndpoint
$shareName = $share.value.shareName
$driveLetter = $share.key
net use ($driveLetter + ":") /delete
$fullURL = Join-Path $storageEndpoint -ChildPath $shareName
if (Test-Path -Path "\\$fullURL")
{
try
{
$result = New-PSDrive -Name "$driveLetter" -PSProvider "FileSystem" -Root "\\$fullURL" -Persist -Scope Global -ErrorAction SilentlyContinue
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName connected ok"
}
catch
{
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName drive failed"
}
}
else
{
Add-Content -Path "$ENV:USERPROFILE\AZURE_MNT_log.txt" -Value "$driveLetter - $shareName access check failed"
}
}
'
$scriptCode | Out-File "C:\Program Files\Intune scripts\azure-mnt.ps1"
Comments about Test-NetConnection and net use:
- Test-NetConnection can block the script and generate a large amount of waiting time in cases with multiple shares
- Remove-SMBMapping and Remove-PSDrive are not dealing in a proper way with the removal of access. Remove-SMBMapping expects a reload of explorer.exe while Remove-PSDrive just disconnects the drive but leaves it in File Explorer.
Section for the Scheduled Task creation:
try
{
if(-not(Get-ScheduledTask | Where-Object {$_.TaskName -like "ACME-Azure-file-mount" }))
{
$taskXml = '<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>2024-06-06T13:07:08.5598173</Date>
<Author>ACME</Author>
<URI>\ACME-Azure-file-mount</URI>
</RegistrationInfo>
<Triggers>
<LogonTrigger>
<Enabled>true</Enabled>
</LogonTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<GroupId>S-1-5-32-545</GroupId>
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>false</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-NonInteractive -WindowStyle Hidden -NoProfile -NoLogo -ExecutionPolicy Bypass -File "C:\Program Files\Intune scripts\azure-mnt.ps1"</Arguments>
</Exec>
</Actions>
</Task>'
if (Register-ScheduledTask -Xml $taskXml -TaskName "ACME-Azure-file-mount")
{
Set-Content -Path "C:\Program Files\Intune scripts\azure_mnt_log.txt" -Value "ACME-Azure-file-mount task registered"
}
else
{
Set-Content -Path "C:\Program Files\Intune scripts\azure_mnt_log.txt" -Value "ACME-Azure-file-mount scheduled task registration failed"
}
}
}
catch
{
Set-Content -Path "C:\Program Files\Intune scripts\azure_mnt_log.txt" -Value $_.Exception.Message
}
Full script available on GitHub AzureFilesMappingIntune
Deploying as Intune Platform Script:
As soon as the script is sent to the devices we can see the results:
And the log in the user profile:
Logs if we remove access to a share, let's try to remove hr-share:
The drive is removed and the log shows that access check failed.
2. deploy the script as an app in Company Portal
This solution can also be deployed as an Intune Company portal app with minimum changes.
Do not deploy the script using a mix of both methods as this will bring up a race condition between the Intune script and the Company Portal app.
The script from the previous step will become the install.ps1 script. We will also need a detection script and an uninstall script.
The detection script will be in this case a detection of the scheduled task creation:
$taskName = "ACME-Azure-file-mount"
$taskExists = Get-ScheduledTask -TaskName $taskName
if ($null -ne $taskExists)
{
Write-Host "Azure files installed."
exit 0
}
else
{
Write-Host "Azure files installation failed."
exit 1
}
* you need to use the same task name as in the install.ps1 script. If the task was created we exit with a code of 0 or code 1 in case of an issue.
The uninstall script will unregister the scheduled task:
$taskName = "ACME-Azure-file-mount"
$taskResult = Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
$taskExists = Get-ScheduledTask -TaskName $taskName
if ($null -ne $taskExists)
{
Write-Host "Azure files uninstalled."
exit 0
}
else
{
Write-Host "Azure files uninstallation failed."
exit 1
}
With this three files we can go ahead and pack them in the intunewin file for the Intune app.
With the intunewin file we add a new app in Intune portal:
For the install command:
%SystemRoot%\SysNative\WindowsPowershell\v1.0\PowerShell.exe -noprofile -ExecutionPolicy ByPass -File .\install.ps1
Uninstall command:
%SystemRoot%\SysNative\WindowsPowershell\v1.0\PowerShell.exe -noprofile -ExecutionPolicy ByPass -File .\uninstall.ps1
Assign based on your tenant and needs.
This should serve as a boilerplate and it was designed to allow multiple deployments per office, location, department.
Project available on GitHub.
Featured image created with Grok.