Servers - Скрипт обслуживания сервера RDS (версия 2)

Реклама:
Записка от 17.02.2017

В заметках "Скрипт обслуживания сервера RDS" и "PowerShell - Удаление профилей с сервера" я уже рассказывал о скрипте для очистки профилей и очереди печати на терминальных серверах. В этой публикации я размещаю обновленный текст кода Powershell, который я оптимизировал для обслуживания терминальных серверов Windows Server 2012.

# --- Description ---
# This script is for maintainance of Windows 2012 RDS server in roaming profile mode.
# - remove users from access to RDP
# - force users logoff from the server
# - delete broken profile folders (folders must be removed after user logoff)
# - delete broken profile registry section (registry sections must be removed after user logoff)
# - delete profile oprimization scheduled tasks
# - stop print spooler, delete all print jobs, run DeleteInactivePortSilently.exe, start print spooler
# - restore users access to RDP
# - reboot the server on Monday

# --- Declare variables ---

$ExcludedProfilePaths = New-Object System.Collections.ArrayList
$ExcludedRegistrySections = New-Object System.Collections.ArrayList

# --- Input data ---

# !!!!!   DON'T FORGET TO EXCLUDE SERVICE ACCOUNTS. LOCAL ACCOUNTS WILL BE EXCLUDED AUTOMATICALLY   !!!!!

$ProfileFolder = "C:\Users"
$ProfileRegistrySection = "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList"
$DomainRDSGroup = "RDS_Application_Users"
$LocalRDSGroup = "Remote Desktop Users"
$ZipArhiverPath = "C:\Scripts\7za.exe"
[void] $ExcludedProfilePaths.AddRange( ("C:\Users\All Users", "C:\Users\Default", "C:\Users\Default User", `
  "C:\Users\Public") )
[void] $ExcludedRegistrySections.AddRange( ("S-1-5-18", "S-1-5-19", "S-1-5-20") )

# --- Functions ---

Function Write-ScriptLog {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Message,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$LogFile
  )
  Process {
    $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
    $LogMessage += "`t"
    $LogMessage += $Message
    $LogMessage | Out-File -FilePath $LogFile -Append
  }
}#End Function

Function Get-ComputerSessions {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Computer
  )
  Process {
    $Report = quser /server:$Computer | Select-Object -Skip 1 | ForEach-Object {
      $CurrentLine = $_.Trim() -Replace '\s+',' ' -Split '\s'
      $HashProps = @{
        UserName = $CurrentLine[0]
        ComputerName = $Computer
      }
      # If session is disconnected, different fields will be selected
      If ($CurrentLine[2] -eq 'Disc') {
        $HashProps.SessionName = $null
        $HashProps.Id = $CurrentLine[1]
        $HashProps.State = $CurrentLine[2]
        $HashProps.IdleTime = $CurrentLine[3]
        $HashProps.LogonTime = $CurrentLine[4..6] -join ' '
      } Else {
        $HashProps.SessionName = $CurrentLine[1]
        $HashProps.Id = $CurrentLine[2]
        $HashProps.State = $CurrentLine[3]
        $HashProps.IdleTime = $CurrentLine[4]
        $HashProps.LogonTime = $CurrentLine[5..7] -join ' '
      }
      New-Object -TypeName PSCustomObject -Property $HashProps |
        Select-Object -Property UserName,ComputerName,SessionName,Id,State,IdleTime,LogonTime
    }
    Return $Report 
  }
}#End Function

