Dobrev.EU Blog

Things I want to share

Adding Custom Parser Functions to Puppet

| Comments

In two consecutive jobs I had to look at a way to manage Linux user names and passwords via Puppet. This is one of mostly discussed topics and there are plenty of solutions around. And exactly this fact confused me the most! Which way should I take? Of course my own…

After multiple researches in Internet and discussions in #puppet I saw a plenty of questions around password management but one feature was mostly requested. A way to provide plain-text password (offtopic: I don’t know even why you want to do that?) in a manifest with a hashing algorithm when creating a user. Others wanted a way to map users with their SSH keys. Some were managing both cases in separate classes with different input sources, others like me were trying to bundle it together. So I had 2 main problems to solve. But first let’s find a way to create unix-like crypted hashes that can easily be used in Puppet on user creation.

This can easily be done with generate() on server side but requires the administrator to install 3rd-party executable script on the system in order to create the hashes. I needed an universal way to “port” my 3rd-party executable together with my module. Of course what else than a custom parser function. And this is how I jumped directly to rubygems.org to search for a way to create Unix-type passwords. I found unix-crypt to be useful – this Ruby gem already has everything I need. Now I just have to wrap it in a Puppet-fashioned way. Following the official guide and some help from Richard Crowley aka rcrowley I came to this simple block of code for

Adding a custom MD5 function

”md5” hashing as a custom parser function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'unix_crypt'

module Puppet::Parser::Functions
  newfunction(:my_md5, :type => :rvalue, :doc => <<-EOS
Returns MD5 hash using provided String
    EOS
  ) do |arguments|

    # If no arguments
    raise(Puppet::ParseError, "my_md5(): Missing arguments " +
      "(#{arguments.size}/1)") if arguments.size < 1

    password = arguments[0]

    return UnixCrypt::MD5.build(password)
  end
end

Now I only need to save it as <my_module>/lib/puppet/parser/functions/my_md5.rb, either restart puppet master or run the agent on it in order to activate this new “hook” and I’m free to use my_md5() in my manifests. In a similar fashion I created functions for

DES Hash

DES and SHA require a salt that can easily be omitted. This way the hashing function is going to generate a random salt for you. Although there is no practical usage for this in Puppet unless you want to generate a new hash on every agent run.

my_des.rb
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
require 'unix_crypt'

module Puppet::Parser::Functions
  newfunction(:my_des, :type => :rvalue, :doc => <<-EOS
Returns DES hash using provided String and Salt
    EOS
  ) do |arguments|

      # If no arguments
    raise(Puppet::ParseError, "my_des(): Missing arguments " +
      "(#{arguments.size}/1)") if arguments.size < 1

    # If arguments more than 2
    raise(Puppet::ParseError, "my_des(): Invalid number of arguments " +
      "given (#{arguments.size}/2)") if arguments.size > 2

    password = arguments[0]
    salt     = arguments[1] || nil

    if salt.nil?
          return UnixCrypt::DES.build(password)
      else
          return UnixCrypt::DES.build(password, salt)
      end
    end
  end
end

SHA256 Hash

my_sha256.rb
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
require 'unix_crypt'

module Puppet::Parser::Functions
  newfunction(:my_sha256, :type => :rvalue, :doc => <<-EOS
Returns SHA256 hash using provided String and Salt
    EOS
  ) do |arguments|

      # If no arguments
    raise(Puppet::ParseError, "my_sha256(): Missing arguments " +
      "(#{arguments.size}/1)") if arguments.size < 1

    # If arguments more than 2
    raise(Puppet::ParseError, "my_sha256(): Invalid number of arguments " +
      "given (#{arguments.size}/2)") if arguments.size > 2

    password = arguments[0]
    salt     = arguments[1] || nil

    if salt.nil?
          return UnixCrypt::SHA256.build(password)
      else
          return UnixCrypt::SHA256.build(password, salt)
      end
    end
  end
end

and SHA512 Hash

my_sha512.rb
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
require 'unix_crypt'

module Puppet::Parser::Functions
  newfunction(:my_sha512, :type => :rvalue, :doc => <<-EOS
Returns SHA512 hash using provided String and Salt
    EOS
  ) do |arguments|

    # If no arguments
    raise(Puppet::ParseError, "my_sha512(): Missing arguments " +
      "(#{arguments.size}/1)") if arguments.size < 1

    # If arguments more than 2
    raise(Puppet::ParseError, "my_sha512(): Invalid number of arguments " +
      "given (#{arguments.size}/2)") if arguments.size > 2

    password = arguments[0]
    salt     = arguments[1] || nil

    if salt.nil?
          return UnixCrypt::SHA512.build(password)
      else
          return UnixCrypt::SHA512.build(password, salt)
      end
    end
  end
end

With this arsenal in my hands I can move on building a single module for user, password and SSH key management.

Edit: While working on the Puppet module I found a proposed patch by dieterdemeyer for stdlib which finally introduces same functionality.

Comments