Google Cloud Associate Cloud Engineer – Qwiklabs Courses Questions & Answers
Unfortunately, citrix has not provided detailed step by step procedure to install VDA on Linux distributions. There are so many articles published to install VDA on windows machines whereas not for Linux distributions.
In one of my recent project, I spent lot of time to install VDA on Red Hat Enterprise Linux due to lack of detailed documentation hence I am publishing below step by step procedure to install VDA on Red Hat Enterprise Linux.
This articles covers below components installation & configuration
• RHEL 8.2 Linux Workstation VM Installation on Nutanix AHV
• XenDesktop VDA 2203 Agent Installation & configuration
• NVIDIA Graphics Drivers Installation
• NVDIA vGPU licensing
The Linux Virtual Delivery Agent (VDA) enables access to the Linux virtual apps and desktops anywhere from any device where Citrix Workspace app is installed. Install the VDA software on your Linux virtual machines, configure the Delivery Controller, and then use Citrix Studio to make the apps and desktops available to users.
Supported Linux distributions
System requirements | Linux Virtual Delivery Agent 2203 LTSR (citrix.com)
RHEL 8.2 Linux Workstation VM Installation
Mount the RHEL 8.2 ISO and boot the VM with ISO

Select “Install Red Hat Enterprise Linux 8.2”


In above options, modify each installation setting based on requirement

As my requirement is Linux “Workstation”, I chosen “Base Environment” , choose options based on your requirement

For VDA, choosing GNOME Applications & GRAPHICS Administration tools is sufficient. Choose based on your requirement

Select Automatic unless you have a specific requirements of partitions

Enable Network to get IP from DHCP and Provide Hostname correctly at this page
Post Selection of all options, you will see below output




Reboot VM post installation
Note:
My Citrix VDI setup do not have proxy connectivity hence to install packages and dependencies, I need to mount YUM repository from locally mounted ISO which avoids internet connectivity for any RPM installations. Ignore YUM repository steps if VM able to connect internet
Steps to mount YUM repository locally
Mount the RHEL installation ISO to a directory like /mnt/disc, e.g.:
mkdir -p /mnt/disc
mount /dev/sr0 /mnt/disc

Create below directories to copy repositories
mkdir /home/BaseOS
mkdir /home/AppStream

