├── .gitignore ├── .ruby-version ├── .travis.yml ├── Appraisals ├── Gemfile ├── README.md ├── Rakefile ├── data └── hosts ├── features ├── catalog_policy.feature ├── steps │ └── catalog_policy.rb └── support │ ├── helpers.rb │ ├── hooks.rb │ ├── matchers.rb │ └── world.rb ├── gemfiles ├── 2.6.18.gemfile ├── 2.7.23.gemfile ├── 3.0.2.gemfile ├── 3.1.1.gemfile └── 3.2.4.gemfile ├── manifests ├── site.pp └── templates.pp └── modules └── dhcp ├── features ├── dhcp_clients.feature ├── dhcpd.conf.feature └── steps │ ├── dhcp_clients.rb │ └── dhcpd.conf.rb ├── lib └── puppet │ └── parser │ └── functions │ └── dhcp_clients.rb ├── manifests └── server.pp └── templates └── dhcpd.conf.erb /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | man/*.1 3 | man/*.7 4 | man/*.html 5 | pkg 6 | *.gem 7 | *.rbc 8 | *~ 9 | *.swp 10 | *.swo 11 | .bundle 12 | .rvmrc 13 | .ruby-version 14 | Gemfile.lock 15 | doc/* 16 | log/* 17 | pkg/* 18 | gemfiles/*.lock -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.8.7 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 1.8.7 3 | script: "bundle exec rake" 4 | gemfile: 5 | - gemfiles/2.6.18.gemfile 6 | allow_failures: 7 | - gemfiles/2.7.23.gemfile 8 | - gemfiles/3.0.2.gemfile 9 | - gemfiles/3.1.1.gemfile 10 | - gemfiles/3.2.4.gemfile -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "2.6.18" do 2 | gem "puppet", "2.6.18" 3 | end 4 | 5 | appraise "2.7.23" do 6 | gem "puppet", "2.7.23" 7 | end 8 | 9 | appraise "3.0.2" do 10 | gem "puppet", "3.0.2" 11 | gem "facter", "~> 1.6.11" 12 | end 13 | 14 | appraise "3.1.1" do 15 | gem "puppet", "3.1.1" 16 | gem "facter", "~> 1.6.11" 17 | end 18 | 19 | appraise "3.2.4" do 20 | gem "puppet", "3.2.4" 21 | gem "facter", "~> 1.6.11" 22 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem 'appraisal' 4 | gem 'cucumber-puppet' 5 | gem 'puppet', '~> 2.6.18' 6 | gem 'rake' 7 | gem 'rspec-expectations' 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cucumber-Puppet Example 2 | 3 | [![Build Status](https://travis-ci.org/petems/cucumber-puppet.example.png?branch=master)](https://travis-ci.org/petems/cucumber-puppet.example) 4 | 5 | An example puppet project, using cucumber-puppet. 6 | 7 | Have a look at the commit log to see how the project evolves. 8 | 9 | There are several points to take away: 10 | * use an overarching catalog policy to specify generic behaviour of all 11 | catalogs 12 | * use module specific features to describe those parts of your manifests or 13 | templates, that contain logic 14 | * use module specific features to describe custom extensions like functions 15 | * write high level feature files, don't get yourself hung up on details 16 | * as a corollary, steps are hardly reusable, they should be tailored to your 17 | specific situation 18 | * keep your step implementations short, just write the code you wish you had 19 | * the bulk of the work should be done by custom helper methods 20 | 21 | Are you wondering how to accomplish some task with cucumber-puppet? Do you have 22 | any feedback on the work presented here? Just open an issue and let me know! 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require 'appraisal' 3 | require "bundler/setup" 4 | require 'cucumber-puppet/rake/task' 5 | 6 | CucumberPuppetRakeTask.new do |t| 7 | t.cucumber_opts = %w{--format progress} 8 | end 9 | 10 | task :default => :cucumber_puppet -------------------------------------------------------------------------------- /data/hosts: -------------------------------------------------------------------------------- 1 | # hostname IP MAC 2 | -------------------------------------------------------------------------------- /features/catalog_policy.feature: -------------------------------------------------------------------------------- 1 | Feature: Catalog policy 2 | In order to ensure basic correctness 3 | I want all catalogs to obey my policy 4 | 5 | Scenario Outline: Generic policy for all server roles 6 | Given a node with role "" 7 | When I compile its catalog 8 | Then compilation should succeed 9 | And puppet should ensure all packages are up-to-date 10 | 11 | Examples: 12 | | server_role | 13 | | dhcp_server | 14 | -------------------------------------------------------------------------------- /features/steps/catalog_policy.rb: -------------------------------------------------------------------------------- 1 | Given /^a node with role "([^\"]*)"$/ do |role| 2 | # server roles map to higher level puppet classes 3 | # (in manifests/templates.pp) 4 | @klass = role 5 | end 6 | 7 | Then /^puppet should ensure all packages are up\-to\-date$/ do 8 | packages.each do |package| 9 | package.should be_latest 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /features/support/helpers.rb: -------------------------------------------------------------------------------- 1 | module CucumberPuppet::Helpers 2 | def call_function(name, arg) 3 | Puppet[:modulepath] = 'modules' 4 | Puppet::Parser::Functions.autoloader.loadall 5 | scope = Puppet::Parser::Scope.new 6 | scope.send("function_#{name}", arg) 7 | end 8 | 9 | def file(name) 10 | catalog_resources.each do |resource| 11 | return resource if resource.type == 'File' and resource.title == name 12 | end 13 | end 14 | 15 | def packages 16 | packages = catalog_resources.map do |resource| 17 | resource if resource.type == 'Package' 18 | end 19 | packages.compact 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /features/support/hooks.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | # adjust local configuration like this 3 | @puppetcfg['confdir'] = File.join(File.dirname(__FILE__), '..', '..') 4 | @puppetcfg['manifest'] = File.join(@puppetcfg['confdir'], 'manifests', 'site.pp') 5 | # @puppetcfg['modulepath'] = "/srv/puppet/modules:/srv/puppet/site-modules" 6 | 7 | # adjust facts like this 8 | @facts['architecture'] = "i386" 9 | end 10 | -------------------------------------------------------------------------------- /features/support/matchers.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/expectations' 2 | 3 | module CucumberPuppet::Matchers 4 | RSpec::Matchers.define :be_latest do 5 | match do |resource| 6 | resource['ensure'] == 'latest' 7 | end 8 | 9 | failure_message_for_should do |resource| 10 | "#{resource}\nexpected: ensure => latest\n got: ensure => #{resource['ensure']}" 11 | end 12 | end 13 | 14 | RSpec::Matchers.define :contain do |expected| 15 | match do |resource| 16 | resource['content'].include?(expected) 17 | end 18 | 19 | failure_message_for_should do |resource| 20 | "expected #{resource} to contain\n#{expected}\n" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /features/support/world.rb: -------------------------------------------------------------------------------- 1 | 2 | require 'cucumber-puppet/puppet' 3 | require 'cucumber-puppet/steps' 4 | 5 | World do 6 | CucumberPuppet.new 7 | end 8 | World(CucumberPuppet::Helpers) 9 | World(CucumberPuppet::Matchers) 10 | -------------------------------------------------------------------------------- /gemfiles/2.6.18.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "cucumber-puppet" 7 | gem "rake" 8 | gem "rspec-expectations" 9 | gem "puppet", "2.6.18" 10 | 11 | -------------------------------------------------------------------------------- /gemfiles/2.7.23.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "cucumber-puppet" 7 | gem "rake" 8 | gem "rspec-expectations" 9 | gem "puppet", "2.7.23" 10 | 11 | -------------------------------------------------------------------------------- /gemfiles/3.0.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "cucumber-puppet" 7 | gem "rake" 8 | gem "rspec-expectations" 9 | gem "puppet", "3.0.2" 10 | gem "facter", "~> 1.6.11" 11 | 12 | -------------------------------------------------------------------------------- /gemfiles/3.1.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "cucumber-puppet" 7 | gem "rake" 8 | gem "rspec-expectations" 9 | gem "puppet", "3.1.1" 10 | gem "facter", "~> 1.6.11" 11 | 12 | -------------------------------------------------------------------------------- /gemfiles/3.2.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "cucumber-puppet" 7 | gem "rake" 8 | gem "rspec-expectations" 9 | gem "puppet", "3.2.4" 10 | gem "facter", "~> 1.6.11" 11 | 12 | -------------------------------------------------------------------------------- /manifests/site.pp: -------------------------------------------------------------------------------- 1 | import 'templates' 2 | 3 | node 'testnode' { 4 | include dhcp_server 5 | } 6 | 7 | node default {} 8 | -------------------------------------------------------------------------------- /manifests/templates.pp: -------------------------------------------------------------------------------- 1 | class dhcp_server { 2 | include dhcp::server 3 | } 4 | -------------------------------------------------------------------------------- /modules/dhcp/features/dhcp_clients.feature: -------------------------------------------------------------------------------- 1 | Feature: Read DHCP clients from file 2 | File format: ip_address hostname mac_address 3 | 4 | Scenario: Read client list from file 5 | Given a file containing DHCP clients 6 | """ 7 | 1.1.1.1 one.example.com 1:1:1:1:1:1 8 | # ignores comments and empty lines 9 | 10 | 1.1.1.2 two.example.com 1:1:1:1:1:2 11 | """ 12 | When I call dhcp_clients with this file 13 | Then it should return the parsed list of clients 14 | -------------------------------------------------------------------------------- /modules/dhcp/features/dhcpd.conf.feature: -------------------------------------------------------------------------------- 1 | Feature: Create static host entries from file 2 | 3 | Scenario: Create an entry for each host 4 | Given a hosts file with 5 | """ 6 | 1.1.1.1 one.example.com 1:1:1:1:1:1 7 | 1.1.1.2 two.example.com 1:1:1:1:1:2 8 | """ 9 | And a node of class "dhcp::server" using this file 10 | When I compile its catalog 11 | Then file "/etc/dhcp/dhcpd.conf" should contain 12 | """ 13 | host one.example.com { 14 | hardware ethernet 1:1:1:1:1:1; 15 | fixed-address 1.1.1.1; 16 | } 17 | 18 | host two.example.com { 19 | hardware ethernet 1:1:1:1:1:2; 20 | fixed-address 1.1.1.2; 21 | } 22 | """ 23 | -------------------------------------------------------------------------------- /modules/dhcp/features/steps/dhcp_clients.rb: -------------------------------------------------------------------------------- 1 | Given /^a file containing DHCP clients$/ do |file_content| 2 | File.open("/tmp/dhcp.clients", "w") { |file| 3 | file.puts(file_content) 4 | } 5 | end 6 | 7 | When /^I call dhcp_clients with this file$/ do 8 | @result = call_function('dhcp_clients', ['/tmp/dhcp.clients']) 9 | end 10 | 11 | Then /^it should return the parsed list of clients$/ do 12 | @result.should == [ 13 | { :name => 'one.example.com', :ip => '1.1.1.1', :mac => '1:1:1:1:1:1' }, 14 | { :name => 'two.example.com', :ip => '1.1.1.2', :mac => '1:1:1:1:1:2' } 15 | ] 16 | end 17 | -------------------------------------------------------------------------------- /modules/dhcp/features/steps/dhcpd.conf.rb: -------------------------------------------------------------------------------- 1 | Given /^a hosts file with$/ do |content| 2 | @host_list = '/tmp/host.list' 3 | File.open(@host_list, 'w') do |file| 4 | file.puts content 5 | end 6 | end 7 | 8 | Given /^a node of class "([^"]*)" using this file$/ do |klass| 9 | @facts['hostname'] = 'dhcp_server' 10 | @klass = { klass => { 'host_list' => @host_list } } 11 | end 12 | 13 | Then /^file "([^"]*)" should contain$/ do |file_name, content| 14 | file = file(file_name) 15 | file.should contain(content) 16 | end 17 | -------------------------------------------------------------------------------- /modules/dhcp/lib/puppet/parser/functions/dhcp_clients.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | newfunction(:dhcp_clients, :type => :rvalue) do |args| 3 | raise Puppet::ParseError, ("dhcp_clients(): missing filename argument") unless args.length == 1 4 | 5 | source = args[0] 6 | clients = [] 7 | 8 | file = File.new(source, 'r') 9 | while (line = file.gets) 10 | next if line =~ /^#/ 11 | next if line =~ /^$/ 12 | (ip, name, mac) = line.split 13 | clients << { :name => name, :ip => ip, :mac =>mac } 14 | end 15 | 16 | clients 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /modules/dhcp/manifests/server.pp: -------------------------------------------------------------------------------- 1 | class dhcp::server($host_list = "data/hosts") { 2 | package { 'isc-dhcp-server': 3 | ensure => latest 4 | } 5 | 6 | $hosts = dhcp_clients($host_list) 7 | file { '/etc/dhcp/dhcpd.conf': 8 | content => template('dhcp/dhcpd.conf.erb'), 9 | require => Package['isc-dhcp-server'] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/dhcp/templates/dhcpd.conf.erb: -------------------------------------------------------------------------------- 1 | # 2 | # Sample configuration file for ISC dhcpd for Debian 3 | # 4 | # Attention: If /etc/ltsp/dhcpd.conf exists, that will be used as 5 | # configuration file instead of this file. 6 | # 7 | # 8 | 9 | # The ddns-updates-style parameter controls whether or not the server will 10 | # attempt to do a DNS update when a lease is confirmed. We default to the 11 | # behavior of the version 2 packages ('none', since DHCP v2 didn't 12 | # have support for DDNS.) 13 | ddns-update-style none; 14 | 15 | # option definitions common to all supported networks... 16 | option domain-name "example.org"; 17 | option domain-name-servers ns1.example.org, ns2.example.org; 18 | 19 | default-lease-time 600; 20 | max-lease-time 7200; 21 | 22 | # If this DHCP server is the official DHCP server for the local 23 | # network, the authoritative directive should be uncommented. 24 | authoritative; 25 | 26 | # Use this to send dhcp log messages to a different log file (you also 27 | # have to hack syslog.conf to complete the redirection). 28 | log-facility local7; 29 | 30 | subnet 10.254.239.0 netmask 255.255.255.224 { 31 | range 10.254.239.10 10.254.239.20; 32 | option routers rtr-239-0-1.example.org, rtr-239-0-2.example.org; 33 | } 34 | 35 | <% hosts.each do |host| -%> 36 | host <%= host[:name] %> { 37 | hardware ethernet <%= host[:mac] %>; 38 | fixed-address <%= host[:ip] %>; 39 | } 40 | 41 | <% end -%> 42 | --------------------------------------------------------------------------------