Ask Your Question

Creating Structured External Facts

asked 2016-04-29 10:05:10 -0500

Rob Ogilvie gravatar image

How does one create structured external facts?

Specifically, I am attempting to write an external fact with bash that returns an array of zero or more strings.

edit retag flag offensive close merge delete

3 answers

Sort by » oldest newest most voted

answered 2016-05-24 03:59:55 -0500

FranzCC gravatar image

updated 2016-05-27 04:10:30 -0500

Hi, maybe it's a little bit late but recently i struggled with the same problem
I wanted to have a structured external custom fact , that enumerates my samba shares
I want to provide a fully documented example and hopefully someone will find it useful
Foresightly, i want to mention , that the documented standard paths for custom facts are wrong
I ended up outputting the structed data to directory: /opt/puppetlabs/facter/facts.d/
OS: Debian or Ubuntu (any os which provides these perl libs will work !!)
Packages you'll need:
Puppet: Puppet PE 2016.1.2

The simple script:
Update !!! ("Include" also honored)


use warnings;
use strict;

use Config::Std;
use Data::Dumper;
use JSON;
my $config  = {};
my $include = {};
my $coder   = JSON->new;
my $retval  = {};
my $outfile = '/opt/puppetlabs/facter/facts.d/samba_share.json';

my $retconfig = eval { read_config '/etc/samba/smb.conf' => %{$config} };

foreach my $key ( keys %{$config} ) {
    if ( $key !~ /^global/i ) {
        $retval->{samba_share}->{$key} = $config->{$key};
    if ( $key =~ /^global/i ) {
        if ( ref $config->{$key} eq 'HASH' ) {
            foreach my $global ( keys %{ $config->{$key} } ) {
                if ( $global eq 'include' ) {
                    my $retinc = eval {
                        read_config qq{$config->{$key}->{$global}} =>
                foreach my $incshare ( keys %{$include} ) {
                    $retval->{samba_share}->{$incshare} = $include->{$incshare};

my $json = $coder->canonical->pretty->encode($retval);
open( FILE, ">$outfile" ) or die "Cannot open file $outfile";
printf( FILE "%s", $json );

Create a cronjob on the node (10m), dontt knox how to get the facter TTL, so 10m should be sufficient

Local node:
Command: facter samba_share

  example-share => {
    browsable => "yes",
    comment => "Example Share",
    force group => test",
    force user => "test",
    guest ok => "no",
    guest only => "no",
    inherit permissions => "yes",
    path => "/var/shares/test",
    read only => "no",
    writable => "yes"
  homes => {
    browseable => "no",
    comment => "Home Directories",
    create mask => "0700",
    directory mask => "0700",
    read only => "yes",
    valid users => "%S"
  print$ => {
    browseable => "yes",
    comment => "Printer Drivers",
    guest ok => "no",
    path => "/var/lib/samba/printers",
    read only => "yes"
  printers => {
    browseable => "no",
    comment => "All Printers",
    create mask => "0700",
    guest ok => "no",
    path => "/var/spool/samba",
    printable => "yes",
    read only => "yes"

Via MCO (using username peadmin and the mco binary !!!):
Command: mco rpc rpcutil -j -I [certname] get_fact fact=samba_share

        "agent": "rpcutil",
        "action": "get_fact",
        "sender": "",
        "statuscode": 0,
        "statusmsg": "OK",
        "data": {
          "fact": "samba_share",
          "value": {
            "example-share": {
              "browsable": "yes",
              "comment": "Example Share",
              "force group": "test",
              "force user": "test",
              "guest ok": "no",
              "guest only": "no",
              "inherit permissions": "yes",
              "path": "/var/shares/test",
              "read only": "no",
              "writable": "yes"
            "homes": {
              "browseable": "no",
              "comment": "Home Directories",
              "create mask": "0700",
              "directory mask": "0700",
              "read only": "yes",
              "valid users": "%S"
            "print$": {
              "browseable": "yes",
              "comment": "Printer Drivers",
              "guest ok": "no",
              "path": "/var/lib/samba/printers",
              "read only": "yes"
            "printers": {
              "browseable": "no",
              "comment": "All Printers",
              "create mask": "0700",
              "guest ok": "no",
              "path": "/var/spool/samba",
              "printable": "yes",
              "read only": "yes"

Conclusion, you don't have to use ruby to get structural data into facter.
It's a great feature though !!!
Hope anyone will find it useful.

Rgds. Franz

edit flag offensive delete link more

answered 2016-04-29 12:12:08 -0500

sigmike gravatar image

It doesn't seem to be possible, but your may be able to use structured data files for that.

I don't know the order of evaluation, but if external facts are evaluated before structured data facts, then your bash script may just write the data fact file and provide a dummy fact.

If data facts are evaluated first you could run your bash script in a cron or a refresh.

edit flag offensive delete link more

answered 2016-04-29 12:57:01 -0500

DarylW gravatar image

updated 2016-04-29 13:21:45 -0500

Here is the official documentation -

Note: Structured facts are supported in Puppet 3.3 and greater, but they aren’t enabled by default. To enable structured facts, set the stringify_facts option to false in the [main] section of puppet.conf on all machines, whether they are agents, masters, or standalone nodes running puppet apply.

Puppet Enterprise 3.7 and later has structured facts enabled by default, as will Puppet 4.0.

I don't currently have structured facts enabled, but I think that all you have to do is the following.. Enable structured facts if appropriate (noted above) and return the object in the type you want from ruby, inside of your fact.. (hash or array)

Here's the link

Facter.add(:interfaces_array) do
  setcode do
   interfaces = Facter.value(:interfaces)
   # the 'interfaces' fact returns a single comma-delimited string, e.g., "lo0,eth0,eth1"
   # this splits the value into an array of interface names

Now, in your case if you have bash returning a set of newline separated values.... You would need to shell out to your command, and then turn the output into a newline separated array.

Facter.add(:ls_tmp_array) do
  setcode do
  ls_output = Facter::Core::Execution::exec('ls -l /tmp')

One additional thing... as per this comment on stack overflow...

You may need to remove the newlines from each line in the array... The linked question suggest using squish, but that didn't work for me, so I used the chomp method

Facter.add(:ls_tmp_array) do
  setcode do
  ls_output = Facter::Core::Execution::exec('ls -l /tmp')
edit flag offensive delete link more


I was hoping to avoid writing Ruby for this solution; and just drop a script in /etc/facter/facter.d for initial prototyping. If I need to write Ruby to wrap the script output and parse it, I think I'll be better off just writing the whole thing in Ruby. Much to consider...

Rob Ogilvie gravatar imageRob Ogilvie ( 2016-04-29 15:42:09 -0500 )edit

Your Answer

Please start posting anonymously - your entry will be published after you log in or create a new account.

Add Answer

Question Tools

1 follower


Asked: 2016-04-29 10:05:10 -0500

Seen: 763 times

Last updated: May 27 '16