2016-11-09

Microsoft VDI User Profile Broken Fix / Recovery / Back & Restore

User Profile Disk 在網路異常或斷線等意外情況下
將導致 Collection Member 與 User Profile Server 失去聯繫
結果便是 User 的註冊機碼等無法在 Sign-Out 時回存
下次登入時就會變成 Temp Profile
且因為是 Pooled VM, 根本無法經由 Local Machine Registry 修復

解決辦法之邏輯如下:

A. User Sign-Out 時 (VBS, 在 Pooled VM 上進行)
User Sign-Out 時檢查 User Profile Disk Link 是否仍然有效
在 User Profile Server 的某一共用資料夾建立 Backup Queue File, 並載明 UserName / Domain / Collection Name 等資訊
B. User Sign-Out 後 (PowerShell, 在 User Profile Server 上進行)
根據上述 Queue File 找出 User Profile Disk 並進行掛載
將 User Profile 的 ntuser* 檔案備份到 AppData\Roaming\BackupNTUserData 路徑
解除掛載
C. User Sign-In 時 (VBS, 在 Pooled VM 上進行)
檢查 User Profile 是否正常, 正常就結束, 如果變為 Temp Profile 就要進行 Restore 作業
先利用 DiskPart 將 User Profile Disk 的 Partition 指派一磁碟機代號
再將 AppData\Roaming\BackupNTUserData 複製到根
移除磁碟機代號並要求 Sign-Out 重新登入
以下為程式碼, 包含 VBS 與 PowerShell
VBS 可用 GPO 派送, PowerShell 以工作排程每五分鐘檢查一次

UserProfileCheckAtLogoff.vbs
===== 程式開始 =====
on error resume next

RemotePath = "\\FileServer.Contoso.com\NTUserDataBackupQueue$"
AdminEmail = "UserName@Contoso.com"

Set objShell = CreateObject("WScript.Shell")
Set wshNetwork = CreateObject("WScript.Network")

ComputerNameArray = Split(wshNetwork.ComputerName,"-")
CollectionName = ComputerNameArray(0)

UserName = wshNetwork.UserName
UserProfilePath = "C:\Users\" & wshNetwork.UserName
EnvUserProfilePath = objShell.ExpandEnvironmentStrings("%UserProfile%")

Set fso = CreateObject("Scripting.FileSystemObject")

ProfileBackupFailMessage = ""
ProfileBackupMessage = ""

if (LCase(UserProfilePath) = LCase(EnvUserProfilePath)) then

 ' Test if backup folder not exist then create

  BackupNTUserDataPath = UserProfilePath & "\AppData\Roaming\BackupNTUserData"

  if not (fso.FolderExists(BackupNTUserDataPath)) then
   Set CreateBackupNTUserDataFolder = fso.CreateFolder(BackupNTUserDataPath)
   if (CreateBackupNTUserDataFolder <> BackupNTUserDataPath) then
    ProfileBackupFailMessage = "Fail to Create Backup Folder, Please Check."
   end if
  end if

  if (ProfileBackupFailMessage = "") then

   ' Test if user can create a test folder in backup folder
    TestBackupNTUserDataPath = BackupNTUserDataPath & "\Test"

    if (fso.FolderExists(TestBackupNTUserDataPath)) then
     fso.DeleteFolder TestBackupNTUserDataPath, True
    end if
    Set CreateTestFolder = fso.CreateFolder(TestBackupNTUserDataPath)
    if (CreateTestFolder <> TestBackupNTUserDataPath) then
     ProfileBackupFailMessage = "Fail to Create Test Folder - Profile Folder not Accessable, Please Check."
    end if
    fso.DeleteFolder TestBackupNTUserDataPath, True

    if (ProfileBackupFailMessage = "") then
 
     ' create backup queue
     if (fso.FolderExists(RemotePath)) then
      QueueFileName = RemotePath & "\" & UserName & "@" & wshNetwork.UserDomain & "@" & CollectionName
      ProfileBackupMessage = "User Profile Backup Queue Create Successed."
      WriteQueue = fso.OpenTextFile(QueueFileName, 2, true, -1)
      WriteQueue.Close
     end if
    end if
  end if
end if

Receiver = AdminEmail
'Receiver = Receiver & ";" & wshNetwork.UserName & "@Contoso.com"