Function Process-DeleteProfileFolders {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [String]$ProfileFolder,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [String[]]$ExcludedProfiles,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
    [String]$LogFile = $Nothing 
  )
  Process {
    $Report = $Nothing
    # Получение объектов из папки профилей
    $SubDirs= Get-ChildItem $ProfileFolder -Force
    # Обработка объектов из папки профилей
    ForEach ($Dir in $SubDirs) {
      $LogMessage = $Nothing
      # Проверка, что объект существуюет и это папка
      If (Test-Path $Dir.FullName -PathType Container) {
        # Проверка, что профиль - это не служебная папка
        $NotDeleteFlag = $False
        ForEach ($ExcludedProfile in $ExcludedProfiles) {
          If ($Dir.FullName -eq $ExcludedProfile) {
            $NotDeleteFlag = $True
          }
        }
        # Removing the profile
        If ($NotDeleteFlag -eq $False) {
          $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
          $LogMessage += "`t"
          $LogMessage += "Delete " + $Dir.FullName + "."
          # Executing removing process
          $CmdLine = "CMD /c RD /S /Q """ + $Dir.FullName + """"
          Invoke-Expression -command $CmdLine
        } Else {
          $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
          $LogMessage += "`t"
          $LogMessage += "Skip " + $Dir.FullName + " - service folder."
        }
      } Else {
        $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
        $LogMessage += "`t"
        $LogMessage += "Skip " + $Dir.FullName + " - file."
      }
      If ($LogFile.Length -gt 0) {
        $LogMessage | Out-File -FilePath $LogFile -Append
      } else {
        Write-Output $LogMessage
      }
    }
  }
}#End Function

Function Process-DeleteProfileRegistry {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [String]$ProfileRegistrySection,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [String[]]$ExcludedRegistrySections,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
    [String]$LogFile = $Nothing
  )
  Process {
    $Report = $Nothing
    # Convering the registry path to Powershell path
    $ProfileRegistrySection = $ProfileRegistrySection.Replace("HKEY_LOCAL_MACHINE","HKLM:")
    # Requesting profile sections from the registry
    $Sections= Get-ChildItem $ProfileRegistrySection
    ForEach ($Section in $Sections) {
      $LogMessage = $Nothing
      # Filtering exclusions
      $NotDeleteFlag = $False
      ForEach ($ExcludedRegistrySection in $ExcludedRegistrySections) {
        If ($Section.Name.Contains($ExcludedRegistrySection)) {
          $NotDeleteFlag = $True
        }
      }
      # Removing profile sections
      If ($NotDeleteFlag -eq $False) {
        $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
        $LogMessage += "`t"
        $LogMessage += "Delete " + $Section.Name + "."
        # Processing deleting the profile section
        $SectionPath = $Section.Name.Replace("HKEY_LOCAL_MACHINE","HKLM:")
        Remove-Item -Path $SectionPath -Force -Recurse
      } Else {
        $LogMessage = get-date -uformat "%d.%m.%Y %H:%M:%S"
        $LogMessage += "`t"
        $LogMessage += "Skip " + $Section.Name + " - service profile."
      }
      If ($LogFile.Length -gt 0) {
        $LogMessage | Out-File -FilePath $LogFile -Append
      } Else {
       # Write-Output $LogMessage
      }
    }
  }
}#End Function

Function Process-ResetPrintSpooler {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,Mandatory=$false)]
    [String]$LogFile = $Nothing
  )
  Process {
    $LogMessage = $Nothing
    $Error.Clear()
    # Остановка сервиса
    Try {
      Stop-Service -Name "Spooler" -Force
      $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + 
        "Print Spooler service is stopped." + "`r`n"
    } Catch {
      $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
    }
    # Очистка заданий
    Try {
        Remove-Item -Path "C:\Windows\System32\spool\PRINTERS\*.*" -Force
        $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print queue is purged." + "`r`n"
    }
    Catch {
        $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error + "`r`n"
    }
    # Очистка неактивных портов принтеров терминального сервера
    $CmdLine = $ScriptTools + "DeleteInactivePortSilently.exe"
    $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + 
      "Delete Inactive TS Ports by DeleteInactivePortSilently.exe command." + "`r`n"
    $CommandOutput = Invoke-Expression -command $CmdLine
    $CommandOutput | ForEach-Object {
      If ($_.Length -gt 0) {
        $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $_ + "`r`n"
      }
    }
    # Запуск сервиса
    Try {
      Start-Service -Name "Spooler"
      $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + "Print Spooler service is started."
    } Catch {
      $LogMessage += (get-date -uformat "%d.%m.%Y %H:%M:%S") + "`t" + $Error
    }
    # Запись события в лог-файл
    If ($LogFile.Length -gt 0) {
      $LogMessage | Out-File -FilePath $LogFile -Append
    } Else {
      Write-Output $LogMessage
    }
  }
}#End Function

Function Get-Monday {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [DateTime]$DateValue
  )
  Process {
    For ($i=0; $i -le 6; $i++) {
      If (($DateValue).adddays(-$i).DayOfWeek -eq "Monday") {
        Return ($DateValue).adddays(-$i)
      }
    }
  }
}#End Function

# --- Start ---

$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length - `
  $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" + `
  $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"
$OSName = (Get-WmiObject -class Win32_OperatingSystem).Caption
$MondayDate = Get-Monday (Get-Date)
$CurrentDate = Get-Date
$DomainRDSGroupObject = [ADSI]"WinNT://glaverbel.com/$DomainRDSGroup,group"
$LocalRDSGroupObject = [ADSI]"WinNT://./$LocalRDSGroup,group"

# Adding local accounts SIDs to exclusions
$LocalAccounts = Get-WmiObject Win32_UserAccount -Filter "Domain='$env:computername'"
ForEach ($LocalAccount in $LocalAccounts) {
  $CurrentSID = $LocalAccount.SID
  If ($CurrentSID -ne $Nothing) {
    [void] $ExcludedRegistrySections.Add( $CurrentSID )
  }
}

# Adding local accounts Profiles paths to exclusions
ForEach ($CurrentSID in $ExcludedRegistrySections) {
  $CurrentPath =  (Get-WmiObject Win32_UserProfile -Filter "SID='$CurrentSID'").LocalPath
  If ($CurrentPath -ne $Nothing) {
    [void] $ExcludedProfilePaths.Add( $CurrentPath )
  }
}

# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
  New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

# Check OS version
If (-Not $OSName.Contains("2012")) {
  Write-ScriptLog -LogFile $LogFile -Message ("The operating system is not match for this script.")
  Exit
}

# Extracting additional files
$ScriptTools = $ScriptFolder + `
  $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + "\"
$ScriptToolsArchive = $ScriptFolder + `
  $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".zip"
If (Test-Path $ScriptToolsArchive -PathType Leaf) {
  Write-ScriptLog -LogFile $LogFile -Message ("Script tools are found in " + `
    $ScriptToolsArchive + " archive.")
  Write-ScriptLog -LogFile $LogFile -Message ("Extracting the archive into " + $ScriptTools +".")
  
  New-Item -Path $ScriptTools -ItemType directory -Force
  $Shell = New-Object -com shell.application
  $ArchiveItem = $shell.NameSpace($ScriptToolsArchive)
  ForEach($Item in $ArchiveItem.items()) {
    $Shell.Namespace($ScriptTools).copyhere($Item, 0x14)
  }

  If (Test-Path $ZipArhiverPath -PathType Leaf) {
    [String]$CmdLine = $ZipArhiverPath
    [Array]$CmdLineArg = 'x', """$ScriptToolsArchive""",  "-o""$ScriptTools""", "-y"
    # Оператор & (ampersand) указывает, то необходимо выполнить внешнюю команду, указанную после него
    # подробнее тут https://technet.microsoft.com/en-us/library/ee176880.aspx
    Write-ScriptLog -LogFile $LogFile -Message (&$CmdLine $CmdLineArg)
  } Else {
    Write-ScriptLog -LogFile $LogFile -Message ("C:\Scripts\7za.exe is not found. " + `
      "Script execution can't be continue.")
    Exit
  }
}  


# Forbiging users for loging  - Removing users from Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("---   Closing the server for users   ---")
$LocalRDSGroupObject.Remove($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + `
  " is removed from " + $LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is closed for users.")

# Logging off active users
$Sessions = Get-ComputerSessions -Computer localhost
ForEach ($Session in $Sessions) {
  logoff $Session.Id /server:localhost
  Write-ScriptLog -LogFile $LogFile -Message ($Session.UserName + " (" + $Session.Id + `
    ") forced to logoff from the server.")
}

# Waiting for 30 seconds
Write-ScriptLog -LogFile $LogFile -Message ("Waiting 30 seconds for users logoff.")
Start-Sleep -s 30

# Check if there are no users sessions
$Sessions = Get-ComputerSessions -Computer localhost
If ($Sessions.Count -gt 0) {

  Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is " + `
    $Sessions.Count + ".")
  $ActiveSessions = "`r`n"
  $ActiveSessions += $Sessions | FT ID, UserName, State, IdleTime, LogonTime
  Write-ScriptLog -LogFile $LogFile -Message ($ActiveSessions)
  Write-ScriptLog -LogFile $LogFile -Message ("The script processing is terminated, " + `
    "please reboot the server to release sessions.")

} Else {

# Executing cleanup
  Write-ScriptLog -LogFile $LogFile -Message ("Currently logged on users number is 0.")
  # Profiles cleanup
  Write-ScriptLog -LogFile $LogFile -Message ("---   Profiles cleanup   ---")
  Process-DeleteProfileFolders -ProfileFolder $ProfileFolder `
     -ExcludedProfiles $ExcludedProfilePaths -LogFile $LogFile
  # Registry cleanup
  Write-ScriptLog -LogFile $LogFile -Message ("---   Registry cleanup   ---")
  Process-DeleteProfileRegistry -ProfileRegistrySection $ProfileRegistrySection `
    -ExcludedRegistrySections $ExcludedRegistrySections -LogFile $LogFile
  # Task scheduler cleanup
  Write-ScriptLog -LogFile $LogFile -Message ("---   Task scheduler cleanup   ---")
  Write-ScriptLog -LogFile $LogFile -Message ("Delete ""Optimize Start Menu Cache Files"" scheduled tasks.")
  Get-ScheduledTask | Where {$_.taskname -like "Optimize Start Menu Cache Files*"} | `
    Unregister-ScheduledTask -Confirm:$false
  # Print prooler cleanup
  Write-ScriptLog -LogFile $LogFile -Message ("---   Print prooler cleanup   ---")
  Process-ResetPrintSpooler -LogFile $LogFile
}

# Allowing users for loging  - Adding users to Remote Desktop group
Write-ScriptLog -LogFile $LogFile -Message ("---   Openning the server for users   ---")
$LocalRDSGroupObject.Add($DomainRDSGroupObject.Path)
Write-ScriptLog -LogFile $LogFile -Message ($DomainRDSGroupObject.Name.ToString() + " is added to " + 
  $LocalRDSGroupObject.Name.ToString() + ".")
Write-ScriptLog -LogFile $LogFile -Message ("The server is opened for users.")

# Remove auxiliary files
$CmdLine = "CMD /c RD /S /Q """ + $ScriptTools + """"
Invoke-Expression -command $CmdLine
Write-ScriptLog -LogFile $LogFile -Message ("$ScriptTools auxiliary folder is deleted.")

#Reboot server
If ($CurrentDate.Date -eq $MondayDate.Date) {
  Write-ScriptLog -LogFile $LogFile -Message ("Today is Monday - rebooting the server.")
  Restart-Computer -ComputerName "localhost"
}

Указанный скрипт рекомендуется поставить в планировщик Windows на ежедневное выполнение в ночные часы, когда на серверах нет пользователей. Запуск задания нужно выполнять от имени системы (от имени пользователя SYSTEM).

Вверх