Shavlik NetChk Pro (or the new VMWare name of VMWare vCenter Protect Essentials Plus Blah Blah Blah thanks for the value add VMWare marketing…) is an excellent package for deploying operating system and application patches across the enterprise. It offers a lot of advantages over WSUS/SCCM including automated patching of numerous 3rd party applications like the virus plagued Flash, Acrobat Reader, and JRE and the ability to launch targeted patches at a moment’s notice allowing for very fine tuned patching schedules.
One big problem with Shavlik is how it deploys patches. By default it deploys patches to the target server’s C drive and it requires the C drive to have as much as five times the total size of the patches to be installed free on the C drive. If you’re like a lot of environments you tend to run with very little free space on your server C drives.. Especially virtualized servers. While it seems like it would be trivial for Shavlik to determine the most appropriate drive to deploy patches to, it doesn’t, and instead it is up to the administrator to change the default drive manually for every single server. A rather painful task for a medium to large environment to be sure. Powershell to the rescue? You bet…
There are two steps to accomplish here: First, determine the most appropriate drive for each server and Second, update the Shavlik database to point the deployment target to that appropriate drive. We will accomplish these tasks with 2 separate scripts…
WHOA! WAITAMINUTE! Don’t forget step 0: BACKUP YOUR SHAVLIK DATABASE. Chances are it is a VM.. Snapshot it!
Script 1: largestDrive.ps1
This script queries Active Directory for all Server objects, connects to each one with WMI and queries their Hard Disk information, and outputs the largest drive for each server to a CSV file which will be accessed by our second script. I am making an assumption here that if your environment is large enough to justify the purchase of Shavlik then querying the server list of Active Directory is probably the most appropriate mechanism for snagging this data. However, if this is not the case it would be trivial to instead feed the data from a CSV or other mechanism. I also added in a second For loop in the script where you can perform some basic filtering of the servers returned from AD to exclude specific systems. In the script I provide an example for filtering out servers with “Test” in the hostname, but this can be changed to anything. In my production environment I used RegEx to remove numerous servers that were outside of our patching scope. Here’s the script!
#############################################
# largestDrive.ps1
#
# Script pulls all servers out of Active Directory
# and then loops through them using WMI to find the
# drive with the greatest amount of free space.
#
# Output is saved to a CSV file.
##############################################
#################
# Script Setup
# Import Active Directory module for AD query
Import-Module ActiveDirectory
# Location and filename for output CSV
$driveOut = "driveSpaceTest.csv"
# Header for drive space CSV
Add-Content $driveOut "hostname,drive_letter,free_space"
# Retrieve all server names listed in AD
$Servers = Get-ADComputer -Filter {operatingsystem -like "*server*"} -Property Name | Select-Object Name
<#
Reading input from Active Directory is just one of many ways
to feed servers into this script. You could also use a CSV:
serverList.csv:
Name
Server1
Server2
Server3
$Servers = Import-CSV "serverList.csv"
Or you could just feed the $colComputers array directly:
$colComputers = New-Object System.Collections.ArrayList
$colComputers.Add("server1")
$colComputers.Add("server2")
$colComputers.Add("server3")
Or...
$colComputers = @()
$colComputers = $colComputers + "server1"
$colComputers = $colComputers + "server2"
$colComputers = $colComputers + "server3"
Or...
$colComputers = "server1","server2","server3"
#>
# New Array to hold server list
$colComputers = New-Object System.Collections.ArrayList
###################
# Script Body
ForEach ($Server in $Servers) {
# Build our server array. We could just process the $servers array
# directly. I am adding this loop to give an opportunity to perform
# some simple filtering on the servers returned from AD.
#
# For example, we will remove any server with the word "test" in the
# name
$serverToAdd = $Server.Name
if ($serverToAdd.Contains("test") -eq $False) {
$colComputers.add($serverToAdd)
}
}
# Loop through each system and get the Hard Disk information
ForEach ($strComputer in $colComputers) {
# Zero out test variables
$currentFreeSpace = 0
$largestFreeSpace = 0
# Use WMI to query disk informaiton
$DiskData = gwmi Win32_LogicalDisk -Comp $StrComputer
# Loop throuh returned data to find largest HD. Will loop
# through each server's HDs in order and allow the largest
# to bubble up
ForEach ($Disk in $DiskData) {
# Set currentFreeSpace to the size in GB of the current Disk
$currentFreeSpace = $Disk.freespace/1024/1024
if ($currentFreespace -gt $largestFreeSpace) {
# Current disk is larger than the previous, set the
# variables to reflect this
$largestFreeSpace = $currentFreeSpace
$largestDisk = $disk.DeviceID
}
}
# Variables should now hold the drive letter and size of the largest disk
# for the current server. Update our output file
Add-Content $driveOut "$strComputer, $largestDisk, $largestFreeSpace"
}
Script 2: ShavlikDrive.ps1
This script reads in the CSV created by script 1, connects to the Shavlik database, and updates the mmDeploymentDrive field in the ManagedMachines table based on the values in the CSV. This script requires the SQL Powershell extensions which can be downloaded here. The script produces a log showing the results of each query. If you have some servers that fail to update then they are more than likely not in the Shavlik database. Try adding them to a machine group and scanning them. You may find some other error with that particular server like the remote registry service being disabled. And here’s the script!
#####################################################
# shavlikDeploymentDriveUpdate.ps1
#
# Script uses the drive space output from largestDrive.ps1
# to configure Shavlik to copy patches to the drive with
# the greatest amount of freespace per server in the
# environment
#
# Uses the pssql Powershell snapin.
#####################################################
##################
# Globals
# Shavlik SQL Server and Database information
$Server = "ShavlikServer\SQLEXPRESS"
$Database = "ShavlikScans"
# Table and fields of concern
$Table = "ManagedMachines"
$updateField = "mmDeploymentDrive"
$whereField = "name"
# File generated by largestDrive.ps1
$spaceFile = "driveSpace.csv"
# Log file
$outputLog = "results.log"
##################
# Script Setup
# Read in the CSV generated by largestDrive.ps1
$freeSpace = import-csv $spaceFile
# Setup connection to Shavlik DB
$Conn = New-Object System.Data.SQLClient.SQLConnection
$Conn.ConnectionString = "server=$Server;database=$Database;trusted_connection=true;"
$Conn.Open()
# Create new SQL Command object
$Command = New-Object System.Data.SQLClient.SQLCommand
$Command.Connection = $Conn
###################
# Script Body
# Loop through each server in the CSV file and update the Shavlik DB
ForEach ($server in $freeSpace) {
# Skipping servers where the C drive is the largest
# as that is Shavlik's default drive
if ($server.drive_letter -ne "C:") {
$newDrive = $server.drive_letter
$serverName = $server.hostname
# Build UPDATE query
$Command.CommandText = "UPDATE $Table SET $updateField = '$newDrive' WHERE $whereField = '$serverName'"
# Execute the Update query. The query will return the number of rows that have been
# changed.
$result = $Command.ExecuteNonQuery()
# If the query did not update at least one row then there was a problem. If other servers
# update correctly then chances are that the servers that did not update do not currently
# exist in the Shavlik database. Try manually adding servers that error out to a machine
# group in Shavlik and perform a scan on them.
# Update log file with success and failures.
if ($result -lt 1) {
write-host "Error updating $serverName" -foregroundcolor red
add-content $outputLog "Error updating $serverName. Query: $($Command.CommandText)"
} else {
write-host "Updated $serverName" -foregroundcolor green
add-content $outputLog "Successfully updated $serverName. Query: $($Command.CommandText)"
}
}
}
####################
# Clean Up
$Conn.Close()
Modify the various connection variables and paths in each script as appropriate for your environment and give it a whirl. These scripts certainly saved me numerous hours of effort!