Puppet - Coding Style



In Puppet, the coding style defines all the standards which one needs to follow while trying to convert the infrastructure on the machine configuration into a code. Puppet works and performs all its defined tasks using resources.

Puppet’s language definition helps in specifying all the resources in a structured way, which is required to manage any target machine that needs to be managed. Puppet uses Ruby as its encoding language, which has multiple inbuilt features that makes it very easy to get things done with a simple configuration on the code side.

Fundamental Units

Puppet uses multiple fundamental coding styles which is easy to understand and manage. Following is a list of few.

Resources

In Puppet, resources are known as fundamental modeling unit which are used to manage or modify any target system. Resources cover all the aspects of a system such as file, service, and package. Puppet comes with an in-built capability wherein it allows the users or developers to develop custom resources, which help in managing any particular unit of a machine

In Puppet, all the resources are aggregated together either by using “define” or “classes”. These aggregation features help in organizing a module. Following is a sample resource which consists of multiple types, a title, and a list of attributes with which Puppet can support multiple attributes. Each resource in Puppet has its own default value, which could be overridden when required.

Sample Puppet Resource for File

In the following command, we are trying to specify a permission for a particular file.

file {  
   '/etc/passwd': 
   owner => superuser, 
   group => superuser, 
   mode => 644, 
}

Whenever the above command gets executed on any machine, it will verify that the passwd file in the system is configured as described. The file before: colon is the title of resource, which can be referred as resource in other parts of Puppet configuration.

Specifying Local Name in Addition to the Title

file { 'sshdconfig': 
   name => $operaSystem ? { 
      solaris => '/usr/local/etc/ssh/sshd_config', 
      default => '/etc/ssh/sshd_config', 
   }, 
   owner => superuser, 
   group => superuser, 
   mode => 644, 
}

By using the title, which is always the same it is very easy to refer file resource in configuration without having to repeat the OS related logic.

Another example could be using a service that depends on a file.

service { 'sshd': 
   subscribe => File[sshdconfig], 
} 

With this dependency, the sshd service will always restart once the sshdconfig file changes. The point to be remember here is File[sshdconfig] is a declaration as File as in lower case but if we change it to FILE[sshdconfig] then it would have been a reference.

One fundamental point that one needs to keep in mind while declaring a resource is, it can be declared only once per config file. Repeating declaration of the same resource more than once will cause an error. Through this fundamental concept, Puppet makes sure that the configuration is well modeled.

We even have the capability to manage resource dependency which helps is managing multiple relationships.

service { 'sshd': 
   require => File['sshdconfig', 'sshconfig', 'authorized_keys']
}   

Metaparameters

Metaparameters are known as global parameters in Puppet. One of the key features of metaparameter is, it works with any type of resource in Puppet.

Resource Default

When one needs to define a default resource attribute value, Puppet provides a set of syntax to archive it, using a capitalized resource specification that has no title.

For example, if we want to set the default path of all the executable it can be done with the following command.

Exec { path => '/usr/bin:/bin:/usr/sbin:/sbin' } 
exec { 'echo Testing mataparamaters.': } 

In the above command, the first statement Exec will set the default value for exec resource. Exec resource requires a fully qualified path or a path which looks like an executable. With this, one can define a single default path for the entire configuration. Defaults work with any resource type in Puppet.

Defaults are not global values, however, they only affect the scope in which they are defined or the very next variable to it. If one wants to define default for a complete configuration, then we define the default and the class in the very next section.

Resource Collections

Aggregation is method of collecting things together. Puppet supports a very powerful concept of aggregation. In Puppet, aggregation is used for grouping resource which is the fundamental unit of Puppet together. This concept of aggregation in Puppet is achieved by using two powerful methods known as classes and definition.

Classes and Definition

Classes are responsible for modeling the fundamental aspects of node. They can say node is a web server and this particular node is one of them. In Puppet, programming classes are singleton and they can get evaluated once per node.

