Monday, 13 April 2015

Auto generating server names

So we spent a fair old time working out a naming convention at our place. I wont go into the details of that here, but suffice to say when picking a naming convention, make sure you write some regex so that you can parse it, and then make use of capturing groups to determine OS, location, role, environment etc.

I can thoroughly recommend Debuggex for visualizing and testing regular expressions if you aren't fluent in them! There are also cheat sheets available and don't worry that there isn't a drop down for powershell the default is good enough to get you where you need to be.

So now we are automating the build it made sense to automate the naming of servers, and given the vast size of our estate and the number of non joined up systems, then checking of names is a bit error prone and time consuming. Given we can do it manually, how hard could it be to automate it?

I did a bit of googling and found this post on stackoverflow which was a nice starting point. It didn't fully work and you had to read between the lines.

After a bit of tweaking I got it working so we could fill in the gaps in sequences, IE 1,2,3,5,6,7 would produce the next server as 4. However 3,4,5,6,7 would produce the next server number as 8. A quick fix to the code to make it think a server 0 already exists fixes that issue. (NB: our server numbers start at 001, not 000 - if your's don't then you may need to add -1 instead - Untested!)

The next issue is that, not every server is domain joined so you need to query more than just AD.. For us I wanted to query a couple of SCCM environments (2007 and 2012 r2) SCOM, Multiple VMware environments, and also DNS.

So the code I have produced (which can be re-factored and probably will be at some point) queries each environment getting just the digits of the server name and add's them to the array.

At the end of the script we combine all of the array's and add the 0 previously mentioned and then strip duplicates and sort into numerical order.. this gives us our definitive list of numbers in use. (we work on worse case, if we see it anywhere then assume its in use)

Once we find the name we create an entry in DNS so that subsequent script use wont issue the same name - we deal with concurrency issues by running this in System Center Orchestrator and setting job concurrency on the runbook to be 1.

Script below, change values to suit your environment.


# Add this script can support -verbose (Convert to function later)
[CmdletBinding()]
param()

# ********************************************************
$startOfName = "xxxYYYZZWEB"
# ********************************************************

# VMWare Details
$ADVIServers = @("vsphere1.blah.local","vsphere2.blah.local","vsphere3.blah.local","vsphere4.blah.local")
$StandAloneHosts = @()

# DNS Details
$DNSServer = "xxxxxx.blah.local"

# SCCM 2012 Details
$SCCM2012SiteServer = "sccm2012.blah.local"
$SCCM2012SiteCode = 'SiteCode' 

# SCCM 2007 Details
$SCCM2007SiteServer = "sccm2007.blah.local"
$SCCM2007SiteCode = 'SiteCode2' 

# SCOM 2007 Details
$SCOMServer = "scom.blah.local"

# Create Empty Arrays
$VMNumbers = @()
$ADnumbers = @()
$DNSNumbers = @()
$SCCM2012Numbers = @()
$SCCM2007Numbers = @()
$SCOM2007Numbers = @()


# VMWare

 Write-Verbose "Processing VMware"
 Add-PSSnapin vmware.vimautomation.core -ErrorAction SilentlyContinue

 # Set options for certificates and connecting to multiple enviroments
 $null = Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$False
 $null = Set-PowerCLIConfiguration -DefaultVIServerMode Multiple -Scope User -Confirm:$False

 # Connect to each AD Authenticated viServer
 foreach ($VIServer in $ADVIServers){$null = Connect-VIServer $VIServer -verbose:$false} 
 
 # Connect to standalone host
 foreach ($Host in $StandAloneHosts){$null = Connect-VIServer $Host -User 'usernamehere' -Password 'passwordhere' -verbose:$false} 
 
 # get next available number in a range of numbers.
 $VMNames = Get-VM -Name "$($startOfName)*" -verbose:$false |select Name
 $VMNames |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
 $VMNumbers = $VMNames |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
 Write-Verbose "$($VMNumbers.Count) Matching entries found"
 
