SNMP Agent Functions

The problem

I want to use some automation tools to help me keep this contact info up to date, but Windows support for getting this string programmatically isn’t very good. Most tutorials for managing this value involve clicking around in the GUI. Below is the code for a couple easy functions to get and set this value remotely on large numbers of Windows machines, and a helper I wrote for gathering the names of the machines I want to manage based on their Active Directory OU’s, just because that happened to be useful to me. Below the code I’ll talk briefly about how I’m using the functions with Jenkins and Pester testing to automate notifications when someone leaves the company and I need to make sure some one new takes responsibility for a server.

function Get-SNMPAgent
{
<# 	.SYNOPSIS 		Reads the SNMP Agent string value from a Windows computers registry. 	.DESCRIPTION 		Uses the Microsoft.Win32.RegistryKey class to open registry keys on remote computers even if WinRM is not enabled. 		Output is a series of PSObject's with two string properties: computer and agent. 	.PARAMETER computer 		The name of the computer from which you would like to read the agent value 	.EXAMPLE 		Get-SNMPAgent "Server1" 		Gets the SNMP agent for the named server 		 	.EXAMPLE 		$computers = "computer1","computer2","computer3" 		C:\PS>$computers | Get-SNMPAgent

		Gets the agent values for an array of computers

	.EXAMPLE
		Get-Content c:\Users\user1\documents\Computers.txt | Get-SNMPAgent

		Read the contents of a text file with computer names and get their SNMP Agent values

	.EXAMPLE
		Get-ComputersByOU -ou "OU=servers,DC=domain,DC=com" | Get-SNMPAgent

		Get the SNMP Agent values for all computers in the specified Active Directory OU.

	.NOTES
		AUTHOR: BILL HURT
		DATE: 2015-7-02
		GITHUB: https://github.com/randomNoun7
#>

	param
	(
		[parameter(Mandatory = $true, ValueFromPipeline = $true)]
		[String]
		$computer
	)

	process
	{

		try
		{
			$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $computer)

			$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\SNMP\Parameters\RFC1156Agent", $true)
		}
		catch
		{
			return (New-Object -TypeName PSObject -Property (@{ computer = "$computer"; agent = "<Failure>" }))
		}

		Write-Output (New-Object -TypeName PSObject -Property (@{
			computer = "$computer";
			agent = $key.GetValue("sysContact")
		})
		)

		$reg = $NULL
		$key = $NULL
	}
}

function Set-SNMPAgent
{
<# 	.SYNOPSIS 		Set the SNMP Agent value of a remote computer via Microsoft.Win32.Registry class. 	.DESCRIPTION 		Open remote registry even if WinRM is not enabled. Set agent to the value passed into the -value parameter. 		Enforce usage of a valid email address (using UserPrincipalName as a proxy) or AD Group name via ADSI 	.PARAMETER  computer 		The name of the computer to modify the Agent string. 	.PARAMETER  value 		Value to set the Agent string. 	 	.PARAMETER passThruFailed 		When set, pass any computers that failed to accept agent string update through to output. 	.EXAMPLE 		Set-SNMPAgent -computer Computer1 -value user1@domain.com 	 		Sets the SNMP Agent string of Computer1 to user1@domain.com 	.EXAMPLE 		"computer1","computer2","computer3" | Set-SNMPAgent -value user1@domain.com 		Sets the SNMP Agent values of all three computers to user1@domain.com 	 	.EXAMPLE 		Get-ComputersByOU -ou "OU=servers,DC=domain,DC=com" | Get-SNMPAgent | Where agent -eq "oldUser@domain.com" | Set-SNMPAgent -value "AD-Group-Name" 	 		Set all computers an in OU with an old users email in the Agent value to a AD Group name instead. 	 	.NOTES 		AUTHOR: BILL HURT 		DATE: 2015-7-02 		GITHUB: https://github.com/randomNoun7 	 #>

	param
	(
		[parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
		[String]
		$computer,
		[parameter(Mandatory = $true, ValuefromPipeline = $false)]
		[string]
		$value,
		[switch]
		$passThruFailed
	)

	begin
	{
		$root = [adsi]''

		$searcher = New-Object DirectoryServices.DirectorySearcher ($root)

		$searcher.Filter = "(&(objectClass=user) (UserPrincipalName=$value))"

		$adObject = $searcher.FindAll()

		if (!($adObject.Properties))
		{
			$root = [adsi]''

			$searcher.Filter = "(&(objectClass=group) (Name=$value))"

			$adObject = $searcher.FindAll()
		}

		if ($adObject.properties)
		{
			$value = ($adObject.Properties.userprincipalname, $adObject.Properties.name -ne $NULL)[0]
		}
		else
		{
			throw "invalid -value parameter. Must be email address for a user or the name of a group."
		}
	}

	process
	{

		try
		{
			$reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $computer)

			$key = $reg.OpenSubKey("SYSTEM\CurrentControlSet\Services\SNMP\Parameters\RFC1156Agent", $true)

			$key.SetValue("sysContact",$value)
		}
		catch
		{
			if ($passThruFailed)
			{
				return (New-Object -TypeName PSObject -Property (@{ computer = "$computer"; agent = "<Failure>" }))
			}
		}
	}
}

