From 2ec07247a2c16f749daddde4a00730e983b039f1 Mon Sep 17 00:00:00 2001
From: Hunter Haugen <hunter@puppetlabs.com>
Date: Mon, 27 Jan 2014 17:31:22 -0800
Subject: [PATCH] (MODULES-439) Work around existing rules

The firewall resource is not intended to be used with rules that are not
also managed by puppet; the behavior when doing so was undefined. This
is an attempt to make it more defined.

The behavior is that any rule added by puppet will be inserted in its
given order in relation to the other rules managed by puppet, but ahead
of any rules not managed by puppet.
---
 Gemfile                                       |  1 +
 lib/puppet/provider/firewall/iptables.rb      | 33 +++++++-
 spec/acceptance/change_source_spec.rb         | 77 +++++++++++++++++
 spec/acceptance/nodesets/centos-59-x64.yml    |  4 +-
 .../nodesets/centos-64-x64-fusion.yml         | 10 +++
 spec/acceptance/nodesets/centos-64-x64-pe.yml | 12 +++
 spec/acceptance/nodesets/centos-64-x64.yml    |  4 +-
 spec/acceptance/nodesets/debian-607-x64.yml   | 10 +++
 spec/acceptance/nodesets/debian-70rc1-x64.yml | 10 +++
 spec/acceptance/nodesets/fedora-18-x64.yml    | 10 +++
 spec/acceptance/nodesets/sles-11sp1-x64.yml   | 10 +++
 .../nodesets/ubuntu-server-10044-x64.yml      | 10 +++
 .../nodesets/ubuntu-server-12042-x64.yml      |  4 +-
 spec/unit/puppet/provider/iptables_spec.rb    | 84 +++++++++++++++++++
 14 files changed, 275 insertions(+), 4 deletions(-)
 create mode 100644 spec/acceptance/change_source_spec.rb
 create mode 100644 spec/acceptance/nodesets/centos-64-x64-fusion.yml
 create mode 100644 spec/acceptance/nodesets/centos-64-x64-pe.yml
 create mode 100644 spec/acceptance/nodesets/debian-607-x64.yml
 create mode 100644 spec/acceptance/nodesets/debian-70rc1-x64.yml
 create mode 100644 spec/acceptance/nodesets/fedora-18-x64.yml
 create mode 100644 spec/acceptance/nodesets/sles-11sp1-x64.yml
 create mode 100644 spec/acceptance/nodesets/ubuntu-server-10044-x64.yml

diff --git a/Gemfile b/Gemfile
index 2909ace..690b772 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,6 +6,7 @@ group :development, :test do
   gem 'serverspec',             :require => false
   gem 'beaker-rspec',           :require => false
   gem 'puppet-lint',            :require => false
+  gem 'pry',                    :require => false
 end
 
 if puppetversion = ENV['PUPPET_GEM_VERSION']
diff --git a/lib/puppet/provider/firewall/iptables.rb b/lib/puppet/provider/firewall/iptables.rb
index 8c07a62..905ab54 100644
--- a/lib/puppet/provider/firewall/iptables.rb
+++ b/lib/puppet/provider/firewall/iptables.rb
@@ -408,8 +408,39 @@ Puppet::Type.type(:firewall).provide :iptables, :parent => Puppet::Provider::Fir
     # No rules at all? Just bail now.
     return 1 if rules.empty?
 
+    # Add our rule to the end of the array of known rules
     my_rule = resource[:name].to_s
     rules << my_rule