Definition on the other hand can be used many times on a single node. They work similarly as one has created his own Puppet type using the language. They are created to be used multiple times with different input each time. This means one can pass variable values into the definition.

Difference between Class and Definition

The only key difference between a class and definition is while defining the building structure and allocating resources, class gets evaluated only once per node, wherein on the other hand, a definition is used multiple times on the same single node.

Classes

Classes in Puppet are introduced using the class keyword and the content of that particular class is wrapped inside the curly braces as shown in the following example.

class unix { 
   file { 
      '/etc/passwd': 
      owner => 'superuser', 
      group => 'superuser', 
      mode => 644; 
      '/etc/shadow': 
      owner => 'vipin', 
      group => 'vipin', 
      mode => 440; 
   } 
}

In the following example, we have used some short hand which is similar to the above.

class unix { 
   file { 
      '/etc/passwd': 
      owner => 'superuser', 
      group => 'superuser', 
      mode => 644; 
   }  
   
   file {'/etc/shadow': 
      owner => 'vipin', 
      group => 'vipin', 
      mode => 440; 
   } 
} 

Inheritance in Puppet Classes

In Puppet, the OOP concept of inheritance is supported by default wherein classes can extend the functionality of previous without copying and pasting the complete code bit again in newly created class. Inheritance allows the subclass to override the resource settings defined in the parent class. One key thing to keep in mind while using inheritance is, a class can only inherit features from only one parent class, not more than one.

class superclass inherits testsubclass { 
   File['/etc/passwd'] { group => wheel } 
   File['/etc/shadow'] { group => wheel } 
}

If there is a need to undo some logic specified in a parent class, we can use undef command.

class superclass inherits testsubcalss { 
   File['/etc/passwd'] { group => undef } 
} 

Alternative Way of Using Inheritance

class tomcat { 
   service { 'tomcat': require => Package['httpd'] } 
} 
class open-ssl inherits tomcat { 
   Service[tomcat] { require +> File['tomcat.pem'] } 
}

Nested Class in Puppet

Puppet supports the concept of nesting of classes in which it allows to use nested classes which means one class inside the other. This helps in achieving modularity and scoping.

class testclass { 
   class nested { 
      file {  
         '/etc/passwd': 
         owner => 'superuser', 
         group => 'superuser', 
         mode => 644; 
      } 
   } 
} 
class anotherclass { 
   include myclass::nested 
} 

Parameterized Classes

In Puppet, classes can extend their functionality to allow the passing of parameters into a class.

To pass a parameter in a class, one can use the following construct −

class tomcat($version) { 
   ... class contents ... 
} 

One key point to remember in Puppet is, classes with parameters are not added using the include function, rather the resulting class can be added as a definition.

node webserver { 
   class { tomcat: version => "1.2.12" } 
}

Default Values As Parameters in Class

class tomcat($version = "1.2.12",$home = "/var/www") { 
   ... class contents ... 
} 

Run Stages

Puppet supports the concept of run stage, which means the user can add multiple number of stages as per the requirement in order to manage any particular resource or multiple resources. This feature is very helpful when the user wants to develop a complex catalog. In a complex catalog, one has large number of resources which needs to be compiled while keeping in mind that the dependencies among the resources defined should not be impacted.

Run Stage is very helpful in managing resource dependencies. This can be done by adding classes in defined stages wherein a particular class contains a collection of resources. With run stage, Puppet guarantees that the defined stages will run in a specified predictable order every time the catalog runs and gets applied on any Puppet node.

In order to use this, one needs to declare additional stages beyond the already present stages and then Puppet can be configured to manage each stage in a specified order using the same resource relationship syntax before require “->” and “+>”. The relationship will then guarantee the order of classes associated with each stage.

Declaring Additional Stages with Puppet Declarative Syntax

stage { "first": before => Stage[main] } 
stage { "last": require => Stage[main] } 

Once the stages have been declared, a class may be associated with the stage other than the main using the stage.

class { 
   "apt-keys": stage => first; 
   "sendmail": stage => main; 
   "apache": stage => last; 
}