MailSender = "Contoso MailMan"

  Set objMessage = CreateObject("CDO.Message")

  objMessage.Subject = "Contoso Sign-Out Notifier: " & UserName

  objMessage.From = MailSender & " <MailMan@Contoso.com>"

  objMessage.To = Receiver

  Set wshNetwork = CreateObject("WScript.Network")

  MessageBody = ""
  MessageBody = MessageBody & "Your account " & UserName & " was Sign-Out from Workstation: " & vbcrlf & vbcrlf
  MessageBody = MessageBody & "Contoso\" & wshNetwork.ComputerName & " @ " & Now & vbcrlf & vbcrlf
  if (ProfileBackupFailMessage <> "") then
   MessageBody = MessageBody & "User Profile Disk Might Offline After You Sign-In." & vbcrlf
   MessageBody = MessageBody & ProfileBackupFailMessage & vbcrlf & vbcrlf
  elseif (ProfileBackupMessage <> "") then
   MessageBody = MessageBody & ProfileBackupMessage & vbcrlf & vbcrlf
  end if
  MessageBody = MessageBody & "Sent by " & wshNetwork.UserDomain & "\"& wshNetwork.UserName & " @ " & wshNetwork.ComputerName & vbcrlf & vbcrlf

  objMessage.TextBody = MessageBody

  'objMessage.Addattachment ""

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
  'determines whether you use local smtp (1) or network (2)

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "SMTP.Contoso.com"
  'You can find your provider's server address somewhere on the homepage or by googling for smtp server lists

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = 1
  'Determines the authentication mode. 0 for none, 1 for basic (clear text), 2 for NTLM

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendusername") = "MailMan@Contoso.com"

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendpassword") = "ContosoG0dMai1"

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
  'This is the default port used by most servers. Find out if yours is using a different one if there are problems

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpusessl") = False
  'Use SSL? True or False

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = 60
  'Maximum time connection is tried to be established

  objMessage.Configuration.Fields.Update

  objMessage.Send

===== 程式結束 =====

NTUserDataBackupProcess.ps1 (ContosoSendMail.exe 是一個用 VBS 寫成用來寄信的程式)
===== 程式開始 =====
$AdminEmail = "Admin@Contoso.com"
$QueuePath = "D:\UserProfileDiskBackupQueue"
$UserProfileDiskRootPath = "D:\VDI\UserProfileDisk"
$MinuteBefore = -10

function Test-FileLock {
param (
[parameter(Mandatory=$true)][string]$Path
);
$oFile = New-Object System.IO.FileInfo $Path
if ((Test-Path -Path $Path) -eq $false) {
return $false
};
try {
$oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($oStream) {
$oStream.Close()
};
$false
} catch {
# file is locked by a process.
return $true
};
};

