Monday, May 13, 2013

Powershell Scripting Games event 2

Voting has started for the second assignment of the 2013 Powershell Scripting Games. This second task was very tough for me because I was on holiday at the seaside and had no fixed Internet connection and no Windows 2000 nor Windows 2003 test machine to test my script with. Anyhow I managed to answer the assignment by using MSDN and checking there which WMI classes I could query depending on the operatin system version.

But let's go back a bit. Here's the full task assignment:

"Dr. Scripto finally has the budget to buy a few new virtualization host servers, but he needs to make some room in the data center to accommodate them. He thinks it makes sense to get rid of his lowest-powered old servers first… but he needs to figure out which ones those are.

This is just the first wave, too – there’s more budget on the horizon so it’s possible he’ll need to run this little report a few times. Better make a reusable tool.

All of the virtualization hosts run Windows Server, but some of them don’t have Windows PowerShell installed, and they’re all running different OS versions.The oldest OS version is Windows 2000 Server (he knows, and he’s embarrassed but he’s just been so darn busy). The good news is that they all belong to the same domain, and that you can rely on having a Domain Admin account to work with.

The good Doctor has asked you to write a PowerShell tool that can show him each server’s name, installed version of Windows, amount of installed physical memory, and number of installed processors. For processors, he’ll be happy getting a count of cores, or sockets, or even both – whatever you can reliably provide across all these different versions of Windows. He has a few text files with computer names – he’d like to pipe the computer names, as strings, to you tool, and have your tool query those computers."

When I saw the scenario I was I little scared by the fact of having to handle all the possible operating systems since Windows 2000. I highlighted the critical points that need to be addressed by the script. I for sure came up with a function in roder to answer to the need of a reusable tool.