function Get-ComputersByOU
{
<# 	.SYNOPSIS 		Get string computer name values for all computers in a specified OU 	.DESCRIPTION 		Takes an AD Path and returns only the string computer name of the computers it finds in that path. By default the search will be recursive. 	.PARAMETER  ou 		The path to an Active Directory OU to search for computers. By default the path subtree is also searched. Accepts normal AD path syntax. 	 	.PARAMETER noRecurse 		Disable Subtree searching. 	.EXAMPLE 		Get-ComputersByOU -ou "OU=servers,OU=region1,DC=domain,DC=com" 		Get all of the servers in the specified OU and all sub OU's. Return only the string Name property. 	 	.EXAMPLE 		Get-ComputersByOU -ou "OU=region1,DC=domain,DC=com" -noRecurse 		 		Get only computers in the region1 OU and not any sub folders. 	.EXAMPLE 		$ouCollection = "OU=SQLServers,OU=region1,DC=domain,DC=com","OU=AppServers,OU=region1,DC=domain,DC=com" 		PS C:\>$ouCollection | Get-ComputersByOU -noRecurse

		Get computers in a list of OU's searching only the immediate paths, not subtree's

	.NOTES
	AUTHOR: BILL HURT
	DATE: 2015-7-02
	GITHUB: https://github.com/randomNoun7
#>

	param
	(
		[parameter(Mandatory = $true,ValueFromPipeline=$true)]
		[String]
		$ou,
		[switch]
		$noRecurse
	)

	process
	{
		$root = [adsi]"LDAP://$ou"

		$searcher = New-Object DirectoryServices.DirectorySearcher ($root)

		$searcher.Filter = "objectCategory=computer"

		if ($noRecurse)
		{
			$searcher.SearchScope = "OneLevel"
		}

		$computers = $searcher.FindAll()

		Write-output $computers.properties.name
	}

}

Lets talk about it

Now we have a nice programatic way of getting the contact info for a bunch of servers, but how do we turn that into an actual process?

For me, the answer is that we turn this into a Jenkins job. Many of you are familiar with Jenkins as a build server, but if you think a little more generally, it’s also just a great general task runner, especially for anything that you can express in terms of pass fail testing, and that’s where Pester comes in.

In the Jenkins job the tests look like this:

  1. Gather my list of computers using the Get-ComputersByOU function.
  2. Feed that list to the Get-SNMPAgent function to get your computer and agent objects.
  3. Use Pester tests to examine the owner/server pair to see if the combination is still valid. You can use any logic you like for this test. In my case the current test is simple. Is this user still an enabled user in Active Directory or a valid group. If not it means they probably left the company, and any servers they are still responsible for will be represented by failed Pester tests.
  4. Jenkins takes the Pester output (did you know Pester can output NUnit XML files? It’s fantastic.) and marks the “build” as either successful (all servers have valid owners), or failed and takes action accordingly. In my case it sends me an email that one of my servers needs a new owner.

In a later blog post we’ll look at some of the code to make this happen. It’s very short and elegant and will help ensure that anyone on the network who wants to know something about a server will always know exactly who to contact.

Thanks for reading.

Advertisements