Copy the media.repo file from the root of the mounted directory
cp -ivr /mnt/disc/BaseOS/* /home/BaseOS/


cp -ivr /mnt/disc/AppStream/* /home/AppStream/


Unmount /mnt/disc -> do after copying files

Delete files if any found under directory /etc/yum.repos.d/ and create the file vi /etc/yum.repos.d/local.repo
Note: you should have only local.repo file under /etc/yum.repos.d/
Copy and Paste the below text in local.repo
[BaseOS]
name = BaseOS
enabled = 1
gpgcheck = 0
baseurl = file:///home/BaseOS/
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
[AppStream]
name = AppStream
enabled = 1
gpgcheck = 0
baseurl = file:///home/AppStream/
gpgkey = file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release

Save the changes by typing wq!
Clear the cache and check whether you can get the packages list from the DVD repo with below commands
yum clean all
yum repolist enabled
You should get below output

To disable Red Hat Subscription Management warning , edit below file and change enabled value to 0
vi /etc/yum/pluginconf.d/subscription-manager.conf

Install & Configure Linux Virtual Delivery Agent
Prerequisite for VDA: Dotnet runtime is required for VDA installation
Upload aspnetcore-runtime file to /root directory and execute below command to extract dotnet runtime
cd /root
mkdir dotnet
chmod 777 dotnet ( Note : command to change directory permissions)


Extract runtime to /root/dotnet with below comand
tar -xf aspnetcore-runtime-6.0.3-linux-x64.tar.gz -C /root/dotnet
Download “XenDesktopVDA-22.03.0.18-1.el8_x.x86_64” and upload XenDesktop VDA file to /tmp directory
Note: The version used is Linux Virtual Delivery Agent 2203 an DDC version is 2112

Run below command to install VDA
sudo yum -y localinstall XenDesktopVDA-22.03.0.18-1.el8_x.x86_64.rpm


Post installation of VDA, configure the VDA with below command
/opt/Citrix/VDA/sbin/ctxinstall.sh


Input below values during configuration
• Host name of the machine on which the Linux VDA is to be installed
• IP address of DNA
• IP address of NTP Server
• Domain name – The NetBIOS name of the domain
• Realm name – The Kerberos realm name (Note: Name should be in CAPITAL LETTERS)
• Fully Qualified Domain Name (FQDN) of the domain controller
While configuring VDA, it prompts for dotnet core runtime path, set the dotnet Core Runtime path as “/root/dotnet”


Note
• As my requirement is for graphics (GPU), here I selected “Y” HDX 3D pro. Select “N” if VDI do not require Graphics
• Linux multisession does not support “HDX 3D PRO” select “NO” if requirement is for multi session and no graphics card required.
• If HDX3D pro enabled then mandatorily NIVIDIA graphics drivers are to be installed else, while launching VDA grey screen appears because Linux by default search for graphics drivers

Cross verify the below Inputs before proceed further



Post VDA Installation and configuration. Need to check the below 2 services status
systemctl status ctxvda
systemctl status ctxhdx

NVIDIA Graphics Drivers installation & vGPU license assignment
Follow below steps, if VDI has requirement to enable Graphics else ignore the steps
Prerequisite for NVIDIA Guest drivers installation: Assign GPU or vGPU to VM
Upload the NVIDIA drivers (highlighted) to /root folder

Note: Upload 2 NVIDIA guest drivers(highlighted above) into /root folder
Install the below dependencies one by one before install of NVIDIA drivers
yum install elfutils-libelf-devel libglvnd-devel
yum install gcc
yum install gcc kernel-devel
yum install gcc kernel-headers


Once successfully installed. Install NVIDIA Package with below command
bash ./NVIDIA-Linux-x86_64-450.102.04-grid.run





Note: Without NVIDIA license server assignment, VDI functionality will be minimum hence map NVIDIA license server for complete functionality.
vGPU License Server Assignment on Linux
run below command to check license status
nvidia-smi –q
Ouptut before License server assignment

To provide NVIDIA License Server on Linux workstation, edit the file gridd.conf with below steps
cd /etc/nvidia/

Create new file “gridd.conf ” by using “gridd.conf.template” file
cp gridd.conf.template gridd.conf

Edit the /etc/nvidia/gridd.conf by vi editor with below highlighted values
ServerAddress=IP Address
ServerPort=7070
FeatureType=0
EnableUI=TRUE ( uncomment by removing #)

Stop & start nvidia service for license reflection
sudo nvidia-gridd stop
sudo nvidia-gridd start

Run command nvidia-smi –q to check license status and verify logs /var/logs/messages
If license not applied then restart VM.
Post License server assignment, you should get below output

Note: My use case is to create Machine Catalog with Unmanaged hence I created catalog and assigned VM’s directly to catalog. If your use case is to create Master Image and rollout VDI with MCS, follow below Citrix KB.
KB References
Quick installation by using easy install
XD Collect Traces
Streaming Linux target devices
Inspired from https://james-rankin.com/articles/spreading-users-over-multiple-file-shares-with-fslogix-profile-containers/

James Rankin blog script created based on free space criteria but our requirement is based on user count. Below is the snippet for your reference
<# This script created for spreading users over multiple file shares with FSLogix Profile Containers
With Ref https://james-rankin.com/articles/spreading-users-over-multiple-file-shares-with-fslogix-profile-containers/
Script customized as per Citrix VDI requirement, Azure Subscription will be connected with User Assigned Managed Idenity(UAMI) and UAMI is assigned to Citrix Master Image.
Script is added in Task Scheduler of Master Image and Machine catalog is created with "Machine Profile" option so that all cloned VDI's get UAMI property #>
# This script count the directories in each share and FSLOgix VHD location is added based on count retrieved. We took directories count as users count can not be measured.
# This Script also created to address the Azure File Handle limit which is 10k per share
# This script fetches directories count from MULTIPLE AZURE FILE STORAGES AND MULTIPLE SHARES
########### Set TLS 1.2 ###########
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
########### Connect Using User Assigned Managed Identity (UAMI) ###########
Connect-AzAccount -Identity -AccountId <UAMI Client ID>
########### Provide Profile Storage Account Details ###########
# Provide "Profile Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$profilestorageaccount1 = ""
$profilestorageaccount1RG = ""
$profilestorageaccount1shares = @(
"profile1",
"profile2"
)
# Provide "Profile Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$profilestorageaccount1sharesfullpath = @(
"\\<storageaccountname>.file.core.windows.net\profile1",
"\\<storageaccountname>.file.core.windows.net\profile2"
)
# Provide "Profile Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$profilestorageaccount2 = ""
$profilestorageaccount2RG = ""
$profilestorageaccount2shares = @(
"profile3",
"profile4"
)
# Provide "Profile Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$profilestorageaccount2sharesfullpath = @(
"\\<storageaccountname>.file.core.windows.net\profile3",
"\\<storageaccountname>c.file.core.windows.net\profile4"
)
# Provide "Profile Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$profilestorageaccount3 = ""
$profilestorageaccount3RG = ""
$profilestorageaccount3shares = @(
)
# Provide "Profile Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$profilestorageaccount3sharesfullpath = @(
)
# Provide "Profile Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$profilestorageaccount4 = ""
$profilestorageaccount4RG = ""
$profilestorageaccount4shares = @(
)
# Provide "Profile Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$profilestorageaccount4sharesfullpath = @(
)
# Provide "Profile Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$profilestorageaccount5 = ""
$profilestorageaccount5RG = ""
$profilestorageaccount5shares = @(
)
# Provide "Profile Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$profilestorageaccount5sharesfullpath = @(
)
########### Provide Office Storage Account Details ###########
# Provide "Office Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$officestorageaccount1 = ""
$officestorageaccount1RG = ""
$officestorageaccount1shares = @(
"office1",
"office2"
)
# Provide "Office Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$officestorageaccount1sharesfullpath = @(
"\\<storageaccountname>.file.core.windows.net\office1",
"\\<storageaccountname>.file.core.windows.net\office2"
)
# Provide "Office Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$officestorageaccount2 = ""
$officestorageaccount2RG = ""
$officestorageaccount2shares = @(
"office3",
"office4"
)
# Provide "Office Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$officestorageaccount2sharesfullpath = @(
"\\<storageaccountname>.file.core.windows.net\office3",
"\\<storageaccountname>.file.core.windows.net\office4"
)
# Provide "Office Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$officestorageaccount3 = ""
$officestorageaccount3RG = ""
$officestorageaccount3shares = @(
)
# Provide "Office Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$officestorageaccount3sharesfullpath = @(
)
# Provide "Office Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$officestorageaccount4 = ""
$officestorageaccount4RG = ""
$officestorageaccount4shares = @(
)
# Provide "Office Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$officestorageaccount4sharesfullpath = @(
)
# Provide "Office Storage Account Name and Storage Resource Group Name" else LEAVE BLANK values
$officestorageaccount5 = ""
$officestorageaccount5RG = ""
$officestorageaccount5shares = @(
)
# Provide "Office Storageaccount Shares FULL PATH" in above SHARES ORDER ONLY
$officestorageaccount5sharesfullpath = @(
)
########### Fetch the Directories in the Profile Shares ###########
$ProfileDirShareCount = @()
$profileorderedShares = @()
if($profilestorageaccount1shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $profilestorageaccount1RG -AccountName $profilestorageaccount1
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $profilestorageaccount1 -StorageAccountKey $value
foreach($sharename in $profilestorageaccount1shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $profilestorageaccount1sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$ProfileDirShareCount += $shareSpace
$i = $i + 1
}
}
if($profilestorageaccount2shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $profilestorageaccount2RG -AccountName $profilestorageaccount2
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $profilestorageaccount2 -StorageAccountKey $value
foreach($sharename in $profilestorageaccount2shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $profilestorageaccount2sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$ProfileDirShareCount += $shareSpace
$i = $i + 1
}
}
if($profilestorageaccount3shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $profilestorageaccount3RG -AccountName $profilestorageaccount3
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $profilestorageaccount3 -StorageAccountKey $value
foreach($sharename in $profilestorageaccount3shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $profilestorageaccount3sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$ProfileDirShareCount += $shareSpace
$i = $i + 1
}
}
if($profilestorageaccount4shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $profilestorageaccount4RG -AccountName $profilestorageaccount4
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $profilestorageaccount4 -StorageAccountKey $value
foreach($sharename in $profilestorageaccount4shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $profilestorageaccount4sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$ProfileDirShareCount += $shareSpace
$i = $i + 1
}
}
if($profilestorageaccount5shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $profilestorageaccount5RG -AccountName $profilestorageaccount5
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $profilestorageaccount5 -StorageAccountKey $value
foreach($sharename in $profilestorageaccount5shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $profilestorageaccount5sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$ProfileDirShareCount += $shareSpace
$i = $i + 1
}
}
$SortedProfileShares = $ProfileDirShareCount | Sort-Object freespace | select share
foreach ($item in $SortedProfileShares) {
$profileorderedShares += $item.Share.ToString()
}
########### Fetch the Directories in the Office Shares ###########
$OfficeDirShareCount = @()
$officeorderedShares = @()
if($officestorageaccount1shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $officestorageaccount1RG -AccountName $officestorageaccount1
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $officestorageaccount1 -StorageAccountKey $value
foreach($sharename in $officestorageaccount1shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $officestorageaccount1sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$officeDirShareCount += $shareSpace
$i = $i + 1
}
}
if($officestorageaccount2shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $officestorageaccount2RG -AccountName $officestorageaccount2
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $officestorageaccount2 -StorageAccountKey $value
foreach($sharename in $officestorageaccount2shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $officestorageaccount2sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$officeDirShareCount += $shareSpace
$i = $i + 1
}
}
if($officestorageaccount3shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $officestorageaccount3RG -AccountName $officestorageaccount3
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $officestorageaccount3 -StorageAccountKey $value
foreach($sharename in $officestorageaccount3shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $officestorageaccount3sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$officeDirShareCount += $shareSpace
$i = $i + 1
}
}
if($officestorageaccount4shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $officestorageaccount4RG -AccountName $officestorageaccount4
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $officestorageaccount4 -StorageAccountKey $value
foreach($sharename in $officestorageaccount4shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $officestorageaccount4sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$officeDirShareCount += $shareSpace
$i = $i + 1
}
}
if($officestorageaccount5shares.Count -gt 0)
{
$i = 0
$keys = Get-AzStorageAccountKey -ResourceGroupName $officestorageaccount5RG -AccountName $officestorageaccount5
$value = $keys[0].Value
$context = New-AzStorageContext -StorageAccountName $officestorageaccount5 -StorageAccountKey $value
foreach($sharename in $officestorageaccount5shares)
{
$directories = Get-AzStorageFile -ShareName $sharename -Context $context
$dircount = $directories.count
$shareSpace = New-Object -TypeName psobject
$path = $officestorageaccount5sharesfullpath[$i]
$sharespace | Add-Member -membertype NoteProperty -Name Share -value $path
$sharespace | Add-Member -membertype NoteProperty -Name freespace -value $dircount
$officeDirShareCount += $shareSpace
$i = $i + 1
}
}
$SortedOfficeShares = $OfficeDirShareCount | Sort-Object freespace | select share
foreach ($item in $SortedOfficeShares) {
$officeorderedShares += $item.Share.ToString()
}
########### Remove the Existing VHDLocations Available ###########
$FSLogixProfilePath="HKLM:\software\FSLogix\Profiles"
$FSLogixODFCPath="HKLM:\SOFTWARE\Policies\FSLogix\ODFC"
$FSLogixKeyName="VHDLocations"
if ((get-item -path $FSLogixProfilePath).GetValue($FSLogixKeyName) -ne $null) {
Remove-itemProperty -path $FSLogixProfilePath -Name $FSLogixKeyName -force
} else {
# do nothing, no key to delete
}
if ((get-item -path $FSLogixODFCPath).GetValue($FSLogixKeyName) -ne $null) {
Remove-itemProperty -path $FSLogixODFCPath -Name $FSLogixKeyName -force
} else {
# do nothing, no key to delete
}
########### Adding New VHDLocation for Profile ###########
### Reason for changing VHDLocation property type to Multi string value is because that FSLogix need to searches the user profile in all shares hence to keep all shares , regkey should be Mutistring
New-ItemProperty $FSLogixProfilePath -Name $FSLogixKeyName -Value $profileorderedShares -PropertyType MultiString -Force
########### Adding New VHDLocation for Office ###########
New-ItemProperty $FSLogixODFCPath -Name $FSLogixKeyName -Value $officeorderedShares -PropertyType MultiString -Force
########### Write OutPut ###########
$folderPath="C:\Temp"
$ProfileDirShareCount | Export-csv -Path "$folderPath\Profile_Shareleastlog_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation
$OfficeDirShareCount | Export-csv -Path "$folderPath\Office_Shareleastlog_$((Get-Date).ToString('MM-dd-yyyy_hh-mm-ss')).csv" -NoTypeInformation
Below script is used to delete user profiles based on user logon names
# Delete the Files in Azure file share by taking input value GPN(logon) which is provided in CSV
### Az.Accounts, Az.Resources & Az.Storage modules are required to execute the script.
# To Set TLS1.2
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$path="C:\Temp" ### Path for input & output files
$NTRUsers=Import-Csv $path\NTRUsersGPN.csv # Input File with UserLogonNames
write-host "NTRUsersGPN Count = $($NTRUsers.Count)"
## Connect Azure Subscription with your own method
# Connect-AzAccount -AccessToken
#FileShare Details from which the user profiles needs to removed
$StorageAccountName="<Name>"
$fileShareName="<Share Name" # Only one fileshare at a time, to avoid the deletion of profiles in different shares.
$ResourceGroup = (Get-AzResource -Name $StorageAccountName).ResourceGroupName
$Key = Get-AzStorageAccountKey -ResourceGroupName $ResourceGroup -Name $StorageAccountName
$context=New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $Key[1].Value
$finalResult=@()
$FilesDirectory=Get-AzStorageFile -Context $context -ShareName $fileShareName
write-host "FilesDirectory Count = $($FilesDirectory.Count)"
$i=1
foreach($FilesD in $FilesDirectory)
{
foreach($NTRUser in $NTRUsers)
{
if($FilesD.Name -cmatch $NTRUser.UserGPN)
{
try
{
$Files=$FilesD|Get-AzStorageFile
foreach($File in $Files)
{
Remove-AzStorageFile -Context $context -ShareName $fileShareName -Path $File.ShareFileClient.Path
}
Remove-AzStorageDirectory -Context $context -ShareName $fileShareName -Path $FilesD.Name
$obj=""|select User,Directory,Status
$obj.User=$NTRUser.UserGPN
$obj.Directory=$FileD.Name
$obj.Status="Success"
$finalResult+=$obj
}
catch
{
$obj=""|select User,Directory,Status
$obj.User=$NTRUser.UserGPN
$obj.Directory=$FileD.Name
$obj.Status=$_.Exception.Message
$finalResult+=$obj
}
}
}
$i=$i+1
}
$finalResult | Export-Csv -Path "$path\$($fileShareName)FileSharesDeleteOutput.csv" -Append -Force -NoTypeInformation
Below script used to assign NTFS permissions( List,Read,Traverse, Create/Append) for FSlogix user profiles, it provides users not to access other folders
#Below script used to assign NTFS permissions( List,Read,Traverse, Create/Append) for FSlogix user profiles, it provides users not to access other folders
# User Groups/Users are given in input file , Keep Heading(top) GROUPS and provide groupnames under that
# Input your Storage account name and Domain Name in below script
# Whoever runs the below code, he should have full admin privilege
$permission = ":(X,RD,AD,RC)"
$Lists = Import-csv -Path "C:\temp\devgroups.csv" #group Accounts
$shares = "<Share Name 1>,<Share Name 2>"
$sharelist = $shares.Split(",")
foreach($share in $sharelist)
{
$share
$shrpath=\\<storageAccountName>.file.core.windows.net\$share
foreach($list in $lists)
{
$UserName = $list.groups
Invoke-Expression -Command ('icacls $shrpath /grant "<Domain Name>\${UserName}${permission}"')
}
}
We had a requirement to migrated 8000+ Citrix User UPM profiles across globe to FSLogix Profiles (.VHDX) as customer wanted to move from Citrix to AVD. This was a very challenging requirement due to new experience
#Below Script used to convert UPM Profile Path to FSlogix profile path by retaining NTFS Permissions
# Fslogix Root profile path
$newprofilepath = "\\<StorageAccountName>.file.core.windows.net\<ShareName>"
# UPM Root profile path
$oldprofilepath = "<Profile Share Path>" # Provide UPM Profile Path
$newusers = @()
ForEach ($user in(gc "C:\temp\users.txt")) {
$path = $oldprofilepath + $user
if(Test-Path -Path $path){
$newusers += $user
}
else{
Write-Output "$user does not exist"
}
}
ForEach ($user in $newusers) {
# Subfolder 1 - First Path to UPM_Profile Folder in UPM Profiles
$subfolder1 = ""
# Subfolder 2 - First Path to UPM_Profile Folder in UPM Profiles
#$subfolder2 =
$sam = "$user"
Start-Sleep -s 10
######################################################
$iniownr = (Get-Acl (Get-ChildItem "C:\temp\$sam" -force -Recurse ).FullName).Owner
takeown /f $sam /r /d y
Start-Sleep -s 10
icacls "C:\Temp\$sam" /grant zs_wvd_prod:f /q /c /t
#####################################################
Start-Sleep -s 10
#########################################################################################
# Do not edit here
#########################################################################################
$old = $oldprofilepath + $sam + $subfolder1;
$sid = (New-Object System.Security.Principal.NTAccount($sam)).translate([System.Security.Principal.SecurityIdentifier]).Value
$regtext = "Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$sid]
`"ProfileImagePath`"=`"C:\\Users\\$sam`"
`"FSL_OriginalProfileImagePath`"=`"C:\\Users\\$sam`"
`"Flags`"=dword:00000000
`"State`"=dword:00000000
`"ProfileLoadTimeLow`"=dword:00000000
`"ProfileLoadTimeHigh`"=dword:00000000
`"RefCount`"=dword:00000000
`"RunLogonScriptSync`"=dword:00000000
"
$nfolder = join-path $newprofilepath ($sid+"_"+$sam)
if (!(test-path $nfolder)) {New-Item -Path $nfolder -ItemType directory | Out-Null}
#& icacls $nfolder /setowner "$env:userdomain\$sam" /T /C
#& icacls $nfolder /grant $env:userdomain\$sam`:`(OI`)`(CI`)F /T
& icacls $nfolder /grant $sam`:`(OI`)`(CI`)F /T
$vhd = Join-Path $nfolder ("Profile_"+$sam+".vhdx")
$script1 = "create vdisk file=`"$vhd`" maximum 30720 type=expandable"
$script2 = "sel vdisk file=`"$vhd`"`r`nattach vdisk"
$script3 = "sel vdisk file=`"$vhd`"`r`ncreate part prim`r`nselect part 1`r`nformat fs=ntfs quick"
$script4 = "sel vdisk file=`"$vhd`"`r`nsel part 1`r`nassign letter=T"
$script5 = "sel vdisk file`"$vhd`"`r`ndetach vdisk"
$script6 = "sel vdisk file=`"$vhd`"`r`nattach vdisk readonly`"`r`ncompact vdisk"
if (!(test-path $vhd)) {
$script1 | diskpart
$script2 | diskpart
Start-Sleep -s 5
$script3 | diskpart
$script4 | diskpart
& label T: Profile-$sam
New-Item -Path T:\Profile -ItemType directory | Out-Null
start-process icacls "T:\Profile /setowner SYSTEM"
Start-Process icacls -ArgumentList "T:\Profile /inheritance:r"
$cmd1 = "T:\Profile /grant $env:userdomain\$sam`:`(OI`)`(CI`)F"
Start-Process icacls -ArgumentList "T:\Profile /grant SYSTEM`:`(OI`)`(CI`)F"
Start-Process icacls -ArgumentList "T:\Profile /grant Administrators`:`(OI`)`(CI`)F"
Start-Process icacls -ArgumentList $cmd1
} else {
$script2 | diskpart
Start-Sleep -s 5
$script4 | diskpart
}
"Copying $old to $vhd"
& robocopy $old T:\Profile /MIR /r:0 | Out-Null
if (!(Test-Path "T:\Profile\AppData\Local\FSLogix")) {
New-Item -Path "T:\Profile\AppData\Local\FSLogix" -ItemType directory | Out-Null
}
if (!(Test-Path "T:\Profile\AppData\Local\FSLogix\ProfileData.reg")) {$regtext | Out-File "T:\Profile\AppData\Local\FSLogix\ProfileData.reg" -Encoding ascii}
$script5 | diskpart
############################################################
Start-Sleep -s 10
icacls "C:\Temp\$sam" /grant $iniownr:f /q /c /t
}
Post Virtual Machine Deletion in Azure, few resources like Disk,NICs,NSG,Public IP etc.. will remain exists so use below script to cleanup all these orphaned resources
## Gather orphaned resources details using below link and use below script to delete all orphaned resources
https://github.com/scautomation/AzureResourceGraph-Examples/blob/master/resourceQueries/Orphaned%20Resources/OrphanedResource.MD
##########################DeleteOrphanedDisk##########################
$csv = Import-Csv "disk.csv" #name of csv file
$filetim = (Get-Date).tostring("dd-MM-yyyy-hh-mm")
$Logfile = "log-deletedisk-$filetim.log"
Function LogWrite
{
Param ([string]$logstring)
Add-content $Logfile -value $logstring
}
Start-Transcript -Path $Logfile
Stop-Transcript
LogWrite " "
LogWrite " "
LogWrite "***** LogFile *****"
LogWrite " "
LogWrite " "
$csv | ForEach-Object {
$diskname = $_.NAME
$state = $_.DISKSTATE
$resourcegroup = $_.RESOURCEGROUP
$location = $_.LOCATION
if($state -like "Unattached")
{
$tim = (Get-Date).tostring("dd-MM-yyyy-hh:mm:ss")
LogWrite "$tim - Deleting disk $diskname in $resourcegroup of Location - $location"
Write-Host -ForegroundColor Cyan "Deleting Disk $diskname in $resourcegroup of Location - $location"
Remove-AzDisk -ResourceGroupName $resourcegroup -DiskName $diskname -Force
}
else
{
Write-Host -ForegroundColor Red "Failed to delete the disk $diskname in $resourcegroup of Location - $location since it is Attached"
$tim = (Get-Date).tostring("dd-MM-yyyy-hh:mm:ss")
LogWrite "$tim - Failed to delete the disk $diskname in $resourcegroup of Location - $location since it is Attached"
}
}
#####################################################################
##########################DeleteOrphanedNICs##########################
$csv = Import-Csv "nic.csv" #name of csv file
$filetim = (Get-Date).tostring("dd-MM-yyyy-hh-mm")
$Logfile = "log-$filetim.log"
Function LogWrite
{
Param ([string]$logstring)
Add-content $Logfile -value $logstring
}
Start-Transcript -Path $Logfile
Stop-Transcript
LogWrite " "
LogWrite " "
LogWrite "***** LogFile *****"
LogWrite " "
LogWrite " "
$csv | ForEach-Object {
$nicname = $_.NAME
$resourcegroup = $_.RESOURCEGROUP
$location = $_.LOCATION
$tim = (Get-Date).tostring("dd-MM-yyyy-hh:mm:ss")
LogWrite "$tim - Deleting NIC $nicname in $resourcegroup of Location - $location"
Write-Host -ForegroundColor Cyan "Deleting NIC $nicname in $resourcegroup of Location - $location"
Remove-AzNetworkInterface -Name $nicname -ResourceGroup $resourcegroup -Force
}
#####################################################################
##########################DeleteOrphanedNSGs#########################
$csv = Import-Csv "nsg.csv" #name of csv file
$filetim = (Get-Date).tostring("dd-MM-yyyy-hh-mm")
$Logfile = "log-deleteNSG-$filetim.log"
Function LogWrite
{
Param ([string]$logstring)
Add-content $Logfile -value $logstring
}
Start-Transcript -Path $Logfile
Stop-Transcript
LogWrite " "
LogWrite " "
LogWrite "***** LogFile *****"
LogWrite " "
LogWrite " "
$csv | ForEach-Object {
$nsgname = $_.RESOURCE
$resourcegroup = $_.RESOURCEGROUP
$location = $_.LOCATION
$tim = (Get-Date).tostring("dd-MM-yyyy-hh:mm:ss")
LogWrite "$tim - Deleting NSG $nsgname in $resourcegroup of Location - $location"
Write-Host -ForegroundColor Cyan "Deleting NSG $nsgname in $resourcegroup of Location - $location"
Remove-AzNetworkSecurityGroup -Name $nsgname -ResourceGroup $resourcegroup -Force
}
#####################################################################
##########################DeleteOrphanedPiP##########################
$csv = Import-Csv "pip.csv" #name of csv file
$filetim = (Get-Date).tostring("dd-MM-yyyy-hh-mm")
$Logfile = "log-deletePIP-$filetim.log"
Function LogWrite
{
Param ([string]$logstring)
Add-content $Logfile -value $logstring
}
Start-Transcript -Path $Logfile
Stop-Transcript
LogWrite " "
LogWrite " "
LogWrite "***** LogFile *****"
LogWrite " "
LogWrite " "
$csv | ForEach-Object {
$pipname = $_.NAME
$resourcegroup = $_.RESOURCEGROUP
$location = $_.LOCATION
$tim = (Get-Date).tostring("dd-MM-yyyy-hh:mm:ss")
LogWrite "$tim - Deleting Public IP $pipname in $resourcegroup of Location - $location"
Write-Host -ForegroundColor Cyan "Deleting Public IP $pipname in $resourcegroup of Location - $location"
Remove-AzPublicIpAddress -Name $pipname -ResourceGroupName $resourcegroup -Force
}
#####################################################################
Below script used to fetch FSLogix User Profile Size Report with Az Commands
#TLS Enablement
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Script fetch details with Azure storage commands
$StorageAccountName="storage account Name" # Dont include file.core.windowsnet, just give storage account name
$fileShares="profile2","profile3","profile4","profile5","profile6"
$ResourceGroup = (Get-AzResource -Name $StorageAccountName).ResourceGroupName
$Key = Get-AzStorageAccountKey -ResourceGroupName $ResourceGroup -Name $StorageAccountName
$context=New-AzStorageContext -StorageAccountName $StorageAccountName -StorageAccountKey $Key[1].Value
$finalResult=@()
foreach($fileShare in $fileShares)
{
$Files=Get-AzStorageFile -Context $context -ShareName $fileShare | Get-AzStorageFile
write-host "$($fileShare) Count = $($Files.Count)"
$i=1
$Files | ForEach-Object{
Write-Host "Executing File $i"
$File=$_.ShareFileClient.Path
#$FileDetails=Get-AzStorageFile -Context $context -ShareName $fileShareName -Path $File
#$obj=""|select UserGPN,UserSID,File,SizeInMB,LastModified
$obj=""|select FileShare,UserGPN,UserSID,File,SizeInMB
$obj.FileShare=$fileShare
$obj.UserGPN=($File.Split("/")[0]).Split("_")[0]
$obj.UserSID=($File.Split("/")[0]).Split("_")[1]
$obj.File=$_.Name
$obj.SizeInMB=$_.Length/1024/1024
#$obj.LastModified=$FileDetails.LastModified
$finalResult+=$obj
$i=$i+1
}
}
$finalResult | Export-Csv -Path C:\Temp\$StorageAccountName"_ProfileSizes_$(get-date -Format yyyyMMdd_hhmmss)".csv -NoTypeInformation
Below script used to fetch FSLogix User Profile Size Report after mapping Azure file share as a Network Drive
Firstly, Map the Azure file share as Mapped Drive and execute below script
$Date = Get-Date -Format dd-MM-yyyy
$FSLogixShareUsers = Get-ChildItem -Recurse -Path "\\<StorageAccountName>.core.windows.net\<Share Name>" | Where-Object {$_.Name -like "*.VHDX"}
ForEach($User in $FSLogixShareUsers){
$Username = ($User.Name).Split('_')[1] | ForEach{$_.SubString(0,$_.length-5)}
$Profile = ($User.Length | Measure-Object -Sum).Sum /1GB | Out-String
$ProfileSize = $Profile | ForEach{$_.SubString(0,$_.Length-8)+"GB"}
$Report = New-Object PSObject # Creates a Custom Report #
$Report | Add-Member -MemberType NoteProperty -Name Username -Value $Username
$Report | Add-Member -MemberType NoteProperty -Name ProfileSize -Value $ProfileSize
$Report | Export-Csv -NoTypeInformation -Append -Path ('C:\reports\FSLogix_Share_Report_profilecontainer'+$Date+'.csv')
}
$FSLogixShare = ((Get-ChildItem -Recurse -Path "\\<StorageAccountName>.core.windows.net\<Share Name>" | Where-Object {$_.Name -like "*.VHDX"} | Measure-Object Length -Sum).Sum /1GB) | Out-String
$FSLogixShareSize = $FSLogixShare.Split('.')[0]+"GB"
Below script is used to migrate the user profiles between two different storage accounts in a single region.
# This script created to move FSlogix user profiles from one storage account to another in same region.
# In CSV input file, userlogon name is taken . Script searches with logon name in storage and move to Destination storage
Set-Location "C:\Migration" #set the path where azopy exists
#Provide the input file with Logon Names
$Lists = Import-csv -Path "C:\Migration\profile4input.csv"
$source = "//<storageaccountname>.file.core.windows.net/<Share Name>"
$sourceSas = "<SAS Key>"
$destination = "//<storageaccountname>.file.core.windows.net/<Share Name>"
$destinationSas = "<SAS Key>"
$items = Get-ChildItem -Path $source
$report = @()
foreach($item in $items){
$itemName = $item.Name
foreach($list in $lists){
$UserName = $list.Users
if($itemName -cmatch $UserName){
#azcopy
$SourceFullPath = "https:$source/$itemName$sourceSas"
$destinationFullPath = "https:$destination$destinationSas"
$output = ./azcopy.exe copy $SourceFullPath $destinationFullPath --overwrite=true --preserve-smb-permissions=true --preserve-smb-info=false --recursive --trusted-microsoft-suffixes= --log-level=INFO;
$output
$No_Of_files_Transferred = $output[-9].split(":")[-1]
$No_of_Folder_Property_Transfers = $output[-8].split(":")[-1]
$Total_No_of_Transfers = $output[-7].split(":")[-1]
$No_of_Transfers_Completed = $output[-6].split(":")[-1]
$No_of_Transfers_Failed = $output[-5].split(":")[-1]
$No_of_Transfers_Skipped = $output[-4].split(":")[-1]
$TotalBytesTransferred = $output[-3].split(":")[-1]
$Final_Job_Status = $output[-2].split(":")[-1]
$report += @{
UserId = $UserName;
No_Of_files_Transferred = $No_Of_files_Transferred;
No_of_Folder_Property_Transfers = $No_of_Folder_Property_Transfers;
Total_No_of_Transfers = $Total_No_of_Transfers;
No_of_Transfers_Completed = $No_of_Transfers_Completed;
No_of_Transfers_Failed = $No_of_Transfers_Failed;
No_of_Transfers_Skipped = $No_of_Transfers_Skipped;
TotalBytesTransferred = $TotalBytesTransferred;
Final_Job_Status = $Final_Job_Status
}
}
}
}
$json = @{Profile = $report}| ConvertTo-Json
($JSONData = $json | ConvertFrom-Json)
$JSONData.profile | Export-Csv -Path "C:\Migration\profile4outputlog_16aug21.csv" -Append -NoTypeInformation
Thanks to Ali Tajran for helping below content
We need to install the Azure PowerShellGet module. The problem is that we are unable to install NuGet provider and a couple of errors are showing. Warning unable to download from URI, unable to download the list of available providers. After the errors, it did not install. In this article, you will learn why this is happening and the solution for installing NuGet provider for PowerShell.
Install-Module Az.Accounts

Based on above error, tried below commands to install NuGet Provider for Powershell
Run PowerShell as administrator. Run the command Install-Module PowershellGet -Force. When asked to install NuGet provider, press Y and follow with Enter.

After pressing Y and follow with Enter, the output is giving us the following warnings.
- WARNING: Unable to download from URI.
- WARNING: Unable to download the list of available providers. Check your internet connection.
- Unable to find package provider ‘NuGet’. It may not be imported yet.

OR , you will not able t
Why are we getting this error and what is the solution for unable to install and download NuGet provider?
Find PowerShell version
Find the PowerShell version that is running on the system. We are going to use the Get-Host cmdlet in Windows Server 2016.

Check Transport Layer Security protocols
PowerShell 5.1 enables SSL 3.0 and TLS 1.0 for secure HTTP connections by default. Let’s confirm that with the next step.
Check the supported security protocols on the system.
[PS] C:\>[Net.ServicePointManager]::SecurityProtocol
Ssl3, Tls
As we can see, the security protocols defined in the system are SSL 3.0 and TLS 1.0. Both of the security protocols are deprecated.

Follow below steps to resolve the above error
First, check the SSL version with below command

Solution for unable to install NuGet provider for PowerShell
Now that we gathered all the information, we are going to enable TLS 1.2 on the system. Run both cmdlets to set .NET Framework strong cryptography registry keys. After that, restart PowerShell and check if the security protocol TLS 1.2 is added
The first cmdlet is to set strong cryptography on 64 bit .Net Framework (version 4 and above).
Set-ItemProperty -Path ‘HKLM:\SOFTWARE\Wow6432Node\Microsoft.NetFramework\v4.0.30319’ -Name ‘SchUseStrongCrypto’ -Value ‘1’ -Type Dword
The second cmdlet is to set strong cryptography on 32 bit .Net Framework (version 4 and above).
Set-ItemProperty -Path ‘HKLM:\SOFTWARE\Microsoft.NetFramework\v4.0.30319’ -Name ‘SchUseStrongCrypto’ -Value ‘1’ -Type Dword

Restart Powershell and check for supported security protocols.

Run again Install-Module Az.Accounts

Post installation, Import Modules ( Installed Modules can be seen in C:\Program Files\WindowsPowerShell\Modules)

Similarly do for Azure storage modules

Conclusion:
In this article, we learned why we are unable to install Azure &NuGet provider for PowerShell. The solution to this problem is configuring TLS1.2 or higher on the system. After that, you can install NuGet & Azure Modules for PowerShell