Using the type system

Older Puppet versions supported a small set of data types only: Bool, String, Array, and Hash. The Puppet DSL had almost no functionality to check for consistent variable types. Consider the following scenario.

A parameterized class enables other users of your code base to change the behavior and output of the class:

class ssh (
  $server = true,
){
  if $server {
    include ssh::server
  }
}

This class definition checks whether the server parameter has been set to true. However, in this example, the class was not protected from wrong data usage:

class { 'ssh':
  server => 'false',
}

In this class declaration, the server parameter has been given a string instead of a bool value. Since the false string is not empty, the if $server condition actually passes. This is not what the user will expect.

Within Puppet 3, it was recommended to add parameter validation using several functions from the stdlib module:

class ssh (
  $server = true,
){
  validate_bool($server)
  if $server {
    include ssh::server
  }
}

With one parameter only, this seems to be a good way. But what if you have many parameters? How do we deal with complex data types like hashes?

This is where the type system comes into play. The type system knows about many generic data types and follows patterns that are also used in many other modern programming languages.

Puppet differentiates between core data types and abstract data types. Core data types are the "real" data types, the ones which are mostly used in Puppet Code:

  • String
  • Integer, Float, and Numeric
  • Boolean
  • Array
  • Hash
  • Regexp
  • Undef
  • Default

In the given example, the server parameter should be checked to always contain a bool value. The code can be simplified to the following pattern:

class ssh (
  Boolean $server = true,
){
  if $server {
    include ssh::server
  }
}

If the parameter is not given a Boolean value, Puppet will throw an error, explaining which parameter has a non-matching data type:

class { 'ssh':
  server = 'false',
}

The error displayed is as follows:

root@puppetmaster# puppet apply ssh.pp
Error: Expected parameter 'server' of 'Class[Ssh]' to have type Boolean, got String at ssh.pp:2 on node puppetmaster.example.net

The Numeric, Float, and Integer data types have some more interesting aspects when it comes to variables and their type.

Puppet will automatically recognize Integers, consisting of numbers (either with or without the minus sign) and not having a decimal point.

Floats are recognized by the decimal point. When doing arithmetic algebra on a combination of an Integer and a Float, the result will always be a Float.

Floats between -1 and 1 must be written with a leading 0 digit prior to the decimal point; otherwise, Puppet will throw an error.

Besides this, Puppet has support for the decimal, octal, and hexadecimal notation, as known from C-like languages:

  • A nonzero decimal must not start with a 0
  • Octal values must start with a leading 0
  • Hexadecimal values have 0x as the prefix

Puppet will automatically convert numbers into strings during the interpolation: ("Value of number: ${number}").

Note

Puppet will not convert strings to numbers. To make this happen, you can simply add 0 to a string to convert:

$ssh_port = '22'
$ssh_port_integer = 0 + $ssh_port

The Default data type is a little special. It does not directly refer to a data type, but can be used in selectors and case statements:

$enable_real = $enable ? {
  Boolean => $enable,
  String  => str2bool($enable),
  default => fail('Unsupported value for ensure. Expected either bool or string.'),
}

Abstract data types are constructs that are useful for a more sophisticated or permissive Type checking:

  • Scalar
  • Collection
  • Variant
  • Data
  • Pattern
  • Enum
  • Tuple
  • Struct
  • Optional
  • Catalogentry
  • Type
  • Any
  • Callable

Assume that a parameter will only accept strings from a limited set. Only checking for being of type String is not sufficient. In this scenario, the Enum type is useful, for which a list of valid values are specified:

class ssh (
  Boolean $server = true,
  Enum['des','3des','blowfish'] $cipher = 'des',
){
  if $server {
    include ssh::server
  }
}

If the listen parameter is not set to one of the listed elements, Puppet will throw an error:

class { 'ssh':
  ciper => 'foo',
}

The following error is displayed:

puppet apply ssh.pp
Error: Expected parameter 'ciper' of 'Class[Ssh]' to have type Enum['des','3des','blowfish'] got String at ssh.pp:2 on node puppetmaster.example.net

Sometimes, it is difficult to use specific data types, because the parameter might be set to an undef value. Think of a userlist parameter that might be empty (undef) or set to an arbitrary array of strings.

This is what the Optional type is for:

class ssh (
  Boolean $server = true,
  Enum['des','3des','blowfish'] $cipher = 'des',
  Optional[Array[String]] $allowed_users = undef,
){
  if $server {
    include ssh::server
  }
}

Again, using a wrong data type will lead to a Puppet error:

class { 'ssh':
  allowed_users => 'foo',
}

The error displayed is as follows:

puppet apply ssh.pp
Error: Expected parameter 'userlist' of 'Class[Ssh]' to have type Optional[Array[String]], got String at ssh.pp:2 on node puppetmaster.example.net

In the previous example, we used a data type composition. This means that data types can have more information for type checking.

Let's assume that we want to set the ssh service port in our class. Normally, ssh should run on a privileged port between 1 and 1023. In this case, we can restrict the Integer Data Type to only allow numbers between 1 and 1023 by passing additional information:

class ssh (
  Boolean $server = true,
  Optional[Array[String]] $allowed_users = undef,
  Integer[1,1023] $sshd_port,
){
  if $server {
    include ssh::server
  }
}

As always, providing a wrong parameter will lead to an error:

class { 'ssh':
  sshd_port => 'ssh',
}

The preceding line of code gives the following error:

puppet apply ssh.pp
Error: Expected parameter 'sshd_port' of 'Class[Ssh]' to have type Integer[1, 1023], got String at ssh.pp:2 on node puppetmaster.example.net

Complex hashes that use multiple data types are very complicated to describe using the new type system.

When using the Hash type, it is only possible to check for a hash in general, or for a hash with keys of a specific type. You can optionally verify the minimum and maximum number of elements in the hash.

The following example provides a working hash type check:

$hash_map = {
  'ben'     => {
    'uid'   => 2203,
    'home'  => '/home/ben',
  },
  'jones'   => {
    'uid'   => 2204,
    'home'  => 'home/jones',
  }
}

Notably, the home entry for user jones is missing the leading slash:

class users (
  Hash $users
){
  notify { 'Valid Hash': }
}
class { 'users':
  users => $hash_map,
}

Running the preceding code, gives us the following output:

puppet apply hash.pp
Notice: Compiled catalog for puppetmaster.example.net in environment production in 0.32 seconds
Notice: Valid hash
Notice: /Stage[main]/Users/Notify[Valid hash]/message: defined 'message' as 'Valid hash'
Notice: Applied catalog in 0.03 seconds

With the preceding notation, the data type is valid. Yet there are errors inside the Hash map.

Checking content of Arrays or Hashes requires the use of another abstract data type: Tuple (used for Arrays) or Struct (used for Hashes).

However, the Struct data type will work only when the key names are from a known limited set, which is not the case in the given example.

In this special case, we have two possibilities:

  • Extend the hash data type to know about the hash internal structure
  • Wrap the type data into a define, which makes use of all keys using the key function (from stdlib)

The first solution is as follows:

class users (
  Hash[
    String,
    Struct[ { 'uid' => Integer,
              'home' => Pattern[ /^\/.*/ ] } ]
  ] $users
){
  notify { 'Valid hash': }
}

However, the error message is hard to understand when the data types are not matching:

puppet apply hash.pp
Error: Expected parameter 'users' of 'Class[Users]' to have type Hash[String, Struct[{'uid'=>Integer, 'home'=>Pattern[/^\/.*/]}]], got Struct[{'ben'=>Struct[{'uid'=>Integer, 'home'=>String}], 'jones'=>Struct[{'uid'=>Integer, 'home'=>String}]}] at hash.pp:32 on node puppetmaster.example.net

The second solution gives a smarter hint on which data might be wrong:

define users::user (
  Integer $uid,
 Pattern[/^\/.*/] $home,
){
  notify { "User: ${title}, UID: ${uid}, HOME: ${home}": }
}

This defined type is then employed from within the users class:

class users (
  Hash[String, Hash] $users
){
  $keys = keys($users)
  each($keys) |String $username| {
    users::user{ $username:
      uid => $users[$username]['uid'],
      home => $users[$username]['home'],
    }
  }
}

With the wrong submitted data in the hash, you will receive the following error message:

puppet apply hash.pp
Error: Expected parameter 'home' of 'Users::User[jones]' to have type Pattern[/^\/.*/], got String at hash.pp:23 on node puppetmaster.example.net

The error message is pointing to the home parameter of the user jones, which is given in the hash.

The correct hash is as follows:

$hash_map = {
  'ben'    => {
    'uid'  => 2203,
    'home' => '/home/ben',
  },
  'jones'  => {
    'uid'  => 2204,
    'home' => '/home/jones',
  }
}

The preceding code produces the expected result as follows:

puppet apply hash.pp
Notice: Compiled catalog for puppetmaster.example.net in environment production in 0.33 seconds
Notice: User: ben, UID: 2203, HOME: /home/ben
Notice: /Stage[main]/Users/Users::User[ben]/Notify[User: ben, UID: 2203, HOME: /home/ben]/message: defined 'message' as 'User: ben, UID: 2203, HOME: /home/ben'
Notice: User: jones, UID: 2204, HOME: /home/jones
Notice: /Stage[main]/Users/Users::User[jones]/Notify[User: jones, UID: 2204, HOME: /home/jones]/message: defined 'message' as 'User: jones, UID: 2204, HOME: /home/jones'
Notice: Applied catalog in 0.03 seconds

The preceding manifest uses the each function, another part of the Puppet 4 language. The next section explores it in greater detail.