Here's the script I wrote:

  1. #Requires -Version 3.0  
  2.    
  3. Function Get-Inventory {  
  4.    
  5. <#  
  6.  .SYNOPSIS  
  7.    Collect important information about a system's hardware in order to decide which ones are too low-powered. 
  8.   
  9.  .DESCRIPTION 
  10.    This function will collect the following information about the target computer: 
  11.         - computer name,  
  12.         - operating system version, 
  13.         - the amount of physical memory, 
  14.         - number of processors,  
  15.         - number of cores, 
  16.         - number of sockets 
  17.    This function has the three classic Begin, Process and End block. 
  18.      - In the Begin block I prepare the ScriptBlock which contains the WMI queries and which is called in the process block. 
  19.      - In the process block I perform Job setup and Jobs throttling. 
  20.      - In the End block I return the results in the form of a object or of a GridView. 
  21.   
  22.   .PARAMETER ComputerName 
  23.     Gets the services running on the specified computer. The default is the local computer. 
  24.   
  25.     Type the NetBIOS name, an IP address, or a fully qualified domain name of a remote computer. 
  26.     To specify the local computer, type the computer name, a dot (.), or "localhost". 
  27.   
  28.     This parameter does not rely on Windows PowerShell remoting. 
  29.     You can use the ComputerName parameter even if your computer is not configured to run remote commands. 
  30.   
  31.   .PARAMETER Credential 
  32.     Specifies a user account that has permission to perform this action. 
  33.   
  34.     This parameter allows you to supply either a PSCredential object or you can specify the domain\username and it will open up 
  35.     the credential window to type in a password. 
  36.   
  37.   .PARAMETER Grid 
  38.     Use this switch to visualize inventory data in the new GridView format for better filtering. 
  39.   
  40.  .EXAMPLE 
  41.     To report on localhost: 
  42.   
  43.     Get-Inventory 
  44.   
  45.  .EXAMPLE 
  46.     To report on a list of servers from a text file: 
  47.   
  48.     Get-Inventory -ComputerName (Get-Content c:\servers.txt) 
  49.   
  50.  .EXAMPLE 
  51.     To report on all available servers in a domain, and see the results in a grid, use the following syntax: 
  52.   
  53.     Get-Inventory -ComputerName ( ([adsisearcher]"objectCategory=computer").findall() |  ForEach-Object { $PSItem.properties.cn } ) -Grid 
  54.   
  55.  .EXAMPLE 
  56.     To report against a specif server: 
  57.   
  58.     Get-Inventory -ComputerName "alphaserver" 
  59.   
  60.  .NOTES 
  61.    Entry for 2013SG Advanced Event 2. To use, dot source script and run 'Get-Inventory'. 
  62.    Use 'get-help Get-Inventory -full' to see how to run complete help. 
  63.  #> 
  64.   
  65.  [CmdletBinding()]   
  66.   
  67.     param( 
  68.   
  69.         [Parameter(Mandatory=$false, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$true)] 
  70.         # Binding by default to localhost. 
  71.         [Alias('CN','__Server')] 
  72.         [String[]]$ComputerName = $env:COMPUTERNAME, 
  73.   
  74.         [Parameter(Mandatory=$false)] 
  75.         [System.Management.Automation.PSCredential] 
  76.         [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty, 
  77.   
  78.         [Parameter(Mandatory=$false)] 
  79.         [switch]$Grid = $false 
  80.   
  81.       ) # End parameter definition. 
  82.   
  83.   Begin { 
  84.   
  85.       Write-Verbose "Started at $(get-date)." 
  86.       # Setting up an array to keep al the objects returned from this function. 
  87.       $AllComputerData = @() 
  88.       # Setting up an array to store all the remote jobs. 
  89.       $Jobs = @() 
  90.       # Number of max computers to query at the same time. This parameter throttle WMI queries in order not to overload the Inventory server. 
  91.       $MaxConcurrentJobs = 10 
  92.   
  93.       # Checking if current is has administrator token for WMI queries. 
  94.       Write-Verbose "Checking for admin rights." 
  95.       $CurrentUser = [Security.Principal.WindowsIdentity]::GetCurrent()             
  96.       $TestAdmin = (New-Object Security.Principal.WindowsPrincipal $CurrentUser).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)             
  97.       if (!$TestAdmin) 
  98.         {             
  99.         Throw "This script must be run with elevated privileges. Try tu use -Credential parameter and suplly administrative credentials. Exiting..." 
  100.         } 
  101.       else 
  102.         { 
  103.         Write-Verbose "Admin rights OK. Continuing." 
  104.         } 
  105.   
  106.       # Scriptblock. Here is where everything happens. This scriptblock is called inside the Process block for each server to query. 
  107.       $Sb = { 
  108.             # The parameter $Computer is passed by Start-Job as an argument for consistency. I could have used args[0] too. 
  109.             param($Computer) 
  110.               # Checking if ping works.. 
  111.               if(!(Test-Connection $Computer -Quiet -ea 0 -Count 2)) 
  112.                 { 
  113.                 # Ping failed!!! 
  114.                 # Creating a dummy object for servers not pinging. I added the Ordered key to keep columns in the order I want. 
  115.                 $Result = New-Object –TypeName PSObject –Property ([ordered]@{ 
  116.                   'Computer Name'= $computer 
  117.                   'State' = "Not pinging" 
  118.                   'Operating System'= "N/A" 
  119.                   'Physical Memory'= "N/A" 
  120.                   'Number Of Processors'= "N/A" 
  121.                   'Number of Cores'= "N/A" 
  122.                   'Number of Sockets'= "N/A" 
  123.                   }) 
  124.                 } # End if. 
  125.               else 
  126.                 { 
  127.                 # Server pings. Checking WMI in a Try Catch block. 
  128.                 try 
  129.                   { 
  130.                   #Adding splatting to keep the line for WMI queries not too long 
  131.                   $Parms = @{ 
  132.                     "Computername" = $Computer; 
  133.                     "ErrorAction" = "Stop"; 
  134.                     "ErrorVariable" = "MyErr"; 
  135.                     "Impersonation" = "impersonate" 
  136.                     } 
  137.                   # Retrieving the three needed WMI instances: Win32_OperatingSystem, Win32_ComputerSystem and Win32_Processor. 
  138.                   $OperatingSystem = Get-WmiObject Win32_OperatingSystem -property Caption @Parms 
  139.                   $ComputerSystem = Get-WmiObject Win32_ComputerSystem -property Name,TotalPhysicalMemory,NumberOfProcessors @Parms 
  140.                   $Processor = Get-WmiObject Win32_Processor -Property SocketDesignation,Numberofcores @Parms 
  141.   
  142.                   # Retrieving information about cores and sockets 
  143.                   $NumberOfCores = 0 
  144.                   $NumberOfSockets = 0 
  145.                   [string]$String = $null 
  146.                   foreach ($Proc in $Processor) 
  147.                     { 
  148.                     # Checking if the NumberOfcores property is present in Win32_Processor. If it is not the case, then it's a Windows 2003 or older.  
  149.                     if ($Proc.numberofcores -eq $null)  
  150.                       {  
  151.                       # Checking if SocketDesignation property is available for each instance of Win32_Processor.  
  152.                       # If it is present I increment the $NumberOfSockets counter.  
  153.                       If (-not $String.contains($Proc.SocketDesignation))  
  154.                         {  
  155.                         $String = $String + $Proc.SocketDesignation  
  156.                         $NumberOfSockets++  
  157.                         }  
  158.                       # In Windows 2003 and older there is one instance of Win32_Processor for each core.  
  159.                       # So I am incrementing this counter for each instance.  
  160.                       $NumberOfCores++  
  161.                       }  
  162.                     else  
  163.                       {  
  164.                       # Since Windows Server 2008 and Windows Vista:  
  165.                       #   - for the Number of Cores,  there is a NumberOfCores property. Easy.  
  166.                       #   - for the Nymber of Sockets, each instance of Win32_Processor is a socket. Incrementing this counter for each instance.  
  167.                       $NumberOfCores = $NumberOfCores + $Proc.numberofcores  
  168.                       $NumberOfSockets++  
  169.                       }  
  170.                     } # End of foreach loop used to retrieve information on sockets and cores.  
  171.    
  172.                   # Retrieving Physical Memory  
  173.                   $PhysicalMemory = $ComputerSystem.TotalPhysicalMemory  
  174.                   # Converting installed RAM to the most appropriate unit. it's a bit long but it makes results easy to read for humans.  
  175.                   Switch ($PhysicalMemory)  
  176.                     {  
  177.                     {($PhysicalMemory -gt 1024) -and ($PhysicalMemory -le 1048576)}  
  178.                       {  
  179.                       # Better to use KB.  
  180.                       $PhysicalMemory = $PhysicalMemory/1Kb  
  181.                       $PhysicalMemory = [math]::Round($PhysicalMemory, 2)  
  182.                       $PhysicalMemory = [string]$PhysicalMemory + ' KB' 
  183.                       Break 
  184.                       } 
  185.                     {($PhysicalMemory -gt 1048576) -and ($PhysicalMemory -le 1073741824)} 
  186.                       { 
  187.                       # Better to use MB. 
  188.                       $PhysicalMemory = $PhysicalMemory/1Mb 
  189.                       $PhysicalMemory = [math]::Round($PhysicalMemory, 2) 
  190.                       $PhysicalMemory = [string]$PhysicalMemory + ' MB' 
  191.                       Break 
  192.                       } 
  193.                     {($PhysicalMemory -gt 1073741824)} 
  194.                       { 
  195.                       # Alot of memory here. Better to use GB. 
  196.                       $PhysicalMemory = $PhysicalMemory/1Gb 
  197.                       $PhysicalMemory = [math]::Round($PhysicalMemory, 2)  
  198.                       $PhysicalMemory = [string]$PhysicalMemory + ' GB' 
  199.                       } 
  200.                     default 
  201.                       { 
  202.                       # Bytes maybe. 
  203.                       $PhysicalMemory = [math]::Round($PhysicalMemory, 2) 
  204.                       $PhysicalMemory = [string]$PhysicalMemory + ' B' 
  205.                       } 
  206.                     } # End Switch. 
  207.   
  208.                   # Preparing a PSObject and filling it with hardware and operating system information. Keeping columns in order with Ordered key. 
  209.                   $Result = New-Object –TypeName PSObject –Property ([ordered]@{ 
  210.                     'Computer Name'= $ComputerSystem.Name 
  211.                     'State' = "Ping and WMI OK" 
  212.                     'Operating System'= $OperatingSystem.Caption 
  213.                     'Physical Memory'= $PhysicalMemory 
  214.                     'Number Of Processors'= $ComputerSystem.NumberOfProcessors 
  215.                     'Number of Cores'=$NumberOfCores 
  216.                     'Number of Sockets'=$NumberOfSockets 
  217.                     }) 
  218.                   } # End Try, starting Catch. 
  219.                 Catch 
  220.                   { 
  221.                   # Cought WMI error. 
  222.                   "WMI Query for $computer failed." 
  223.                   "The error was: $MyErr" 
  224.                   # Preparing a dummy object for servers that did not answer WMI query. 
  225.                   $Result = New-Object –TypeName PSObject –Property ([ordered]@{ 
  226.                     'Computer Name'= $computer 
  227.                     'State' = "Ping OK but WMI Query failed" 
  228.                     'Operating System'= "N/A" 
  229.                     'Physical Memory'= "N/A" 
  230.                     'Number Of Processors'= "N/A" 
  231.                     'Number of Cores'= "N/A" 
  232.                     'Number of Sockets'= "N/A" 
  233.                     }) 
  234.                   } # End Catch block. 
  235.                  } # End if else    
  236.             # Returning the result for this server to the calling function. 
  237.             Return $Result 
  238.         } # End of Scriptblock 
  239.   
  240.   } # End Begin section 
  241.   
  242.   Process { 
  243.   
  244.     foreach ($Computer in $ComputerName) { 
  245.   
  246.       # Preparing a progress bar to be used if the number of servers to query is greater than 1. 
  247.       # The parameters for the progress bar are stored in a variable named $ProgressSplatting. 
  248.       if ($ComputerName.Count -gt 1) { 
  249.         $ProgressSplatting = @{ 
  250.           Activity = 'Inventorying:' 
  251.           Status = $Computer 
  252.           Id = 1 
  253.           PercentComplete = ([array]::IndexOf($ComputerName, $Computer))/$ComputerName.Count*100 
  254.         } 
  255.         Write-Progress @ProgressSplatting 
  256.       } 
  257.   
  258.       #Replacing dot and localhost with proper computername. 
  259.       switch ($computer) 
  260.         { 
  261.         "."         {$computer="$env:COMPUTERNAME"}             
  262.         "localhost" {$computer="$env:COMPUTERNAME"}             
  263.         } 
  264.   
  265.       Write-Verbose "Starting job for $Computer" 
  266.       # Adding a job for each server in the pipe. 
  267.       $Jobs += Start-Job -ArgumentList $Computer -ScriptBlock $sb  
  268.       $Running = @($Jobs | Where-Object {$PSItem.State -eq 'Running'}) 
  269.       # Throttling jobs 
  270.       while ($Running.Count -ge $MaxConcurrentJobs) 
  271.         { 
  272.         write-verbose "Waiting for all jobs to complete" 
  273.         $Finished = Wait-Job -Job $Jobs -Any 
  274.         $Running = @($Jobs | Where-Object {$PSItem.State -eq 'Running'})  
  275.         } # End while loop.  
  276.     } # End of foreach loop against each server.  
  277.     # Waiting for every job inside $Jobs array to finish.  
  278.     Wait-Job -Job $Jobs > $Null  
  279.     Write-Verbose "All jobs finished!"  
  280.    
  281.   } # End Process block.  
  282.    
  283.   End {   
  284.     # Receiving content of jobs and storing them in a $AllComputerData array.  
  285.     $Jobs | ForEach-Object { $AllComputerData  += $PSItem | Receive-Job }  
  286.     Write-Verbose "Finished at $(get-date)."  
  287.     if($Grid)  
  288.       {  
  289.       # If you selected the -Grid parameters I am going to show the results in a GridView.  
  290.       Write-Verbose "Displaying output in a GridView."  
  291.       $AllComputerData | Out-GridView  
  292.       }  
  293.     else  
  294.       {  
  295.       # Returning an object for post-processing.  
  296.       Write-Verbose "Returning the array that contains all the inventory results."  
  297.       Return $AllComputerData  
  298.       } # End If  
  299.    
  300.     } # End of End block  
  301.    
  302. # End of Function Get-Inventory  
  303.    
  304. Get-Inventory -ComputerName . -Verbose  
For the moment my script was very well received, since I am in the Top 10 as you can see here:
This is a huge result for me since I din't spend more than three hours coding and that I had no test machine apart from my laptop.

I tried to be as prolific as I could, so I added jobs, which is a must when you are querying a lot of remote hosts. I also added the possibility to show the result in a GridView, which is a new feature of Powershell.

To vote for me on this entry, visit the link below and click on the rightmost star (5 stars)!
Thanks for your support!

Powershell Scripting Games Event 1, lesson learned

The 2013 Powershell Scripting Games have started for real. I am competing this is for the firs time in the Advanced category. For the moment two tasks have been published. In the first one we had to develop a function aimed at moving old logfiles to a location off the system drive. Easy right? Well, not really. The judges (as well as the other competitors) are very nitty-picky and I have soon discovered that any small mistake can bring very low scores.

Even tough I paid careful attention to what I was doing, I overlooked a few things which I want to share here.

Here's the function I wrote for the assignment:
  1. #Requires -Version 3.0  
  2.   
  3. set strict-mode latest  
  4.   
  5. function Archive-Logs {  
  6. <#  
  7.  .SYNOPSIS  
  8.    Archives application logs older than a certain amount of days to a specific location.  
  9.   
  10.  .DESCRIPTION  
  11.    Moves all the application logs older than a certain amount of days to a specific location.  
  12.    The subfolder structure is maintained.  
  13.   
  14.  .PARAMETER SourcePath  
  15.    Specifies the current path of the folder containing the logs to be moved.  
  16.   
  17.  .PARAMETER DestinationPath  
  18.    Specifies the destination path where to recreate the subfolders structure and move the logs.  
  19.   
  20.  .PARAMETER Days  
  21.    Specifies the minimum age in days of the logs that have to be archived.  
  22.    The default is 90 days.  
  23.   
  24.  .PARAMETER Log  
  25.    Switch parameter to enable error logging.  
  26.    The default is False.  
  27.   
  28.  .PARAMETER FailedLogFile  
  29.    Specifies the path of the file where the errors are logged if the Log switch is set to True.  
  30.    The default is $env:temp\failed-log-move.log  
  31.   
  32.  .PARAMETER ViewErrorLog  
  33.    Opens the error log for viewing using the default associated program (i.e. Notepad).  
  34.    The default is False.  
  35.    
  36.  .EXAMPLE  
  37.    Archives logs older than 100 days from C:\Application\Log to \\nasserver\Archives  
  38.   
  39.    Archive-Logs C:\Application\Log \\nasserver\Archives 100  
  40.    
  41.  .EXAMPLE  
  42.    Archives logs older than 100 days from C:\Application\Log to \\nasserver\Archives. It also logs errors to $env:temp\failed-log-move.log and opens this log for reading.  
  43.   
  44.    Archive-Logs C:\Application\Log \\nasserver\Archives 100 -Log -ViewErrorLog  
  45.   
  46.  .EXAMPLE  
  47.    Archives logs older than 2 years from C:\Application\Log to \\nasserver\Archives and shows verbose output  
  48.      
  49.    Archive-Logs C:\Application\Log \\nasserver\Archives 730 -Verbose  
  50.    
  51.  .EXAMPLE  
  52.    Simulates archival of logs older than 3 months from C:\Application\Log to d:\backup  
  53.      
  54.    Archive-Logs C:\Application\Log d:\backup 90 -Whatif  
  55.   
  56.  .NOTES  
  57.    Entry for 2013SG Advanced Event 1. To use, dot source script and run 'Archive-Logs'.  
  58.    Use 'get-help Archive-Logs -full' to see how to run complete help.  
  59.      
  60.    This function leverages Powershell V3 cmdlets and parameters, such as OlderThan in Test-Path or $PSItem instead of $_.  
  61.    I am also taking advantage of Get-ChildItem -File and -Directory.  
  62.      
  63. #>  
  64.     
  65.   [CmdletBinding(SupportsShouldProcess=$True)]  
  66.     
  67.   param(  
  68.     [Parameter(Mandatory, Position=0)]  
  69.     [ValidateScript({Test-Path $_ -PathType Container})]  
  70.     [String]$SourcePath,  
  71.   
  72.     [Parameter(Mandatory, Position=1)]  
  73.     [ValidateScript({Test-Path $_ -PathType Container})]  
  74.     [String]$DestinationPath,  
  75.   
  76.     [Parameter(Mandatory=$False, Position=2)]  
  77.     [Int]$Days = 90,  
  78.   
  79.     [Parameter(Mandatory=$False, Position=3, ParameterSetName = 'Logging')]  
  80.     [Switch]$Log = $False,  
  81.   
  82.     [Parameter(Mandatory=$False, Position=4, ParameterSetName = 'Logging')]  
  83.     [ValidateScript(  
  84.       {  
  85.       Try {  
  86.         Add-Content -LiteralPath $_ -Value $null -ErrorAction Stop  
  87.         $true  
  88.         }  
  89.       Catch { $false }  
  90.      })]  
  91.     [String]$FailedLogFile=(Join-Path $env:Temp 'failed-log-move.log'),  
  92.   
  93.     [Parameter(Mandatory=$false, Position=5, ParameterSetName = 'Logging')]  
  94.     [switch]$ViewErrorLog = $false  
  95.     )  
  96.   
  97.     Write-Verbose ("Script started processing at {0:hh}:{0:mm}:{0:ss}" -f (Get-Date))  
  98.     $ErrorTracker = 'without errors'  
  99.   
  100.     ForEach($SubFolder in (Get-ChildItem $SourcePath -Directory))   
  101.       {  
  102.       $DestSubFolder = Join-Path -Path $DestinationPath -ChildPath $SubFolder  
  103.       if(!(Test-Path $DestSubFolder))  
  104.         {  
  105.         Try {  
  106.           [void] (New-Item -path $DestinationPath -name $SubFolder -ItemType directory -Force -ErrorAction Stop)  
  107.           }  
  108.         Catch  
  109.           {  
  110.           Write-Warning "Failed creating $SubFolder in $DestinationPath"  
  111.           $ErrorTracker = 'with errors'  
  112.           if ($Log)  
  113.             {  
  114.             Add-Content -LiteralPath $FailedLogFile -Value "$(Get-Date) - Failed creating $SubFolder in $DestinationPath"  
  115.             }  
  116.           } # end Try/Catch  
  117.         }  
  118.       Try {  
  119.           $OldLogs = Get-ChildItem -Path $SubFolder.FullName -Recurse -File -Include *.log -ErrorAction Stop |  
  120.             Where-Object { Test-Path -Path $PSItem.FullName -OlderThan (Get-Date).AddDays(-$Days) }  
  121.           }  
  122.       Catch  
  123.           {  
  124.           Write-Warning "An error occurred retrieving logs from $($SubFolder.fullname)"  
  125.           $ErrorTracker = 'with errors'  
  126.           if ($Log)  
  127.             {  
  128.             Add-Content -LiteralPath $FailedLogFile -Value "$(Get-Date) - An error occurred retrieving logs from $($SubFolder.fullname)"  
  129.             }  
  130.           } # end Try/Catch  
  131.       if($OldLogs)  
  132.         {  
  133.         $OldLogs | Foreach-Object {  
  134.           Try {  
  135.             $CurrentLog = $PSItem  
  136.             Write-Verbose "Archiving $($CurrentLog.name) [$($CurrentLog.lastWriteTime)]"  
  137.             $CurrentLog | Move-Item -Destination $DestSubFolder -Force -ErrorAction Stop  
  138.             }  
  139.           Catch {  
  140.             Write-Warning "An error occurred moving $($CurrentLog.fullname)"  
  141.             $ErrorTracker = 'with errors'  
  142.           if ($Log)  
  143.             {  
  144.             Add-Content -LiteralPath $FailedLogFile -Value "$(Get-Date) - An error occurred moving $($CurrentLog.fullname)"  
  145.             }  
  146.             } # end Try/Catch  
  147.           }  
  148.         }  
  149.       } # end ForEach-Object  
  150.     Write-Verbose ("Script finished processing $ErrorTracker at {0:hh}:{0:mm}:{0:ss}" -f (Get-Date))  
  151.     if($ViewErrorLog)  
  152.       {  
  153.       & $FailedLogFile  
  154.       }  
  155.   } # end Function Archive-Logs  
Apparently everything is well. Unfortunately there are a few mistakes that make me score 2.8 out of 5 possible points.

Let's start with the biggest mistake, which is: 
  1. Get-ChildItem -Path $SubFolder.FullName -Recurse -File -Include *.log -ErrorAction Stop  
The problem here is that I have misinterpreted the use of Get-ChildItem. When I tell this cmdlet to Recurse, I am going to return also files which are stored in any subdirectory. At the same time if I omitted this switch, no files where returned.

The solution to this, I give credit to Mike for this, was to include the desired file extension into the Path parameter:
  1. Get-ChildItem -Path (Join-Path -Path $SourcePath -ChildPath '*\*.log') -ErrorAction Stop  
The lesson I get from this is always to read the cmdlet full help, even if I am pretty sure of how it works.

Another big error of mine was not to comment enough. Many people stated that my script was hard to follow. The fact is that I deemed unnecessary to explain how my code worked to people who already have a proficient knowledge of Powershell. I was wrong.

The lesson I get from this is always to put comments before complicated lines of code as well as the end of each scriptblock. Here's two examples:
  1.  foreach ($Computer in $ComputerName) {  
  2. ... any action ...  
  3. # end of foreach $Computer
or
  1. if($pingworks){  
  2. get-wmiobject...  
  3. # end of If block  
This makes my script much more readable.

The final thing that I learned from Event 1 and that I want to share with you is never to use superflous code. An example could be specifying that a parameters is not mandatory:
  1. [Parameter(Mandatory=$False]  
This is unneded in Powershell. So better to omit it.

That's all for the for Event 1. If you want to see it on the ScriptingGames website click on the image. Unfortunately is too late to vote for my entry on this assignment.

In the next post I will talk about the Second Event of the Games. Stay tuned!

Wednesday, April 24, 2013

The Scripting Games are about to start!

A few hours left to the 2013 Powershell Scripting Games! I am very excited because for the first time I will be taking part in such a competition. In the meanwhile, to stay focused on my objective of learning as much as possible, I am browsing the Net and reading some pretty interesting resources.


Heres' a list:
  • The two first posts by Jeffery Hicks about the evolution of Powershell toward CIM and WSMAN (post 1, post2). 
  • An article of The Scripting Guy about WMI classes related to disks (see here).
  • Another post by Ed Wilson on using Get-ADComputer (see here)
  • A post by jeff Wouters about Compare-Object (see here)
  • A post by Richard Siddaway on updating the Help for V3 (see here)
  • ... and last but not least a post by Bartek Bielawski on $Matches automatic variable

In the meantime I have just downloaded the Windows Management Framework 3  for one of my Windows 7 x64 SP1 (6.1.7601) computers and when I tried to run it quit saying 'The update is not applicable to your computer'.

The problem here is that I though I had .NET 4.0 installed on that computer and I was wrong. When I run the WMI query to verify that .NET 4 was installed I got the following result and I thought it was good:
Get-WmiObject Win32_Product | where { $_.name -match '.NET Framework 4'}

Name              : Microsoft .NET Framework 4 Client Profile
Vendor            : Microsoft Corporation
Version           : 4.0.30319
Caption           : Microsoft .NET Framework 4 Client Profile
Unfortunately having this is not enough. For reference, after I installed the real Full version of the .NET 4 framework, the previous query returned:
Get-WmiObject Win32_Product |where { $_.name -match '.NET Framework 4'}

Name              : Microsoft .NET Framework 4 Extended
Vendor            : Microsoft Corporation
Version           : 4.0.30319
Caption           : Microsoft .NET Framework 4 Extended

Name              : Microsoft .NET Framework 4 Client Profile
Vendor            : Microsoft Corporation
Version           : 4.0.30319
Caption           : Microsoft .NET Framework 4 Client Profile
Much better!

Ok, the Games are coming in about 8 hours. Go and register if you want to compare your knowledge to other Powershell aficionados. This is the link to the registration form. Tomorrow it will be too late!

Monday, April 22, 2013

New Powershell 3.0 params for Get-ChildItem and Test-Path

Regarding Powershell 3.0 new parameters, today I want to talk about something pretty new and interesting that some of you may yet not to be aware of and that could be useful for your administration tasks. The parameters I am referring to are:
  • File
  • Directory
  • Newerthan
  • Olderthan
The first two parameters are new to Get-Childitem cmdlet and are used to return either the files or the folder in a specific path. They replace the check on PSIsContainer to determine if an object is a folder or a file. Let's see an example of each:
PS C:\>  Get-Childitem c:\windows -File

    Directory: C:\windows

Mode   LastWriteTime            Length Name
----   -------------            ------ ----
-a---  26/07/2012     05:08      75264 bfsvc.exe
-a--s  19/04/2013     16:27      67584 bootstat.dat
-a---  16/03/2013     16:34       1720 DtcInstall.log
-a---  26/07/2012     06:49    2380440 explorer.exe
-a---  26/07/2012     05:08     883712 HelpPane.exe
-a---  26/07/2012     05:08      17408 hh.exe
-a---  25/07/2012     22:37      43131 mib.bin
-a---  19/04/2013     16:24       4626 PFRO.log
-a---  26/07/2012     05:08     159232 regedit.exe
-a---  25/07/2012     22:15      27120 ServerWeb.xml
-a---  16/03/2013     16:34      12813 setupact.log
-a---  26/07/2012     09:13          0 setuperr.log
-a---  26/07/2012     05:08     126464 splwow64.exe
-a---  26/07/2012     07:26        219 system.ini
-a---  26/07/2012     09:21       1585 vmgcoinstall.log
-a---  26/07/2012     07:26         92 win.ini
-a---  26/07/2012     05:21      10752 winhlp32.exe
-a---  26/07/2012     05:08      10752 write.exe

PS C:\> Get-Childitem c:\windows\logs\* -Directory

    Directory: C:\windows\logs

Mode   LastWriteTime     Length Name
----   -------------     ------ ----
d----  26/07/2012     10:04            BPA
d----  16/03/2013     16:36            CBS
d----  16/03/2013     16:48            DISM
d---s  26/07/2012     10:04            MeasuredBoot
The third and fourth parameters are new to the Test-Path cmdlet and are used to check whether a given path is older or newer than a specific DateTime object. They have been added to replace the check on the LastWriteTime property, which was far too laborious.
PS C:\> Test-Path -Path c:\windows\logs -NewerThan "April 01, 2013 10:00 PM"
False
As you can see the resulting output is either True or False, depending on the result of the comparison. So, if we change -Newerthan into -Olderthan for the same folder we get True:
PS C:\> Test-Path -Path c:\windows\logs -OlderThan "April 01, 2013 10:00 PM"
True
Now you might be wondering why I present the parameters for two different cmdlets in the same post. Well, this is because you can make use of both together to find all the files older than a given date (i.e. in order to archive them or to delete them, this is up to you).

Here's a sample one-liner that can be used to find all the files older than 5 years in two file paths:
PS C:\> 'd:\IISlogs','h:\application_logs' | Get-ChildItem -File -Recurse -Force | ? { Test-Path -Path $_.FullName -OlderThan (Get-Date).AddYears(-5) }
I hope you will find this post useful and that you are getting used to the new Powershell 3.0 syntax. Keep in mind that this is a generic one-liner that has to be customised upon your needs. If you have any question, feel free to leave a comment.

Friday, April 19, 2013

Problem staging VMWare patches due to SSL inspection

Two months ago I described a problem with downloading patches for a brand new vCenter 5.1 and VMWare Update Manager installation. Today I faced the same problem upgrading an existing vCenter 5.0 to 5.1 where the SslVerifyDownloadCertificate parameter had already been set to 0:

Download patch definitions
Web sites: 
https://hostupdate.vmware.com/software/VUM/PRODUCTION/main/vmw-depot-index.xml;https://hostupdate.vmware.com/software/VUM/PRODUCTION/cscomain/cscodepot-index.xml; 
hosting the patch definitions and patches cannot be accessed or have no patch data. Check the Internet connectivity.

Heres' a screenshot of this error message:


Solved (as described in the other post) by setting once again that registry key from 1 to 0.

Wednesday, April 3, 2013

Finding paths that exceed MAX_PATH with Powershell

A lot as been said and written about the infamous MAX_PATH limitation imposed by the Windows API. Still in 2013 this keeps being a major pain for Windows File Server administrators and as such has been over-discussed but never properly solved. My opinion is that, unless a major architectural rework is done at Redmond, it is up to system administrators to keep an eye on their filesystems and try to evite as much as possible to have paths going over that hardcoded maximum.

That's why I want to share the Powershell script I use to find those paths that exceed 260 characters in lenght, being 260 the value of MAX_PATH ever since.
  1. function Get-Longpaths {  
  2. <  
  3. .SYNOPSIS   
  4.   Retrieves a list of the paths that are too long for being managed with get-childitem.  
  5.     
  6. .DESCRIPTION   
  7.   Retrieves a list of the paths that are too long for being managed with get-childitem.  
  8.   An object is returned containing the list of the paths longer then 260 characters.  
  9.   The aim of this function is to help sysadmins to handle paths which could cause the  
  10.   “System.IO.PathTooLongException“ due to their excessive length comared to what is  
  11.   supported by .NET and Windows API.  
  12.   
  13. .PARAMETER Path  
  14.   The parent path that you need to recursively check.  
  15.   
  16. .PARAMETER Csvpath
  17.   The name of the csv log file you optionally want to export your list of long paths to.  
  18.   
  19. .EXAMPLE  
  20.   Get-Longpaths c:\  
  21.   Retrieves long path on the system partition c: and show on the current host.  
  22.   
  23. .EXAMPLE  
  24.   Get-Longpaths f:\documents c:\mylogs\longpathnames.csv -verbose  
  25.   Retrieves long path inside the folder f:\documents and saves the  
  26.   output to a csv file named longpathnames.csv under c:\mylogs\.  
  27.   It also shows additional information on the task being performed.  
  28. #>  
  29. [CmdletBinding()]  
  30.   
  31.     param(  
  32.         [Parameter(Mandatory=$true)]  
  33.         [string] $Path,  
  34.         [Parameter(Mandatory=$false)]  
  35.         [string] $csvlog  
  36.     )  
  37.   
  38. $options = [system.IO.SearchOption]::AllDirectories  
  39. $allfiles = [system.IO.Directory]::GetFiles($path"*",$options)  
  40. $allfolders = [system.IO.Directory]::GetDirectories($path"*",$options)  
  41. $toolongcontainer = @()  
  42.   
  43. foreach($file in $allfiles)  
  44.   {  
  45.   if($file.Length -gt 259)  
  46.     {  
  47.     write-verbose "Adding $file to the list of too long paths"  
  48.     $toolongcontainer  += $file  
  49.     }  
  50.   }  
  51.   
  52. foreach($folder in $allfolders)  
  53.   {  
  54.   if(($folder|out-string).Length -gt 259)  
  55.     {  
  56.     write-verbose "Adding $folder to the list of too long paths"  
  57.     $toolongcontainer  += $folder  
  58.     }  
  59.   }  
  60. if(!($toolongcontainer))  
  61.   {  
  62.   write-verbose "No too long paths found. Good."  
  63.   $toolongcontainer = "No invalid path under $path"  
  64.   }  
  65. if($csvlog)  
  66.   {  
  67.   write-verbose "Exporting output to $csvpath"  
  68.   $toolongcontainer | Export-Csv -Path $csvpath -NoTypeInformation -USECULTURE -ErrorAction Stop   
  69.   }  
  70. return $toolongcontainer  
  71. }  
  72.   
  73. Get-Longpaths C:\longpath -verbose  
As you can see I used the methods GetFiles and GetDirectories of the system.IO.Directory class. In particular GetFiles returns the names of files in a specified directory, while GetDirectories gets the names of the subdirectories. The cool thing with this methods is that, for some mystical reasons explained here and here, they return the full path as a string or arrays and that's all we need.

Then all the objects returned by these two methods are added as strings to a string array named $toolongcontainer for later analysis. The content of this object can be sent to a CSV file as a  record by passing the csv file path as a parameter.

Once you have this information, you can take appropriate actions such as shortening the path, deleting files and folders that exceed MAX_PATH or moving them to a shorter path. This is up to you.

If you need any clarification about this script, feel free to leave a comment and I'll be happy to answer. But remember that the easier way to get help for a function is to dot source the script then use the built-in help function this way: man Get-Longpaths  -full where 'man' is an interesting alias for get-help.
Related Posts Plugin for WordPress, Blogger...