Forum Discussion

ldoodle's avatar
ldoodle
Icon for Advisor rankAdvisor
8 months ago

Dynamic Device Groups - constrain membership to parent folder

Hi,

We have the following hierarchy setup:

Our company is top level, e.g. LOGICMONITOR
|--Customer Name, e.g. GOOGLE, MICROSOFT
|----’All Devices’ static group (when we add new devices, we add them here)
|----Category named group, e.g. ‘Infrastructure’ static group (with no actual devices added here)
|------Function named group, e.g. ‘Active Directory’, ‘DNS Server’, etc. dynamic group with custom query (hasCategory("MicrosoftDomainController"), hasCategory("Windows_DNS") etc.)

So:

LOGICMONITOR
|--GOOGLE
|----All Devices
|------DEVICE1 (pretend this is an AD server)
|------DEVICE2 (pretend this is an AD server)
|----Infrastructure
|------Active Directory
|--------DEVICE1 (correct)
|--------DEVICE2 (correct)
|--------DEVICE3 (incorrect)
|--------DEVICE4 (incorrect)
|------DNS Server
|--MICROSOFT
|----All Devices
|------DEVICE3 (pretend this is an AD server)
|------DEVICE4 (pretend this is an AD server)
|----Infrastructure
|------Active Directory
|--------DEVICE1 (incorrect)
|--------DEVICE2 (incorrect)
|--------DEVICE3 (correct)
|--------DEVICE4 (correct)
|------DNS Server
 

The problem with this, and it is partly a misunderstanding on my part, is that I expected the (nested) dynamic groups to automatically be constrained to their parent container.

How do I achieve this? The only thing I can think of is having the function-named group custom query be something like this:

join(system.groups,",")=~"GOOGLE/_All Devices" && hasCategory("MicrosoftDomainController")

But that means every dynamic group for every customer will need that, which is fine. But, is there a better, more native/default way?

