commit 03d9e7daab529101204acd9b22d888103860b545
Author: Steve Traylen <steve.traylen@cern.ch>
Date:   Fri Nov 27 14:42:08 2020 +0100

    New parameter noflush_tables to selectivly skip flush
    
    Introduces a new structured fact nftables
    
    ```yaml
    nftables:
      tables:
        - inet-filter
        - ip-nat
        - ip6-nat
        - inet-f2b-table
    ```
    
    By default the nft script will continue to contain `nft flush ruleset`
    
    If noflush_tables is specified e.g.
    
    ```puppet
    class{nftables:
      noflush_tables => ['inet-f2b-table'],
    }
    ```
    the results script will explicity flush the other tables only.
    i.e.
    
    ```
    flush table inet filter
    flush table ip nat
    flush table ip6 nat
    ```
    
    Motivation here is to allow a maintence of chain to managed by something else.
    This example for fail2ban but could be docker, ...

diff --git a/files/systemd/puppet_nft.conf b/files/systemd/puppet_nft.conf
deleted file mode 100644
index fa66c53..0000000
--- a/files/systemd/puppet_nft.conf
+++ /dev/null
@@ -1,7 +0,0 @@
-# Specify directory to look for relative includes
-[Service]
-ExecStart=
-ExecStart=/sbin/nft -I /etc/nftables/puppet -f /etc/sysconfig/nftables.conf
-ExecReload=
-ExecReload=/sbin/nft -I /etc/nftables/puppet 'flush ruleset; include "/etc/sysconfig/nftables.conf";'
-
diff --git a/lib/facter/nftables.rb b/lib/facter/nftables.rb
new file mode 100644
index 0000000..4a00c74
--- /dev/null
+++ b/lib/facter/nftables.rb
@@ -0,0 +1,21 @@
+#
+# Produce an array of nftables.
+# nft list tables
+# table inet filter
+# table ip nat
+# table ip6 nat
+# table inet f2b-table
+#
+Facter.add(:nftables) do
+  @nft_cmd = Facter::Util::Resolution.which('nft')
+  confine { @nft_cmd }
+
+  setcode do
+    tables = []
+    table_result = Facter::Core::Execution.execute(%(#{@nft_cmd} list tables))
+    table_result.each_line do |line|
+      tables.push(line.split(' ')[1, 2].join('-'))
+    end
+    { 'tables' => tables }
+  end
+end
diff --git a/manifests/init.pp b/manifests/init.pp
index 89d10ea..b2b91a7 100644
--- a/manifests/init.pp
+++ b/manifests/init.pp
@@ -1,11 +1,18 @@
 # @summary Configure nftables
 #
-# @example
+# @example allow dns out and do not allow ntp out
 #   class{'nftables:
 #     out_ntp = false,
 #     out_dns = true,
 #   }
 #
+# @example do not flush particular tables
+# In this case ignoring the fail2ban maintained
+# table
+#   class{'nftables':
+#     noflush_tables = ['inet-f2b-table'],
+#   }
+#
 # @param out_all
 #   Allow all outbound connections. If `true` then all other
 #   out parameters `out_ntp`, `out_dns`, ... will be assuemed
@@ -64,6 +71,10 @@
 #   useful to set this to false if you're externaly removing firewalld from
 #   the system completely.
 #
+# @param noflush_tables
+#   If specified only other existings tables will be flushed.
+#   If left unset all tables will be flushed via a `flush ruleset`
+#
 class nftables (
   Boolean $in_ssh                = true,
   Boolean $in_icmp               = true,
@@ -85,6 +96,8 @@ class nftables (
     $reject_with                 = 'icmpx type port-unreachable',
   Variant[Boolean[false], Enum['mask']]
     $firewalld_enable            = 'mask',
+  Optional[Array[Pattern[/^(ip|ip6|inet)-[-a-zA-Z0-9_]+$/],1]]
+    $noflush_tables = undef,
 ) {
 
   package{'nftables':
@@ -107,7 +120,7 @@ class nftables (
       recurse => true;
     '/etc/nftables/puppet-preflight.nft':
       ensure  => file,
-      content => epp('nftables/config/puppet.nft.epp', { 'nat' => $nat });
+      content => epp('nftables/config/puppet.nft.epp', { 'nat' => $nat, 'noflush' => $noflush_tables });
   } ~> exec{
     'nft validate':
       refreshonly => true,
@@ -119,7 +132,7 @@ class nftables (
       mode  => '0640';
     '/etc/nftables/puppet.nft':
       ensure  => file,
-      content => epp('nftables/config/puppet.nft.epp', { 'nat' => $nat });
+      content => epp('nftables/config/puppet.nft.epp', { 'nat' => $nat, 'noflush' => $noflush_tables });
     '/etc/nftables/puppet':
       ensure  => directory,
       mode    => '0750',
@@ -134,10 +147,10 @@ class nftables (
   }
 
   systemd::dropin_file{'puppet_nft.conf':
-    ensure => present,
-    unit   => 'nftables.service',
-    source => 'puppet:///modules/nftables/systemd/puppet_nft.conf',
-    notify => Service['nftables'],
+    ensure  => present,
+    unit    => 'nftables.service',
+    content => epp('nftables/systemd/puppet_nft.conf.epp', { 'noflush' => $noflush_tables }),
+    notify  => Service['nftables'],
   }
 
   service{'firewalld':
diff --git a/spec/classes/nftables_spec.rb b/spec/classes/nftables_spec.rb
index 6e076e5..fe7ab8a 100644
--- a/spec/classes/nftables_spec.rb
+++ b/spec/classes/nftables_spec.rb
@@ -167,6 +167,51 @@ describe 'nftables' do
           )
         }
       end
+
+      context 'with with noflush_tables parameter' do
+        let(:params) do
+          {
+            noflush_tables: ['inet-f2b-table'],
+          }
+        end
+
+        context 'with no nftables fact' do
+          it {
+            is_expected.to contain_systemd__dropin_file('puppet_nft.conf')
+              .with_content(%r{^ExecReload.*flush ruleset; include.*$})
+          }
+          it { is_expected.to contain_file('/etc/nftables/puppet-preflight.nft').with_content(%r{^flush ruleset$}) }
+        end
+
+        context 'with nftables fact matching' do
+          let(:facts) do
+            super().merge(nftables: { tables: ['inet-abc', 'inet-f2b-table'] })
+          end
+
+          it {
+            is_expected.to contain_systemd__dropin_file('puppet_nft.conf')
+              .with_content(%r{^ExecReload.*flush table inet abc; include.*$})
+          }
+          it {
+            is_expected.to contain_file('/etc/nftables/puppet-preflight.nft')
+              .with_content(%r{^flush table inet abc$})
+          }
+        end
+        context 'with nftables fact not matching' do
+          let(:facts) do
+            super().merge(nftables: { tables: ['inet-abc', 'inet-ijk'] })
+          end
+
+          it {
+            is_expected.to contain_systemd__dropin_file('puppet_nft.conf')
+              .with_content(%r{^ExecReload.*flush table inet abc; flush table inet ijk; include.*$})
+          }
+          it {
+            is_expected.to contain_file('/etc/nftables/puppet-preflight.nft')
+              .with_content(%r{^flush table inet abc; flush table inet ijk$})
+          }
+        end
+      end
     end
   end
 end
diff --git a/spec/unit/facter/nftables_spec.rb b/spec/unit/facter/nftables_spec.rb
new file mode 100644
index 0000000..fcab400
--- /dev/null
+++ b/spec/unit/facter/nftables_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'nftables' do
+  before(:each) do
+    Facter.clear
+    Process.stubs(:uid).returns(0)
+    Facter::Util::Resolution.stubs(:which).with('nft').returns('/usr/sbin/nft')
+    Facter::Core::Execution.stubs(:execute).with('/usr/sbin/nft list tables').returns(nft_result)
+  end
+
+  context 'nft rules present' do
+    let(:nft_result) { "table inet firewalld\ntable ip firewalld\n" }
+
+    it 'returns valid tables' do
+      expect(Facter.fact('nftables').value).to eq('tables' => ['inet-firewalld', 'ip-firewalld'])
+    end
+  end
+
+  context 'nft fails' do
+    let(:nft_result) { :failed }
+
+    it 'does not return a fact' do
+      Facter::Core::Execution.stubs(:execute).with('/usr/sbin/nft list tables', on_fail: :failed).returns(:failed)
+
+      expect(Facter.fact('nftables').value).to be_nil
+    end
+  end
+end
diff --git a/templates/config/puppet.nft.epp b/templates/config/puppet.nft.epp
index cfc32cb..bd92830 100644
--- a/templates/config/puppet.nft.epp
+++ b/templates/config/puppet.nft.epp
@@ -1,3 +1,16 @@
+<%- |
+  Boolean $nat,
+  Optional[Array[String[1],1]] $noflush = undef,
+|-%>
+<%-
+if $noflush and $facts['nftables'] and $facts['nftables']['tables'] {
+ $_flush_command = $facts['nftables']['tables'].filter |$_tab| { ! ($_tab in $noflush) }.map |$_table| {
+   "flush table ${regsubst($_table,'-',' ')}"
+ }
+} else {
+ $_flush_command = ['flush ruleset']
+}
+-%>
 # puppet-preflight.nft is only used by puppet for validating new configs
 # puppet.nft is real configuration that the nftables services uses.
 # To process either the -I flag must be specified.
@@ -5,7 +18,7 @@
 # nft -c -I /etc/nftables/puppet-preflight -f /etc/nftables/puppet-preflight.nft
 
 # drop any existing nftables ruleset
-flush ruleset
+<%= $_flush_command.join('; ') %>
 
 include "custom-*.nft"
 include "inet-filter.nft"
diff --git a/templates/systemd/puppet_nft.conf.epp b/templates/systemd/puppet_nft.conf.epp
new file mode 100644
index 0000000..a1da0d2
--- /dev/null
+++ b/templates/systemd/puppet_nft.conf.epp
@@ -0,0 +1,19 @@
+<%- |
+  Optional[Array[String[1]]] $noflush = undef,
+| -%>
+<%-
+if $noflush and $facts['nftables'] and $facts['nftables']['tables'] {
+ $_flush_command = $facts['nftables']['tables'].filter |$_tab| { !( $_tab in $noflush) }.map |$_table| {
+     "flush table ${regsubst($_table,'-',' ')}"
+ }
+} else {
+ $_flush_command = ['flush ruleset']
+}
+-%>
+# Specify directory to look for relative includes
+[Service]
+ExecStart=
+ExecStart=/sbin/nft -I /etc/nftables/puppet -f /etc/sysconfig/nftables.conf
+ExecReload=
+ExecReload=/sbin/nft -I /etc/nftables/puppet '<%= $_flush_command.join('; ') %>; include "/etc/sysconfig/nftables.conf";'
+