All resources associated with class apt-key will run first. All the resources in Sendmail will be the main class and the resources associated with Apache will be the last stage.

Definitions

In Puppet, collection of resources in any manifest file is done either by classes or definitions. Definitions are very much similar to a class in Puppet however they are introduced with a define keyword (not class) and they support argument not inheritance. They can run on the same system multiple times with different parameters.

For example, if one wants to create a definition that controls the source code repositories where one is trying to create multiple repositories on the same system, then one can use the definition not class.

define perforce_repo($path) { 
   exec {  
      "/usr/bin/svnadmin create $path/$title": 
      unless => "/bin/test -d $path", 
   } 
} 
svn_repo { puppet_repo: path => '/var/svn_puppet' } 
svn_repo { other_repo: path => '/var/svn_other' }

The key point to be noted here is how a variable can be used with a definition. We use ($) dollar sign variable. In the above, we have used $title. Definitions can have both a $title and $name with which the name and the title can be represented. By default, $title and $name are set to the same value, but one can set a title attribute and pass different name as a parameter. $title and $name only works in definition, not in class or other resource.

Modules

A module can be defined as a collection of all the configurations which would be used by the Puppet master to apply configurational changes on any particular Puppet node (agent). They are also known as portable collection of different kind of configurations, which are required to perform a specific task. For example, a module might contain all the resources required to configure Postfix and Apache.

Nodes

Nodes are very simple remaining step which is how we match what we defined (“this is what a webserver looks like”) to what machines are chosen to fulfill those instructions.

Node definition exactly looks like classes, including the supporting inheritance, however they are special such that when a node (a managed computer running a puppet client) connects to the Puppet master daemon, its name will be looked in the defined list of nodes. The information defined will be evaluated for node, and then the node will send that configuration.

Node name can be a short host name or the fully qualified domain name (FQDN).

node 'www.vipin.com' { 
   include common 
   include apache, squid 
}

The above definition creates a node called www.vipin.com and includes the common, Apache and Squid classe

We can send the same configuration to different nodes by separating each with comma.

node 'www.testing.com', 'www.testing2.com', 'www3.testing.com' { 
   include testing 
   include tomcat, squid 
}

Regular Expression for Matching Nodes

node /^www\d+$/ { 
   include testing 
}

Node Inheritance

Node supports a limited inheritance model. Like classes, nodes can only inherit from one other node.

node 'www.testing2.com' inherits 'www.testing.com' { 
   include loadbalancer 
}

In the above code, www.testing2.com inherits all the functionalities from www.testing.com in addition to an additional loadbalancer class.

Advanced Supported Features

Quoting − In most of the cases, we don’t need to quote a string in Puppet. Any alpha numeric string starting with a letter is to be left without quoting. However, it is always a best practice to quote a string for any non-negative values.

Variable Interpolation with Quotes

So far we have mentioned variable in terms of definition. If one needs to use those variables with a string, use double quotes, not single quotes. Single quotes string will not do any variable interpolation, double quotes string will do. The variable can be bracketed in {} which makes them easier to use together and easier to understand.

$value = "${one}${two}" 

As a best practice, one should use single quotes for all the strings that do not require string interpolation.

Capitalization

Capitalization is a process which is used for referencing, inheritance, and setting default attributes of a particular resource. There are basically two fundamental ways of using it.

  • Referencing − It is the way of referencing an already created resource. It is mainly used for dependency purposes, one has to capitalize the name of the resource. Example, require => file [sshdconfig]

  • Inheritance − When overriding the setting for parent class from subclass, use the upper case version of the resource name. Using the lower case version will result in an error.

  • Setting Default Attribute Value − Using the capitalized resource with no title works to set the default of the resource.

Arrays

Puppet allows the use of arrays in multiple areas [One, two, three].

Several type members, such as alias in the host definition accepts arrays in their values. A host resource with multiple aliases will look like something as follows.