-    rules.sort.index(my_rule) + 1
+
+    # Find if this is a new rule or an existing rule, then find how many
+    # unmanaged rules preceed it.
+    if rules.length == rules.uniq.length
+      # This is a new rule so find its ordered location.
+      new_rule_location = rules.sort.uniq.index(my_rule)
+      if new_rule_location == 0
+        # The rule will be the first rule in the chain because nothing came
+        # before it.
+        offset_rule = rules[0]
+      else
+        # This rule will come after other managed rules, so find the rule
+        # immediately preceeding it.
+        offset_rule = rules.sort.uniq[new_rule_location - 1]
+      end
+    else
+      # This is a pre-existing rule, so find the offset from the original
+      # ordering.
+      offset_rule = my_rule
+    end
+    # Count how many unmanaged rules are ahead of the target rule so we know
+    # how much to add to the insert order
+    unnamed_offset = rules[0..rules.index(offset_rule)].inject(0) do |sum,rule|
+      # This regex matches the names given to unmanaged rules (a number
+      # 9000-9999 followed by an MD5 hash).
+      sum + (rule.match(/^9[0-9]{3}\s[a-f0-9]{32}$/) ? 1 : 0)
+    end
+
+    # Insert our new or updated rule in the correct order of named rules, but
+    # offset for unnamed rules.
+    rules.sort.index(my_rule) + 1 + unnamed_offset
   end
 end
diff --git a/spec/acceptance/change_source_spec.rb b/spec/acceptance/change_source_spec.rb
new file mode 100644
index 0000000..02760b0
--- /dev/null
+++ b/spec/acceptance/change_source_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper_acceptance'
+
+describe 'firewall type' do
+  describe 'reset' do
+    it 'deletes all rules' do
+      shell('iptables --flush; iptables -t nat --flush; iptables -t mangle --flush')
+    end
+  end
+
+  describe 'when unmanaged rules exist' do
+    it 'applies with 8.0.0.1 first' do
+      pp = <<-EOS
+          class { '::firewall': }
+          firewall { '101 test source changes':
+            proto  => tcp,
+            port   => '101',
+            action => accept,
+            source => '8.0.0.1',
+          }
+          firewall { '100 test source static':
+            proto  => tcp,
+            port   => '100',
+            action => accept,
+            source => '8.0.0.2',
+          }
+      EOS
+
+      apply_manifest(pp, :catch_failures => true)
+    end
+
+    it 'contains the changable 8.0.0.1 rule' do
+      shell('iptables -S') do |r|
+        expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.1\/32 -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/)
+      end
+    end
+    it 'contains the static 8.0.0.2 rule' do
+      shell('iptables -S') do |r|
+        expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2\/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/)
+      end
+    end
+
+    it 'adds a unmanaged rule without a comment' do
+      shell('/sbin/iptables -R INPUT 1 -t filter -s 8.0.0.3/32 -p tcp -m multiport --ports 100 -j ACCEPT')
+      expect(shell('iptables -S').stdout).to match(/-A INPUT -s 8\.0\.0\.3\/32 -p tcp -m multiport --ports 100 -j ACCEPT/)
+    end
+
+    it 'changes to 8.0.0.4 second' do
+      pp = <<-EOS
+          class { '::firewall': }
+          firewall { '101 test source changes':
+            proto  => tcp,
+            port   => '101',
+            action => accept,
+            source => '8.0.0.4',
+          }
+      EOS
+
+      expect(apply_manifest(pp, :catch_failures => true).stdout).to match(/Notice: \/Stage\[main\]\/Main\/Firewall\[101 test source changes\]\/source: source changed '8\.0\.0\.1\/32' to '8\.0\.0\.4\/32'/)
+    end
+
+    it 'does not contain the old changing 8.0.0.1 rule' do
+      shell('iptables -S') do |r|
+        expect(r.stdout).to_not match(/8\.0\.0\.1/)
+      end
+    end
+    it 'contains the staic 8.0.0.2 rule' do
+      shell('iptables -S') do |r|
+        expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2\/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/)
+      end
+    end
+    it 'contains the changing new 8.0.0.4 rule' do
+      shell('iptables -S') do |r|
+        expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.4\/32 -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/)
+      end
+    end
+  end
+end
diff --git a/spec/acceptance/nodesets/centos-59-x64.yml b/spec/acceptance/nodesets/centos-59-x64.yml
index b0a4ba8..b41a947 100644
--- a/spec/acceptance/nodesets/centos-59-x64.yml
+++ b/spec/acceptance/nodesets/centos-59-x64.yml
@@ -2,7 +2,9 @@ HOSTS:
   centos-59-x64:
     roles:
       - master
