Firewall and firewallchain persistence
authorDan Carley <dan.carley@gmail.com>
Tue, 26 Feb 2013 21:07:01 +0000 (21:07 +0000)
committerDan Carley <dan.carley@gmail.com>
Thu, 28 Feb 2013 22:31:24 +0000 (22:31 +0000)
Call the necessary commands from the provider to persist rules between
reboots. Tested on the following distros:

- CentOS 5.8
- CentOS 6.3
- Ubuntu 10.04
- Ubuntu 12.04
- Debian 6

Cavaets:

- Persistence may fail on the first run if Firewall resources are actioned
  before the Package resource.
- Older iptables-persistent doesn't support the restoration of ip6tables.
- ebtables cannot be restored.

README.markdown
lib/facter/iptables_persistent_version.rb [new file with mode: 0644]
lib/puppet/provider/firewall/ip6tables.rb
lib/puppet/provider/firewall/iptables.rb
lib/puppet/provider/firewallchain/iptables_chain.rb
lib/puppet/type/firewallchain.rb
lib/puppet/util/firewall.rb
manifests/init.pp
spec/classes/firewall_linux_debian_spec.rb
spec/classes/firewall_linux_redhat_spec.rb
spec/unit/facter/iptables_persistent_version_spec.rb [new file with mode: 0644]

index 5003a428f7482f43eb2712e2352e0fa8c83b9479..0686715d0887cae99d7799ddbb66e7fc7daaf7f8 100644 (file)
@@ -79,33 +79,15 @@ case.
 ### Recommended Setup
 
 At the moment you need to provide some setup outside of what we provide in the 
-module to support proper ordering, purging and firewall peristence.
+module to support proper ordering and purging.
 
-So It is recommended that you provide the following in top scope somewhere
-(such as your site.pp):
+Persistence of rules between reboots is handled automatically for the
+supported distributions listed below. Although there are known issues with
+ip6tables on older Debian/Ubuntu and ebtables.
 
-    # Always persist firewall rules
-    exec { 'persist-firewall':
-      command     => $operatingsystem ? {
-        'debian'          => '/sbin/iptables-save > /etc/iptables/rules.v4',
-        /(RedHat|CentOS)/ => '/sbin/iptables-save > /etc/sysconfig/iptables',
-      },
-      refreshonly => true,
-    }
+It is recommended that you provide the following in top scope somewhere
+(such as your site.pp):
 
-    # These defaults ensure that the persistence command is executed after 
-    # every change to the firewall, and that pre & post classes are run in the
-    # right order to avoid potentially locking you out of your box during the
-    # first puppet run.
-    Firewall {
-      notify  => Exec['persist-firewall'],
-      before  => Class['my_fw::post'],
-      require => Class['my_fw::pre'],
-    }
-    Firewallchain {
-      notify  => Exec['persist-firewall'],
-    }
-    
     # Purge unmanaged firewall resources
     #
     # This will clear any existing rules, and make sure that only rules
@@ -113,6 +95,14 @@ So It is recommended that you provide the following in top scope somewhere
     resources { "firewall":
       purge => true
     }
+    
+    # These defaults ensure that the pre & post classes are run in the right
+    # order to avoid potentially locking you out of your box during the
+    # first puppet run.
+    Firewall {
+      before  => Class['my_fw::post'],
+      require => Class['my_fw::pre'],
+    }
 
 You also need to declare the 'my_fw::pre' & 'my_fw::post' classes so that 
 dependencies are satisfied. This can be achieved using an External Node 
