diff --git a/.fixtures.yml b/.fixtures.yml
new file mode 100644
index 0000000..bc259d8
--- /dev/null
+++ b/.fixtures.yml
@@ -0,0 +1,4 @@
+fixtures:
+ repositories:
+ stdlib: 'https://github.com/puppetlabs/puppetlabs-stdlib.git'
+ systemd: 'https://github.com/camptocamp/puppet-systemd.git'
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..9032a01
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+*.rb eol=lf
+*.erb eol=lf
+*.pp eol=lf
+*.sh eol=lf
+*.epp eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f1e7f60
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+.git/
+.*.sw[op]
+.metadata
+.yardoc
+.yardwarns
+*.iml
+/.bundle/
+/.idea/
+/.vagrant/
+/coverage/
+/bin/
+/doc/
+/Gemfile.local
+/junit/
+/log/
+/pkg/
+/spec/fixtures/manifests/
+/spec/fixtures/modules/
+/tmp/
+/vendor/
+/convert_report.txt
+/update_report.txt
+.DS_Store
+Gemfile.lock
+.vendor/
diff --git a/.puppet-lint.rc b/.puppet-lint.rc
new file mode 100644
index 0000000..cc96ece
--- /dev/null
+++ b/.puppet-lint.rc
@@ -0,0 +1 @@
+--relative
diff --git a/.rspec b/.rspec
new file mode 100644
index 0000000..16f9cdb
--- /dev/null
+++ b/.rspec
@@ -0,0 +1,2 @@
+--color
+--format documentation
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..f5a6c2a
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,122 @@
+---
+require: rubocop-rspec
+AllCops:
+ DisplayCopNames: true
+ TargetRubyVersion: '2.1'
+ Include:
+ - "./**/*.rb"
+ Exclude:
+ - bin/*
+ - ".vendor/**/*"
+ - "**/Gemfile"
+ - "**/Rakefile"
+ - pkg/**/*
+ - spec/fixtures/**/*
+ - vendor/**/*
+ - "**/Puppetfile"
+ - "**/Vagrantfile"
+ - "**/Guardfile"
+Metrics/LineLength:
+ Description: People have wide screens, use them.
+ Max: 200
+GetText/DecorateString:
+ Description: We don't want to decorate test output.
+ Exclude:
+ - spec/*
+RSpec/BeforeAfterAll:
+ Description: Beware of using after(:all) as it may cause state to leak between tests.
+ A necessary evil in acceptance testing.
+ Exclude:
+ - spec/acceptance/**/*.rb
+RSpec/HookArgument:
+ Description: Prefer explicit :each argument, matching existing module's style
+ EnforcedStyle: each
+Style/BlockDelimiters:
+ Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to
+ be consistent then.
+ EnforcedStyle: braces_for_chaining
+Style/ClassAndModuleChildren:
+ Description: Compact style reduces the required amount of indentation.
+ EnforcedStyle: compact
+Style/EmptyElse:
+ Description: Enforce against empty else clauses, but allow `nil` for clarity.
+ EnforcedStyle: empty
+Style/FormatString:
+ Description: Following the main puppet project's style, prefer the % format format.
+ EnforcedStyle: percent
+Style/FormatStringToken:
+ Description: Following the main puppet project's style, prefer the simpler template
+ tokens over annotated ones.
+ EnforcedStyle: template
+Style/Lambda:
+ Description: Prefer the keyword for easier discoverability.
+ EnforcedStyle: literal
+Style/RegexpLiteral:
+ Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168
+ EnforcedStyle: percent_r
+Style/TernaryParentheses:
+ Description: Checks for use of parentheses around ternary conditions. Enforce parentheses
+ on complex expressions for better readability, but seriously consider breaking
+ it up.
+ EnforcedStyle: require_parentheses_when_complex
+Style/TrailingCommaInArguments:
+ Description: Prefer always trailing comma on multiline argument lists. This makes
+ diffs, and re-ordering nicer.
+ EnforcedStyleForMultiline: comma
+Style/TrailingCommaInLiteral:
+ Description: Prefer always trailing comma on multiline literals. This makes diffs,
+ and re-ordering nicer.
+ EnforcedStyleForMultiline: comma
+Style/SymbolArray:
+ Description: Using percent style obscures symbolic intent of array's contents.
+ EnforcedStyle: brackets
+RSpec/MessageSpies:
+ EnforcedStyle: receive
+Style/Documentation:
+ Exclude:
+ - lib/puppet/parser/functions/**/*
+ - spec/**/*
+Style/WordArray:
+ EnforcedStyle: brackets
+Style/CollectionMethods:
+ Enabled: true
+Style/MethodCalledOnDoEndBlock:
+ Enabled: true
+Style/StringMethods:
+ Enabled: true
+Layout/EndOfLine:
+ Enabled: false
+Layout/IndentHeredoc:
+ Enabled: false
+Metrics/AbcSize:
+ Enabled: false
+Metrics/BlockLength:
+ Enabled: false
+Metrics/ClassLength:
+ Enabled: false
+Metrics/CyclomaticComplexity:
+ Enabled: false
+Metrics/MethodLength:
+ Enabled: false
+Metrics/ModuleLength:
+ Enabled: false
+Metrics/ParameterLists:
+ Enabled: false
+Metrics/PerceivedComplexity:
+ Enabled: false
+RSpec/DescribeClass:
+ Enabled: false
+RSpec/ExampleLength:
+ Enabled: false
+RSpec/MessageExpectation:
+ Enabled: false
+RSpec/MultipleExpectations:
+ Enabled: false
+RSpec/NestedGroups:
+ Enabled: false
+Style/AsciiComments:
+ Enabled: false
+Style/IfUnlessModifier:
+ Enabled: false
+Style/SymbolProc:
+ Enabled: false
diff --git a/.sync.yml b/.sync.yml
new file mode 100644
index 0000000..1a94c33
--- /dev/null
+++ b/.sync.yml
@@ -0,0 +1,9 @@
+---
+.travis.yml:
+ docker_sets:
+ - set: ubuntu1604-64
+ - set: ubuntu1804-64
+ - set: centos7-64
+ - set: debian8-64
+ - set: debian9-64
+ - set: debian10-64
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..003610e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,95 @@
+---
+dist: xenial
+language: ruby
+cache: bundler
+before_install:
+ - gem update --system
+ - gem update bundler
+ - bundle --version
+script:
+ - 'bundle exec rake $CHECK'
+matrix:
+ fast_finish: true
+ include:
+ - rvm: 2.4.4
+ bundler_args: --without system_tests development release
+ env: PUPPET_VERSION="~> 5.0" CHECK=test
+ - rvm: 2.5.3
+ bundler_args: --without system_tests development release
+ env: PUPPET_VERSION="~> 6.0" CHECK=test_with_coveralls
+ - rvm: 2.5.3
+ bundler_args: --without system_tests development release
+ env: PUPPET_VERSION="~> 6.0" CHECK=rubocop
+ - rvm: 2.4.4
+ bundler_args: --without system_tests development release
+ env: PUPPET_VERSION="~> 5.0" CHECK=build DEPLOY_TO_FORGE=yes
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=ubuntu1604-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=ubuntu1604-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=ubuntu1804-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=ubuntu1804-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=centos7-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=centos7-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=debian8-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=debian8-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=debian9-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=debian9-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet5 BEAKER_debug=true BEAKER_setfile=debian10-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+ - rvm: 2.5.3
+ bundler_args: --without development release
+ env: PUPPET_INSTALL_TYPE=agent BEAKER_IS_PE=no BEAKER_PUPPET_COLLECTION=puppet6 BEAKER_debug=true BEAKER_setfile=debian10-64 BEAKER_HYPERVISOR=docker CHECK=beaker
+ services: docker
+branches:
+ only:
+ - master
+ - /^v\d/
+notifications:
+ email: false
+ irc:
+ on_success: always
+ on_failure: always
+ channels:
+ - "chat.freenode.org#voxpupuli-notifications"
+deploy:
+ provider: puppetforge
+ user: puppet
+ password:
+ secure: ""
+ on:
+ tags: true
+ # all_branches is required to use tags
+ all_branches: true
+ # Only publish the build marked with "DEPLOY_TO_FORGE"
+ condition: "$DEPLOY_TO_FORGE = yes"
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000..29c933b
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1 @@
+--markup markdown
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..5508c73
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,86 @@
+source ENV['GEM_SOURCE'] || "https://rubygems.org"
+
+def location_for(place, fake_version = nil)
+ if place =~ /^(git[:@][^#]*)#(.*)/
+ [fake_version, { :git => $1, :branch => $2, :require => false }].compact
+ elsif place =~ /^file:\/\/(.*)/
+ ['>= 0', { :path => File.expand_path($1), :require => false }]
+ else
+ [place, { :require => false }]
+ end
+end
+
+group :test do
+ gem 'puppetlabs_spec_helper', '>= 2.14.0', :require => false
+ gem 'rspec-puppet-facts', '>= 1.8.0', :require => false
+ gem 'rspec-puppet-utils', :require => false
+ gem 'puppet-lint-leading_zero-check', :require => false
+ gem 'puppet-lint-trailing_comma-check', :require => false
+ gem 'puppet-lint-version_comparison-check', :require => false
+ gem 'puppet-lint-classes_and_types_beginning_with_digits-check', :require => false
+ gem 'puppet-lint-unquoted_string-check', :require => false
+ gem 'puppet-lint-variable_contains_upcase', :require => false
+ gem 'puppet-lint-absolute_classname-check', :require => false
+ gem 'puppet-lint-topscope-variable-check', :require => false
+ gem 'metadata-json-lint', :require => false
+ gem 'redcarpet', :require => false
+ gem 'rubocop', '~> 0.49.1', :require => false
+ gem 'rubocop-rspec', '~> 1.15.0', :require => false
+ gem 'mocha', '~> 1.4.0', :require => false
+ gem 'coveralls', :require => false
+ gem 'simplecov-console', :require => false
+ gem 'parallel_tests', :require => false
+ gem 'fakefs', '0.13.3', :require => false if RUBY_VERSION < '2.3.0'
+ gem 'fakefs', :require => false if RUBY_VERSION >= '2.3.0'
+ gem 'zabbixapi', :require => false
+end
+
+group :development do
+ gem 'travis', :require => false
+ gem 'travis-lint', :require => false
+ gem 'guard-rake', :require => false
+ gem 'overcommit', '>= 0.39.1', :require => false
+end
+
+group :system_tests do
+ gem 'winrm', :require => false
+ if beaker_version = ENV['BEAKER_VERSION']
+ gem 'beaker', *location_for(beaker_version)
+ else
+ gem 'beaker', '>= 4.2.0', :require => false
+ end
+ if beaker_rspec_version = ENV['BEAKER_RSPEC_VERSION']
+ gem 'beaker-rspec', *location_for(beaker_rspec_version)
+ else
+ gem 'beaker-rspec', :require => false
+ end
+ gem 'serverspec', :require => false
+ gem 'beaker-hostgenerator', '>= 1.1.22', :require => false
+ gem 'beaker-docker', :require => false
+ gem 'beaker-puppet', :require => false
+ gem 'beaker-puppet_install_helper', :require => false
+ gem 'beaker-module_install_helper', :require => false
+ gem 'rbnacl', '>= 4', :require => false
+ gem 'rbnacl-libsodium', :require => false
+ gem 'bcrypt_pbkdf', :require => false
+end
+
+group :release do
+ gem 'github_changelog_generator', :require => false, :git => 'https://github.com/github-changelog-generator/github-changelog-generator'
+ gem 'puppet-blacksmith', :require => false
+ gem 'voxpupuli-release', :require => false, :git => 'https://github.com/voxpupuli/voxpupuli-release-gem'
+ gem 'puppet-strings', '>= 2.2', :require => false
+end
+
+
+
+if facterversion = ENV['FACTER_GEM_VERSION']
+ gem 'facter', facterversion.to_s, :require => false, :groups => [:test]
+else
+ gem 'facter', :require => false, :groups => [:test]
+end
+
+ENV['PUPPET_VERSION'].nil? ? puppetversion = '~> 6.0' : puppetversion = ENV['PUPPET_VERSION'].to_s
+gem 'puppet', puppetversion, :require => false, :groups => [:test]
+gem 'irb'
+# vim: syntax=ruby
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dbbe355
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ef2cc5e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,102 @@
+# ipset
+
+#### Table of Contents
+
+1. [Overview](#overview)
+2. [Usage](#usage)
+3. [Reference](#reference)
+4. [Limitations](#limitations)
+5. [Changelog](#changelog)
+6. [Development](#development)
+7. [Thanks](#thanks)
+
+## Overview
+
+This module manages Linux IP sets.
+
+* Checks for current ipset state, before doing any changes to it.
+* Applies ipset every time it drifts from target state,
+ not only on config file change.
+* Handles type changes.
+* Autostart support for RHEL 6 and RHEL 7 family (upstart, systemd).
+
+## Usage
+
+### Array
+
+IP sets can be filled from an array data structure.
+Typically passed from Hiera.
+
+```puppet
+ipset { 'foo':
+ ensure => present,
+ set => ['1.2.3.4', '5.6.7.8'],
+ type => 'hash:ip',
+}
+```
+
+### String
+
+You can also pass a pre-formatted string directly, using one entry per line
+(with ``\n`` as a separator).
+This pattern is practical when generating the IP set entries using a template.
+
+```puppet
+ipset { 'foo':
+ ensure => present,
+ set => "1.2.3.4\n5.6.7.8",
+ type => 'hash:ip',
+}
+```
+
+### Module file
+
+IP sets content can also be stored in a module file:
+
+```puppet
+ipset { 'foo':
+ ensure => present,
+ set => "puppet:///modules/${module_name}/foo.ipset",
+}
+```
+
+### Local file
+
+Or using a plain text file stored on the filesystem:
+
+```puppet
+file { '/tmp/bar_set_content':
+ ensure => present,
+ content => "1.2.3.0/24\n5.6.7.8/32"
+}
+-> ipset { 'bar':
+ ensure => present,
+ set => 'file:///tmp/bar_set_content',
+ type => 'hash:net',
+}
+```
+
+## Reference
+
+The module uses puppet-strings for documentation. The result is the
+[REFERENCE.md](REFERENCE.md) file.
+
+## Limitations
+
+* Tested on Debian and RedHat-like Linux distributions
+* Only **hash** ipsets are supported (this excludes *bitmap* and *list:set*)
+
+## Changelog
+
+See [CHANGELOG](https://github.com/sl0m0ZA/puppet-ipset/blob/master/CHANGELOG.md)
+
+## Development
+
+See [development](https://github.com/sl0m0ZA/puppet-ipset/blob/master/doc/development.md)
+
+## Thanks
+
+This module is a complete rewrite of [sl0m0ZA/ipset](https://github.com/sl0m0ZA/puppet-ipset),
+which is a fork of [pmuller/ipset](https://forge.puppet.com/pmuller/ipset),
+which was forked from [mighq/ipset](https://github.com/mighq/puppet-ipset),
+which was based on [thias/ipset](https://github.com/thias/puppet-ipset).
diff --git a/REFERENCE.md b/REFERENCE.md
new file mode 100644
index 0000000..ec30baa
--- /dev/null
+++ b/REFERENCE.md
@@ -0,0 +1,263 @@
+# Reference
+
+
+## Table of Contents
+
+**Classes**
+
+* [`ipset`](#ipset): module to install the ipset tooling and to manage individual ipsets
+
+**Defined types**
+
+* [`ipset::set`](#ipsetset): Declare an IP Set.
+* [`ipset::unmanaged`](#ipsetunmanaged): Declare an IP set, without managing its content. Useful when you have a dynamic process that generates an IP set content, but still want to
+
+**Data types**
+
+* [`IPSet::Options`](#ipsetoptions):
+* [`IPSet::Set`](#ipsetset):
+* [`IPSet::Set::Array`](#ipsetsetarray):
+* [`IPSet::Set::File_URL`](#ipsetsetfile_url):
+* [`IPSet::Set::Puppet_URL`](#ipsetsetpuppet_url):
+* [`IPSet::Type`](#ipsettype):
+
+## Classes
+
+### ipset
+
+module to install the ipset tooling and to manage individual ipsets
+
+#### Parameters
+
+The following parameters are available in the `ipset` class.
+
+##### `packages`
+
+Data type: `Array[String[1]]`
+
+The name of the package we want to install
+
+##### `service`
+
+Data type: `String[1]`
+
+The name of the service that we're going to manage
+
+##### `service_ensure`
+
+Data type: `Stdlib::Ensure::Service`
+
+Desired state of the service
+
+##### `enable`
+
+Data type: `Boolean`
+
+Boolean to decide if we want to have the service in autostart or not
+
+##### `package_ensure`
+
+Data type: `Enum['present', 'absent', 'latest']`
+
+
+
+##### `config_path`
+
+Data type: `Stdlib::Absolutepath`
+
+
+
+## Defined types
+
+### ipset::set
+
+Declare an IP Set.
+
+#### Examples
+
+##### An IP set containing individual IP addresses, specified in the code.
+
+```puppet
+ipset::set { 'a-few-ip-addresses':
+ set => ['10.0.0.1', '10.0.0.2', '10.0.0.42'],
+}
+```
+
+##### An IP set containing IP networks, specified with Hiera.
+
+```puppet
+ipset::set { 'hiera-networks':
+ set => lookup('foo', IP::Address::V4::CIDR),
+ type => 'hash:net',
+}
+```
+
+##### An IP set of IP addresses, based on a file stored in a module.
+
+```puppet
+ipset::set { 'from-puppet-module':
+ set => "puppet:///modules/${module_name}/ip-addresses",
+}
+```
+
+##### An IP set of IP networks, based on a file stored on the filesystem.
+
+```puppet
+ipset::set { 'from-filesystem':
+ set => 'file:///path/to/ip-addresses',
+}
+```
+
+#### Parameters
+
+The following parameters are available in the `ipset::set` defined type.
+
+##### `set`
+
+Data type: `IPSet::Set`
+
+IP set content or source.
+
+##### `ensure`
+
+Data type: `Enum['present', 'absent']`
+
+Should the IP set be created or removed ?
+
+Default value: 'present'
+
+##### `type`
+
+Data type: `IPSet::Type`
+
+Type of IP set.
+
+Default value: 'hash:ip'
+
+##### `options`
+
+Data type: `IPSet::Options`
+
+IP set options.
+
+Default value: {}
+
+##### `ignore_contents`
+
+Data type: `Boolean`
+
+If ``true``, only the IP set declaration will be
+managed, but not its content.
+
+Default value: `false`
+
+##### `keep_in_sync`
+
+Data type: `Boolean`
+
+If ``true``, Puppet will update the IP set in the kernel
+memory. If ``false``, it will only update the IP sets on the filesystem.
+
+Default value: `true`
+
+### ipset::unmanaged
+
+Declare an IP set, without managing its content.
+
+Useful when you have a dynamic process that generates an IP set content,
+but still want to define and use it from Puppet.
+
+
+
+#### Examples
+
+#####
+
+```puppet
+ipset::unmanaged { 'unmanaged-ipset-name': }
+```
+
+#### Parameters
+
+The following parameters are available in the `ipset::unmanaged` defined type.
+
+##### `ensure`
+
+Data type: `Enum['present', 'absent']`
+
+Should the IP set be created or removed ?
+
+Default value: 'present'
+
+##### `type`
+
+Data type: `IPSet::Type`
+
+Type of IP set.
+
+Default value: 'hash:ip'
+
+##### `options`
+
+Data type: `IPSet::Options`
+
+IP set options.
+
+Default value: {}
+
+##### `keep_in_sync`
+
+Data type: `Boolean`
+
+If ``true``, Puppet will update the IP set in the kernel
+memory. If ``false``, it will only update the IP sets on the filesystem.
+
+Default value: `true`
+
+## Data types
+
+### IPSet::Options
+
+The IPSet::Options data type.
+
+Alias of `Struct[{
+ Optional[family] => Enum['inet', 'inet6'],
+ Optional[hashsize] => Integer[128],
+ Optional[maxelem] => Integer[128],
+ Optional[netmask] => IP::Address,
+ Optional[timeout] => Integer[1],
+}]`
+
+### IPSet::Set
+
+The IPSet::Set data type.
+
+Alias of `Variant[IPSet::Set::Array, IPSet::Set::Puppet_URL, IPSet::Set::File_URL, String]`
+
+### IPSet::Set::Array
+
+The IPSet::Set::Array data type.
+
+Alias of `Array[String]`
+
+### IPSet::Set::File_URL
+
+The IPSet::Set::File_URL data type.
+
+Alias of `Pattern[/^file:\/\/\//]`
+
+### IPSet::Set::Puppet_URL
+
+The IPSet::Set::Puppet_URL data type.
+
+Alias of `Pattern[/^puppet:\/\//]`
+
+### IPSet::Type
+
+The IPSet::Type data type.
+
+Alias of `Enum['hash:ip', 'hash:ip,port', 'hash:ip,port,ip', 'hash:ip,port,net', 'hash:ip,mark', 'hash:net', 'hash:net,net', 'hash:net,iface', 'hash:net,port', 'hash:net,port,net', 'hash:mac']`
+
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..09701d0
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,82 @@
+require 'puppetlabs_spec_helper/rake_tasks'
+
+# load optional tasks for releases
+# only available if gem group releases is installed
+begin
+ require 'voxpupuli/release/rake_tasks'
+rescue LoadError
+end
+
+PuppetLint.configuration.log_format = '%{path}:%{line}:%{check}:%{KIND}:%{message}'
+PuppetLint.configuration.absolute_classname_reverse = true
+
+exclude_paths = %w(
+ pkg/**/*
+ vendor/**/*
+ .vendor/**/*
+ spec/**/*
+)
+PuppetLint.configuration.ignore_paths = exclude_paths
+PuppetSyntax.exclude_paths = exclude_paths
+
+desc 'Auto-correct puppet-lint offenses'
+task 'lint:auto_correct' do
+ Rake::Task[:lint_fix].invoke
+end
+
+desc 'Run acceptance tests'
+RSpec::Core::RakeTask.new(:acceptance) do |t|
+ t.pattern = 'spec/acceptance'
+end
+
+desc 'Run tests'
+task test: [:release_checks]
+
+namespace :check do
+ desc 'Check for trailing whitespace'
+ task :trailing_whitespace do
+ Dir.glob('**/*.md', File::FNM_DOTMATCH).sort.each do |filename|
+ next if filename =~ %r{^((modules|acceptance|\.?vendor|spec/fixtures|pkg)/|REFERENCE.md)}
+ File.foreach(filename).each_with_index do |line, index|
+ if line =~ %r{\s\n$}
+ puts "#{filename} has trailing whitespace on line #{index + 1}"
+ exit 1
+ end
+ end
+ end
+ end
+end
+Rake::Task[:release_checks].enhance ['check:trailing_whitespace']
+
+desc "Run main 'test' task and report merged results to coveralls"
+task test_with_coveralls: [:test] do
+ if Dir.exist?(File.expand_path('../lib', __FILE__))
+ require 'coveralls/rake/task'
+ Coveralls::RakeTask.new
+ Rake::Task['coveralls:push'].invoke
+ else
+ puts 'Skipping reporting to coveralls. Module has no lib dir'
+ end
+end
+
+desc 'Generate REFERENCE.md'
+task :reference, [:debug, :backtrace] do |t, args|
+ patterns = ''
+ Rake::Task['strings:generate:reference'].invoke(patterns, args[:debug], args[:backtrace])
+end
+
+begin
+ require 'github_changelog_generator/task'
+ GitHubChangelogGenerator::RakeTask.new :changelog do |config|
+ version = (Blacksmith::Modulefile.new).version
+ config.future_release = "v#{version}" if version =~ /^\d+\.\d+.\d+$/
+ config.header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\nEach new release typically also includes the latest modulesync defaults.\nThese should not affect the functionality of the module."
+ config.exclude_labels = %w{duplicate question invalid wontfix wont-fix modulesync skip-changelog}
+ config.user = 'voxpupuli'
+ metadata_json = File.join(File.dirname(__FILE__), 'metadata.json')
+ metadata = JSON.load(File.read(metadata_json))
+ config.project = metadata['name']
+ end
+rescue LoadError
+end
+# vim: syntax=ruby
diff --git a/data/RedHat.yaml b/data/RedHat.yaml
new file mode 100644
index 0000000..5e158e3
--- /dev/null
+++ b/data/RedHat.yaml
@@ -0,0 +1,5 @@
+---
+ipset::config_path: '/etc/sysconfig/ipset.d'
+ipset::packages:
+ - ipset
+ - ipset-service
diff --git a/data/common.yaml b/data/common.yaml
new file mode 100644
index 0000000..de16af2
--- /dev/null
+++ b/data/common.yaml
@@ -0,0 +1,8 @@
+---
+ipset::packages:
+ - ipset
+ipset::service: ipset
+ipset::package_ensure: present
+ipset::service_ensure: true
+ipset::enable: true
+ipset::config_path: '/etc/ipset.d/'
diff --git a/files/ipset_init b/files/ipset_init
new file mode 100755
index 0000000..feed733
--- /dev/null
+++ b/files/ipset_init
@@ -0,0 +1,61 @@
+#!/bin/bash
+
+##
+# THIS FILE IS MANAGED BY PUPPEY
+##
+
+### config
+
+default_cfg='/etc/sysconfig/ipset.d'
+
+### functions
+
+usage() {
+cat << EOF
+Usage: ${0/*\//} [-c CONFIG_DIR]
+
+Ipset autostarter.
+
+Options:
+ -c Configuration directory for ipsets
+ -h Shows this help message
+EOF
+}
+
+### === main ===
+
+scd=$(dirname $(readlink -f "${BASH_SOURCE[0]}"))
+
+### cli params
+
+cfg="${default_cfg}"
+
+while getopts "c:h" OPTION; do
+ case ${OPTION} in
+ c)
+ cfg=${OPTARG}
+ ;;
+ h)
+ usage
+ exit 1
+ ;;
+ esac
+done
+
+if [ ! -d "${cfg}" ]; then
+ echo "ERROR: Config directory '${cfg}' does not exist!" >&2
+ exit 7
+fi
+
+### autostart
+
+# iterate over basenames in config directory
+for item in $(find "${cfg}" -mindepth 1 -maxdepth 1 -type f -and \( -name '*.set' -or -name '*.hdr' \) -printf '%f\n' | sed -re 's/\.[^.]+$//' | sed -e '/^$/d' | sort -u); do
+ # skip if the configuration is not complete
+ if ! ( [ -f "${cfg}/${item}.hdr" ] && [ -f "${cfg}/${item}.set" ] ); then
+ continue
+ fi
+
+ # sync
+ ${scd}/ipset_sync -c "${cfg}" -i ${item}
+done
diff --git a/files/ipset_sync b/files/ipset_sync
new file mode 100755
index 0000000..73b1e13
--- /dev/null
+++ b/files/ipset_sync
@@ -0,0 +1,261 @@
+#!/bin/bash
+
+##
+# THIS FILE IS MANAGED BY PUPPET
+##
+
+### config
+
+default_cfg='/etc/sysconfig/ipset.d'
+
+### functions
+
+usage() {
+cat << EOF
+Usage: ${0/*\//} [-c CONFIG_DIR] [-d] [-v] [-n] -i SETNAME
+
+Ipset synchronization helper. Applies configured ipset to the kernel.
+
+Meant to be used together with puppet module. Puppet will take care
+of creating config files with desired content.
+
+Options:
+ -c Configuration directory for ipsets
+ -d Checks if in-kernel set SETNAME is in sync with set config files
+ Does not apply configured state to the kernel
+ * 0 exit code, when set is in sync
+ * non-0 exit code, when the set differs from configured state
+ -n Do not sync ipset contents, just headers
+ -v Verbose output
+ -h Shows this help message
+ -i Name of managed ipset
+EOF
+}
+
+function get_header_file() {
+ echo "${cfg}/$1.hdr"
+}
+
+function get_content_file() {
+ echo "${cfg}/$1.set"
+}
+
+function construct_ipset_dump() {
+ local id=$1
+ local alias=${2:-${id}} # alias for swapping
+ local no_content=${3:-0}
+
+ local f_header=$(get_header_file ${id})
+ local f_content=$(get_content_file ${id})
+
+ if [ ! -f "${f_header}" ]; then
+ echo "Set configuration file '${f_header}' does not exist!" >&2
+ exit 5
+ fi
+ if [ ! -f "${f_content}" ]; then
+ echo "Set configuration file '${f_content}' does not exist!" >&2
+ exit 6
+ fi
+
+ # recreate the dump format from config files manually
+ (
+ cat "${f_header}" | sed "s/^create ${id} /create ${alias} /"
+
+ if [ ${no_content} -eq 0 ]; then
+ # * skip
+ # * comment lines
+ # * empty lines
+ # * cleanup
+ # * network mask suffix in complete IP (IPv4=32, IPv6=128)
+ # * remove inline comments
+ # * trim whitespaces
+ cat "${f_content}" | \
+ grep -v '^[[:space:]]*#' | \
+ grep -v '^[[:space:]]*$' | \
+ sed -re 's@^[[:space:]]*@@' | \
+ sed -re 's@[[:space:]]*$@@' | \
+ sed -re 's@/((32)|(128))$@@' | \
+ sed -re 's@[[:space:]]*#.*$@@' | \
+ sed -re "s/(.*)/add ${alias} \\1/" | \
+ LC_ALL=C sort --unique
+ fi
+ )
+}
+
+function get_ipset_dump() {
+ local id=$1
+
+ ipset save ${id} | egrep "^(add|create) ${id} " | (read hdr; echo ${hdr}; LC_ALL=C sort --unique)
+}
+
+function import_ipset() {
+ local id=$1
+ local alias=${2:-${id}}
+ local no_content=${3:-0}
+
+ ipset restore < <(construct_ipset_dump ${id} ${alias} ${no_content})
+}
+
+function ipset_exists() {
+ local id=$1
+
+ ipset list -name ${id} > /dev/null 2>&1
+}
+
+function ipset_hdr_insync() {
+ local id=$1
+
+ ipset_insync_common ${id} create
+}
+
+function ipset_set_insync() {
+ local id=$1
+
+ ipset_insync_common ${id} add
+}
+
+function ipset_insync_common() {
+ local id=$1
+ local pfx=$2
+
+
+ # compare configured and runtime config
+ tf=$(mktemp)
+ diff <(get_ipset_dump ${id} | grep ^${pfx}) <(construct_ipset_dump ${id} | grep ^${pfx}) > ${tf}
+ local rv=$?
+
+ # show differences, if requested by CLI option
+ if [ -n "${verbose_output}" ] && [ ${verbose_output} -gt 0 ]; then
+ cat "${tf}"
+ fi
+
+ # cleanup
+ rm -f "${tf}"
+
+ # return result of comparison
+ return $rv
+}
+
+
+### === main ===
+
+### cli params
+
+set_id=''
+check_only=0
+ignore_contents=0
+cfg="${default_cfg}"
+
+while getopts "vc:dhi:n" OPTION; do
+ case ${OPTION} in
+ c)
+ cfg=${OPTARG}
+ ;;
+ d)
+ check_only=1
+ ;;
+ h)
+ usage
+ exit 1
+ ;;
+ n)
+ ignore_contents=1
+ ;;
+ i)
+ set_id=${OPTARG}
+ ;;
+ v)
+ verbose_output=1
+ ;;
+ esac
+done
+
+if [ -z "${set_id}" ]; then
+ echo "ERROR: Specify set id!" >&2
+ usage
+ exit 2
+fi
+
+if [ ! -d "${cfg}" ]; then
+ echo "ERROR: Config directory '${cfg}' does not exist!" >&2
+ exit 7
+fi
+
+### sync runtime
+
+if ipset_exists ${set_id}; then
+ # check for differences
+ if ! ipset_hdr_insync ${set_id}; then
+ # loaded hdr is different
+
+ # checking for diff only
+ if [ ${check_only} -ne 0 ]; then
+ # indicate a difference
+ # and don't continue
+ exit 3
+ fi
+
+ # drop the old one
+ ipset destroy ${set_id}
+
+ # create it with content as expected
+ import_ipset ${set_id} ${set_id} ${ignore_contents}
+ else
+ # hdr is the same
+
+ # don't do anything more for dynamic sets
+ if [ ${ignore_contents} -gt 0 ]; then
+ # nothing to do
+ exit 0
+ fi
+
+ if ! ipset_set_insync ${set_id}; then
+ # loaded set is different
+
+ # checking for diff only
+ if [ ${check_only} -ne 0 ]; then
+ # indicate a difference
+ # and don't continue
+ exit 8
+ fi
+
+ swap_alias="SWAP_${set_id}"
+
+ # create a new set with expected content, to swap with old set
+ import_ipset ${set_id} ${swap_alias}
+ result=$?
+
+ if [ ${result} -eq 0 ]; then
+ # if everything went fine
+
+ # swap the contents of the sets, making it active
+ ipset swap ${swap_alias} ${set_id}
+ fi
+
+ # cleanup, drop the unused one
+ ipset destroy ${swap_alias}
+
+ if [ ${result} -eq 0 ]; then
+ # success
+ exit 0
+ else
+ # return error code, if loading went wrong
+ exit 9
+ fi
+ else
+ # no difference
+ exit 0
+ fi
+ fi
+else
+ # set not present yet
+
+ # checking for presence and the set does not exist
+ if [ ${check_only} -ne 0 ]; then
+ # indicate a difference
+ exit 4
+ fi
+
+ # create it with content as expected
+ import_ipset ${set_id} ${set_id} ${ignore_contents}
+fi
diff --git a/hiera.yaml b/hiera.yaml
new file mode 100644
index 0000000..d11de8b
--- /dev/null
+++ b/hiera.yaml
@@ -0,0 +1,23 @@
+---
+version: 5
+
+defaults:
+ datadir: 'data'
+ data_hash: 'yaml_data'
+
+hierarchy:
+ - name: 'Full Version'
+ path: '%{facts.os.name}-%{facts.os.release.full}.yaml'
+
+ - name: 'Major Version'
+ path: '%{facts.os.name}-%{facts.os.release.major}.yaml'
+
+ - name: 'Distribution Name'
+ path: '%{facts.os.name}.yaml'
+
+ - name: 'Operating System Family'
+ path: '%{facts.os.family}.yaml'
+
+ - name: 'common'
+ path: 'common.yaml'
+
diff --git a/manifests/init.pp b/manifests/init.pp
new file mode 100644
index 0000000..61e1b8b
--- /dev/null
+++ b/manifests/init.pp
@@ -0,0 +1,74 @@
+# @summary module to install the ipset tooling and to manage individual ipsets
+#
+# @param packages
+# The name of the package we want to install
+#
+# @param service
+# The name of the service that we're going to manage
+#
+# @param service_ensure
+# Desired state of the service. If true, the service will be running. If false, the service will be stopped
+#
+# @param enable
+# Boolean to decide if we want to have the service in autostart or not
+#
+# @param firewall_service
+# An optional service name. if provided, the ipsets will be configured before this. So your firewall will depend on the chains. The name should end with `.service`
+#
+class ipset (
+ Array[String[1]] $packages,
+ String[1] $service,
+ Boolean $service_ensure,
+ Boolean $enable,
+ Enum['present', 'absent', 'latest'] $package_ensure,
+ Stdlib::Absolutepath $config_path,
+ Optional[String[1]] $firewall_service = undef,
+){
+ package{$ipset::packages:
+ ensure => $package_ensure,
+ }
+
+ # create the config directory
+ file{$config_path:
+ ensure => 'directory',
+ }
+
+ # setup the helper scripts
+ file{'/usr/local/bin/ipset_sync':
+ ensure => 'file',
+ owner => 'root',
+ group => 'root',
+ mode => '754',
+ source => "puppet:///modules/${module_name}/ipset_sync",
+ }
+ file{'/usr/local/bin/ipset_init':
+ ensure => 'file',
+ owner => 'root',
+ group => 'root',
+ mode => '754',
+ source => "puppet:///modules/${module_name}/ipset_init",
+ }
+
+ # configure custom unit file
+ if $facts['systemd'] == true {
+ # It's quite common that people use systemd-networkd to cofigure their network
+ # that will provide us with systemd-networkd-wait-online.service. If it's enabled,
+ # the ipset service should wait for it to become online.
+ $service_dependency = fact('systemd_internal_services."systemd-networkd-wait-online.service"') ? {
+ undef => undef,
+ default => 'systemd-networkd-wait-online.service'
+ }
+ systemd::unit_file{"${service}.service":
+ enable => $enable,
+ active => $service_ensure,
+ content => epp("${module_name}/ipset.service.epp",{
+ 'firewall_service' => $firewall_service,
+ 'cfg' => $config_path,
+ 'service_dependency' => $service_dependency,
+ }),
+ subscribe => [File['/usr/local/bin/ipset_init'], File['/usr/local/bin/ipset_sync']],
+ }
+ } else {
+ fail("The ipset module only supports systemd based distributions")
+ }
+}
diff --git a/manifests/set.pp b/manifests/set.pp
new file mode 100644
index 0000000..1db815c
--- /dev/null
+++ b/manifests/set.pp
@@ -0,0 +1,156 @@
+# Declare an IP Set.
+#
+# @param set IP set content or source.
+# @param ensure Should the IP set be created or removed ?
+# @param type Type of IP set.
+# @param options IP set options.
+# @param ignore_contents If ``true``, only the IP set declaration will be
+# managed, but not its content.
+# @param keep_in_sync If ``true``, Puppet will update the IP set in the kernel
+# memory. If ``false``, it will only update the IP sets on the filesystem.
+#
+# @example An IP set containing individual IP addresses, specified in the code.
+# ipset::set { 'a-few-ip-addresses':
+# set => ['10.0.0.1', '10.0.0.2', '10.0.0.42'],
+# }
+#
+# @example An IP set containing IP networks, specified with Hiera.
+# ipset::set { 'hiera-networks':
+# set => lookup('foo', IP::Address::V4::CIDR),
+# type => 'hash:net',
+# }
+#
+# @example An IP set of IP addresses, based on a file stored in a module.
+# ipset::set { 'from-puppet-module':
+# set => "puppet:///modules/${module_name}/ip-addresses",
+# }
+#
+# @example An IP set of IP networks, based on a file stored on the filesystem.
+# ipset::set { 'from-filesystem':
+# set => 'file:///path/to/ip-addresses',
+# }
+#
+define ipset::set (
+ IPSet::Set $set,
+ Enum['present', 'absent'] $ensure = 'present',
+ IPSet::Type $type = 'hash:ip',
+ IPSet::Options $options = {},
+ # do not touch what is inside the set, just its header (properties)
+ Boolean $ignore_contents = false,
+ # keep definition file and in-kernel runtime state in sync
+ Boolean $keep_in_sync = true,
+) {
+
+ include ipset
+
+ $config_path = $ipset::config_path
+
+ $default_options = {
+ 'family' => 'inet',
+ 'hashsize' => '1024',
+ 'maxelem' => '65536',
+ }
+
+ $actual_options = merge($default_options, $options)
+
+ if $ensure == 'present' {
+ # assert "present" target
+
+ $opt_string = inline_template('<%= (@actual_options.sort.map { |k,v| k.to_s + " " + v.to_s }).join(" ") %>')
+
+ # header
+ file { "${config_path}/${title}.hdr":
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0640',
+ content => "create ${title} ${type} ${opt_string}\n",
+ notify => Exec["sync_ipset_${title}"],
+ }
+
+ # content
+ case $set {
+ IPSet::Set::Array: {
+ # create file with ipset, one record per line
+ file { "${config_path}/${title}.set":
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0640',
+ content => inline_template('<%= (@set.map { |i| i.to_s }).join("\n") %>'),
+ }
+ }
+ IPSet::Set::Puppet_URL: {
+ # passed as puppet file
+ file { "${config_path}/${title}.set":
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0640',
+ source => $set,
+ }
+ }
+ IPSet::Set::File_URL: {
+ # passed as target node file
+ file { "${config_path}/${title}.set":
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0640',
+ source => regsubst($set, '^.{7}', ''),
+ }
+ }
+ String: {
+ # passed directly as content string (from template for example)
+ file { "${config_path}/${title}.set":
+ ensure => file,
+ owner => 'root',
+ group => 'root',
+ mode => '0640',
+ content => $set,
+ }
+ }
+ default: {
+ fail('Typing prevent reaching this branch')
+ }
+ }
+
+ # add switch to script, if we
+ if $ignore_contents {
+ $ignore_contents_opt = ' -n'
+ } else {
+ $ignore_contents_opt = ''
+ }
+
+ # sync if needed by helper script
+ exec { "sync_ipset_${title}":
+ path => [ '/sbin', '/usr/sbin', '/bin', '/usr/bin', '/usr/local/bin', '/usr/local/sbin' ],
+ # use helper script to do the sync
+ command => "ipset_sync -c '${config_path}' -i ${title}${ignore_contents_opt}",
+ # only when difference with in-kernel set is detected
+ unless => "ipset_sync -c '${config_path}' -d -i ${title}${ignore_contents_opt}",
+ require => Package['ipset'],
+ }
+
+ if $keep_in_sync {
+ File["${config_path}/${title}.set"] ~> Exec["sync_ipset_${title}"]
+ }
+ } elsif $ensure == 'absent' {
+ # ensuring absence
+
+ # do not contain config files
+ file { ["${config_path}/${title}.set", "${config_path}/${title}.hdr"]:
+ ensure => absent,
+ }
+
+ # clear ipset from kernel
+ exec { "ipset destroy ${title}":
+ path => [ '/sbin', '/usr/sbin', '/bin', '/usr/bin' ],
+ command => "ipset destroy ${title}",
+ onlyif => "ipset list ${title}",
+ require => Package['ipset'],
+ }
+ } else {
+ fail('Typing prevent reaching this branch')
+ }
+}
diff --git a/manifests/unmanaged.pp b/manifests/unmanaged.pp
new file mode 100644
index 0000000..fcdf468
--- /dev/null
+++ b/manifests/unmanaged.pp
@@ -0,0 +1,34 @@
+# Declare an IP set, without managing its content.
+#
+# Useful when you have a dynamic process that generates an IP set content,
+# but still want to define and use it from Puppet.
+#
+#
+#
+# @param ensure Should the IP set be created or removed ?
+# @param type Type of IP set.
+# @param options IP set options.
+# @param keep_in_sync If ``true``, Puppet will update the IP set in the kernel
+# memory. If ``false``, it will only update the IP sets on the filesystem.
+#
+# @example
+# ipset::unmanaged { 'unmanaged-ipset-name': }
+#
+define ipset::unmanaged(
+ Enum['present', 'absent'] $ensure = 'present',
+ IPSet::Type $type = 'hash:ip',
+ IPSet::Options $options = {},
+ Boolean $keep_in_sync = true,
+) {
+ ipset::set { $title:
+ ensure => $ensure,
+ set => '',
+ ignore_contents => true,
+ type => $type,
+ options => $options,
+ keep_in_sync => $keep_in_sync,
+ }
+}
diff --git a/metadata.json b/metadata.json
new file mode 100644
index 0000000..5b09264
--- /dev/null
+++ b/metadata.json
@@ -0,0 +1,80 @@
+{
+ "name": "puppet-ipset",
+ "version": "1.0.0",
+ "author": "Vox Pupuli",
+ "summary": "Linux ipsets management",
+ "license": "AGPL-3.0",
+ "source": "https://github.com/voxpupuli/puppet-ipset",
+ "project_page": "https://github.com/voxpupuli/puppet-ipset",
+ "issues_url": "https://github.com/voxpupuli/puppet-ipset/issues",
+ "dependencies": [
+ {
+ "name": "puppetlabs/stdlib",
+ "version_requirement": ">= 6.1.0 < 7.0.0"
+ },
+ {
+ "name": "camptocamp/systemd",
+ "version_requirement": ">= 2.6.0 < 3.0.0"
+ }
+ ],
+ "operatingsystem_support": [
+ {
+ "operatingsystem": "Debian",
+ "operatingsystemrelease": [
+ "9",
+ "8",
+ "10"
+ ]
+ },
+ {
+ "operatingsystem": "Ubuntu",
+ "operatingsystemrelease": [
+ "16.04",
+ "18.04"
+ ]
+ },
+ {
+ "operatingsystem": "RedHat",
+ "operatingsystemrelease": [
+ "7",
+ "8"
+ ]
+ },
+ {
+ "operatingsystem": "CentOS",
+ "operatingsystemrelease": [
+ "7",
+ "8"
+ ]
+ },
+ {
+ "operatingsystem": "OracleLinux",
+ "operatingsystemrelease": [
+ "7",
+ "8"
+ ]
+ },
+ {
+ "operatingsystem": "Scientific",
+ "operatingsystemrelease": [
+ "7",
+ "8"
+ ]
+ },
+ {
+ "operatingsystem": "Archlinux"
+ }
+ ],
+ "requirements": [
+ {
+ "name": "puppet",
+ "version_requirement": ">= 5.5.8 < 7.0.0"
+ }
+ ],
+ "tags": [
+ "ipset",
+ "linux",
+ "firewall",
+ "iptables"
+ ]
+}
diff --git a/spec/acceptance/ipset_spec.rb b/spec/acceptance/ipset_spec.rb
new file mode 100644
index 0000000..f9041d6
--- /dev/null
+++ b/spec/acceptance/ipset_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper_acceptance'
+
+describe 'ipset class' do
+ context 'default parameters' do
+ it 'works idempotently with no errors' do
+ pp = 'include ipset'
+ # Run it twice and test for idempotency
+ apply_manifest(pp, catch_failures: true)
+ apply_manifest(pp, catch_changes: true)
+ end
+ describe service('ipset') do
+ it { is_expected.to be_running }
+ it { is_expected.to be_enabled }
+ end
+ end
+ context 'with a basic ipset' do
+ it 'works idempotently with no errors' do
+ pp = <<-EOS
+ include ipset
+ ipset::set{'basic-set':
+ set => ['10.0.0.1', '10.0.0.2', '10.0.0.42'],
+ type => 'hash:net',
+ }
+ EOS
+ # Run it twice and test for idempotency
+ apply_manifest(pp, catch_failures: true)
+ apply_manifest(pp, catch_changes: true)
+ end
+
+ describe command('ipset list basic-set') do
+ its(:stdout) { is_expected.to match %r{.*basic-set.*Number of entries: 3.*10\.0\.0\.2.*}m }
+ end
+ end
+
+ context 'can delete ipsets' do
+ it 'it works even here idempotently with no errors' do
+ pp = <<-EOS
+ include ipset
+ ipset::set{'basic-set':
+ ensure => 'absent',
+ set => ['10.0.0.1', '10.0.0.2', '10.0.0.42'],
+ type => 'hash:net',
+ }
+ EOS
+ # Run it twice and test for idempotency
+ apply_manifest(pp, catch_failures: true)
+ apply_manifest(pp, catch_changes: true)
+ end
+ describe command('ipset list') do
+ #its(:stdout) { is_expected.to match %r{.*The set with the given name does not exist.*} }
+ its(:stdout) { is_expected.to match "" }
+ end
+ end
+end
diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb
new file mode 100644
index 0000000..3e2abfd
--- /dev/null
+++ b/spec/classes/init_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'ipset' do
+ let :node do
+ 'agent.example.com'
+ end
+
+ on_supported_os.each do |os, facts|
+ context "on #{os} " do
+ let :facts do
+ facts.merge({systemd: true})
+ end
+ context 'with all defaults' do
+ it { is_expected.to compile.with_all_deps }
+ it { is_expected.to contain_package('ipset') }
+ it { is_expected.to contain_file('/usr/local/bin/ipset_init') }
+ it { is_expected.to contain_file('/usr/local/bin/ipset_sync') }
+ it { is_expected.to contain_systemd__unit_file('ipset.service') }
+
+ if facts[:os]['family'] == 'RedHat'
+ it { is_expected.to contain_package('ipset-service') }
+ it { is_expected.not_to contain_file('/etc/ipset.d/') }
+ it { is_expected.to contain_file('/etc/sysconfig/ipset.d') }
+ else
+ it { is_expected.not_to contain_package('ipset-service') }
+ it { is_expected.to contain_file('/etc/ipset.d/') }
+ it { is_expected.not_to contain_file('/etc/sysconfig/ipset.d') }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/default_facts.yml b/spec/default_facts.yml
new file mode 100644
index 0000000..ea1e480
--- /dev/null
+++ b/spec/default_facts.yml
@@ -0,0 +1,7 @@
+# Use default_module_facts.yml for module specific facts.
+#
+# Facts specified here will override the values provided by rspec-puppet-facts.
+---
+ipaddress: "172.16.254.254"
+is_pe: false
+macaddress: "AA:AA:AA:AA:AA:AA"
diff --git a/spec/defines/init_spec.rb b/spec/defines/init_spec.rb
new file mode 100644
index 0000000..3ba2c3a
--- /dev/null
+++ b/spec/defines/init_spec.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+def check_classes
+ it do
+ is_expected.to contain_class('ipset::params')
+ is_expected.to contain_class('ipset::install')
+ end
+end
+
+def check_file_set_header(name, attributes)
+ it do
+ is_expected.to contain_file("/etc/sysconfig/ipset.d/#{name}.hdr")
+ .only_with({
+ ensure: 'file',
+ owner: 'root',
+ group: 'root',
+ mode: '0640',
+ notify: "Exec[sync_ipset_#{name}]"
+ }.merge(attributes))
+ end
+end
+
+def check_file_set_content(name, attributes)
+ it do
+ is_expected.to contain_file("/etc/sysconfig/ipset.d/#{name}.set")
+ .with({ ensure: 'file' }.merge(attributes))
+ end
+end
+
+def check_exec_sync(name, attributes)
+ it do
+ is_expected.to contain_exec("sync_ipset_#{name}")
+ .with({
+ path: ['/sbin', '/usr/sbin', '/bin', '/usr/bin', '/usr/local/bin', '/usr/local/sbin'],
+ require: 'Package[ipset]'
+ }.merge(attributes))
+ .that_subscribes_to("File[/etc/sysconfig/ipset.d/#{name}.set]")
+ end
+end
+
+simple_test_cases = [
+ [
+ 'array',
+ ['10.0.0.1', '192.168.0.1'],
+ { content: "10.0.0.1\n192.168.0.1" }
+ ],
+ [
+ 'string',
+ "10.0.0.1\n192.168.0.1\n",
+ { content: "10.0.0.1\n192.168.0.1\n" }
+ ],
+ [
+ 'puppet url',
+ 'puppet:///foo/bar',
+ { source: 'puppet:///foo/bar' }
+ ],
+ [
+ 'file url',
+ 'file:///foo/bar',
+ { source: '/foo/bar' }
+ ],
+ [
+ 'array',
+ ['10.0.0.1 #Comment 1', '192.168.0.1 #Comment 2'],
+ { content: "10.0.0.1 #Comment 1\n192.168.0.1 #Comment 2" }
+ ],
+ [
+ 'array',
+ ['10.0.0.1,80', '192.168.0.1,443'],
+ { content: "10.0.0.1,80\n192.168.0.1,443" }
+ ],
+ [
+ 'string',
+ ["10.0.0.1 #Comment 1\n192.168.0.1 #Comment 2"],
+ { content: "10.0.0.1 #Comment 1\n192.168.0.1 #Comment 2" }
+ ],
+ [
+ 'string',
+ ["10.0.0.1,80\n192.168.0.1,443"],
+ { content: "10.0.0.1,80\n192.168.0.1,443" }
+ ]
+]
+
+describe 'ipset::set' do
+ simple_test_cases.each do |test_name, set, set_file_attributes|
+ context "set type #{test_name}" do
+ let :pre_condition do
+ 'include ipset'
+ end
+ let(:title) { 'simple' }
+ let(:params) { { set: set } }
+ let :facts do
+ {
+ os: {
+ family: 'RedHat',
+ release: {
+ major: 7
+ }
+ },
+ systemd: true
+ }
+ end
+
+ check_file_set_header(
+ 'simple',
+ # rubocop:disable Metrics/LineLength
+ content: "create simple hash:ip family inet hashsize 1024 maxelem 65536\n",
+ # rubocop:enable Metrics/LineLength
+ )
+ check_file_set_content('simple', set_file_attributes)
+ check_exec_sync(
+ 'simple',
+ # rubocop:disable Metrics/LineLength
+ command: "ipset_sync -c '/etc/sysconfig/ipset.d' -i simple",
+ unless: "ipset_sync -c '/etc/sysconfig/ipset.d' -d -i simple",
+ # rubocop:enable Metrics/LineLength
+ )
+ end
+ end
+end
+
+describe 'ipset::set' do
+ context 'custom parameters' do
+ let :pre_condition do
+ 'include ipset'
+ end
+ let(:title) { 'custom' }
+ let :params do
+ {
+ set: ['10.0.0.0/8', '192.168.0.0/16'],
+ type: 'hash:net',
+ options: { hashsize: 2048 },
+ ignore_contents: true
+ }
+ end
+ let :facts do
+ {
+ os: {
+ family: 'RedHat',
+ release: {
+ major: 7
+ }
+ },
+ systemd: true
+ }
+ end
+ check_file_set_header(
+ 'custom',
+ # rubocop:disable Metrics/LineLength
+ content: "create custom hash:net family inet hashsize 2048 maxelem 65536\n",
+ # rubocop:enable Metrics/LineLength
+ )
+ check_file_set_content('custom', content: "10.0.0.0/8\n192.168.0.0/16")
+ check_exec_sync(
+ 'custom',
+ # rubocop:disable Metrics/LineLength
+ command: "ipset_sync -c '/etc/sysconfig/ipset.d' -i custom -n",
+ unless: "ipset_sync -c '/etc/sysconfig/ipset.d' -d -i custom -n",
+ # rubocop:enable Metrics/LineLength
+ )
+ end
+end
+
+describe 'ipset::set' do
+ context 'absent' do
+ let :pre_condition do
+ 'include ipset'
+ end
+
+ let(:title) { 'absent' }
+ let :params do
+ {
+ ensure: 'absent',
+ set: ['10.0.0.0/8', '192.168.0.0/16']
+ }
+ end
+
+ let :facts do
+ {
+ os: {
+ family: 'RedHat',
+ release: {
+ major: 7
+ }
+ },
+ systemd: true
+ }
+ end
+
+ it do
+ is_expected.to contain_file('/etc/sysconfig/ipset.d/absent.hdr')
+ .with(ensure: 'absent')
+ is_expected.to contain_file('/etc/sysconfig/ipset.d/absent.set')
+ .with(ensure: 'absent')
+ is_expected.to contain_exec('ipset destroy absent')
+ .with(
+ path: ['/sbin', '/usr/sbin', '/bin', '/usr/bin'],
+ command: 'ipset destroy absent',
+ onlyif: 'ipset list -name absent &>/dev/null',
+ require: 'Package[ipset]'
+ )
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..1570a73
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,48 @@
+require 'puppetlabs_spec_helper/module_spec_helper'
+require 'rspec-puppet-facts'
+
+require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb'))
+
+include RspecPuppetFacts
+
+default_facts = {
+ puppetversion: Puppet.version,
+ facterversion: Facter.version,
+}
+
+default_fact_files = [
+ File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml')),
+ File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml')),
+]
+
+default_fact_files.each do |f|
+ next unless File.exist?(f) && File.readable?(f) && File.size?(f)
+
+ begin
+ default_facts.merge!(YAML.safe_load(File.read(f), [], [], true))
+ rescue => e
+ RSpec.configuration.reporter.message "WARNING: Unable to load #{f}: #{e}"
+ end
+end
+
+RSpec.configure do |c|
+ c.default_facts = default_facts
+ c.before :each do
+ # set to strictest setting for testing
+ # by default Puppet runs at warning level
+ Puppet.settings[:strict] = :warning
+ end
+ c.filter_run_excluding(bolt: true) unless ENV['GEM_BOLT']
+ c.after(:suite) do
+ RSpec::Puppet::Coverage.report!
+ end
+end
+
+def ensure_module_defined(module_name)
+ module_name.split('::').reduce(Object) do |last_module, next_module|
+ last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module, false)
+ last_module.const_get(next_module, false)
+ end
+end
+
+# 'spec_overrides' from sync.yml will appear below this line
diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb
new file mode 100644
index 0000000..49b1012
--- /dev/null
+++ b/spec/spec_helper_acceptance.rb
@@ -0,0 +1,14 @@
+require 'beaker-rspec'
+require 'beaker-puppet'
+require 'beaker/puppet_install_helper'
+require 'beaker/module_install_helper'
+
+run_puppet_install_helper unless ENV['BEAKER_provision'] == 'no'
+install_ca_certs unless ENV['PUPPET_INSTALL_TYPE'] =~ %r{pe}i
+install_module_on(hosts)
+install_module_dependencies_on(hosts)
+
+RSpec.configure do |c|
+ # Readable test descriptions
+ c.formatter = :documentation
+end
diff --git a/templates/init.upstart.erb b/templates/init.upstart.erb
new file mode 100644
index 0000000..a245813
--- /dev/null
+++ b/templates/init.upstart.erb
@@ -0,0 +1,6 @@
+task
+
+exec /usr/local/sbin/ipset_init -c "<%= @cfg %>"
+
+# run before all sysv-init scripts, where the firewall (iptables) is
+start on starting rc RUNLEVEL=[2345]
diff --git a/templates/ipset.service.epp b/templates/ipset.service.epp
new file mode 100644
index 0000000..5aead58
--- /dev/null
+++ b/templates/ipset.service.epp
@@ -0,0 +1,25 @@
+<%- | Optional[String[1]] $firewall_service,
+ String[1] $cfg,
+ Optional[String[1]] $service_dependency,
+| -%>
+# THIS FILE IS MANAGED BY PUPPET
+[Unit]
+Description=define and fill-in ipsets
+Documentation=https://github.com/voxpupuli/puppet-ipset
+<% if $firewall_service { -%>
+Before=<%= $firewall_service %>
+<% } -%>
+<% if $firewall_service { -%>
+Require=<%= $service_dependency %>
+<% } -%>
+After=network-online.target
+Wants=network-online.target
+
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/local/bin/ipset_init -c "<%= $cfg %>"
+
+[Install]
+WantedBy=basic.target
diff --git a/types/options.pp b/types/options.pp
new file mode 100644
index 0000000..0afebc1
--- /dev/null
+++ b/types/options.pp
@@ -0,0 +1,7 @@
+type IPSet::Options = Struct[{
+ Optional[family] => Enum['inet', 'inet6'],
+ Optional[hashsize] => Integer[128],
+ Optional[maxelem] => Integer[128],
+ Optional[netmask] => IP::Address,
+ Optional[timeout] => Integer[1],
+}]
diff --git a/types/set.pp b/types/set.pp
new file mode 100644
index 0000000..5491e40
--- /dev/null
+++ b/types/set.pp
@@ -0,0 +1,6 @@
+type IPSet::Set = Variant[
+ IPSet::Set::Array,
+ IPSet::Set::Puppet_URL,
+ IPSet::Set::File_URL,
+ String,
+]
diff --git a/types/set/array.pp b/types/set/array.pp
new file mode 100644
index 0000000..5f0d668
--- /dev/null
+++ b/types/set/array.pp
@@ -0,0 +1 @@
+type IPSet::Set::Array = Array[String]
diff --git a/types/set/file_url.pp b/types/set/file_url.pp
new file mode 100644
index 0000000..9c257f3
--- /dev/null
+++ b/types/set/file_url.pp
@@ -0,0 +1 @@
+type IPSet::Set::File_URL = Pattern[/^file:\/\/\//]
diff --git a/types/set/puppet_url.pp b/types/set/puppet_url.pp
new file mode 100644
index 0000000..c7ddfd3
--- /dev/null
+++ b/types/set/puppet_url.pp
@@ -0,0 +1 @@
+type IPSet::Set::Puppet_URL = Pattern[/^puppet:\/\//]
diff --git a/types/type.pp b/types/type.pp
new file mode 100644
index 0000000..2a9502f
--- /dev/null
+++ b/types/type.pp
@@ -0,0 +1,13 @@
+type IPSet::Type = Enum[
+ 'hash:ip',
+ 'hash:ip,port',
+ 'hash:ip,port,ip',
+ 'hash:ip,port,net',
+ 'hash:ip,mark',
+ 'hash:net',
+ 'hash:net,net',
+ 'hash:net,iface',
+ 'hash:net,port',
+ 'hash:net,port,net',
+ 'hash:mac',
+]