Thanks

  • Anonymous's avatar
    Anonymous
    8 months ago

    So it seems tenant id would only work if each customer resourced is statically placed in a customer nested folder in the first place.

    Yes, the relationship between the device and the customer will have to be defined by you somewhere.

    It does seem a shame (maybe omission) that you can’t map a collector to its collector group and in turn the collector group name, since for us the collector group name is the exact acronym we need.

    You can do this. You just need to make an API call to grab the collector groups. Unintuitively, that wouldn’t be a call to /setting/collector/groups as that doesn’t return the IDs of the collectors in the group. Instead, do a get to /setting/collector/collectors/:id. This will give you back the collector’s group name (among other things, but we can specify that we only want collectorGroupName). 

    So, it would be something like this:

    import com.santaba.agent.util.Settings
    import groovy.json.*
    import javax.crypto.Mac
    import javax.crypto.spec.SecretKeySpec
    import org.apache.commons.codec.binary.Hex
    import org.apache.http.client.methods.*
    import org.apache.http.entity.*
    import org.apache.http.HttpEntity
    import org.apache.http.impl.client.*
    import org.apache.http.util.EntityUtils

    deviceCollectorId = hostProps.get("system.collectorId")
    collector = LM_API("GET","/setting/collector/collectors/${deviceCollectorId}","lmaccess",[:],["fields": "collectorGroupName"])
    if (collector.code == 200){
    println("organization=${collector?.body?.collectorGroupName}")
    }
    return 0

    def LM_API(httpVerb, endpointPath, creds="lmaccess", data = [:], queryParams=[:]){
    def api_id = hostProps.get("${creds}.id")
    def api_key = hostProps.get("${creds}.key")
    def api_company = hostProps.get("${creds}.company") ?: Settings.getSetting(Settings.AGENT_COMPANY)
    def queryParamsString = queryParams.collect{k,v->"${k}=${v}"}.join("&")
    def url = "https://${api_company}.logicmonitor.com/santaba/rest${endpointPath}?${queryParamsString}"
    epoch_time = System.currentTimeMillis()
    def json_data = JsonOutput.toJson(data)
    if ((httpVerb == "GET") || (httpVerb == "DELETE")){requestVars = httpVerb + epoch_time + endpointPath}
    else {requestVars = httpVerb + epoch_time + json_data + endpointPath}
    hmac = Mac.getInstance("HmacSHA256")
    secret = new SecretKeySpec(api_key.getBytes(), "HmacSHA256")
    hmac.init(secret)
    hmac_signed = Hex.encodeHexString(hmac.doFinal(requestVars.getBytes()))
    signature = hmac_signed.bytes.encodeBase64()
    CloseableHttpClient httpclient = HttpClients.createDefault()
    if (httpVerb == "GET"){http_request = new HttpGet(url)}
    else if (httpVerb == "PATCH"){
    http_request = new HttpPatch(url)
    http_request.setEntity(new StringEntity(json_data, ContentType.APPLICATION_JSON))}
    else if (httpVerb == "PUT"){
    http_request = new HttpPut(url)
    http_request.setEntity(new StringEntity(json_data, ContentType.APPLICATION_JSON))}
    else if (httpVerb == "POST"){
    http_request = new HttpPost(url)
    http_request.setEntity(new StringEntity(json_data, ContentType.APPLICATION_JSON))}
    else {http_request = null}
    if (http_request){
    http_request.setHeader("Authorization" , "LMv1 " + api_id + ":" + signature + ":" + epoch_time)
    http_request.setHeader("Accept", "application/json")
    http_request.setHeader("Content-type", "application/json")
    http_request.setHeader("X-Version", "3")
    response = httpclient.execute(http_request)
    body = EntityUtils.toString(response.getEntity())
    code = response.getStatusLine().getStatusCode()
    return ["body":new JsonSlurper().parseText(body), "code":code]}
    else {return ["body":"", "code": -1]}
    }

    You’d need to set properties at the root for lmaccess.id, lmaccess.key and optionally, lmaccess.company. If you wanted to generate an API credential set specifically to this task (and you should), just change the prefix in line 13 from “lmaccess” to whatever you want to prefix the properties with.

    This outputs a property called auto.organization with the value of the collector group name of the collector identified by system.collectorId.

  • No I haven’t.

    In Portal Settings it’s already set at ‘tenant.identifier’. How do I then use it on a folder?

    If I applied it to the master _All Devices group, does it just work out what the tenantid is for a given resource and populate the appropriate value?

  • Yeah that’s what I thought. But, if my customer _All Devices group is now a dynamic group, resources won’t land anywhere inside the customer parent folder now to pick up that tenant id value.

    So it seems tenant id would only work if each customer resourced is statically placed in a customer nested folder in the first place.

    I initially interpreted the suggestion of tenant id that LM picks the value from somewhere automatically (like the collector that’s polling it).

    New structure:

    _All Devices static group ← everything gets added here
    |--GOOGLE
    |-----_All Devices dynamic group ← this previously populated based on system.collectordesc value, but I changed the propertysource (shown below) so now use system.collectorid

    Current plan would be to just add additional case statements to cover multiple collectors and when adding new customers.

    It does seem a shame (maybe omission) that you can’t map a collector to its collector group and in turn the collector group name, since for us the collector group name is the exact acronym we need.

    It seems like a resource-level built in system.collectorgroup (collectorgroupid and collectorgroupdesc) property (just like collectorid and collectordesc) would quite easily solve this. Then I could simplify my propertysource to just return collectorgroupdesc and not need any logic at all. If I rename the collectorgroup, next time the propertysource is run devices would pick up that update.

  • Wowsers, that’s amazing! Thank you so much.

    Now… is it possible to define the LM_API function somewhere else for re-use, rather than embedding it in each script?

  • One thing you may want to also keep in mind is supporting multiple collector groups per customer. Since collector groups also group together fail overs, you may have to create multiple ones if you don’t want to failover any customer collector to just any other customer collector. Especially across multiple sites which might not have connections between them or you don’t want to failover to another site over vpn.

  • I’ve just found a little issue with my overall setup. Since I now have a global _All Devices static group where every resource gets added, when adding resources that are say ESX hosts or Windows hosts running Veeam, it’s not going to know the creds for those until it’s been added and the PropertySource has run to apply the property that makes the resource be pulled into a customer group to then inherit these properties! So during the add wizard, I will have to manually specify them when prompted.

    Unlike wmi.user / wmi.pass which I have at the collector group level, the esx and veeam properties don’t seem to work at the collector/collector group level so must be configured at the customer folder level.

    Is there anyway around this, excluding adding resources directly in the customer folder again?

  • Unlike wmi.user / wmi.pass which I have at the collector group level, the esx and veeam properties don’t seem to work at the collector/collector group level so must be configured at the customer folder level.

    the credentials of the user running the collector service will be used for WMI calls 

    Ah, that makes total sense. I didn’t even think of that. Our collector service account uses that same creds as wmi.user/wmi.pass

  • Interestingly, the ‘New UI’ preview Add Wizard seem to handle my scenario; it doesn’t fail on creds and then prompt for appropriate ones. It seems a bit ‘softer’ than old UI.

  • Unlike wmi.user / wmi.pass which I have at the collector group level, the esx and veeam properties don’t seem to work at the collector/collector group level so must be configured at the customer folder level.

    the credentials of the user running the collector service will be used for WMI calls 

    Ah, that makes total sense. I didn’t even think of that. Our collector service account uses that same creds as wmi.user/wmi.pass

    Right, so defining them on the collector/collector group doesn’t really do anything.

    yeah. I just thought the collector service creds were just to get the collector going.