Wednesday, March 31, 2010

Using powershell to get ntfs info (such as cluster size)

20100331 - slight update.  Converting the hex items to dec and allowing passing of multiple drives.
20131230 - slight update.  Changed the convert to use an Int64 instead of int32 as I was getting overflow on larger drives

A new function for my library.   Base idea copped from Jacques Barathon [MS] at the bottom of this thread and hashed out to get all the properties.

function get-ntfsinfo {
      param ([char[]]$drive = "c")

      $drive | foreach {
            if (test-path "$($_):") {
                  $cs = new-object PSObject
                  $cs | add-member NoteProperty Drive $_
                  $output = (fsutil fsinfo ntfsinfo "$($_):")
                  foreach ($line in $output) {
                        $info = $line.split(':')
                        #if the value is hex, convert to dec and put hex in ()
                        if ($info[1].trim().startswith('0x0')) {
                              $info[1] = [Convert]::ToInt64(($info[1].Trim()),16).toString() + " (" + $info[1].Trim().toString() + ")"
                        $cs | add-member NoteProperty $info[0].trim().Replace(' ','_') $info[1].trim()
                        $info = $null
            } else {
                  throw "Drive '$_' not found"

A basic run gives you:
PS C:\Windows\system32> get-ntfsinfo e

Drive                           : e
NTFS_Volume_Serial_Number       : 0xeabe7aefbe7ab423
Version                         : 3.1
Number_Sectors                  : 558614527 (0x00000000214bc7ff)
Total_Clusters                  : 69826815 (0x00000000042978ff)
Free_Clusters                   : 36249097 (0x0000000002291e09)
Total_Reserved                  : 0 (0x0000000000000000)
Bytes_Per_Sector                : 512
Bytes_Per_Cluster               : 4096
Bytes_Per_FileRecord_Segment    : 1024
Clusters_Per_FileRecord_Segment : 0
Mft_Valid_Data_Length           : 262144 (0x0000000000040000)
Mft_Start_Lcn                   : 786432 (0x00000000000c0000)
Mft2_Start_Lcn                  : 2 (0x0000000000000002)
Mft_Zone_Start                  : 786496 (0x00000000000c0040)
Mft_Zone_End                    : 837664 (0x00000000000cc820)
RM_Identifier                   : 04E578BF-2D2F-11DF-9782-18A9055883A2

But the nice thing here is being able to say
PS C:\Windows\system32> (get-ntfsinfo e).Bytes_Per_Cluster

I then looped it to check cluster size (what I really wanted) on each drive on the server.

foreach ($drive in Get-PSDrive | where {$ -eq "FileSystem"}) {
      if ($ -ne $null) {
            "$drive - $((get-ntfsinfo $"

gives output like:

C - 4096
E - 4096
G - 65536
J - 4096
K - 4096
L - 4096
M - 65536
N - 65536
O - 65536
Q - 4096
S - 65536
T - 65536
Z – 4096

Let me know if you use it…

Monday, March 29, 2010

powershell: delete files older than X

It really is that easy.

Get-ChildItem $env:temp/myfiles | where {$_.Lastwritetime -lt (date).addminutes(-15)} | remove-item

Everybody has needed the 'delete files older than X' script.  One more thing that is easier in powershell.  In this case I have $env:temp/myfiles as my directory containing the files.  I could also add -recurse to get-childitem if I wanted to get subdirectories as well.  Anonymous noted below that you can add a test to exclude directories if you just want to clean the files out of the directory structure.  I am using addminutes to delete files older than now - 15 minutes but you have other options.  More on that here.

Note that if we want to delete all files older than X we would use -lt.  ie, the timestamp of the file is older than 15 minutes ago.  If we want to delete files newer than X we would use -gt.  ie, the timestamp of the file is newer than 15 minutes ago.  X, in our case, is the time now minus 15 minutes.

 We are going to 'at' it.  Although the Vista and later scheduling engine is vastly improved and renamed to schtasks.

We are going to run this every 15 minutes.
schtasks /create /ru "NT Authority\NETWORK SERVICE" /F /sc daily /mo 1 /ri 15  /st 00:00 /du 0023:50 /tr "powershell -command {
Get-ChildItem $env:temp/myfiles | where {`$_.Lastwritetime -lt (date).addminutes(-15)} | remove-item -recurse"  /TN "File Delete"}

Updated: 20110310 - fixed some issues w/ the schtask line (notice the `) and added a note about the pscontainers test noted by anonymous below.

Friday, March 26, 2010

Fully patch a server using powershell and vbs

Update: 201006018 – changed the reboot portion of the script to watch uptime instead of ping, some of my servers weren’t coming down fast enough.  B)

For our last patch cycle we used Harry Johnston's vbs script to manage our updates.  His script is here. (His very basic home page is here. I found the script from a newsgroup post or I would ping back or similar.  Thanks Harry if you see this.)

If you want to do this completely with powershell, James O’Neill has posted is a pretty definitive discussion, Managing Windows Update with PowerShell.  I don’t have powershell on all of my servers and don’t want to make that a requirement for this solution.  As such, we are doing the actual work w/ the already present cscript engine and just managing the process w/ powershell.

Harry’s very useful script works whether you are pointing to windows updates or your own WSUS server.  It checks for new updates, downloads, installs and reports whether you need a reboot.  If you don't need a reboot, it runs itself again until it either needs a reboot or finds no patches.  Cool script.  we modified it slightly to spit out the hostname we were working w/ as we psexec'd it to run on a bunch of computers.

When we did this at first, we ran a few loops.  First we copied the winupdates.vbs script to the c:\ on each computer to be updated.  We would have run it from the fileshare but we wanted to have a single solution for our servers.  Win2k8 servers run into a UAC problem w/ psexec.  there may be a better way but we decided to pass -s to psexec to run as system (thus avoiding the UAC issues).  Because we need to run as system, we cannot call the script from a network location (more specifically, we didn’t want to manage opening a network location that could be read by all the machine accounts)

We then ran a loop to pop out a command window for each server and run the script through sysinternal's psexec.  When the script finished, it would report if it needed a boot.  We would boot and then run the script again until the script had nothing to do.

This was much easier than getting on the servers and doing it but meh, it was still too much work.

The below powershell script will automate the copying the script and then loop the running, booting, waiting until there are no more patches to install.  I did put in a 10 boot limit and a 15 minute limit waiting for reboots.

Save the script below as PSPatch.ps1.  you will need to update the path to your winupdates.vbs and psexec at the top.   To run it, just pass it a server name.
./pspatch.ps1 servertopath

Let me know if you use it...

#patch server

param (




$scriptpath = "c:\toolkit\scripts\winupdates.vbs"

$psexec = "C:\Toolkit\sysint\psexec.exe"


if (-not (Test-Path $scriptpath)) { throw "Could not find Winupdates.vbs.  Set location at top of script." }

if (-not (Test-Path $psexec)) { throw "Could not find psexec.  Set location at top of script." }


#make sure the name is resolvable

try {$dnsresult = [System.Net.DNS]::GetHostEntry($server)}

catch {

      throw "Servername '$server' does not resolve to an IP"


#check if the server is alive

$ping = new-object System.Net.NetworkInformation.Ping

$Reply = $ping.Send($server)

if ($($Reply.status) -ne "Success") {

      throw "Could not ping $server.  Response $($reply.Status).  Skipping..."




#check if winupdate.vbs is in place

if (-not (test-path "\\$server\c$\winupdates.vbs")) {

      Write-Host "Copying winupdates.vbs to \\$server\c$" -ForegroundColor Yellow

      copy $scriptpath \\$server\c$



$i = 0 #as a sanity check we are only allowing 10 loops

while ($i -lt 10) {


      #run update

      &$psexec \\$server -s cscript c:\winupdates.vbs


      #check exit code

      switch ($LASTEXITCODE) {

      2     { # no more updates to install

                  write-host "Servername '$server' is fully patched after $i loops" -ForegroundColor green



      3     { #reboot needed

                  Write-Host "Reboot required..." -ForegroundColor Yellow

                  $j = 0 #as a sanity check we are only allowing 15 loops here as well

                  $uptime = (Get-WmiObject -computer $server -class Win32_PerfFormattedData_PerfOS_System).SystemUptime

                  shutdown /r /m $server

                  while ($uptime -gt 600 )  # We are checking the uptime and waiting until we get a value of less than 5 minutes uptime as our ‘reboot finished’ test


                        sleep 60

                        $ErrorActionPreference = "Stop"

                        try {

                              $uptime = (Get-WmiObject -computer $server -class Win32_PerfFormattedData_PerfOS_System).SystemUptime

                              Write-Host "Waiting for server to reboot ($uptime)"


                        catch {

                              Write-Host "Waiting for server to reboot ($uptime)"


                        if ($j -gt 15) {  #if we have looped 15 times we have a problem

                              throw "Server has not rebooted in 15 minutes. Something is wrong"







      default { #error or unexpected code

            throw "We either have an error above or an unexpected error code.  Please check above for details"





Tuesday, March 23, 2010

Using powershell to mass update SSRS Reports

I have often needed this and decided to finally sit down and write it.  It basically, takes a set of reports and changes the data source to something else.  In my case, I had a folder of reports that were pointing to one datasource and I wanted to point them all to another.

#update report server data sources with powershell
$computer = "repcomp"
$uri = "http://$($computer)/ReportServer/ReportService.asmx?WSDL"

$reporting = New-WebServiceProxy -uri $uri -UseDefaultCredential -namespace "ReportingWebService"
#I wanted to change the datasource for all reports in this folder, ymmv
$colReports = $reporting.listchildren("/Test Audit Reports", $true) | where {$_.Type -eq "Report"}

foreach ($rep in $colReports)  {
      #I also knew each of my reports only had one datasource, you may need more logic here.
      $datasource = $reporting.GetReportDataSources($rep.path)[0]

      $datasource.Item.Reference = $datasource.Item.Reference.ToString().Replace("/Audit Reports", "/Test Audit Reports")
      Write-Host "Updating: $($rep.Name) to $($datasource.item.reference)"
      $reporting.SetReportDataSources($rep.Path, $datasource)