24 February, 2013

List Collection membership of computers - SCCM

Continuing from the previous article - where I wrote about SCCM Resource Explorer and how to handle multiple machines when you want to see the inventoried data about them in SCCM - there's another important parameter of computers in SCCM which can be listed: the SCCM Collections computer(s) are member of.

Why is this needed? When you advertise a package or a baseline in Desired Configuration Management, you target collections. You do this for years on a couple of 1000s computers and then one day you get 100 computer names from someone saying, I want to make sure all automated deployments and baselinings stop on these for the next 6 months. Then you would need to look through all 750 collections to see which of them have some of those 100 computers as members. So you want a way to generate a list of collections for each computer.

To do it on the SCCM console, it's not really possible, so let's see a PowerShell way. We need to put some functions and supporting actions into the script, let's have a look at them.

Function for executing SQL query to get data out of SCCM DB:
function execSQLQuery ([string]$fSQLServer, [string]$db, [string]$query){
   $objConnection = New-Object System.Data.SqlClient.SqlConnection
   $objConnection.ConnectionString = "Server = $fSQLServer; Database = $db; trusted_connection=true;"
   $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $query, $objConnection
   trap {Write-Host -ForegroundColor 'red' "($sqlsrv/$db not accessible)";continue}
   $SqlCmd.Connection.Open()
   if ($SqlCmd.Connection.State -ine 'Open') {
      $SqlCmd.Connection.Close()
      return
   }   $dr = $SqlCmd.ExecuteReader()

  
#get the data
   $dt = new-object "System.Data.DataTable"
   $dt.Load($dr)
   $SqlCmd.Connection.Close()
   $dr.Close()
   $dr.Dispose()
   $objConnection.Close()
   return $dt
}

Read SCCM site name from SCCM site server remotely:
$site = (gwmi -ComputerName $sccmsrv -Namespace root\sms -Class SMS_ProviderLocation).sitecode