# Active Directory

 Write-Verbose "Processing Active Directory"
 
 # Issue Query
 $searcher = [ADSISearcher]"(&(objectCategory=computer)(name=$($StartOfName)*))"
 $searcher.PageSize = 1000

 # get next available number in a range of numbers. returns 5 for 1,2,3,4,6,7,9 From AD
 $ADNames = $searcher.FindAll() | Foreach-Object {[string]$_.Properties.name} | Sort-Object
 $ADNames | Foreach-Object {Write-Verbose $_} | Sort-Object  
 $ADnumbers = $ADNames | Foreach-Object {[int]($_ -replace '\D').Trim() } | Sort-Object  
 Write-Verbose "$($ADnumbers.Count) Matching entries found"
 
# Search DNS

 Write-Verbose "Processing DNS"
 
 # Import DNS module
 Import-Module dnsShell -Verbose:$false
 $DNSNames = get-dnsRecord -server $DNSServer -RecordType A -Zone blah.local | select Name |where {$_.Name -like "$($startOfName)*"} 
 $DNSNames | Foreach-Object {Write-Verbose $_.Name} | Sort-Object -Unique
 $DNSNumbers = $DNSNames | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object -Unique
 Write-Verbose "$($DNSNumbers.Count) Matching entries found"
 
# Search SCCM

 Write-Verbose "Processing SCCM 2012"
 
 # Query SCCM2012 Env
 $SCCM2012Members = Get-WmiObject -ComputerName $SCCM2012SiteServer -Namespace  "ROOT\SMS\site_$SCCM2012SiteCode" -Query "SELECT * FROM SMS_FullCollectionMembership WHERE CollectionID='SMS00001' AND Name LIKE '$($startOfName)%' order by name" | select Name -Unique
 $SCCM2012Members |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
 $SCCM2012Numbers = $SCCM2012Members |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
 Write-Verbose "$($SCCM2012Numbers.Count) Matching entries found"
 
 Write-Verbose "Processing SCCM 2007"
 
 # Query SCCM2007 Env
 $SCCM2007Names = Get-WMIObject -ComputerName $SCCM2007SiteServer -Namespace "root\sms\site_$SCCM2007SiteCode" -class "SMS_R_System" -filter "Name LIKE `"$startOfName%`"" |select Name | Sort-Object -Property Name -Unique
 $SCCM2007Names |select Name | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
 $SCCM2007Numbers = $SCCM2007Names |select Name | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
 Write-Verbose "$($SCCM2007Numbers.Count) Matching entries found"
 
# Search Production SCOM 2007

 Write-Verbose "Processing SCOM 2007"
 
 #Initialize SCOM SnapIn
 Add-PSSnapin Microsoft.EnterpriseManagement.OperationsManager.Client -ErrorAction SilentlyContinue -verbose:$false

 #Connect to Production SCOM 2007 Env.
 $null = New-ManagementGroupConnection -ConnectionString $SCOMServer
  
 #Connect to SCOM Provider
 Push-Location 'OperationsManagerMonitoring::'

 # Get Agents Matching Name
 $SCOM2007Names = Get-ManagementServer |Get-Agent |Where {$_.Name -like "$($startOfName)*"}
 $SCOM2007Names | Foreach-Object {Write-Verbose $_.Name} | Sort-Object
 $SCOM2007Numbers = $SCOM2007Names | Foreach-Object {[int]($_.Name -replace '\D').Trim() } | Sort-Object
 Write-Verbose "$($SCOM2007Numbers.Count) Matching entries found"
 
 # Return to previous location
 Pop-Location
 
# Merge arrays adding a zero so we allways start issuing numbers from the beginning (ie 001)
$list = @(0) + $VMNumbers + $ADnumbers + $DNSNumbers + $SCCM2012Numbers + $SCCM2007Numbers + $SCOM2007Numbers

# Remove Duplicates numbers from the array and sort into numerical order
$list = $list | Sort-Object -Unique

Write-Verbose "Used numbers after sorting: $($list)"

# Determine if next server name is a gap in the sequence in the array
for($i=0; $i -lt $list.length; $i++) {
 if( $list[$i+1]-$list[$i] -gt 1) {
  # The gap between the current server number and the next element in the array is greater than 1
  # So we have an available number we can use.
  # TODO: - Add support for consecutive numbers IE build 6 servers with consecutive numbers.
  $num = "{0:000}" -f ($list[$i]+1)
  break
 }
}

# If no gap found in the sequence then use the next number from the sequence in the array
if ($num -eq $null) {
 $num = "{0:000}" -f (($list[-1]+1))
}

# Construct new name
$NewComputerName = "{0}{1}" -f $startOfName,$num

# Create DNS Record to 'reserve / mark the name as in use'
Write-Verbose "Creating DNS Reservation"
New-DnsRecord -Name $NewComputerName -IPAddress "127.0.0.1" -Zone blah.local -Type A -Server $DNSServer

write-output $NewComputerName

Thursday, 2 April 2015

Status messages from SCCM task sequences via SQL.

I've got an automated server build process working well now using System Center Orchestrator and System Center Configuration Manager (SCCM), currently we poll the status messages via custom c# code within an Integration pack to determine the task sequence status. What I wanted to do was replicate the status message viewer output within our email step.

I've knocked together a crude SQL query to get this using the provided view. Please bear in mind SQL isn't my strong point.. I can get by with the help of Google! So I just thought I would provide a copy of the query here in case it is of use to anyone else..

You just need to remove or edit the WHERE clauses for the date and the machinename values where appropriate.

SELECT
 CASE [Severity] 
  WHEN '1073741824' THEN 'Informational' 
  WHEN '-1073741824' THEN 'Error' 
  WHEN '-2147483648' THEN 'Warning' 
 END AS Severity
  ,[SiteCode]
  ,[Time]
  ,[MachineName]
  ,[Component]
  ,[MessageID],
 CASE [MessageID] 
  WHEN '11124' THEN ('The task sequence execution engine started the group (' + [InsStrValue3] + ').')
  WHEN '11127' THEN ('The task sequence execution engine successfully completed the group (' + [InsStrValue3] + ').') 
  WHEN '11128' THEN ('The task sequence execution engine skipped the disabled action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ').') 
  WHEN '11130' THEN ('The task sequence execution engine skipped the action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ').')
  WHEN '11134' THEN ('The task sequence execution engine successfully completed the action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ') with exit code ' + [InsStrValue4] + ' Action output: ' + (COALESCE([InsStrValue5], '') + '' + COALESCE([InsStrValue6], '') + '' + COALESCE([InsStrValue7],'')+ COALESCE([InsStrValue8],'')+ COALESCE([InsStrValue9],'')+ COALESCE([InsStrValue10],''))) 
  WHEN '11135' THEN ('The task sequence execution engine failed execuiting the action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ') with exit code ' + [InsStrValue4] + ' Action output: ' + (COALESCE([InsStrValue5], '') + '' + COALESCE([InsStrValue6], '') + '' + COALESCE([InsStrValue7],'')+ COALESCE([InsStrValue8],'')+ COALESCE([InsStrValue9],'')+ COALESCE([InsStrValue10],'')))  
  WHEN '11138' THEN ('The task sequence execution engine ignored execution failure of the action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ').')  
  WHEN '11140' THEN ('The task sequence execution engine started execution of a task sequence.')  
  WHEN '11142' THEN ('The task sequence execution engine performed a system reboot initiated by the action (' + [InsStrValue2] + ') in the group (' + [InsStrValue3] + ').')  
  WHEN '11144' THEN ('The task sequence execution engine from a non-client started execution of a task sequence.')
 END AS Description 
FROM [CM_SiteCodeHere].[dbo].[vStatusMessagesWithStrings] (NOLOCK) 
WHERE MachineName = 'MyServerNameHere'
 AND Component in ('Task Sequence Engine','Task Sequence Manager','Task Sequence Action')
 AND Time BETWEEN '2015-04-02 08:30' AND GETDATE() 
ORDER BY Time DESC
  
Change the site code in the query.. CM_SiteCodeHere and apollogies for the formatting here.