host { 'one.vipin.com': 
   alias => [ 'satu', 'dua', 'tiga' ], 
   ip => '192.168.100.1', 
   ensure => present, 
}

The above code will add a host ‘one.brcletest.com’ to the host list with three aliases ‘satu’ ‘dua’ ‘tiga’. If one wants to add multiple resources to one resource, it can be done as shown in the following example.

resource { 'baz': 
   require => [ Package['rpm'], File['testfile'] ], 
}

Variables

Puppet supports multiple variables like most of the other programming languages. Puppet variables are denoted with $.

$content = 'some content\n' 
file { '/tmp/testing': content => $content } 

As stated earlier Puppet is a declarative language, which means that its scope and assignment rules are different than the imperative language. The primary difference is that one cannot change the variable within a single scope, because they rely on order in the file to determine the value of a variable. Order does not matter in the declarative language.

$user = root 
file {  
   '/etc/passwd': 
   owner => $user, 
} 

$user = bin 
   file {  
      '/bin': 
      owner => $user, 
      recurse => true, 
   }

Variable Scope

Variable scope defines if all the variables which are defined are valid. As with the latest features, Puppet is currently dynamically scoped which in Puppet terms means that all the variables which are defined gets evaluated on their scope rather than the location which they are defined.

$test = 'top' 
class Testclass { 
   exec { "/bin/echo $test": logoutput => true } 
} 

class Secondtestclass { 
   $test = 'other' 
   include myclass 
} 

include Secondtestclass 

Qualified Variable

Puppet supports the use of qualified variables inside a class or a definition. This is very helpful when the user wishes to use the same variable in other classes, which he has defined or is going to define.

class testclass { 
   $test = 'content' 
} 

class secondtestclass { 
   $other = $myclass::test 
} 

In the above code, the value of $other variable evaluates the content.

Conditionals

Conditions are situations when the user wishes to execute a set of statement or code when the defined condition or the required condition is satisfied. Puppet supports two types of conditions.

The selector condition which can only be used within the defined resources to pick the correct value of the machine.

Statement conditions are more widely used conditions in manifest which helps in including additional classes which the user wishes to include in the same manifest file. Define a distinct set of resources within a class, or make other structural decisions.

Selectors

Selectors are useful when the user wishes to specify a resource attribute and variables which are different from the default values based on the facts or other variables. In Puppet, the selector index works like a multivalued three-way operator. Selectors are also capable of defining the custom default values in no values, which are defined in manifest and matches the condition.

$owner = $Sysoperenv ? { 
   sunos => 'adm', 
   redhat => 'bin', 
   default => undef, 
}

In later versions of Puppet 0.25.0 selectors can be used as regular expressions.

$owner = $Sysoperenv ? { 
   /(Linux|Ubuntu)/ => 'bin', 
   default => undef, 
}

In the above example, the selector $Sysoperenv value matches either Linux or Ubuntu, then the bin will be the selected result, otherwise the user will be set as undefined.

Statement Condition

Statement condition is other type of conditional statement in Puppet which is very much similar to switch case condition in Shell script. In this, a multiple set of case statements are defined and the given input values are matched against each condition.

The case statement which matches the given input condition gets executed. This case statement condition does not have any return value. In Puppet, a very common use case for condition statement is running a set of code bit based on the underlying operating system.

case $ Sysoperenv { 
   sunos: { include solaris }  
   redhat: { include redhat }  
   default: { include generic}  
}

Case Statement can also specify multiple conditions by separating them with a comma.

case $Sysoperenv { 
   development,testing: { include development } testing,production: { include production }
   default: { include generic }  
} 

If-Else Statement

Puppet supports the concept of condition-based operation. In order to achieve it, If/else statement provides branching options based on the return value of the condition. As shown in the following example −

if $Filename { 
   file { '/some/file': ensure => present } 
} else { 
   file { '/some/other/file': ensure => present } 
} 

The latest version of Puppet supports variable expression in which the if statement can also branch based on the value of an expression.