Get-ChildItem $QueuePath | Where {$_.LastWriteTime -gt (Get-Date).Addminutes($MinuteBefore)} | select Name | foreach {

$Info = $_.Name.Split("@")

$UserName = $Info[0]
$DomainName = $Info[1]
$CollectionName = $Info[2]

$objUser = New-Object System.Security.Principal.NTAccount($DomainName, $UserName)
$strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
$UserProfileDisk = $UserProfileDiskRootPath + "\" + $CollectionName + "\UVHD-" + $strSID.Value + ".vhdx"

$BackupSuccess = 0

if ($UserProfileDisk) {
if (Test-Path $UserProfileDisk) {
$FileLock = Test-FileLock($UserProfileDisk)
if ($FileLock -ne $True) {
  Mount-DiskImage -ImagePath $UserProfileDisk $Drive = Get-DiskImage -ImagePath $UserProfileDisk | Get-disk | Get-partition | Get-Volume | select DriveLetter,FileSystemLabel | where {$_.FileSystemLabel -eq "User Disk"} | select DriveLetter  $BackupNTUserDataSourceFiles = ($Drive.DriveLetter + ":\ntuser*")
  $BackupNTUserDataTargetPath = ($Drive.DriveLetter + ":\AppData\Roaming\BackupNTUserData")
if (-Not (Test-Path $BackupNTUserDataTargetPath)) {New-Item -ItemType Directory -Path $BackupNTUserDataTargetPath}
C:\Windows\System32\xcopy.exe $BackupNTUserDataSourceFiles $BackupNTUserDataTargetPath /h /y
Dismount-DiskImage -ImagePath $UserProfileDisk
Remove-Item ($QueuePath +"\" + $_.Name)
$BackupSuccess = 1
} else {
$BackupSuccess = 3
};
} else {
$BackupSuccess = 2
};
};

if ($BackupSuccess -eq 1) {
$MessageBody = "$DomainName\$UserName User Profile at Contoso $CollectionName Backup Process Complete"
} elseif ($BackupSuccess -eq 2) {
$MessageBody = "$DomainName\$UserName User Profile Disk at Contoso $CollectionName Not Found"
} elseif ($BackupSuccess -eq 3) {
$MessageBody = "$DomainName\$UserName User Profile Disk at Contoso $CollectionName is Locked"
} else{
$MessageBody = "$DomainName\$UserName User Profile at Contoso $CollectionName Backup Failed"
};
#C:\Command\ContosoSendMail.exe "ContosoMailMan" "$UserName@Contoso.com" "Contoso User Profile Backup Notifier" "$MessageBody"
C:\Command\ContosoSendMail.exe "ContosoMailMan" "$AdminEmail" "Contoso User Profile Backup Notifier" "$MessageBody"
};

Get-ChildItem $QueuePath | Where {$_.LastWriteTime -lt (Get-Date).Addminutes($MinuteBefore)} | Remove-Item
===== 程式結束 =====

UserProfileCheckAtLogon.vbs
===== 程式開始 =====
on error resume next

Set objShell = CreateObject("WScript.Shell")
Set wshNetwork = CreateObject("WScript.Network")
Set fso = CreateObject("Scripting.FileSystemObject")
Set dtmConvertedDate = CreateObject("WbemScripting.SWbemDateTime")
Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")

VirtualDiskModel = GetVirtualDiskModel()

UserName = wshNetwork.UserName
UserProfilePath = "C:\Users\" & wshNetwork.UserName
EnvUserProfilePath = objShell.ExpandEnvironmentStrings("%UserProfile%")

 BackupExist = 0

 if (LCase(UserProfilePath) <> LCase(EnvUserProfilePath)) then
  Warning = "--== Warning ==--" & vbCrlf & vbCrlf
  Warning = Warning & "User Profile Broken Detected: " & UserName & vbcrlf & vbcrlf
  Warning = Warning & "Your User Profile seems had been broken." & vbCrlf & vbCrlf
  Warning = Warning & "Press [OK] to Check If There is a Backup." & vbCrlf & vbCrlf
  wscript.echo Warning

  Logoff = 0

  AssignUserDiskLetter = "AssignUserDiskLetter.txt"
  UnAssignUserDiskLetter = "UnAssignUserDiskLetter.txt"

  Set colItems = objWMIService.ExecQuery ("Select * from Win32_DiskDrive")
  For Each objItem in colItems
   if (UCase(objItem.Name) = "\\.\PHYSICALDRIVE1") and (UCase(objItem.Model) = VirtualDiskModel) then
    Set objFile = fso.GetFile(Wscript.ScriptFullName)
    ScriptPath = fso.GetParentFolderName(objFile)
    ' Mount UserDisk to B:
     ReturnValue = objShell.Run("""C:\Windows\System32\diskpart.exe"" /s """ & ScriptPath & "\" & AssignUserDiskLetter & """",0,true)
    ' Check if Backup Exist
     NTUserDataBackupPath = "B:\AppData\Roaming\BackupNTUserData"
     if (fso.FolderExists(NTUserDataBackupPath)) then
      if (fso.FileExists(NTUserDataBackupPath & "\ntuser.dat")) then
       BackupExist = 1
      end if
     end if
    exit for
   end if
  Next

  ' Restore When Backup Exist
   if (BackupExist = 1) then
    ObjShell.Run "cmd /c ""del """ & UserProfilePath & "\ntuser*"" /a /q /f""",0
    ObjShell.Run "cmd /c """"C:\windows\system32\xcopy.exe"" """ & NTUserDataBackupPath & """ ""B:\"" /h /y""",0
    Logoff = 1
   end if
  ' UnMount UserDisk From B:
   ReturnValue = objShell.Run("""C:\Windows\System32\diskpart.exe"" /s """ & ScriptPath & "\" & UnAssignUserDiskLetter & """",0,true)
   
   Warning = "--== Warning ==--" & vbCrlf & vbCrlf
   Warning = Warning & "User Profile Broken Detected: " & UserName & vbcrlf & vbcrlf
 
   if (Logoff = 1) then
    Warning = Warning & "We Found a Backup and Has Tried to Restore it." & vbCrlf
    Warning = Warning & "Press [OK] to Sign-Out and Sign-In Again." & vbCrlf & vbCrlf
    Warning = Warning & "If It Still Shows Broken," & vbCrlf
   else
    Warning = Warning & "Cannot Fix it Automatically." & vbCrlf & vbCrlf
   end if

   Warning = Warning & "Please contact MIS to help you fix it." & vbCrlf & vbCrlf

   wscript.echo Warning

   MessageBody = Warning & "Sent by " & wshNetwork.UserDomain &"\"& wshNetwork.UserName & " @ " & wshNetwork.ComputerName
 
   MessageSubject = "User Profile Broken Detected: " & UserName
 
 else

  NTUserDataBackupPath = UserProfilePath & "\AppData\Roaming\BackupNTUserData"
  if (fso.FolderExists(NTUserDataBackupPath)) then
   if (fso.FileExists(NTUserDataBackupPath & "\ntuser.dat")) then
    BackupExist = 1
   end if
  end if

  MessageSubject = "Contoso Sign-in Notifier: " & UserName

  MessageBody = ""
  MessageBody = MessageBody & "Your account " & UserName & " was Sign-in to Workstation: " & vbcrlf & vbcrlf
  MessageBody = MessageBody & "Contoso\" & wshNetwork.ComputerName & " @ " & Now & vbcrlf & vbcrlf

  if (BackupExist = 1) then
   MessageBody = MessageBody & "A User Profile Backup was Found." & vbcrlf & vbcrlf
  else
   MessageBody = MessageBody & "User Profile Backup was not Found." & vbcrlf
   MessageBody = MessageBody & "It Would Be Created after Your Normally Sign-Out." & vbcrlf & vbcrlf
  end if

  MessageBody = MessageBody & "Sent by " & wshNetwork.UserDomain & "\"& wshNetwork.UserName & " @ " & wshNetwork.ComputerName & vbcrlf & vbcrlf

 end if

AdminEmail = "UserName@Contoso.com"

Receiver = AdminEmail
'Receiver = Receiver & ";" & wshNetwork.UserName & "@Contoso.com"

MailSender = "Contoso MailMan"

  Set objMessage = CreateObject("CDO.Message")

  objMessage.Subject = MessageSubject

  objMessage.From = MailSender & " <MailMan@Contoso.com>"

  objMessage.To = Receiver

  objMessage.TextBody = MessageBody

  'objMessage.Addattachment ""

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
  'determines whether you use local smtp (1) or network (2)

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "SMTP.Contoso.com"
  'You can find your provider's server address somewhere on the homepage or by googling for smtp server lists

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = 1
  'Determines the authentication mode. 0 for none, 1 for basic (clear text), 2 for NTLM

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendusername") = "MailMan@Contoso.com"

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendpassword") = "ContosoG0dMai1"

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
  'This is the default port used by most servers. Find out if yours is using a different one if there are problems

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpusessl") = False
  'Use SSL? True or False

  objMessage.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = 60
  'Maximum time connection is tried to be established

  objMessage.Configuration.Fields.Update

  objMessage.Send

if (Logoff = 1) then
 ObjShell.Run "cmd /c """"C:\windows\system32\logoff.exe""""",0

end if

Function GetVirtualDiskModel()

 Set oss = objWMIService.ExecQuery ("Select * from Win32_OperatingSystem")

 For Each os in oss
  OSVersionCaption = os.Caption
  VirtualDiskModel = ""
  if (Len(Replace(LCase(OSVersionCaption),LCase("Windows 7"),"")) <> Len(OSVersionCaption)) then
   OSVersion = "Win7"
   VirtualDiskModel = "MSFT VIRTUAL DISK SCSI DISK DEVICE"
  elseif (Len(Replace(LCase(OSVersionCaption),LCase("Windows 8.1"),"")) <> Len(OSVersionCaption)) then
   OSVersion = "Win81"
   VirtualDiskModel = "MICROSOFT VIRTUAL DISK"
  elseif (Len(Replace(LCase(OSVersionCaption),LCase("Windows 10"),"")) <> Len(OSVersionCaption)) then
   OSVersion = "Win10"
   VirtualDiskModel = "MICROSOFT VIRTUAL DISK"
  end if
 Next

 GetVirtualDiskModel = VirtualDiskModel

End Function
===== 程式結束 =====

AssignUserDiskLetter.txt (DiskPart 指令 Script, UserProfileCheckAtLogon.vbs 調用)
===== 程式開始 =====
select disk 1
select partition 1
assign letter=b
exit
===== 程式結束 =====

UnAssignUserDiskLetter.txt (DiskPart 指令 Script, UserProfileCheckAtLogon.vbs 調用)
===== 程式開始 =====
select disk 1
select partition 1
remove letter=b
exit
===== 程式結束 =====

沒有留言: