Puppet: How to set line order within a file

puppetresolv.conf

Linux servers in my company are managed by Puppet.

There's a DNS module which configures /etc/resolv.conf on all servers based on physical location which is configured as a facter value.

As you know a /etc/resolv.conf file looks like so:

search domain.local
nameserver 1.1.1.1
nameserver 2.2.2.2

All servers's host names in the company end with two digits, for example:

proxy73

In order to split DNS network traffic between the two DNS servers I've written a new puppet module which cuts the last two digits of the host name and if it's an odd number then the /etc/resolv.conf file should look like shown above, but if the digits create an uneven number then the /etc/resolv.conf file should look like so:

search domain.local
nameserver 2.2.2.2
nameserver 1.1.1.1

But my problem is that no matter how I write the manifest, the lines are always ordered as the first server and then the second instead of second server and then first server.

The relevant portion of the manifest I wrote looks like that (please refer to the part under if $::oddip == false cause that's the part that doesn't work):

class dns_new::config {
  case $::dcd {
 'ny4': {
      if $::oddip == 'true' {
        file_line { "ny4 search domain":
          ensure => present,
          line   => "${::dns_new::params::searchdomny4}",
          path   => "/etc/resolv.conf",
          }
        file_line { "ny4dns1 first":
          ensure => present,
          line   => "${::dns_new::params::ny4dns1}",
          path   => "/etc/resolv.conf",
          }
        file_line { "ny4dns2 second":
          ensure => present,
          line   => "${::dns_new::params::ny4dns2}",
          path   => "/etc/resolv.conf",
          }
      }
      elsif $::oddip == 'false'  {
        file_line { "ny4 search domain":
          ensure => present,
          line   => "${::dns_new::params::searchdomny4}",
          path   => "/etc/resolv.conf",
          }
        file_line { "ny4dns2 first":
          ensure => present,
          line   => "${::dns_new::params::ny4dns2}",
          path   => "/etc/resolv.conf",
          require => File_line["ny4 search domain"],
          before => File_line["ny4dns1 second"],
          }
        file_line { "ny4dns1 second":
          ensure => present,
          line   => "${::dns_new::params::ny4dns1}",
          path   => "/etc/resolv.conf",
          require => File_line["ny4dns2 first"],
          }
      }
    }

You can see that I've tried setting the order using the before directive.

This is all in regard to setting a new server, but how can I set the order of lines for an already installed server?

Edit #2:

sysadmin1183, I've added the "do" as you showed, now the error is this:

[root@nyproxy33 ~]# puppet agent -t
Info: Retrieving plugin
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 400 on SERVER: compile error
/etc/puppet/environments/production/modules/dns_new/templates/resolv.conf.erb:27: syntax error, unexpected $end, expecting kEND
; _erbout
         ^
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

The 27th line is:

<% end %>

Tried to edit it to:

<% end -%>

But getting the same output…

Edit #3:
config.pp looks like so:

class dns_new::config {
  file { "/etc/resolv.conf":
      path    => '/etc/resolv.conf',
      ensure  => present,
      owner   => "root",
      group   => "root",
      mode    => "775",
      content => template("dns_new/resolv.conf.erb"),
      }

  case $::dcd {
    'ny4': {
      $search_dom = $::dns_new::params::searchdomny4
      if $::oddip == 'true' {
        $dns_list = [ "${::dns_new::params::ny4dns1}", "${::dns_new::params::ny4dns2}" ]
        }
      elsif $::oddip == 'false' {
        $dns_list = [ "${::dns_new::params::ny4dns2}", "${::dns_new::params::ny4dns1}" ]
        }
    }

The resolv.conf.erb file looks like this:

search <%= @search_dom %>
<% dns_list.each do |serv| -%>
nameserver <%= serv %>
<% end -%>

Running Puppet:

[root@nyproxy33 ~]# puppet agent -t
Info: Retrieving plugin
Info: Loading facts
Error: Could not retrieve catalog from remote server: Error 400 on SERVER: Failed to parse template dns_new/resolv.conf.erb:
  Filepath: /usr/lib/ruby/site_ruby/1.8/puppet/parser/templatewrapper.rb
  Line: 81
  Detail: Could not find value for 'dns_list' at /etc/puppet/environments/production/modules/dns_new/templates/resolv.conf.erb:2
 at /etc/puppet/environments/production/modules/dns_new/manifests/config.pp:8 on node nyproxy33.ny4.peer39.com
Warning: Not using cache on failed catalog
Error: Could not retrieve catalog; skipping run

Best Answer

You may be better served with a template in this case. Something like...

# This is done to bring the variable into scope.
$search_dom = $::dns_new::params::searchdomny4
if $::oddip == 'true' {
  $dns_order = [ '1.1.1.1', '2.2.2.2' ]
} elsif $::oddip == 'false' {
  $dns_order = [ '2.2.2.2', '1.1.1.1' ]
}

file { '/etc/resolv.conf':
  [usual stuff]
  content => template('dns_new/resolv.conf.erb'),
}

With an ERB template that looks something like this:

new_dns/templates/resolv.conf.erb:

search <%= @search_dom %>
<% @dns_order.each do |serv| %>
nameserver <%= serv %>
<% end -%>

Which should emit a resolv.conf file in the order you want.

Another option is to just encode the DNS server list in the module, and use ruby code in the ERB template to determine if you walk down the array (each) or walk up it (reverse.each). That would look like:

$search_dom = $::dns_new::params::searchdomny4
$dns_list   = $::dns_new::params::dnslist
$oddip      = $::oddip
file { '/etc/resolv.conf':
  [usual stuff]
  content => template('dns_new/resolv.conf.erb')
}

With the more complex ERB formatted like:

search <%= @search_dom %>
<% if @odd_ip == 'true' %>
  <% @dns_list.each do |srv| -%>
nameserver <%= srv %>
  <% end -%>
<% elsif @odd_ip == 'false' -%>
  <% @dns_list.reverse.each do |srv| -%>
nameserver <%= srv %>
  <% end -%>
<% end -%>

In case you haven't done templates before, the key for the markup is roughly:

<%   : Here is ruby code.
<%=  : Here is an evaluated value. Replace this block with the 
         result, or enter a blank line.
-%>  : End a block. Don't render a blank line if it doesn't evaluate to anything.

The template will be evaluated line by line. The first line is a simple thing that drops the 'server' part of the resolv conf. The second line is where things get more complex: we're calling ruby functions. This is what allows us to drop as many nameserver lines as there are in the array.


I see what you're trying to do in the ERB, but I think you're over-complicating it. You're encoding the localization logic (nj vs ams vs lax) in the ERB itself. That can be done, but you might have better luck doing that part in the puppet-code. It's more likely to be readable to someone else if that part of the logic is in with the puppet code.

dns_new/manifests/config.pp:

case $::dcd {
  'ny4': {
            $search_domain = $::dns_new::params::searchdomny4
            $dns_list = [ "${::dns_new::params::ny4dns1}",    "${::dns_new::params::ny4dns2}" ]
         }
  'nj':  {
            $search_domain = $::dns_new::params::searchdomnj
            $dns_list = [ "${::dns_new::params::njdns1}",    "${::dns_new::params::njdns2}" ]
         }
  'ams2': {
            $search_domain = $::dns_new::params::searchdomams2
            $dns_list = [ "${::dns_new::params::ams2dns1}",    "${::dns_new::params::ams2dns2}" ]
          }
}

file { '/etc/resolv.conf':
  [the usual stuff]
  content = template('dns_new/resolv.conf.erb')
}

You now have two variables to deal with in the ERB. search_domain and dns_list. This shortens the ERB quite a bit:

dns_new/templates/resolv.conf.erb:

search <%= @search_domain %>
<% dns_list.each do |serv| -%>
nameserver <%= serv %>
<% end -%>

If you're wondering why I'm assigning variables in the class and using those in the ERB instead of using the variables in the params class, its because of the non-intuitive way that out-of-scope variables work in ERB files. It's much easier, and good style, to assign variables that will be used in an ERB file in the same class that calls the template.

Related Topic