if $machine == 'production' { 
   include ssl 
} else { 
   include nginx 
}

In order to achieve more diversity in code and perform complex conditional operations, Puppet supports nested if/else statement as shown in the following code.

if $ machine == 'production' { 
   include ssl 
} elsif $ machine == 'testing' { 
   include nginx
} else { 
   include openssl 
} 

Virtual Resource

Virtual resources are those that are not sent to the client unless realized.

Following is the syntax of using virtual resource in Puppet.

@user { vipin: ensure => present } 

In the above example, the user vipin is defined virtually to realize the definition one can use in the collection.

User <| title == vipin |>

Comments

Comments are used in any code bit to make an additional node about a set of lines of code and its functionality. In Puppet, there are currently two types of supported comments.

  • Unix shell style comments. They can be on their own line or the next line.
  • Multi-line c-style comments.

Following is an example of shell style comment.

# this is a comment

Following is an example of multiline comment.

/* 
This is a comment 
*/ 

Operator Precedence

The Puppet operator precedence conforms to the standard precedence in most systems, from the highest to the lowest.

Following is the list of expressions

  • ! = not
  • / = times and divide
  • - + = minus, plus
  • << >> = left shift and right shift
  • == != = not equal, equal
  • >= <= > < = greater equal, less or equal, greater than, less than

Comparison Expression

Comparison expression are used when the user wants to execute a set of statements when the given condition is satisfied. Comparison expressions include tests for equality using the == expression.

if $environment == 'development' { 
   include openssl 
} else { 
   include ssl 
} 

Not Equal Example

if $environment != 'development' { 
   $otherenvironment = 'testing' 
} else { 
   $otherenvironment = 'production' 
} 

Arithmetic Expression

$one = 1 
$one_thirty = 1.30 
$two = 2.034e-2 $result = ((( $two + 2) / $one_thirty) + 4 * 5.45) - 
   (6 << ($two + 4)) + (0×800 + -9)

Boolean Expression

Boolean expressions are possible using or, and, & not.

$one = 1 
$two = 2 
$var = ( $one < $two ) and ( $one + 1 == $two ) 

Regular Expression

Puppet supports regular expression matching using =~ (match) and !~ (not-match).

if $website =~ /^www(\d+)\./ { 
   notice('Welcome web server #$1') 
}

Like case and selector regex match creates limited scope variable for each regex.

exec { "Test": 
   command => "/bin/echo now we don’t have openssl installed on machine > /tmp/test.txt", 
   unless => "/bin/which php" 
}

Similarly, we can use unless, unless execute the command all the time, except the command under unless exits successfully.

exec { "Test": 
   command => "/bin/echo now we don’t have openssl installed on machine > /tmp/test.txt", 
   unless => "/bin/which php" 
}

Working with Templates

Templates are used when one wishes to have a pre-defined structure which is going be used across multiple modules in Puppet and those modules are going to be distributed on multiple machines. The first step in order to use template is to create one that renders the template content with template methods.

file { "/etc/tomcat/sites-available/default.conf": 
   ensure => "present", 
   content => template("tomcat/vhost.erb")  
}

Puppet makes few assumptions when dealing with local files in order to enforce organization and modularity. Puppet looks for vhost.erb template inside the folder apache/templates, inside the modules directory.

Defining and Triggering Services

In Puppet, it has a resource called service which is capable of managing the life cycle of all the services running on any particular machine or environment. Service resources are used to make sure services are initialized and enabled. They are also used for service restart.

For example, in the previous template of tomcat that we have where we set the apache virtual host. If one wants to make sure apache is restarted after a virtual host change, we need to create a service resource for the apache service using the following command.

service { 'tomcat': 
   ensure => running, 
   enable => true 
}

When defining the resources, we need to include the notify option in order to trigger the restart.

file { "/etc/tomcat/sites-available/default.conf": 
   ensure => "present", 
   content => template("vhost.erb"), 
   notify => Service['tomcat']  
}
Advertisements