Avere un server WSUS in azienda aiuta gli amministratori a risparmiare principalmente preziosa banda, e gestire al meglio il rilascio degli aggiornamenti sui client all’interno della stessa rete, fare report, ritirare un fix (KB) in caso di un errore Microsoft (già successo in passato, ogni tanto può capitare). Una mano santa e bla, bla, bla. Ora che il cappello è stato scritto e abbiamo saltato i convenevoli, possiamo parlare del succo della questione e di questo articolo, si parla di pulizia (del WSUS), semplice manutenzione ordinaria! ;-)
Si parte dal presupposto che l’operazione permessa dal software Microsoft vada già più che bene. Sto parlando di quella che puoi lanciare manualmente dalla console andando in Options
→ Server Cleanup Wizard
. In base alle risorse messe a disposizione della tua macchina WSUS, tale operazione potrebbe non essere necessaria per mesi, per poi diventare la tua migliore amica quando ti ritroverai con il disco dati (dedicato alle patch del WSUS) arrivato ormai a tappo.
Un documento di Technet viene in aiuto del malcapitato di turno, suggerendo un intervento via PowerShell, alla quale ci si può affidare per risparmiare tempo e imprecazioni. Il comando è quello di Invoke-WsusServerCleanup
, descritto qui. L’unico pre-requisito è quello di andare a sbloccare l’esecuzione di script non firmati, realizzati tipicamente da te (o da me), cosa alla quale sei abituato sul tuo portatile di lavoro per gestire Exchange in Cloud, un po’ meno forse sul server 2012 R2 dove gira WSUS. Apri quindi PowerShell ed esegui:
Set-ExecutionPolicy Unrestricted
Una volta confermato, sei pronto per salvare il seguente codice in un file PS1 che dovrai poi caricare in una cartella del tuo server:
$logfile="C:\Scripts\WSUS-Cleanup-Log.log" $ErrorActionPreference="SilentlyContinue" Stop-Transcript | out-null $ErrorActionPreference = "Continue" Start-Transcript -path $logfile $Error.Clear() Write-Output "$((get-date).ToLongTimeString()) $server - WSUS Cleanup starting..." Invoke-WsusServerCleanup -CleanupObsoleteUpdates -CleanupUnneededContentFiles -CompressUpdates -DeclineExpiredUpdates -DeclineSupersededUpdates Write-Output "$((get-date).ToLongTimeString()) $server - WSUS Cleanup complete." Stop-Transcript $emailbody = Get-Content $logfile | Out-String $emailsubject = "WSUS cleanup report on $Env:ComputerName" if ($error -ne $null){ $emailsubject = "WSUS cleanup report ERROR on $Env:ComputerName"} Send-MailMessage ` -From "Script WSUS-Clean <wsus@contoso.com>" ` -To "Gruppo Alert WSUS <wsusalert@contoso.com>"` -Subject "$emailsubject" ` -Body "$emailbody" ` -SmtpServer "smtp.contoso.com" # Examples: # -From "$env:COMPUTERNAME-alert@domain.tld" ` # -To "Gruppo Alert WSUS <wsusalert@emmelibri.it>", "Altro Admin <altroadmin@domain.tld>"`
Non si tratta di farina del mio sacco, mi sono limitato a modificare lo script proposto sul blog di “BrianCanFixIT“, un lavoro assolutamente perfetto per l’esigenza, che aggiunge qualche piccolo dettaglio molto comodo alla già comune e conosciuta stringa relativa all’Invoke-WsusServerCleanup
.
Ti basterà ritoccare le informazioni che riguardano il server SMTP (per l’invio dei log dell’operazione) e -se vuoi- modificare le operazioni eseguite da PowerShell, quindi salvare lo script e preparare un’operazione schedulata che lo lancerà una volta a settimana, o al mese se preferisci, lo scopo del gioco è quello di tenere un server WSUS in forma, funzionante e pulito da file non più utili che occupano inutilmente spazio su disco.
Se non sai come fare, ti rimando a una semplice guida pubblicata su SpiceWorks: community.spiceworks.com/how_to/17736-run-powershell-scripts-from-task-scheduler. Ti riepilogo cosa c’è da sapere (quindi fare sul tuo server):
- Crea una nuova operazione semplice, una di quelle che esegue un programma.
- Ricorda di specificare l’intervallo di esecuzione dell’operazione (settimanale, scegli il giorno e l’ora).
- Il programma sarà
Powershell.exe
(il sistema sa già dove andarlo a trovare), negli Arguments dovrai specificare la posizione dello script e -per mera sicurezza- il bypass della policy di esecuzione script di PowerShell, ottenendo quindi qualcosa di simile a-ExecutionPolicy Bypass C:\Script\WSUSClean.ps1
(partendo dal presupposto cheWSUSClean.ps1
sia il tuo script PowerShell contenente il codice sopra pubblicato e che tu lo abbia salvato inC:\Script
). - Lo script da utilizzare questa volta, non prevede parametri, non è necessario quindi specificare null’altro e ti basterà completare la configurazione e ricordarti di aprire le opzioni dell’operazione schedulata al termine. Una volta fatto, modifica il tipo di privilegio di esecuzione specificando che si tratta di un’operazione da eseguire a prescindere che qualcuno sia collegato alla macchina e che servono privilegi elevati.
Ti verrà richiesto di inserire la password dell’account amministrativo che lancerà l’operazione, e se non avrai commesso alcun errore tutto filerà liscio già alla prima occasione. Se vuoi puoi forzare l’esecuzione dell’operazione per assicurarti che tutto vada bene, al termine dovresti ricevere una mail contenente il log delle operazioni.
Enjoy.
La pulizia passa anche per SQL
Aprile 2017: Si è reso necessario aggiornare l’articolo perché, in effetti, avevo lasciato in disparte il database del WSUS, cosa che in realtà è meglio non fare per evitare di lasciare sporco le tabelle utilizzate dal software, andando così a occupare dello spazio inutile che può essere recuperato ogni volta che si lascia intervenire lo script di PowerShell. Ecco quindi come fondere le due cose insieme con un ulteriore script (stavolta di SQL) e una piccola modifica al PowerShell (che ne permetterà l’integrazione).
Si perché in realtà, dopo aver messo in moto e lasciato a lavorare lo script di PowerShell per il clean delle patch non più utili, ci si accorge che nel database si vanno a lasciare delle voci anch’esse non più utili, orfane di ciò che non esiste più. Per portare a termine un lavoro più completo, ti basterà salvare questo script nella stessa cartella dove risiede l’altro di PowerShell:
Maggio 2017: Nuovo giro, nuovo aggiornamento. Per evitare possibili problemi, ho modificato lo script SQL forzandolo a puntare al database SUSDB, utilizzato per contenere i dati di WSUS. Grazie a Daniel per avermi segnalato la modifica, utile per chi non ha quell’unico database sulla macchina 2012 R2 destinata a fare da servizio WSUS.
/****************************************************************************** | |
This sample T-SQL script performs basic maintenance tasks on SUSDB | |
1. Identifies indexes that are fragmented and defragments them. For certain | |
tables, a fill-factor is set in order to improve insert performance. | |
Based on MSDN sample at http://msdn2.microsoft.com/en-us/library/ms188917.aspx | |
and tailored for SUSDB requirements | |
2. Updates potentially out-of-date table statistics. | |
******************************************************************************/ | |
USE SUSDB; | |
GO | |
SET NOCOUNT ON; | |
-- Rebuild or reorganize indexes based on their fragmentation levels | |
DECLARE @work_to_do TABLE ( | |
objectid int | |
, indexid int | |
, pagedensity float | |
, fragmentation float | |
, numrows int | |
) | |
DECLARE @objectid int; | |
DECLARE @indexid int; | |
DECLARE @schemaname nvarchar(130); | |
DECLARE @objectname nvarchar(130); | |
DECLARE @indexname nvarchar(130); | |
DECLARE @numrows int | |
DECLARE @density float; | |
DECLARE @fragmentation float; | |
DECLARE @command nvarchar(4000); | |
DECLARE @fillfactorset bit | |
DECLARE @numpages int | |
-- Select indexes that need to be defragmented based on the following | |
-- * Page density is low | |
-- * External fragmentation is high in relation to index size | |
PRINT 'Estimating fragmentation: Begin. ' + convert(nvarchar, getdate(), 121) | |
INSERT @work_to_do | |
SELECT | |
f.object_id | |
, index_id | |
, avg_page_space_used_in_percent | |
, avg_fragmentation_in_percent | |
, record_count | |
FROM | |
sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'SAMPLED') AS f | |
WHERE | |
(f.avg_page_space_used_in_percent < 85.0 and f.avg_page_space_used_in_percent/100.0 * page_count < page_count - 1) | |
or (f.page_count > 50 and f.avg_fragmentation_in_percent > 15.0) | |
or (f.page_count > 10 and f.avg_fragmentation_in_percent > 80.0) | |
PRINT 'Number of indexes to rebuild: ' + cast(@@ROWCOUNT as nvarchar(20)) | |
PRINT 'Estimating fragmentation: End. ' + convert(nvarchar, getdate(), 121) | |
SELECT @numpages = sum(ps.used_page_count) | |
FROM | |
@work_to_do AS fi | |
INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id | |
INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id | |
-- Declare the cursor for the list of indexes to be processed. | |
DECLARE curIndexes CURSOR FOR SELECT * FROM @work_to_do | |
-- Open the cursor. | |
OPEN curIndexes | |
-- Loop through the indexes | |
WHILE (1=1) | |
BEGIN | |
FETCH NEXT FROM curIndexes | |
INTO @objectid, @indexid, @density, @fragmentation, @numrows; | |
IF @@FETCH_STATUS < 0 BREAK; | |
SELECT | |
@objectname = QUOTENAME(o.name) | |
, @schemaname = QUOTENAME(s.name) | |
FROM | |
sys.objects AS o | |
INNER JOIN sys.schemas as s ON s.schema_id = o.schema_id | |
WHERE | |
o.object_id = @objectid; | |
SELECT | |
@indexname = QUOTENAME(name) | |
, @fillfactorset = CASE fill_factor WHEN 0 THEN 0 ELSE 1 END | |
FROM | |
sys.indexes | |
WHERE | |
object_id = @objectid AND index_id = @indexid; | |
IF ((@density BETWEEN 75.0 AND 85.0) AND @fillfactorset = 1) OR (@fragmentation < 30.0) | |
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE'; | |
ELSE IF @numrows >= 5000 AND @fillfactorset = 0 | |
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90)'; | |
ELSE | |
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD'; | |
PRINT convert(nvarchar, getdate(), 121) + N' Executing: ' + @command; | |
EXEC (@command); | |
PRINT convert(nvarchar, getdate(), 121) + N' Done.'; | |
END | |
-- Close and deallocate the cursor. | |
CLOSE curIndexes; | |
DEALLOCATE curIndexes; | |
IF EXISTS (SELECT * FROM @work_to_do) | |
BEGIN | |
PRINT 'Estimated number of pages in fragmented indexes: ' + cast(@numpages as nvarchar(20)) | |
SELECT @numpages = @numpages - sum(ps.used_page_count) | |
FROM | |
@work_to_do AS fi | |
INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id | |
INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id | |
PRINT 'Estimated number of pages freed: ' + cast(@numpages as nvarchar(20)) | |
END | |
--Update all statistics | |
PRINT 'Updating all statistics.' + convert(nvarchar, getdate(), 121) | |
EXEC sp_updatestats | |
PRINT 'Done updating statistics.' + convert(nvarchar, getdate(), 121) |
A questo punto, modifica lo script PowerShell che già avevi messo in funzione e inserisci la stringa:
sqlcmd -i C:\Scripts\db_maint.sql -S \\.\pipe\MICROSOFT##WID\tsql\query
prima dello Stop-Transcript
. Occhio però: modifica la cartella dove pescare lo script se nel tuo caso è differente (cambia quindi C:\Scripts
in qualcosa che riflette l’attuale posizionamento del file sul tuo server!). A questo punto PowerShell si occuperà di fare pulizia delle patch e del database di WSUS, in un colpo solo. Tu riceverai il solito log delle operazioni che ora conterrà anche le informazioni di pulizia del database :-)
Questione di spazio disco
Un paragrafo nuovo dedicato a un piccolo snippet che serve a ricordarti quanto spazio disco hai usato (e quanto ne resta libero a disposizione) sul server del WSUS. Ho inserito il codice, disponibile su Gist (te lo propongo qui di seguito), all’inizio dello script di pulizia del WSUS, prima che parta il comando di maintenance SQL (sqlcmd -i c:\scripts\db_maint.sql -S \\.\pipe\MICROSOFT##WID\tsql\query -I
)
Write-Output "" | |
$drive = Get-PSDrive C | |
Write-Output "$((get-date).ToLongTimeString()) $server - $($drive.root) Disk Usage..." | |
$used_size = $drive.used | |
$free_size = $drive.free | |
Write-Output "Used space:" ($used_size / 1GB) | |
Write-Output "Free space:" ($free_size / 1GB) | |
Write-Output "$((get-date).ToLongTimeString()) $server - $($drive.root) Disk Usage complete." | |
Write-Output "" |
Inutile dire che sostituendo la lettera associata al drive (Get-PSDrive C
) potrai monitorare un differente disco della macchina. Nessuno inoltre ti nega la possibilità di clonare la porzione nuova di codice per monitorare più dischi della stessa macchina.
L'articolo potrebbe non essere aggiornato
Questo post è stato scritto più di 5 mesi fa, potrebbe non essere aggiornato. Per qualsiasi dubbio ti invito a lasciare un commento per chiedere ulteriori informazioni! :-)