-    platform: centos-59-x64
+    platform: el-5-x86_64
     box : centos-59-x64-vbox4210-nocm
     box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box
     hypervisor : vagrant
+CONFIG:
+  type: foss
diff --git a/spec/acceptance/nodesets/centos-64-x64-fusion.yml b/spec/acceptance/nodesets/centos-64-x64-fusion.yml
new file mode 100644
index 0000000..d516673
--- /dev/null
+++ b/spec/acceptance/nodesets/centos-64-x64-fusion.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  centos-64-x64:
+    roles:
+      - master
+    platform: el-6-x86_64
+    box : centos-64-x64-fusion503-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-fusion503-nocm.box
+    hypervisor : fusion
+CONFIG:
+  type: foss
diff --git a/spec/acceptance/nodesets/centos-64-x64-pe.yml b/spec/acceptance/nodesets/centos-64-x64-pe.yml
new file mode 100644
index 0000000..7d9242f
--- /dev/null
+++ b/spec/acceptance/nodesets/centos-64-x64-pe.yml
@@ -0,0 +1,12 @@
+HOSTS:
+  centos-64-x64:
+    roles:
+      - master
+      - database
+      - dashboard
+    platform: el-6-x86_64
+    box : centos-64-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: pe
diff --git a/spec/acceptance/nodesets/centos-64-x64.yml b/spec/acceptance/nodesets/centos-64-x64.yml
index 8f57028..05540ed 100644
--- a/spec/acceptance/nodesets/centos-64-x64.yml
+++ b/spec/acceptance/nodesets/centos-64-x64.yml
@@ -2,7 +2,9 @@ HOSTS:
   centos-64-x64:
     roles:
       - master
-    platform: el-6-i386
+    platform: el-6-x86_64
     box : centos-64-x64-vbox4210-nocm
     box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box
     hypervisor : vagrant
+CONFIG:
+  type: foss
diff --git a/spec/acceptance/nodesets/debian-607-x64.yml b/spec/acceptance/nodesets/debian-607-x64.yml
new file mode 100644
index 0000000..4c8be42
--- /dev/null
+++ b/spec/acceptance/nodesets/debian-607-x64.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  debian-607-x64:
+    roles:
+      - master
+    platform: debian-6-amd64
+    box : debian-607-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: git
diff --git a/spec/acceptance/nodesets/debian-70rc1-x64.yml b/spec/acceptance/nodesets/debian-70rc1-x64.yml
new file mode 100644
index 0000000..19181c1
--- /dev/null
+++ b/spec/acceptance/nodesets/debian-70rc1-x64.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  debian-70rc1-x64:
+    roles:
+      - master
+    platform: debian-7-amd64
+    box : debian-70rc1-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: git
diff --git a/spec/acceptance/nodesets/fedora-18-x64.yml b/spec/acceptance/nodesets/fedora-18-x64.yml
new file mode 100644
index 0000000..624b537
--- /dev/null
+++ b/spec/acceptance/nodesets/fedora-18-x64.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  fedora-18-x64:
+    roles:
+      - master
+    platform: fedora-18-x86_64
+    box : fedora-18-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: git
diff --git a/spec/acceptance/nodesets/sles-11sp1-x64.yml b/spec/acceptance/nodesets/sles-11sp1-x64.yml
new file mode 100644
index 0000000..554c37a
--- /dev/null
+++ b/spec/acceptance/nodesets/sles-11sp1-x64.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  sles-11sp1-x64:
+    roles:
+      - master
+    platform: sles-11-x86_64
+    box : sles-11sp1-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: git
diff --git a/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml b/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml
new file mode 100644
index 0000000..5047017
--- /dev/null
+++ b/spec/acceptance/nodesets/ubuntu-server-10044-x64.yml
@@ -0,0 +1,10 @@
+HOSTS:
+  ubuntu-server-10044-x64:
+    roles:
+      - master
+    platform: ubuntu-10.04-amd64
+    box : ubuntu-server-10044-x64-vbox4210-nocm
+    box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box
+    hypervisor : vagrant
+CONFIG:
+  type: git
diff --git a/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml b/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml
index 2b8fe4a..d065b30 100644
--- a/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml
+++ b/spec/acceptance/nodesets/ubuntu-server-12042-x64.yml
@@ -2,7 +2,9 @@ HOSTS:
   ubuntu-server-12042-x64:
     roles:
       - master
