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!

No comments:

Post a Comment

Related Posts Plugin for WordPress, Blogger...