diff --git a/lib/facter/iptables_persistent_version.rb b/lib/facter/iptables_persistent_version.rb
new file mode 100644 (file)
index 0000000..2bae97a
--- /dev/null
@@ -0,0 +1,13 @@
+Facter.add(:iptables_persistent_version) do
+  confine :operatingsystem => %w{Debian Ubuntu}
+  setcode do
+    cmd = "dpkg-query -Wf '${Version}' iptables-persistent"
+    version = Facter::Util::Resolution.exec(cmd)
+
+    if version.nil? or !version.match(/\d+\.\d+/)
+      nil
+    else
+      version
+    end
+  end
+end
index 57599206d9bebb7c61c6ec8d49b283454dacc9bd..3654046849435d0e8e7fc60267eddb29b5983e8a 100644 (file)
@@ -29,6 +29,8 @@ Puppet::Type.type(:firewall).provide :ip6tables, :parent => :iptables, :source =
     ip6tables_save(*args)
   end
 
+  @protocol = "IPv6"
+
   @resource_map = {
     :burst => "--limit-burst",
     :destination => "-d",
index b34e73613279b88ab34cf5a141c7563508e2b5c3..b193cd1a13cf66b8d741c363d49c194d63944278 100644 (file)
@@ -36,6 +36,8 @@ Puppet::Type.type(:firewall).provide :iptables, :parent => Puppet::Provider::Fir
     mark_flag = '--set-xmark'
   end
 
+  @protocol = "IPv4"
+
   @resource_map = {
     :burst => "--limit-burst",
     :destination => "-d",
@@ -100,6 +102,7 @@ Puppet::Type.type(:firewall).provide :iptables, :parent => Puppet::Provider::Fir
       notice("Properties changed - updating rule")
       update
     end
+    persist_iptables(self.class.instance_variable_get(:@protocol))
     @property_hash.clear
   end
 
index 0efb55d4bb4768b2a6b198a49b3d1a58bd842f12..049fb4e0fb89e3821d7a6faeb4640ca913958e05 100644 (file)
@@ -1,4 +1,6 @@
 Puppet::Type.type(:firewallchain).provide :iptables_chain do
+  include Puppet::Util::Firewall
+
   @doc = "Iptables chain provider"
 
   has_feature :iptables_chain
@@ -96,6 +98,7 @@ Puppet::Type.type(:firewallchain).provide :iptables_chain do
 
   def flush
     debug("[flush]")
+    persist_iptables(@resource[:name].match(Nameformat)[3])
     # Clear the property hash so we re-initialize with updated values
     @property_hash.clear
   end
index 0bab44a924ccab04aedf50bb45d17fc82cdc5212..7eade4bc8eedd1dbac659a81e5268d1742933244 100644 (file)
@@ -1,4 +1,14 @@
+# This is a workaround for bug: #4248 whereby ruby files outside of the normal
+# provider/type path do not load until pluginsync has occured on the puppetmaster
+#
+# In this case I'm trying the relative path first, then falling back to normal
+# mechanisms. This should be fixed in future versions of puppet but it looks
+# like we'll need to maintain this for some time perhaps.
+$LOAD_PATH.unshift(File.join(File.dirname(__FILE__),"..",".."))
+require 'puppet/util/firewall'
+
 Puppet::Type.newtype(:firewallchain) do
+  include Puppet::Util::Firewall
 
   @doc = <<-EOS
     This type provides the capability to manage rule chains for firewalls.
index cdf20aa88e2b871cacaeeccc0057e9f9a065eb6a..b388bcae9dfb5098b2518b5dc3d1866fc5e9cfa9 100644 (file)
@@ -129,4 +129,59 @@ module Puppet::Util::Firewall
       end
     return nil
   end
+
+  def persist_iptables(proto)
+    debug("[persist_iptables]")
+
+    # Basic normalisation for older Facter
+    os_key = Facter.value(:osfamily)
+    os_key ||= case Facter.value(:operatingsystem)
+    when 'RedHat', 'CentOS', 'Fedora'
+      'RedHat'
+    when 'Debian', 'Ubuntu'
+      'Ubuntu'
+    else
+      Facter.value(:operatingsystem)
+    end
+
+    # Older iptables-persistent doesn't provide save action.
+    if os_key == 'Debian'
+      persist_ver = Facter.value(:iptables_persistent_version)
+      if (persist_ver and Puppet::Util::Package.versioncmp(persist_ver, '0.5.0') < 0)
+        os_key = 'Debian_manual'
+      end
+    end
+
+    cmd = case os_key.to_sym
+    when :RedHat
+      case proto.to_sym
+      when :IPv4
+        %w{/sbin/service iptables save}
+      when :IPv6
+        %w{/sbin/service ip6tables save}
+      end
+    when :Debian
+      case proto.to_sym
+      when :IPv4, :IPv6
+        %w{/usr/sbin/service iptables-persistent save}
+      end
+    when :Debian_manual
+      case proto.to_sym
+      when :IPv4
+        ["/bin/sh", "-c", "/sbin/iptables-save > /etc/iptables/rules"]
+      end
+    end
+
+    # Catch unsupported OSs from the case statement above.
+    if cmd.nil?
+      debug('firewall: Rule persistence is not supported for this type/OS')
+      return
+    end
+
+    begin
+      execute(cmd)
+    rescue Puppet::ExecutionFailure => detail
+      warning("Unable to persist firewall rules: #{detail}")
+    end
+  end
 end
index 2aa61558135e46f876ce1389cd33bbde7e345403..9730d3b8eab91dfaf21424d5f95d17513e177d8a 100644 (file)
@@ -1,7 +1,7 @@
 # Class: firewall
 #
 # Manages the installation of packages for operating systems that are
-#   currently supported by the firewall type.
+# currently supported by the firewall type.
 #
 class firewall {
   case $::kernel {
index 2d7581ec6c887d0bc0a34cdb4c6213d9cfd8991f..3215ce78c61b2fbb8785bd3241283ed0c03772fa 100644 (file)
@@ -2,11 +2,11 @@ require 'spec_helper'
 
 describe 'firewall::linux::debian' do
   it { should contain_package('iptables-persistent').with(
-    :ensure => 'present',
+    :ensure => 'present'
   )}
   it { should contain_service('iptables-persistent').with(
     :ensure   => nil,
     :enable   => 'true',
-    :require  => 'Package[iptables-persistent]',
+    :require  => 'Package[iptables-persistent]'
   )}
 end
index 89f30fc5b51f0fd8885542b2410ea54789459f73..a4a307bb73d49d6369ff5190c1ce87dad8c4559e 100644 (file)
@@ -3,6 +3,6 @@ require 'spec_helper'
 describe 'firewall::linux::redhat' do
   it { should contain_service('iptables').with(
     :ensure => 'running',
-    :enable => 'true',
+    :enable => 'true'
   )}
 end
diff --git a/spec/unit/facter/iptables_persistent_version_spec.rb b/spec/unit/facter/iptables_persistent_version_spec.rb
new file mode 100644 (file)
index 0000000..2c79a1b
--- /dev/null
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe "Facter::Util::Fact iptables_persistent_version" do
+  before { Facter.clear }
+  let(:dpkg_cmd) { "dpkg-query -Wf '${Version}' iptables-persistent" }
+
+  {
+    "Debian" => "0.0.20090701",
+    "Ubuntu" => "0.5.3ubuntu2",
+  }.each do |os, ver|
+    describe "#{os} package installed" do
+      before {
+        Facter.fact(:operatingsystem).stubs(:value).returns(os)
+        Facter::Util::Resolution.stubs(:exec).with(dpkg_cmd).returns(ver)
+      }
+      it { Facter.fact(:iptables_persistent_version).value.should == ver }
+    end
+  end
+
+  describe 'Ubuntu package not installed' do
+    before {
+      Facter.fact(:operatingsystem).stubs(:value).returns("Ubuntu")
+      Facter::Util::Resolution.stubs(:exec).with(dpkg_cmd).returns(nil)
+    }
+    it { Facter.fact(:iptables_persistent_version).value.should be_nil }
+  end
+
+  describe 'CentOS not supported' do
+    before { Facter.fact(:operatingsystem).stubs(:value).returns("CentOS") }
+    it { Facter.fact(:iptables_persistent_version).value.should be_nil }
+  end
+end