Network Discovery – Part 2
Time for Part 2 of talking about Network Discovery, the Powershell version. Part 1 can be found here.
Changing Scope
Not that unusual, but any script will evolve as you write it and this one was no different. My original intention was to simply recreate what the vbScript version of this script was doing but add a multi-threading twist so it ran much faster. But one of the things about Powershell is that it is very different from vbScript and I soon had to come to grips with that. Yes, I can easily create objects and CSV’s with Powershell (much easier than I can with vbScript in fact) and text files too but is that really what Powershell is about? Also, one thing about Powershell is its flexibility–the whole point of having a script output objects instead of text is so someone can manipulate the data anyway they want.
So how do I apply these principles to Network Discovery? I honestly wrestled with this for a while. Should I write two scripts? One that discovers the data and emails it back “home” and then a second script to process it? That seems clunky, not to mention trying to write something to anticipate everyone’s needs is pretty difficult and time-consuming. On the other hand I want to produce something to show and not just output raw data. One other thing to consider is that as a consultant you might not be at the client while the script is running, what if you have to leave before it finishes?
The solution here is to produce some HTML reports of the discovery as well as the raw data and email it. The raw data can be saved as an XML file, and this will retain all of the properties and variable types so it can be easily imported into your own scripts to manipulate any way you want.
Subnet Discovery and Multi-threading
If you’ve read my blog at all, you know the next part is pretty standard. Now we just have to submit the Scriptblock we defined in my last post here, with some multi-threading here and some progress feedback from here. The cool part is the resulting PSObject that comes back has all the information about network devices on it, enough for an IP list, workstation list and server list. All we have to do is extract it out of the object.
$WSFragment = $Data | Where { $_.OS -notlike "*Server*" -and $_.OS -ne $null } | Select 'Computer Name',OS,'OS Service Pack','Make/Model','Serial Number','Service Tag',IP,'Number of CPUs',Memory,@{Label="Hard Drives";Expression={ConvertTo-ArrayToString $_.'Hard Drives'}} | ConvertTo-Html -Fragment | Set-AlternatingRows -CSSEvenClass even -CSSOddClass odd $IPFragment = $Data | Select IP,'Computer Name' | ConvertTo-Html -Fragment | Set-AlternatingRows -CSSEvenClass even -CSSOddClass odd $HTML = $Data | Where { $_.OS -like "*Server*" } | Select 'Computer Name',OS,'OS Service Pack','Make/Model','Serial Number','Service Tag',IP,'Number of CPUs',Memory,@{Label="Hard Drives";Expression={ConvertTo-ArrayToString $_.'Hard Drives'}} $HTML = $HTML | ConvertTo-Html -Head $HTMLCSS -PreContent "<h2>Network Discovery for $Company</h2><br><h3>Server List</h3>" -PostContent "<br><h3>Workstation List</h3><br>$WSFragment<br><h3>IP List</h3><br>$IPFragment" $HTML | Set-AlternatingRows -CSSEvenClass even -CSSOddClass odd | Out-File "$MyPath\nd.html"
We have 3 reports, so we’ll have to split the $Data object into three too. What I did here is create a “Workstation” fragment and an “IP Fragment”. The idea here is use ConvertTo-HTML but use the -Fragment parameter so that I only get the HTML table, not the rest of the HTML code. On the “Server Fragment” we don’t use the -Fragment parameter but I include the other two fragments in the -PostContent parameter which ultimately allows me to combine all three reports into a single HTML file. The scary part was it worked the first time, which almost never happen to me!
Active Directory Discovery
I know I said I’d be using LDAP to do some Active Directory discovery but it turns out that .NET had some much more efficient ways of doing this. And once I found one that could do it than I found I could do all of them using .Net. Very fast and pretty easy to do.
There were some complications though, isn’t there always? It’s not enough to know all the domain controllers and to know all of your Active Directory sites–if you’re not sure what a site is, read here–but you need to know which domain controllers are in which sites! Turns out that’s easy to do but the hard part is how do you store the data? I could use a technique of combining the two, something like “domaincontroller:sitename” and then you’d have to use the Split method on the “:” to pull the two strings apart but that’s not too intuitive and would make using the data in your own script much more difficult. I decided to store both in separate arrays that have a 1 for 1 relationship. No way to do that in Powershell, unfortunately, other than just having two arrays and just knowing that element 0 in one array relates to element 0 in the other, and so forth. I guess I could have created two PSObjects, one with all the other AD information in it, and than one with Site and computer name in it but I really wanted to keep things in one object so decided to go that way.
Let me know what you think!
HTML and Me
My HTML skills are pretty weak, and I have to be honest this script sat finished and ready to go all except for a simple HTML page to display the Active Directory information. This was mostly me avoiding having to write more HTML. It’s not so much that it’s hard as I find it very messy, and the formatting doesn’t work the way I expect it to. In fact, originally I had created a method within the Set-AlternatingRows function to create an ordered list within a standard table (created from ConvertTo-HTML) and this worked “OK”, but the final output left a lot to be desired. Which meant I had to do a full page.
So I busted out Notepad++ and began the painful process of writing HTML from scratch. This turned out to be not that bad, but the HTML coding is very basic. This was OK with me because ultimately this was as much about getting the information to you as making it look nice. But, me being me, I had to make it look pretty decent.
I did use the ConvertTo-HTML cmdlet’s to create the tables for Domain Controllers and FSMO Role Holders, which involved similar techniques I used above. Using the -Fragment parameter to just produce the table than simply including the fragment variable in my HTML here-string.
For the Sites and Global Catalogs sections I created a simple function that takes an array and creates a simple ordered list out of it. Here’s the function:
Function ConvertTo-OrderedList { Param ( [array]$List ) $Fragment = "<ul>" ForEach ($Line in $List) { $Fragment += "<li>$Line</li>`n" } $Fragment += "</ul>" Return $Fragment }
Nothing much to it, create a variable with the start of the ordered list code in it, than loop through the array and put the lines in it. Finally add the ordered list ending tag and return it to the calling code.
Sending Email
The last requirement was to send an email with all of this information in it–including XML files of the 2 objects we’ve created with all of the information in it. Normally I would just use the Send-MailMessage cmdlet but I ran into a snag with this. Since the intention of the script is to be running in environments that are new to you, you will need to relay the email off of a public server (can’t rely on a local SMTP relay). This will mean authentication. This works great in Powershell 3.0 as the Send-MailMessage cmdlet supports SSL and alternate port assignments, but Powershell 2.0 doesn’t and it’s more likely this script will be running on Powershell 2.0 than anything else. That means we’ll have to break out the .Net commands for sending email. Not as familiar with this as with Send-MailMessage but eventually figured it out and got some good sending code out there:
$Email = New-Object System.Net.Mail.MailMessage $Email.To.Add($To) $Email.From = "<a href="mailto:martin9700@thesurlyadmin.com">martin9700@thesurlyadmin.com</a>" $Email.Subject = "Network Discovery for $Company" $Email.Body = "Network discovery completed." ForEach ($Attachment in $Attachments) { $Att = New-Object System.Net.Mail.Attachment $Attachment $Email.Attachments.Add($Att) } $SMTPClient = New-Object System.Net.Mail.SmtpClient($SMTPServer) If ($MailAuthUser) { $SMTPClient.Port = $Port $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($MailAuthUser,$Password) $SMTPClient.EnableSSL = $true } $SMTPClient.Send($Email)
Should be easy enough to follow, except for $Attachments. This is an array I created during the creation of the reports to point where the files are located. You have to use the System.Net.Mail.Attachment object and add each element of the array to the $Email object. After that I detect if we have a Mail Authorization User, and if we do we’re going to assume an SSL connection is required and setup the credentials as needed. After that we send it off on its way.
Hope you enjoy! As usual you can find the script on Spiceworks!
[…] Read Part 2 of this exciting series! […]