Search code examples
puppet

Puppet Facts - Searching Array of Objects


I'm running Puppet Agents on Azure VMs.

There is a fact that is auto-generated called "az_metadata" which holds lots of good information about the machine.

Here's a snippet of the az_metadata fact that I'm trying to search:

"az_metadata":{
   "compute":{
      "tagsList":[
       {
         "name": "Comp"
         "value": "Tech"
       },
       {
         "name": "Dept"
         "value": "R&D"
       },
       {
         "name": "Env"
         "value": "Dev"
       },
       {
         "name": "Purp"
         "value": "Port"
       },
       ]
   }
}

The value I need from this fact, is the value of the hash object whose name is "Env". In the example above, the value I'm searching for is "Dev."

What's the best way to get that value?

What I've tried so far:

I'm working inside Puppet Agent Cmd Prompt on the client to debug. I've set stringify_facts=false in the puppet.conf on the agent machine.

I can peek at the value by running:

$found = $facts[az_metadata][compute][tagsList][2]['value']
notify{"$found":}

Returns:

Notice: /Stage[main]/Main/Node[default]/Notify[Dev]/message: defined 'message' as 'Dev'

This looks promising, but this isn't going to work out in the wild as I will not know which order the tags are in. So, it could be in object 2 or 1 or any number of elements. Next, I thought maybe I could iterate the array to find the value I want.

each($facts[az_metadata][compute][tagsList]) | $hash |{
    if($hash['name'] == 'Environment'){
        
        notify{ "$hash":
            name => $hash[value]
        }
        $department = $hash[value]
    }    
}
notify { "Testing Fact Data":
    name => $department,
}

This yields results that are both encouraging, and confusing. The notify inside the each statement looks correct, but when I try and save that value to a variable and use it later (in this case, just running another notify) the value appears to be blank.

Notice: Dev
Notice: /Stage[main]/Main/Node[default]/Notify[{name => Environment, value => Dev}]/message: defined 'message' as 'Dev'
Notice: Testing Fact Data
Notice: /Stage[main]/Main/Node[default]/Notify[Testing Fact Data]/message: defined 'message' as 'Testing Fact Data'
Notice: Applied catalog in 0.39 seconds

So, it kind of worked. But my variable didn't hold the value? I tried playing with variable scopes and didn't find much. The above code doesn't feel ideal, at all. Is there a better way? Or, is there a way to fix my not-so-great solution of iterating through the array?


Solution

  • This yields results that are both encouraging, and confusing. The notify inside the each statement looks correct, but when I try and save that value to a variable and use it later (in this case, just running another notify) the value appears to be blank.

    Yes, I imagine that would take a lot of people by surprise.

    Remember always that Puppet variables can be set only once. But it is relatively common to want to use variables in a code block such as you are using, and those may need to be set to different values for different executions of the block. For that reason, every execution of a block establishes a local scope for variable names, with the effect that a variable created (by assignment) within a block is separate from and unrelated to any variable of the same name existing outside the block, and also from any variable of that name created during a different execution of the block.

    On the other hand, such local scopes are "child scopes" of the enclosing scope -- such as that established by a class or defined-type body -- which means that names defined in their parent scopes are visible to them unless overridden by a local definition.

    Note well that most Puppet statements do not establish local scopes. Code blocks are the context where you are most likely to run into that.


    For your purposes, I would start by engaging Puppet's filter() function to select the hash(es) of interest. That might look like this:

    $envs = $facts['az_metadata']['compute']['tagsList'].filter() |$hash| {
      $hash['name'] == 'Env'
    }
    

    The result will be an array of those tagsList hashes whose names are exactly 'Env'. If you are prepared to assume that there will always be exactly one match then you could proceed with

    $department = $envs[0]['value']
    

    That would also be ok for the case where there are multiple matches but you don't care which you use.

    On the other hand, if you want to accommodate the possibility that there are no matches, then you might want to use the dig() function:

    $department = $envs.dig(0, 'value')
    

    That will get you an undef value instead of an error in the event that there are no matches or that the first match does not have a 'value' key.