This post builds on the recent sterling work of Tao Yang, leveraging the System Center 2012 R2 U2 PowerShell Web Browser widget to display a Google Map, centred on address attributes derived via a custom discovery which interrogates a custom key seeded in the Windows Registry. Please check out Tao’s inspiring post here before you proceed, as the following makes NO SENSE without doing that. Mandatory reading people!
I noticed and discussed (well, if you can call exchanging tweets a discussion) some of the limitations and design choices Tao made when putting his initial offering together. The ones I thought that were interesting problems and likely beatable were:
- Being welded to Google Maps, as it’s accuracy on matching geographical location to address lookups is higher than the alternative mapping providers
- No support for multiple object selection, or put in Google Maps terms, only one pin per map view, which turns out to be the last SCOM object selected
To tackle these two challenges, I needed to do three things:
- Get the longitude & latitude co-ordinates Google is using when it renders any given address search, so we can feed these to other mapping services
- Work out the URL syntax for dropping multiple pins onto a map in a mapping service
- Work out how to construct the PowerShell Microsoft.SystemCenter.Visualization.Component.Library!Microsoft.SystemCenter.Visualization.Component.Library.WebBrowser.Schema/Request request to handle the multiple objects being selected
Who needs Geocaching? Geocoding is where it’s at…
The first action was pretty easy. I took to learning how to leverage the Google Geocode API service, which allows you enter a plain English address or location query and it will return, in either JSON or XML (requesters choice), the relevant geographical data. It’s pretty accessible, and XML is so easily parsed via PowerShell that it’s frankly trivial.
Feeding the address attributes from the SCOM discoveries associated with our custom class into Geocode API calls return the XML (captured to variable $XML, in this example) from which we get the attribute we care about, specifically $XML.GeocodeResponse.result.geometry.location.lat & $XML.GeocodeResponse.result.geometry.location.lng
One Map to Rule Them All (once we’ve stolen Google’s co-ordinates)
The second action was interesting from a research point of view. Some observations:
- Google Maps does not support insertion of multiple pins onto a map via URL construction
- Google Static maps does support multiple pins, but the sizes are limited and fixed. It also requires an API key, which may expose you to cost, depending on your use-case
- Bing maps supports multiple pins via URL construction and these pins can be assigned properties (A tag, name & description), all in band!
- Bing ‘Static Maps’ AKA Virtual Earth supports labelling of pins & colouring of pins. However, it also requires an access API, looks like puke (frankly) and only puts out fixed size maps, albeit that the user can define.
So, given these experiences, I decided to work with straight Bing Maps. Although it has the same ‘task area open by default’ issue that Google Maps has, it has enough other features available to make it worth my time. The Bing Maps Map URL structure is also pretty well documented, which reduces our ‘time to value’
Finally, The (refried) SCOM Bit!
The final challenge was in working out how to adjust Tao’s PowerShell Web Browser script to build a multiple pin map, on Bing Maps rather than Google Maps.
Thanks to Tao’s clean initial work, the code pretty much speaks for itself, but I will talk about it, just in case you’re interested…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
Param($globalSelectedItems) $dataObject = $ScriptContext.CreateInstance("xsd://Microsoft.SystemCenter.Visualization.Component.Library!Microsoft.SystemCenter.Visualization.Component.Library.WebBrowser.Schema/Request") $dataObject["BaseUrl"]="http://bing.com/maps/default.aspx" $parameterCollection = $ScriptContext.CreateCollection("xsd://Microsoft.SystemCenter.Visualization.Component.Library!Microsoft.SystemCenter.Visualization.Component.Library.WebBrowser.Schema/UrlParameter[]") $WebConsoleURL = [System.Uri]((Get-SCOMWebAddressSetting).WebConsoleURL) $BaseExplorerURL = $WebConsoleURL.Scheme + "://" + $WebConsoleURL.Host + "/MonitoringView/ResultViews/ViewTypeHealthExplorer.aspx?targetId=" foreach ($globalSelectedItem in $globalSelectedItems) { $globalSelectedItemInstance = Get-SCOMClassInstance -Id $globalSelectedItem["Id"] $DisplayNameProperty = $globalSelectedItemInstance.GetMonitoringProperties() | Where-Object {$_.name -match "DisplayName"} $StreetProperty = $globalSelectedItemInstance.GetMonitoringProperties() | Where-Object {$_.name -match "Street"} $CityProperty = $globalSelectedItemInstance.GetMonitoringProperties() | Where-Object {$_.name -match "City"} $StateProperty = $globalSelectedItemInstance.GetMonitoringProperties() | Where-Object {$_.name -match "State"} $CountryProperty = $globalSelectedItemInstance.GetMonitoringProperties() | Where-Object {$_.name -match "Country"} $ID = $globalSelectedItem["Id"] $DisplayName = $globalSelectedItemInstance.GetMonitoringPropertyValue($DisplayNameProperty) $Street = $globalSelectedItemInstance.GetMonitoringPropertyValue($StreetProperty) $City = $globalSelectedItemInstance.GetMonitoringPropertyValue($CityProperty) $State = $globalSelectedItemInstance.GetMonitoringPropertyValue($StateProperty) $Country = $globalSelectedItemInstance.GetMonitoringPropertyValue($CountryProperty) $Address = $Street + ",%20" +$City + ",%20" + $State + ",%20" +$Country $GeocodeXML = New-Object System.Xml.XmlDocument $GeocodeXML.Load("https://maps.googleapis.com/maps/api/geocode/xml?address="+$Street+",+"+$City+",+"+$State+",+"+"$Country"+"&sensor=true") $GeoCodeResult = $GeocodeXML.GeocodeResponse.result If ($GeoCodeResult -is [System.Array]) { $TheResult = $GeoCodeResult[0] } else { $TheResult = $GeoCodeResult } $lat = $TheResult.geometry.location.lat $lng = $TheResult.geometry.location.lng If ($WebConsoleURL) {$ParameterString = $ParameterString + 'point.'+$lat+'_'+$lng+'_'+ $DisplayName +'_'+$Address+'_' + $BaseExplorerURL + $ID +'~'} Else {$ParameterString = $ParameterString + 'point.'+$lat+'_'+$lng+'_'+ $DisplayName +'_'+$Address+'__~'} } $SubstringCount = $ParameterString | Select-String "point" -AllMatches $PointParameter = $ScriptContext.CreateInstance("xsd://Microsoft.SystemCenter.Visualization.Component.Library!Microsoft.SystemCenter.Visualization.Component.Library.WebBrowser.Schema/UrlParameter") $PointParameter["Name"] = "sp" If ($SubstringCount.Matches.Count -eq 1 ) { $PointParameter["Value"] = ($ParameterString.Substring(0,$ParameterString.Length-1)) $LevelParameter = $ScriptContext.CreateInstance("xsd://Microsoft.SystemCenter.Visualization.Component.Library!Microsoft.SystemCenter.Visualization.Component.Library.WebBrowser.Schema/UrlParameter") $LevelParameter["Name"] = "lvl" $LevelParameter["Value"] = "15" $parameterCollection.Add($LevelParameter) } Else {$PointParameter["Value"] = $parameterString } $parameterCollection.Add($PointParameter) $dataObject["Parameters"]= $parameterCollection $ScriptContext.ReturnCollection.Add($dataObject) |
The significant technical changes are:
- The base URL is Bing Maps
- The constructed parameters use Bing Maps Syntax
- Instead of constructing the $parameter variable in the foreach ($globalSelectedItem in $globalSelectedItems) loop, which gets overridden each time leaving the values calculated from the last $globalSelectedItem, we construct a string, which we append to during each loop
- Each time we loop, we lookup the address of the monitored object against the Google Geocode API and capture the returned longitude and latitude to variable
- The constructed string is built from properties of the monitored object, the Web Console URL setting of the management group and the Geocode co-ordinates
- AFTER the foreach ($globalSelectedItem in $globalSelectedItems) loop completes, we instantiate the URLparameter parameters
- Some logic here – If only one pin is applied to the map, we drop the trailing tilde from our URL string, as isn’t needed. We also create a new parameter which sets the zoom level to 15 (street view, rather than satellite level). We count the pins by querying for substrings in our constructed string
- We then pass all populated parameters into the parameter collection, which is then passed into the data object
They do say seeing is believing, so if you want to see it in action, just replace the Map widget PowerShell script block section with my fork of Tao’s code when following his blog post (you read that, yeah?), and you’ll get the following benefits:
- Bing Maps over Google Maps, if you’re a Microsoft purist, or have to be for a living
- Ability to select multiple SCOM objects, resulting in an auto-scaled multi-pin map
- Single object selection results in a sensible zoom level on the map view
- Pins are labelled, with address in the description
- The pins hyperlink will take you to the SCOM Web Console Health Explorer for the object concerned
One enhancement I do like the idea of – If one was to expand the custom registry with, say, a URL for an internal Wiki or CMDB, you could have the hyperlink on Bing Maps take you there. Linking it to the Health Explorer is a little redundant, as being in a SCOM console you have other ways of getting there. Surfacing your other internal (non-SCOM) data would be more valuable (but less universally applicable). Also wondering if I might make a ‘static map’ version to get rid of the ‘task area of death’ issue for non-interactive use cases, such as NOCs or the like.
Please note – I’m not as kind as Tao, nor do I want to steer well deserved traffic away from his site, so I wont be uploading a management pack, sealed or otherwise, with my revisions contain within. Please download them from his post, and retrofit my modification. It’ll be fun, trust me!
Here’s a video the dashboard in action, so you can decide if the ten minutes it will take you to implement is going to be time well invested:
SCOM 2012 PowerShell Web Widget Dashboard calling Bing Maps & Google Geocode
*** Update ***
Tao spotted a bug in my code, where if the Google Geocode API returned an array of results/address matches, bad things happened. We’ve put a catch in there for that now! Thanks again Tao!
Leave a Reply