-    platform: ubuntu-server-12.04-amd64
+    platform: ubuntu-12.04-amd64
     box : ubuntu-server-12042-x64-vbox4210-nocm
     box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box
     hypervisor : vagrant
+CONFIG:
+  type: foss
diff --git a/spec/unit/puppet/provider/iptables_spec.rb b/spec/unit/puppet/provider/iptables_spec.rb
index d4dcea5..d34679f 100644
--- a/spec/unit/puppet/provider/iptables_spec.rb
+++ b/spec/unit/puppet/provider/iptables_spec.rb
@@ -79,6 +79,90 @@ describe 'iptables provider' do
     expect(provider.instances.length).to be_zero
   end
 
+  describe '#insert_order' do
+    let(:iptables_save_output) { [
+      '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT',
+      '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -m comment --comment "200 test" -j ACCEPT',
+      '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT'
+    ] }
+    let(:resources) do
+      iptables_save_output.each_with_index.collect { |l,index| provider.rule_to_hash(l, 'filter', index) }
+    end
+    let(:providers) do
+      resources.collect { |r| provider.new(r) }
+    end
+    it 'understands offsets for adding rules to the beginning' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '001 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(1) # 1-indexed
+    end
+    it 'understands offsets for editing rules at the beginning' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '100 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(1)
+    end
+    it 'understands offsets for adding rules to the middle' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '101 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(2)
+    end
+    it 'understands offsets for editing rules at the middle' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '200 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(2)
+    end
+    it 'understands offsets for adding rules to the end' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '301 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(4)
+    end
+    it 'understands offsets for editing rules at the end' do
+      resource = Puppet::Type.type(:firewall).new({ :name => '300 test', })
+      allow(resource.provider.class).to receive(:instances).and_return(providers)
+      expect(resource.provider.insert_order).to eq(3)
+    end
+
+    context 'with an unname rule' do
+      let(:iptables_save_output) { [
+        '-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT',
+        '-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT',
+        '-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT',
+        '-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 400 -j ACCEPT',
+        '-A INPUT -s 8.0.0.6/32 -p tcp -m multiport --ports 500 -m comment --comment "500 test" -j ACCEPT'
+      ] }
+      it 'understands offsets for adding rules before unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '001 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(1)
+      end
+      it 'understands offsets for editing rules before unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '100 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(1)
+      end
+      it 'understands offsets for adding rules between unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '301 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(4)
+      end
+      it 'understands offsets for editing rules between unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '300 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(3)
+      end
+      it 'understands offsets for adding rules after unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '501 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(6)
+      end
+      it 'understands offsets for editing rules after unnamed rules' do
+        resource = Puppet::Type.type(:firewall).new({ :name => '500 test', })
+        allow(resource.provider.class).to receive(:instances).and_return(providers)
+        expect(resource.provider.insert_order).to eq(5)
+      end
+    end
+  end
+
   # Load in ruby hash for test fixtures.
   load 'spec/fixtures/iptables/conversion_hash.rb'
 
-- 
2.45.2