Read SQL server name from SCCM:
$sccmCompquery = gwmi -q "Select distinct SiteSystem, Role, SiteCode FROM SMS_SiteSystemSummarizer where role = 'SMS SQL Server' and siteCode = '$site' ORDER BY SiteCode" -namespace "root\sms\site_$site" -ComputerName $sccmsrv
[string]$tmpstr = [regex]::Match($sccmCompquery.sitesystem, "\\\\\w+\\$")
$sccmSQLServer = $tmpstr.replace("\", "")


SQL query to list collection memberships of computers
$memberQuery = "SELECT dbo.v_Collection.Name "
$memberQuery += "FROM dbo.v_FullCollectionMembership INNER JOIN dbo.v_Collection ON dbo.v_FullCollectionMembership.CollectionID = dbo.v_Collection.CollectionID "
$memberQuery += "WHERE (LOWER(dbo.v_FullCollectionMembership.Name) = LOWER('$srv'))"

At the end the script will list the names of computers and list their collection memberships.

The full script looks like:

 param (   [string] $hosts = "",  
       [string] $sccmsrv = "")  
    
  #### Function for executing a SQL query with integrated authentication  
  function execSQLQuery ([string]$fSQLServer, [string]$db, [string]$query){  
    $objConnection = New-Object System.Data.SqlClient.SqlConnection  
    $objConnection.ConnectionString = "Server = $fSQLServer; Database = $db; trusted_connection=true;"  
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $query, $objConnection  
    trap {Write-Host -ForegroundColor 'red' "($sqlsrv/$db not accessible)";continue}   
    $SqlCmd.Connection.Open()  
    
    if ($SqlCmd.Connection.State -ine 'Open') {  
       $SqlCmd.Connection.Close()  
       return  
    }  
    $dr = $SqlCmd.ExecuteReader()  
    
    #get the data  
    $dt = new-object "System.Data.DataTable"  
    $dt.Load($dr)  
    $SqlCmd.Connection.Close()  
    $dr.Close()  
    $dr.Dispose()  
    $objConnection.Close()  
    return $dt  
  }  
    
  # read the SCCM site name of the SCCM site server  
  $site = (gwmi -ComputerName $sccmsrv -Namespace root\sms -Class SMS_ProviderLocation).sitecode  
    
  # enumerating SQL server name for the given SCCM site server  
  $sccmCompquery = gwmi -q "Select distinct SiteSystem, Role, SiteCode FROM SMS_SiteSystemSummarizer where role = 'SMS SQL Server' and siteCode = '$site' ORDER BY SiteCode" -namespace "root\sms\site_$site" -ComputerName $sccmsrv  
  [string]$tmpstr = [regex]::Match($sccmCompquery.sitesystem, "\\\\\w+\\$")  
  $sccmSQLServer = $tmpstr.replace("\", "")  
  $objColl = @()  
    
  #### Collate the host list.  
  $hostlist = @($Input)  
  if ($hosts) {  
    if($hosts -imatch " "){  
       $hostsArr = @($hosts.split(" "))  
       $hostlist += $hostsArr  
    }  
    else{  
       $hostlist += $hosts  
    }  
  }  
    
  # going through the list of hosts  
  foreach($srv in $hostlist){  
    $memberQuery = "SELECT dbo.v_Collection.Name "  
    $memberQuery += "FROM dbo.v_FullCollectionMembership INNER JOIN dbo.v_Collection ON dbo.v_FullCollectionMembership.CollectionID = dbo.v_Collection.CollectionID "  
    $memberQuery += "WHERE (LOWER(dbo.v_FullCollectionMembership.Name) = LOWER('$srv'))"  
    
    # running sql query to enumerate list of collections the computer is member of  
    $membership = execSQLQuery $sccmSQLServer "SMS_$site" $memberQuery  
    
    # if we have a result, go through it and build an object collection with the computer name and the collection(s) it is member of  
    if($membership){  
       foreach($enumColl in $membership){  
         $sObject ="" | select ComputerName,Result  
         $sObject.ComputerName = $srv  
         $sObject.Result = $enumColl.name  
         $objColl += $sObject  
       }  
    }  
  }  
    
  $objColl 

May the Force...
t


17 February, 2013

Start Resource Explorer from PowerShell - SCCM

Here is another SCCM snippet. The other day, I put in some custom entries into the SCCM hardware inventory config to be able to report on firmware version of RAID controllers of IBM and HP servers. When you do that, you are either lucky to get everything right first or you need to go down the harder route to troubleshoot why the data is not flowing into the SCCM database.

A good place to look at whether you have the data in SCCM already is SCCM Resource Explorer, where you can see the software and hardware inventory data of one machine:


Resource Explorer - SCCM 2007


















I'm an server ops guy, I've been in operations for too long, so I'm not very patient or I should say I like to be efficient. Opening Resource Explorer for 3-5 hosts is a pain in my view. You need to open the SCCM console, go to Collections, find a collection where your machine is probably included, open the collection (I usually have 10000+ machines in those collections), filter for your machine, wait a bit until Mr. MMC thinks about life and Einstein's relativity theory and then it shows your the machine. Then right click, Start, Resource Explorer. So this is about 90 seconds and 6-8 click (depending on how much you like to use mouse instead of keyboard).

Because this blog is mainly about Powershell, you could figure it by now, that the solution will be a PS script. Let's take it step by step:
First we need to find the SCCM installation bin folder where the resourceexplorer.msc file is located:
$adminui = Get-Itemproperty -path hklm:\SOFTWARE\Microsoft\ConfigMgr\Setup -name "UI Installation Directory" | %{$_."UI Installation Directory"}
$bin = $adminui + "\bin"
$command = "$bin\resourceexplorer.msc"

Obviously a fully fledged strip will contain a test-path check against the file, error handling if we couldn't find the msc file...etc. but I don't want to bore you with this.
The next step is to go through each host in the list and open Resource Explorer for them.
foreach($srv in $hostlist){
   $inputhost = "`'" + $srv + "`'"
   $cmd = $command + " -s -sms:ResExplrQuery=`"SELECT ResourceID FROM SMS_R_SYSTEM WHERE NAME = $inputhost`" -sms:connection=\\$sccmsrv\root\sms\site_cen"
   $tmpVal = [diagnostics.process]::start("mmc", $cmd)
}

There are two tricks in this code, the first was to find out how to specify the necessary parameters for the msc file, it's basically a WQL query:
SELECT ResourceID FROM SMS_R_SYSTEM WHERE NAME = $inputhost

You also need to escape the quotes for the WQL query string, that's why the `" characters are there.

The second trick is to put the server name into single quotes:
$inputhost = "`'" + $srv + "`'"

The full script:
 param ([string] $hosts = "")
  
 $adminui = Get-Itemproperty -path hklm:\SOFTWARE\Microsoft\ConfigMgr\Setup -name "UI Installation Directory" | %{$_."UI Installation Directory"}
 $bin = $adminui + "\bin"
 $command = "$bin\resourceexplorer.msc"
 $sccmsrv = $env:COMPUTERNAME
  
 #### Collate the host list.
 $hostlist = @($Input)
 if ($hosts) {
      if($hosts -imatch " "){
           $hostsArr = @($hosts.split(" "))
           $hostlist += $hostsArr
      }
      else{
           $hostlist += $hosts
      }
 }
  
 foreach($srv in $hostlist){
      $inputhost = "`'" + $srv + "`'"
      $cmd = $command + " -s -sms:ResExplrQuery=`"SELECT ResourceID FROM SMS_R_SYSTEM WHERE NAME = $inputhost`" -sms:connection=\\$sccmsrv\root\sms\site_cen"
      $tmpval = [diagnostics.process]::start("mmc", $cmd)
 }  


Let me know if there's any suggestion, comment...etc. May The Force be with you all.
t


02 February, 2013

Verify ComputerName remotely - OS

Quick question for you, my fellow IT engineers, how much do you trust the content of your DNS zone? Good question, isn't it? If a DNS zone and its servers are setup correctly (according to the size and requirements of the given infrastructure) then the content of DNS is supposed to be up-to-date, so DNS scavenging is setup to delete old records...etc.

However, in a global IT infrastructure, there's always someone rebuilding a server with a new name. Imagine this: there's a very enthusiastic engineer who is testing a new multicast OS build method by installing OS on 200+ virtual machines (VM). And to make this test more comprehensive, he/she rebuilds these VMs with new names 4-5 times - but keeps the IP addresses because changing the MAC <-> IP assignment would be too much hassle... yeah, you got it right, cleaning up DNS is not that much hassle at all?! :)
So at the end of the test we end up with ~800 incorrect DNS records (which we initially don't know about).

I've got scripts to check out different aspects of list of servers remotely and if I hit a name which points to an IP of one of those VMs and it already has a new name, then all remote queries will fail (while ping will be successful). Why? Because Windows has a so called Strict Name checking, it does not allow incoming connections if the connection has a different target name. (unless you disable this behaviour).

Solution:
To make sure my checks are transparent, I need to see if a server's name is different to what it was on my list. Here is an example code which can do that. For the sake of the simple example, I want to query the last boot time of each host on a list:

   
# get the IP of the server from DNS

   $ip = [System.Net.Dns]::GetHostAddresses($srv)[0].IPAddressToString

      # creating an object to store results
      $obj = "" | Select Computername,Real_computername,LastBootTime
      $obj.Computername = $srv

     
# reading the name of the computer (contacted via its IP) from WMI

      $obj.Real_computername = (gwmi -class Win32_ComputerSystem -ComputerName $ip).name

      
# reading boot time of the remote host

      $obj.LastBootTime = (gwmi -class Win32_OperatingSystem -ComputerName $ip).lastbootuptime

To explain the essential line:
$ip = [System.Net.Dns]::GetHostAddresses($srv)
This line returns an array, but this time I only care about the first IP, so the [0] element of the array, because I have only 1 IP for each sever.
The rest of the script is straightforward and commented.

Example output:
Computername          Real_computername       LastBootTime
------------          -----------------       ------------
R2D2                  NEWR2D2                 20130129000920.125000+060



Entire example code:
 ## Usage: PS C:\> gc hostlist.txt | getBootTime.ps1
  
 function PingServer ([string]$srv){
      $ping = new-object System.Net.Networkinformation.Ping
      Trap {Continue} $pingresult = $ping.send($srv, 3000, [byte[]][char[]]"z"*16)
      if($pingresult.Status -ieq "Success"){$true} else {$false}
 }
  
 $hostlist = @($Input)
 foreach($srv in $hostlist){
  
      # get the IP of the server from DNS
      $ip = [System.Net.Dns]::GetHostAddresses($srv)[0].IPAddressToString
  
      if(PingServer $ip){
  
           # creating an object to store results
           $obj = "" | Select Computername,Real_computername,LastBootTime
           $obj.Computername = $srv
  
           # reading the name of the computer (contacted via its IP) from WMI
           $obj.Real_computername = (gwmi -class Win32_ComputerSystem -ComputerName $ip).name
           # reading boot time of the remote host
           $obj.LastBootTime = (gwmi -class Win32_OperatingSystem -ComputerName $ip).lastbootuptime
           $objcoll += $obj
      }
 }  


May the Force...
t