From d7f26af180862351c719a15c532aa843d5323bb3 Mon Sep 17 00:00:00 2001 From: Brandon Burnett Date: Sun, 14 Feb 2016 19:18:58 +0000 Subject: [PATCH] Initial commit. --- .fixtures.yml | 6 + .gitignore | 9 + .nodeset.yml | 11 + .travis.yml | 50 ++ CONTRIBUTORS | 5 + Gemfile | 28 + LICENSE | 202 ++++++ Makefile | 8 + README.md | 72 +++ Rakefile | 41 ++ files/.gitkeep | 0 lib/facter/nomad_version.rb | 12 + .../parser/functions/nomad_sorted_json.rb | 166 +++++ .../parser/functions/nomad_validate_checks.rb | 55 ++ manifests/config.pp | 103 ++++ manifests/init.pp | 155 +++++ manifests/install.pp | 111 ++++ manifests/params.pp | 72 +++ manifests/reload_service.pp | 29 + manifests/run_service.pp | 31 + manifests/service.pp | 70 +++ metadata.json | 103 ++++ spec/acceptance/nodesets/default.yml | 9 + spec/acceptance/standard_spec.rb | 39 ++ spec/acceptance/unsupported_spec.rb | 10 + spec/classes/init_spec.rb | 578 ++++++++++++++++++ spec/functions/nomad_sorted_json_spec.rb | 84 +++ spec/functions/nomad_validate_checks_spec.rb | 100 +++ spec/spec_helper.rb | 19 + spec/spec_helper_acceptance.rb | 23 + spec/unit/facter/nomad_version_spec.rb | 23 + templates/.gitkeep | 0 templates/nomad.debian.erb | 178 ++++++ templates/nomad.launchd.erb | 27 + templates/nomad.sles.erb | 100 +++ templates/nomad.systemd.erb | 18 + templates/nomad.sysv.erb | 132 ++++ templates/nomad.upstart.erb | 40 ++ tests/init.pp | 12 + 39 files changed, 2731 insertions(+) create mode 100644 .fixtures.yml create mode 100644 .gitignore create mode 100644 .nodeset.yml create mode 100644 .travis.yml create mode 100644 CONTRIBUTORS create mode 100644 Gemfile create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 files/.gitkeep create mode 100644 lib/facter/nomad_version.rb create mode 100644 lib/puppet/parser/functions/nomad_sorted_json.rb create mode 100644 lib/puppet/parser/functions/nomad_validate_checks.rb create mode 100644 manifests/config.pp create mode 100644 manifests/init.pp create mode 100644 manifests/install.pp create mode 100644 manifests/params.pp create mode 100644 manifests/reload_service.pp create mode 100644 manifests/run_service.pp create mode 100644 manifests/service.pp create mode 100644 metadata.json create mode 100644 spec/acceptance/nodesets/default.yml create mode 100644 spec/acceptance/standard_spec.rb create mode 100644 spec/acceptance/unsupported_spec.rb create mode 100644 spec/classes/init_spec.rb create mode 100644 spec/functions/nomad_sorted_json_spec.rb create mode 100644 spec/functions/nomad_validate_checks_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 spec/spec_helper_acceptance.rb create mode 100644 spec/unit/facter/nomad_version_spec.rb create mode 100644 templates/.gitkeep create mode 100644 templates/nomad.debian.erb create mode 100644 templates/nomad.launchd.erb create mode 100644 templates/nomad.sles.erb create mode 100644 templates/nomad.systemd.erb create mode 100644 templates/nomad.sysv.erb create mode 100644 templates/nomad.upstart.erb create mode 100644 tests/init.pp diff --git a/.fixtures.yml b/.fixtures.yml new file mode 100644 index 0000000..168a72f --- /dev/null +++ b/.fixtures.yml @@ -0,0 +1,6 @@ +fixtures: + repositories: + stdlib: "https://github.com/puppetlabs/puppetlabs-stdlib.git" + staging: "https://github.com/nanliu/puppet-staging.git" + symlinks: + nomad: "#{source_dir}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46e5569 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.*.sw? +pkg +spec/fixtures +.rspec_system +Gemfile.lock +log/ +vendor/ +.bundle/ +.DS_Store diff --git a/.nodeset.yml b/.nodeset.yml new file mode 100644 index 0000000..250d847 --- /dev/null +++ b/.nodeset.yml @@ -0,0 +1,11 @@ +--- +default_set: 'ubuntu-server-12042-x64' +sets: + 'ubuntu-server-10044-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-10044-x64' + 'ubuntu-server-12042-x64': + nodes: + "main.foo.vm": + prefab: 'ubuntu-server-12042-x64' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f6fb831 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,50 @@ +--- +language: ruby +bundler_args: --without development +before_install: rm Gemfile.lock || true +sudo: false +cache: bundler +rvm: + - 1.8.7 + - 1.9.3 + - 2.0.0 + - 2.1.0 +script: bundle exec rake test +env: + - PUPPET_VERSION="~> 3.1.0" + - PUPPET_VERSION="~> 3.3.0" + - PUPPET_VERSION="~> 3.7.4" FUTURE_PARSER=yes STRICT_VARIABLES=yes + - PUPPET_VERSION="~> 3.8.4" + - PUPPET_VERSION="~> 3.8.4" FUTURE_PARSER=yes STRICT_VARIABLES=yes + - PUPPET_VERSION="~> 4.0.0" + - PUPPET_VERSION="~> 4.1.0" +matrix: + exclude: + - rvm: 1.9.3 + env: PUPPET_VERSION="~> 2.7.0" + - rvm: 2.0.0 + env: PUPPET_VERSION="~> 2.7.0" + - rvm: 2.0.0 + env: PUPPET_VERSION="~> 3.1.0" + - rvm: 2.1.0 + env: PUPPET_VERSION="~> 2.7.0" + - rvm: 2.1.0 + env: PUPPET_VERSION="~> 3.1.0" + - rvm: 2.1.0 + env: PUPPET_VERSION="~> 3.2.0" + - rvm: 2.1.0 + env: PUPPET_VERSION="~> 3.3.0" + - rvm: 2.1.0 + env: PUPPET_VERSION="~> 3.4.0" + - rvm: 1.9.3 + env: PUPPET_VERSION="~> 4.0.0" + - rvm: 1.8.7 + env: PUPPET_VERSION="~> 4.0.0" + - rvm: 2.0.0 + env: PUPPET_VERSION="~> 4.0.0" + - rvm: 1.8.7 + env: PUPPET_VERSION="~> 4.1.0" + - rvm: 1.9.3 + env: PUPPET_VERSION="~> 4.1.0" + - rvm: 2.0.0 + env: PUPPET_VERSION="~> 4.1.0" diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..b49b226 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,5 @@ +Kyle Anderson +Kenny Gatdula +Simon Croome +Dan Tehranian +Vik Bhatti diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..71fdd99 --- /dev/null +++ b/Gemfile @@ -0,0 +1,28 @@ +source "https://rubygems.org" + +group :development do + gem "beaker", "> 2.0.0" + gem "beaker-rspec", ">= 5.1.0" + gem "beaker-puppet_install_helper" + gem "pry" + gem "puppet-blacksmith" + gem "serverspec" + gem "vagrant-wrapper" +end + +group :test do + gem "json" + gem "rake" + gem "puppet", ENV['PUPPET_VERSION'] || '~> 3.7.0' + gem "puppet-lint" + + # Pin for 1.8.7 compatibility for now + gem "rspec", '< 3.2.0' + gem "rspec-core", "3.1.7" + gem "rspec-puppet", "~> 2.1" + + gem "puppet-syntax" + gem "puppetlabs_spec_helper" + gem "hiera" + gem "hiera-puppet-helper" +end diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9f71055 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e1ae2be --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +# Make target for TravisCI to run beaker tests in docker +beaker: + bundle config + rm .bundle/config + bundle config + bundle install + curl -sLo - http://j.mp/install-travis-docker | UML_DOCKERCOMPOSE=0 UML_FIG=0 sh -e + ./run 'bundle exec rake beaker' diff --git a/README.md b/README.md new file mode 100644 index 0000000..6890464 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# puppet-consul + +## Compatibility + +| Consul Version | Recommended Puppet Module Version | +| ---------------- | ----------------------------------- | +| >= 0.6.0 | latest | +| 0.5.x | 1.0.3 | +| 0.4.x | 0.4.6 | +| 0.3.x | 0.3.0 | + +### What This Module Affects + +* Installs the consul daemon (via url or package) + * If installing from zip, you *must* ensure the unzip utility is available. +* Optionally installs a user to run it under +* Installs a configuration file (/etc/consul/config.json) +* Manages the consul service via upstart, sysv, or systemd +* Optionally installs the Web UI + +## Usage + +To set up a single consul server, with several agents attached: +On the server: +```puppet +class { '::consul': + config_hash => { + 'bootstrap_expect' => 1, + 'data_dir' => '/opt/consul', + 'datacenter' => 'east-aws', + 'log_level' => 'INFO', + 'node_name' => 'server', + 'server' => true, + } +} +``` +On the agent(s): +```puppet +class { '::consul': + config_hash => { + 'data_dir' => '/opt/consul', + 'datacenter' => 'east-aws', + 'log_level' => 'INFO', + 'node_name' => 'agent', + 'retry_join' => ['172.16.0.1'], + } +} +``` +Disable install and service components: +```puppet +class { '::consul': + install_method => 'none', + init_style => false, + manage_service => false, + config_hash => { + 'data_dir' => '/opt/consul', + 'datacenter' => 'east-aws', + 'log_level' => 'INFO', + 'node_name' => 'agent', + 'retry_join' => ['172.16.0.1'], + } +} +``` + +## Limitations + +Depends on the JSON gem, or a modern ruby. (Ruby 1.8.7 is not officially supported) + +## Development +Open an [issue](https://github.com/solarkennedy/puppet-consul/issues) or +[fork](https://github.com/solarkennedy/puppet-consul/fork) and open a +[Pull Request](https://github.com/solarkennedy/puppet-consul/pulls) diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..cae4cdf --- /dev/null +++ b/Rakefile @@ -0,0 +1,41 @@ +require 'bundler/setup' +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet-lint/tasks/puppet-lint' +require 'puppet-syntax/tasks/puppet-syntax' + +# These two gems aren't always present, for instance +# on Travis with --without development +begin + require 'puppet_blacksmith/rake_tasks' +rescue LoadError +end + +PuppetLint.configuration.send("disable_80chars") +PuppetLint.configuration.log_format = "%{path}:%{linenumber}:%{check}:%{KIND}:%{message}" +PuppetLint.configuration.fail_on_warnings = true + +# Forsake support for Puppet 2.6.2 for the benefit of cleaner code. +# http://puppet-lint.com/checks/class_parameter_defaults/ +PuppetLint.configuration.send('disable_class_parameter_defaults') +# http://puppet-lint.com/checks/class_inherits_from_params_class/ +PuppetLint.configuration.send('disable_class_inherits_from_params_class') + +exclude_paths = [ + "pkg/**/*", + "vendor/**/*", + "spec/**/*", +] +PuppetLint.configuration.ignore_paths = exclude_paths +PuppetSyntax.exclude_paths = exclude_paths + +desc "Run acceptance tests" +RSpec::Core::RakeTask.new(:acceptance) do |t| + t.pattern = 'spec/acceptance' +end + +desc "Run syntax, lint, and spec tests." +task :test => [ + :syntax, + :lint, + :spec, +] diff --git a/files/.gitkeep b/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/facter/nomad_version.rb b/lib/facter/nomad_version.rb new file mode 100644 index 0000000..c971554 --- /dev/null +++ b/lib/facter/nomad_version.rb @@ -0,0 +1,12 @@ +# nomad_version.rb + +Facter.add(:nomad_version) do + confine :kernel => 'Linux' + setcode do + begin + Facter::Util::Resolution.exec('nomad --version 2> /dev/null').lines.first.split[1].tr('v','') + rescue + nil + end + end +end diff --git a/lib/puppet/parser/functions/nomad_sorted_json.rb b/lib/puppet/parser/functions/nomad_sorted_json.rb new file mode 100644 index 0000000..58a1f64 --- /dev/null +++ b/lib/puppet/parser/functions/nomad_sorted_json.rb @@ -0,0 +1,166 @@ +require 'json' + +module JSON + class << self + @@loop = 0 + + def sorted_generate(obj) + case obj + when NilClass,Fixnum, Float, TrueClass, FalseClass,String + return simple_generate(obj) + when Array + arrayRet = [] + obj.each do |a| + arrayRet.push(sorted_generate(a)) + end + return "[" << arrayRet.join(',') << "]"; + when Hash + ret = [] + obj.keys.sort.each do |k| + ret.push(k.to_json << ":" << sorted_generate(obj[k])) + end + return "{" << ret.join(",") << "}"; + else + raise Exception("Unable to handle object of type <%s>" % obj.class.to_s) + end + end + + def sorted_pretty_generate(obj, indent_len=4) + + # Indent length + indent = " " * indent_len + + case obj + when NilClass,Fixnum, Float, TrueClass, FalseClass,String + return simple_generate(obj) + when Array + arrayRet = [] + + # We need to increase the loop count before #each so the objects inside are indented twice. + # When we come out of #each we decrease the loop count so the closing brace lines up properly. + # + # If you start with @@loop = 1, the count will be as follows + # + # "start_join": [ <-- @@loop == 1 + # "192.168.50.20", <-- @@loop == 2 + # "192.168.50.21", <-- @@loop == 2 + # "192.168.50.22" <-- @@loop == 2 + # ] <-- closing brace <-- @@loop == 1 + # + @@loop += 1 + obj.each do |a| + arrayRet.push(sorted_pretty_generate(a, indent_len)) + end + @@loop -= 1 + + return "[\n#{indent * (@@loop + 1)}" << arrayRet.join(",\n#{indent * (@@loop + 1)}") << "\n#{indent * @@loop}]"; + + when Hash + ret = [] + + # This loop works in a similar way to the above + @@loop += 1 + obj.keys.sort.each do |k| + ret.push("#{indent * @@loop}" << k.to_json << ": " << sorted_pretty_generate(obj[k], indent_len)) + end + @@loop -= 1 + + return "{\n" << ret.join(",\n") << "\n#{indent * @@loop}}"; + else + raise Exception("Unable to handle object of type <%s>" % obj.class.to_s) + end + + end # end def + private + # simplify jsonification of standard types + def simple_generate(obj) + case obj + when NilClass + 'null' + when Fixnum, Float, TrueClass, FalseClass + "#{obj}" + else + # Should be a string + # keep string integers unquoted + (obj =~ /\A[-]?\d+\z/) ? obj : obj.to_json + end + end + + end # end class + +end # end module + + +module Puppet::Parser::Functions + newfunction(:nomad_sorted_json, :type => :rvalue, :doc => <<-EOS +This function takes unsorted hash and outputs JSON object making sure the keys are sorted. +Optionally you can pass 2 additional parameters, pretty generate and indent length. + +*Examples:* + + ------------------- + -- UNSORTED HASH -- + ------------------- + unsorted_hash = { + 'client_addr' => '127.0.0.1', + 'bind_addr' => '192.168.34.56', + 'start_join' => [ + '192.168.34.60', + '192.168.34.61', + '192.168.34.62', + ], + 'ports' => { + 'rpc' => 8567, + 'https' => 8500, + 'http' => -1, + }, + } + + ----------------- + -- SORTED JSON -- + ----------------- + + nomad_sorted_json(unsorted_hash) + + {"bind_addr":"192.168.34.56","client_addr":"127.0.0.1", + "ports":{"http":-1,"https":8500,"rpc":8567}, + "start_join":["192.168.34.60","192.168.34.61","192.168.34.62"]} + + ------------------------ + -- PRETTY SORTED JSON -- + ------------------------ + Params: data , pretty , indent . + + nomad_sorted_json(unsorted_hash, true, 4) + + { + "bind_addr": "192.168.34.56", + "client_addr": "127.0.0.1", + "ports": { + "http": -1, + "https": 8500, + "rpc": 8567 + }, + "start_join": [ + "192.168.34.60", + "192.168.34.61", + "192.168.34.62" + ] + } + + EOS + ) do |args| + + unsorted_hash = args[0] || {} + pretty = args[1] || false + indent_len = args[2].to_i || 4 + + unsorted_hash.reject! {|key, value| value == :undef } + + if pretty + return JSON.sorted_pretty_generate(unsorted_hash, indent_len) << "\n" + else + return JSON.sorted_generate(unsorted_hash) + end + end +end diff --git a/lib/puppet/parser/functions/nomad_validate_checks.rb b/lib/puppet/parser/functions/nomad_validate_checks.rb new file mode 100644 index 0000000..91c0bd7 --- /dev/null +++ b/lib/puppet/parser/functions/nomad_validate_checks.rb @@ -0,0 +1,55 @@ +def validate_checks(obj) + case obj + when Array + obj.each do |c| + validate_checks(c) + end + when Hash + if ( (obj.key?("http") || obj.key?("script") || obj.key?("tcp")) && (! obj.key?("interval")) ) + raise Puppet::ParseError.new('interval must be defined for tcp, http, and script checks') + end + + if obj.key?("ttl") + if (obj.key?("http") || obj.key?("script") || obj.key?("tcp") || obj.key?("interval")) + raise Puppet::ParseError.new('script, http, tcp, and interval must not be defined for ttl checks') + end + elsif obj.key?("http") + if (obj.key?("script") || obj.key?("tcp")) + raise Puppet::ParseError.new('script and tcp must not be defined for http checks') + end + elsif obj.key?("tcp") + if (obj.key?("http") || obj.key?("script")) + raise Puppet::ParseError.new('script and http must not be defined for tcp checks') + end + elsif obj.key?("script") + if (obj.key?("http") || obj.key?("tcp")) + raise Puppet::ParseError.new('http and tcp must not be defined for script checks') + end + else + raise Puppet::ParseError.new('One of ttl, script, tcp, or http must be defined.') + end + else + raise Puppet::ParseError.new("Unable to handle object of type <%s>" % obj.class.to_s) + end +end + +module Puppet::Parser::Functions + newfunction(:nomad_validate_checks, :doc => <<-EOS +This function validates the contents of an array of checks + +*Examples:* + + nomad_validate_checks({'key'=>'value'}) + nomad_validate_checks([ + {'key'=>'value'}, + {'key'=>'value'} + ]) + +Would return: true if valid, and raise exception otherwise + EOS + ) do |arguments| + raise(Puppet::ParseError, "validate_checks(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size != 1 + return validate_checks(arguments[0]) + end +end diff --git a/manifests/config.pp b/manifests/config.pp new file mode 100644 index 0000000..d980b94 --- /dev/null +++ b/manifests/config.pp @@ -0,0 +1,103 @@ +# == Class nomad::config +# +# This class is called from nomad::init to install the config file. +# +# == Parameters +# +# [*config_hash*] +# Hash for Consul to be deployed as JSON +# +# [*purge*] +# Bool. If set will make puppet remove stale config files. +# +class nomad::config( + $config_hash, + $purge = true, +) { + + if $nomad::init_style { + + case $nomad::init_style { + 'upstart' : { + file { '/etc/init/nomad.conf': + mode => '0444', + owner => 'root', + group => 'root', + content => template('nomad/nomad.upstart.erb'), + } + file { '/etc/init.d/nomad': + ensure => link, + target => '/lib/init/upstart-job', + owner => 'root', + group => 'root', + mode => '0755', + } + } + 'systemd' : { + file { '/lib/systemd/system/nomad.service': + mode => '0644', + owner => 'root', + group => 'root', + content => template('nomad/nomad.systemd.erb'), + }~> + exec { 'nomad-systemd-reload': + command => 'systemctl daemon-reload', + path => [ '/usr/bin', '/bin', '/usr/sbin' ], + refreshonly => true, + } + } + 'sysv' : { + file { '/etc/init.d/nomad': + mode => '0555', + owner => 'root', + group => 'root', + content => template('nomad/nomad.sysv.erb') + } + } + 'debian' : { + file { '/etc/init.d/nomad': + mode => '0555', + owner => 'root', + group => 'root', + content => template('nomad/nomad.debian.erb') + } + } + 'sles' : { + file { '/etc/init.d/nomad': + mode => '0555', + owner => 'root', + group => 'root', + content => template('nomad/nomad.sles.erb') + } + } + 'launchd' : { + file { '/Library/LaunchDaemons/io.nomad.daemon.plist': + mode => '0644', + owner => 'root', + group => 'wheel', + content => template('nomad/nomad.launchd.erb') + } + } + default : { + fail("I don't know how to create an init script for style ${nomad::init_style}") + } + } + } + + file { $nomad::config_dir: + ensure => 'directory', + owner => $nomad::user, + group => $nomad::group, + purge => $purge, + recurse => $purge, + } -> + file { 'nomad config.json': + ensure => present, + path => "${nomad::config_dir}/config.json", + owner => $nomad::user, + group => $nomad::group, + mode => $nomad::config_mode, + content => nomad_sorted_json($config_hash, $nomad::pretty_config, $nomad::pretty_config_indent), + } + +} diff --git a/manifests/init.pp b/manifests/init.pp new file mode 100644 index 0000000..4edb5ab --- /dev/null +++ b/manifests/init.pp @@ -0,0 +1,155 @@ +# == Class: nomad +# +# Installs, configures, and manages nomad +# +# === Parameters +# +# [*version*] +# Specify version of nomad binary to download. +# +# [*config_hash*] +# Use this to populate the JSON config file for nomad. +# +# [*config_mode*] +# Use this to set the JSON config file mode for nomad. +# +# [*pretty_config*] +# Generates a human readable JSON config file. Defaults to `false`. +# +# [*pretty_config_indent*] +# Toggle indentation for human readable JSON file. Defaults to `4`. +# +# [*install_method*] +# Valid strings: `package` - install via system package +# `url` - download and extract from a url. Defaults to `url`. +# `none` - disable install. +# +# [*package_name*] +# Only valid when the install_method == package. Defaults to `nomad`. +# +# [*package_ensure*] +# Only valid when the install_method == package. Defaults to `latest`. +# +# +# [*restart_on_change*] +# Determines whether to restart nomad agent on $config_hash changes. +# This will not affect reloads when service, check or watch configs change. +# Defaults to `true`. +# +# [*extra_options*] +# Extra arguments to be passed to the nomad agent +# +# [*init_style*] +# What style of init system your system uses. +# +# [*purge_config_dir*] +# Purge config files no longer generated by Puppet +class nomad ( + $manage_user = true, + $user = 'nomad', + $manage_group = true, + $extra_groups = [], + $purge_config_dir = true, + $group = 'nomad', + $join_wan = false, + $bin_dir = '/usr/local/bin', + $arch = $nomad::params::arch, + $version = $nomad::params::version, + $install_method = $nomad::params::install_method, + $os = $nomad::params::os, + $download_url = undef, + $download_url_base = $nomad::params::download_url_base, + $download_extension = $nomad::params::download_extension, + $package_name = $nomad::params::package_name, + $package_ensure = $nomad::params::package_ensure, + $config_dir = '/etc/nomad', + $extra_options = '', + $config_hash = {}, + $config_defaults = {}, + $config_mode = $nomad::params::config_mode, + $pretty_config = false, + $pretty_config_indent = 4, + $service_enable = true, + $service_ensure = 'running', + $manage_service = true, + $restart_on_change = true, + $init_style = $nomad::params::init_style, + $services = {}, + $watches = {}, + $checks = {}, + $acls = {}, +) inherits nomad::params { + + $real_download_url = pick($download_url, "${download_url_base}${version}/${package_name}_${version}_${os}_${arch}.${download_extension}") + + validate_bool($purge_config_dir) + validate_bool($manage_user) + validate_array($extra_groups) + validate_bool($manage_service) + validate_bool($restart_on_change) + validate_hash($config_hash) + validate_hash($config_defaults) + validate_bool($pretty_config) + validate_integer($pretty_config_indent) + validate_hash($services) + validate_hash($watches) + validate_hash($checks) + validate_hash($acls) + + $config_hash_real = deep_merge($config_defaults, $config_hash) + validate_hash($config_hash_real) + + if $config_hash_real['data_dir'] { + $data_dir = $config_hash_real['data_dir'] + } else { + $data_dir = undef + } + + + if ($config_hash_real['ports'] and $config_hash_real['ports']['rpc']) { + $rpc_port = $config_hash_real['ports']['rpc'] + } else { + $rpc_port = 8400 + } + + if ($config_hash_real['addresses'] and $config_hash_real['addresses']['rpc']) { + $rpc_addr = $config_hash_real['addresses']['rpc'] + } elsif ($config_hash_real['client_addr']) { + $rpc_addr = $config_hash_real['client_addr'] + } else { + $rpc_addr = $::ipaddress_lo + } + + if $services { + create_resources(nomad::service, $services) + } + + if $watches { + create_resources(nomad::watch, $watches) + } + + if $checks { + create_resources(nomad::check, $checks) + } + + if $acls { + create_resources(nomad_acl, $acls) + } + + $notify_service = $restart_on_change ? { + true => Class['nomad::run_service'], + default => undef, + } + + anchor {'nomad_first': } + -> + class { 'nomad::install': } -> + class { 'nomad::config': + config_hash => $config_hash_real, + purge => $purge_config_dir, + notify => $notify_service, + } -> + class { 'nomad::run_service': } -> + class { 'nomad::reload_service': } -> + anchor {'nomad_last': } +} diff --git a/manifests/install.pp b/manifests/install.pp new file mode 100644 index 0000000..dd31c35 --- /dev/null +++ b/manifests/install.pp @@ -0,0 +1,111 @@ +# == Class nomad::install +# +# Installs nomad based on the parameters from init +# +class nomad::install { + + if $nomad::data_dir { + file { $nomad::data_dir: + ensure => 'directory', + owner => $nomad::user, + group => $nomad::group, + mode => '0755', + } + } + + case $nomad::install_method { + 'url': { + include staging + staging::file { "nomad-${nomad::version}.${nomad::download_extension}": + source => $nomad::real_download_url, + } -> + file { "${::staging::path}/nomad-${nomad::version}": + ensure => directory, + } -> + staging::extract { "nomad-${nomad::version}.${nomad::download_extension}": + target => "${::staging::path}/nomad-${nomad::version}", + creates => "${::staging::path}/nomad-${nomad::version}/nomad", + } -> + file { + "${::staging::path}/nomad-${nomad::version}/nomad": + owner => 'root', + group => 0, # 0 instead of root because OS X uses "wheel". + mode => '0555'; + "${nomad::bin_dir}/nomad": + ensure => link, + notify => $nomad::notify_service, + target => "${::staging::path}/nomad-${nomad::version}/nomad"; + } + + if ($nomad::ui_dir and $nomad::data_dir) { + + # The 'dist' dir was removed from the web_ui archive in Consul version 0.6.0 + if (versioncmp($::nomad::version, '0.6.0') < 0) { + $staging_creates = "${nomad::data_dir}/${nomad::version}_web_ui/dist" + $ui_symlink_target = $staging_creates + } else { + $staging_creates = "${nomad::data_dir}/${nomad::version}_web_ui/index.html" + $ui_symlink_target = "${nomad::data_dir}/${nomad::version}_web_ui" + } + + file { "${nomad::data_dir}/${nomad::version}_web_ui": + ensure => 'directory', + owner => 'root', + group => 0, # 0 instead of root because OS X uses "wheel". + mode => '0755', + } -> + staging::deploy { "nomad_web_ui-${nomad::version}.zip": + source => $nomad::real_ui_download_url, + target => "${nomad::data_dir}/${nomad::version}_web_ui", + creates => $staging_creates, + } -> + file { $nomad::ui_dir: + ensure => 'symlink', + target => $ui_symlink_target, + } + } + } + 'package': { + package { $nomad::package_name: + ensure => $nomad::package_ensure, + } + + if $nomad::ui_dir { + package { $nomad::ui_package_name: + ensure => $nomad::ui_package_ensure, + require => Package[$nomad::package_name] + } + } + + if $nomad::manage_user { + User[$nomad::user] -> Package[$nomad::package_name] + } + + if $nomad::data_dir { + Package[$nomad::package_name] -> File[$nomad::data_dir] + } + } + 'none': {} + default: { + fail("The provided install method ${nomad::install_method} is invalid") + } + } + + if $nomad::manage_user { + user { $nomad::user: + ensure => 'present', + system => true, + groups => $nomad::extra_groups, + } + + if $nomad::manage_group { + Group[$nomad::group] -> User[$nomad::user] + } + } + if $nomad::manage_group { + group { $nomad::group: + ensure => 'present', + system => true, + } + } +} diff --git a/manifests/params.pp b/manifests/params.pp new file mode 100644 index 0000000..2876e74 --- /dev/null +++ b/manifests/params.pp @@ -0,0 +1,72 @@ +# == Class nomad::params +# +# This class is meant to be called from nomad +# It sets variables according to platform +# +class nomad::params { + + $install_method = 'url' + $package_name = 'nomad' + $package_ensure = 'latest' + $download_url_base = 'https://releases.hashicorp.com/nomad/' + $download_extension = 'zip' + $version = '0.5.2' + $config_mode = '0660' + + case $::architecture { + 'x86_64', 'amd64': { $arch = 'amd64' } + 'i386': { $arch = '386' } + default: { + fail("Unsupported kernel architecture: ${::architecture}") + } + } + + $os = downcase($::kernel) + + if $::operatingsystem == 'Ubuntu' { + if versioncmp($::operatingsystemrelease, '8.04') < 1 { + $init_style = 'debian' + } elsif versioncmp($::operatingsystemrelease, '15.04') < 0 { + $init_style = 'upstart' + } else { + $init_style = 'systemd' + } + } elsif $::operatingsystem =~ /Scientific|CentOS|RedHat|OracleLinux/ { + if versioncmp($::operatingsystemrelease, '7.0') < 0 { + $init_style = 'sysv' + } else { + $init_style = 'systemd' + } + } elsif $::operatingsystem == 'Fedora' { + if versioncmp($::operatingsystemrelease, '12') < 0 { + $init_style = 'sysv' + } else { + $init_style = 'systemd' + } + } elsif $::operatingsystem == 'Debian' { + if versioncmp($::operatingsystemrelease, '8.0') < 0 { + $init_style = 'debian' + } else { + $init_style = 'systemd' + } + } elsif $::operatingsystem == 'Archlinux' { + $init_style = 'systemd' + } elsif $::operatingsystem == 'OpenSuSE' { + $init_style = 'systemd' + } elsif $::operatingsystem =~ /SLE[SD]/ { + if versioncmp($::operatingsystemrelease, '12.0') < 0 { + $init_style = 'sles' + } else { + $init_style = 'systemd' + } + } elsif $::operatingsystem == 'Darwin' { + $init_style = 'launchd' + } elsif $::operatingsystem == 'Amazon' { + $init_style = 'sysv' + } else { + $init_style = undef + } + if $init_style == undef { + fail('Unsupported OS') + } +} diff --git a/manifests/reload_service.pp b/manifests/reload_service.pp new file mode 100644 index 0000000..cfc4189 --- /dev/null +++ b/manifests/reload_service.pp @@ -0,0 +1,29 @@ +# == Class nomad::reload_service +# +# This class is meant to be called from certain +# configuration changes that support reload. +# +# https://www.nomad.io/docs/agent/options.html#reloadable-configuration +# +class nomad::reload_service { + + # Don't attempt to reload if we're not supposed to be running. + # This can happen during pre-provisioning of a node. + if $nomad::manage_service == true and $nomad::service_ensure == 'running' { + + # Make sure we don't try to connect to 0.0.0.0, use 127.0.0.1 instead + # This can happen if the nomad agent RPC port is bound to 0.0.0.0 + if $nomad::rpc_addr == '0.0.0.0' { + $rpc_addr = '127.0.0.1' + } else { + $rpc_addr = $nomad::rpc_addr + } + + exec { 'reload nomad service': + path => [$nomad::bin_dir,'/bin','/usr/bin'], + command => "nomad reload -rpc-addr=${rpc_addr}:${nomad::rpc_port}", + refreshonly => true, + } + } + +} diff --git a/manifests/run_service.pp b/manifests/run_service.pp new file mode 100644 index 0000000..200ef06 --- /dev/null +++ b/manifests/run_service.pp @@ -0,0 +1,31 @@ +# == Class nomad::service +# +# This class is meant to be called from nomad +# It ensure the service is running +# +class nomad::run_service { + + $init_selector = $nomad::init_style ? { + 'launchd' => 'io.nomad.daemon', + default => 'nomad', + } + + if $nomad::manage_service == true { + service { 'nomad': + ensure => $nomad::service_ensure, + name => $init_selector, + enable => $nomad::service_enable, + } + } + + if $nomad::join_wan { + exec { 'join nomad wan': + cwd => $nomad::config_dir, + path => [$nomad::bin_dir,'/bin','/usr/bin'], + command => "nomad join -wan ${nomad::join_wan}", + unless => "nomad members -wan -detailed | grep -vP \"dc=${nomad::config_hash_real['datacenter']}\" | grep -P 'alive'", + subscribe => Service['nomad'], + } + } + +} diff --git a/manifests/service.pp b/manifests/service.pp new file mode 100644 index 0000000..7c1d6fa --- /dev/null +++ b/manifests/service.pp @@ -0,0 +1,70 @@ +# == Define nomad::service +# +# Sets up a Consul service definition +# http://www.nomad.io/docs/agent/services.html +# +# == Parameters +# +# [*ensure*] +# Define availability of service. Use 'absent' to remove existing services. +# Defaults to 'present' +# +# [*service_name*] +# Name of the service. Defaults to title. +# +# [*id*] +# The unique ID of the service on the node. Defaults to title. +# +# [*tags*] +# Array of strings. +# +# [*address*] +# IP address the service is running at. +# +# [*port*] +# TCP port the service runs on. +# +# [*checks*] +# If provided an array of checks that will be added to this service +# +# [*token*] +# ACL token for interacting with the catalog (must be 'management' type) +# +define nomad::service( + $ensure = present, + $service_name = $title, + $id = $title, + $tags = [], + $address = undef, + $port = undef, + $checks = [], + $token = undef, +) { + include nomad + + nomad_validate_checks($checks) + + $basic_hash = { + 'id' => $id, + 'name' => $service_name, + 'address' => $address, + 'port' => $port, + 'tags' => $tags, + 'checks' => $checks, + 'token' => $token, + } + + $service_hash = { + service => delete_undef_values($basic_hash) + } + + $escaped_id = regsubst($id,'\/','_','G') + file { "${nomad::config_dir}/service_${escaped_id}.json": + ensure => $ensure, + owner => $nomad::user, + group => $nomad::group, + mode => $nomad::config_mode, + content => nomad_sorted_json($service_hash, $nomad::pretty_config, $nomad::pretty_config_indent), + require => File[$nomad::config_dir], + } ~> Class['nomad::reload_service'] +} diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..678d7c5 --- /dev/null +++ b/metadata.json @@ -0,0 +1,103 @@ +{ + "name": "dudemcbacon-consul", + "version": "1.0.0", + "author": "Kyle Anderson ", + "summary": "Configures Nomad by Hashicorp", + "license": "Apache-2.0", + "source": "https://github.com/dudemcbacon/puppet-nomad", + "project_page": "https://github.com/dudemcbacon/puppet-nomad", + "issues_url": "https://github.com/dudemcbacon/puppet-nomad/issues", + "description": "Configures Nomad by Hashicorp", + "dependencies": [ + { + "name": "puppetlabs/stdlib", + "version_requirement": ">= 4.6.0 <5.0.0" + }, + { + "name": "nanliu/staging", + "version_requirement": ">=0.4.0 <2.0.0" + } + ], + "operatingsystem_support": [ + { + "operatingsystem": "RedHat", + "operatingsystemrelease": [ + "5.0", + "6.0", + "7.0" + ] + }, + { + "operatingsystem": "CentOS", + "operatingsystemrelease": [ + "5.0", + "6.0", + "7.0" + ] + }, + { + "operatingsystem": "Scientific Linux", + "operatingsystemrelease": [ + "5.0", + "6.0", + "7.0" + ] + }, + { + "operatingsystem": "Oracle Linux", + "operatingsystemrelease": [ + "5.0", + "6.0", + "7.0" + ] + }, + { + "operatingsystem": "Ubuntu", + "operatingsystemrelease": [ + "12.04", + "10.04", + "14.04", + "15.04" + ] + }, + { + "operatingsystem": "Archlinux" + }, + { + "operatingsystem": "Fedora", + "operatingsystemrelease": [ + "11", + "12", + "13", + "14", + "16", + "17", + "18", + "19", + "20", + "21" + ] + }, + { + "operatingsystem": "OpenSuSE", + "operatingsystemrelease": [ + "13.1", + "13.2" + ] + }, + { + "operatingsystem": "SLES", + "operatingsystemrelease": [ + "11.4", + "12.0" + ] + }, + { + "operatingsystem": "SLED", + "operatingsystemrelease": [ + "11.4", + "12.0" + ] + } + ] +} diff --git a/spec/acceptance/nodesets/default.yml b/spec/acceptance/nodesets/default.yml new file mode 100644 index 0000000..ad5440e --- /dev/null +++ b/spec/acceptance/nodesets/default.yml @@ -0,0 +1,9 @@ +HOSTS: + ubuntu-12-04: + platform: ubuntu-12.04-x64 + image: solarkennedy/ubuntu-12.04-puppet + hypervisor: docker + docker_cmd: '["/sbin/init"]' + docker_preserve_image: true +CONFIG: + type: foss diff --git a/spec/acceptance/standard_spec.rb b/spec/acceptance/standard_spec.rb new file mode 100644 index 0000000..f162437 --- /dev/null +++ b/spec/acceptance/standard_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper_acceptance' + +describe 'nomad class' do + + context 'default parameters' do + # Using puppet_apply as a helper + it 'should work with no errors based on the example' do + pp = <<-EOS + class { 'nomad': + version => '0.5.2', + config_hash => { + 'datacenter' => 'east-aws', + 'data_dir' => '/opt/nomad', + 'log_level' => 'INFO', + 'node_name' => 'foobar', + 'server' => true + } + } + EOS + + # Run it twice and test for idempotency + expect(apply_manifest(pp).exit_code).to_not eq(1) + expect(apply_manifest(pp).exit_code).to eq(0) + end + + describe file('/opt/nomad') do + it { should be_directory } + end + + describe service('nomad') do + it { should be_enabled } + end + + describe command('nomad version') do + its(:stdout) { should match /Consul v0\.5\.2/ } + end + + end +end diff --git a/spec/acceptance/unsupported_spec.rb b/spec/acceptance/unsupported_spec.rb new file mode 100644 index 0000000..4615c1f --- /dev/null +++ b/spec/acceptance/unsupported_spec.rb @@ -0,0 +1,10 @@ +#require 'spec_helper_acceptance' +# +#describe 'unsupported distributions and OSes', :if => UNSUPPORTED_PLATFORMS.include?(fact('operatingsystem')) do +# it 'should fail' do +# pp = <<-EOS +# class { 'consul': } +# EOS +# expect(apply_manifest(pp, :expect_failures => true).stderr).to match(/unsupported osfamily/i) +# end +#end diff --git a/spec/classes/init_spec.rb b/spec/classes/init_spec.rb new file mode 100644 index 0000000..46d4861 --- /dev/null +++ b/spec/classes/init_spec.rb @@ -0,0 +1,578 @@ +require 'spec_helper' + +describe 'nomad' do + + RSpec.configure do |c| + c.default_facts = { + :architecture => 'x86_64', + :operatingsystem => 'Ubuntu', + :osfamily => 'Debian', + :operatingsystemrelease => '10.04', + :kernel => 'Linux', + } + end + # Installation Stuff + context 'On an unsupported arch' do + let(:facts) {{ :architecture => 'bogus' }} + let(:params) {{ + :install_method => 'package' + }} + it { expect { should compile }.to raise_error(/Unsupported kernel architecture:/) } + end + + context 'When not specifying whether to purge config' do + it { should contain_file('/etc/nomad').with(:purge => true,:recurse => true) } + end + + context 'When passing a non-bool as purge_config_dir' do + let(:params) {{ + :purge_config_dir => 'hello' + }} + it { expect { should compile }.to raise_error(/is not a boolean/) } + end + + context 'When passing a non-bool as manage_service' do + let(:params) {{ + :manage_service => 'hello' + }} + it { expect { should compile }.to raise_error(/is not a boolean/) } + end + + context 'When disable config purging' do + let(:params) {{ + :purge_config_dir => false + }} + it { should contain_class('nomad::config').with(:purge => false) } + end + + context 'nomad::config should notify nomad::run_service' do + it { should contain_class('nomad::config').that_notifies(['Class[nomad::run_service]']) } + end + + context 'nomad::config should not notify nomad::run_service on config change' do + let(:params) {{ + :restart_on_change => false + }} + it { should_not contain_class('nomad::config').that_notifies(['Class[nomad::run_service]']) } + end + + context 'When joining nomad to a wan cluster by a known URL' do + let(:params) {{ + :join_wan => 'wan_host.test.com' + }} + it { should contain_exec('join nomad wan').with(:command => 'nomad join -wan wan_host.test.com') } + end + + context 'By default, should not attempt to join a wan cluster' do + it { should_not contain_exec('join nomad wan') } + end + + context 'When requesting to install via a package with defaults' do + let(:params) {{ + :install_method => 'package' + }} + it { should contain_package('nomad').with(:ensure => 'latest') } + end + + context 'When requesting to install via a custom package and version' do + let(:params) {{ + :install_method => 'package', + :package_ensure => 'specific_release', + :package_name => 'custom_nomad_package' + }} + it { should contain_package('custom_nomad_package').with(:ensure => 'specific_release') } + end + + context "When installing via URL by default" do + it { should contain_staging__file('nomad-0.5.2.zip').with(:source => 'https://releases.hashicorp.com/nomad/0.5.2/nomad_0.5.2_linux_amd64.zip') } + it { should contain_file('/usr/local/bin/nomad').that_notifies('Class[nomad::run_service]') } + #it { should contain_notify(['Class[nomad::run_service]']) } + end + + context "When installing via URL by with a special version" do + let(:params) {{ + :version => '42', + }} + it { should contain_staging__file('nomad-42.zip').with(:source => 'https://releases.hashicorp.com/nomad/42/nomad_42_linux_amd64.zip') } + it { should contain_file('/usr/local/bin/nomad').that_notifies('Class[nomad::run_service]') } + end + + context "When installing via URL by with a custom url" do + let(:params) {{ + :download_url => 'http://myurl', + }} + it { should contain_staging__file('nomad-0.5.2.zip').with(:source => 'http://myurl') } + it { should contain_file('/usr/local/bin/nomad').that_notifies('Class[nomad::run_service]') } + end + + + context 'When requesting to install via a package with defaults' do + let(:params) {{ + :install_method => 'package' + }} + it { should contain_package('nomad').with(:ensure => 'latest') } + end + + context 'When requesting to not to install' do + let(:params) {{ + :install_method => 'none' + }} + it { should_not contain_package('nomad') } + it { should_not contain_staging__file('nomad.zip') } + end + + + + context "By default, a user and group should be installed" do + it { should contain_user('nomad').with(:ensure => :present) } + it { should contain_group('nomad').with(:ensure => :present) } + end + + context "When data_dir is provided" do + let(:params) {{ + :config_hash => { + 'data_dir' => '/dir1', + }, + }} + it { should contain_file('/dir1').with(:ensure => :directory) } + end + + context "When data_dir not provided" do + it { should_not contain_file('/dir1').with(:ensure => :directory) } + end + + context 'The bootstrap_expect in config_hash is an int' do + let(:params) {{ + :config_hash => + { 'bootstrap_expect' => '5' } + }} + it { should contain_file('nomad config.json').with_content(/"bootstrap_expect":5/) } + it { should_not contain_file('nomad config.json').with_content(/"bootstrap_expect":"5"/) } + end + + context 'Config_defaults is used to provide additional config' do + let(:params) {{ + :config_defaults => { + 'data_dir' => '/dir1', + }, + :config_hash => { + 'bootstrap_expect' => '5', + } + }} + it { should contain_file('nomad config.json').with_content(/"bootstrap_expect":5/) } + it { should contain_file('nomad config.json').with_content(/"data_dir":"\/dir1"/) } + end + + context 'Config_defaults is used to provide additional config and is overridden' do + let(:params) {{ + :config_defaults => { + 'data_dir' => '/dir1', + 'server' => false, + 'ports' => { + 'http' => 1, + 'rpc' => '8300', + }, + }, + :config_hash => { + 'bootstrap_expect' => '5', + 'server' => true, + 'ports' => { + 'http' => -1, + 'https' => 8500, + }, + } + }} + it { should contain_file('nomad config.json').with_content(/"bootstrap_expect":5/) } + it { should contain_file('nomad config.json').with_content(/"data_dir":"\/dir1"/) } + it { should contain_file('nomad config.json').with_content(/"server":true/) } + it { should contain_file('nomad config.json').with_content(/"http":-1/) } + it { should contain_file('nomad config.json').with_content(/"https":8500/) } + it { should contain_file('nomad config.json').with_content(/"rpc":8300/) } + end + + context 'When pretty config is true' do + let(:params) {{ + :pretty_config => true, + :config_hash => { + 'bootstrap_expect' => '5', + 'server' => true, + 'ports' => { + 'http' => -1, + 'https' => 8500, + }, + } + }} + it { should contain_file('nomad config.json').with_content(/"bootstrap_expect": 5,/) } + it { should contain_file('nomad config.json').with_content(/"server": true/) } + it { should contain_file('nomad config.json').with_content(/"http": -1,/) } + it { should contain_file('nomad config.json').with_content(/"https": 8500/) } + it { should contain_file('nomad config.json').with_content(/"ports": \{/) } + end + + context "When asked not to manage the user" do + let(:params) {{ :manage_user => false }} + it { should_not contain_user('nomad') } + end + + context "When asked not to manage the group" do + let(:params) {{ :manage_group => false }} + it { should_not contain_group('nomad') } + end + + context "When asked not to manage the service" do + let(:params) {{ :manage_service => false }} + + it { should_not contain_service('nomad') } + end + + context "When a reload_service is triggered with service_ensure stopped" do + let (:params) {{ + :service_ensure => 'stopped', + :services => { + 'test_service1' => { + 'port' => '5' + } + } + }} + it { should_not contain_exec('reload nomad service') } + end + + context "When a reload_service is triggered with manage_service false" do + let (:params) {{ + :manage_service => false, + :services => { + 'test_service1' => { + 'port' => '5' + } + } + }} + it { should_not contain_exec('reload nomad service') } + end + + context "With a custom username" do + let(:params) {{ + :user => 'custom_nomad_user', + :group => 'custom_nomad_group', + }} + it { should contain_user('custom_nomad_user').with(:ensure => :present) } + it { should contain_group('custom_nomad_group').with(:ensure => :present) } + it { should contain_file('/etc/init/nomad.conf').with_content(/env USER=custom_nomad_user/) } + it { should contain_file('/etc/init/nomad.conf').with_content(/env GROUP=custom_nomad_group/) } + end + + context "Config with custom file mode" do + let(:params) {{ + :user => 'custom_nomad_user', + :group => 'custom_nomad_group', + :config_mode => '0600', + }} + it { should contain_file('nomad config.json').with( + :owner => 'custom_nomad_user', + :group => 'custom_nomad_group', + :mode => '0600' + )} + end + + context "When nomad is reloaded" do + let (:params) {{ + :services => { + 'test_service1' => {} + } + }} + let (:facts) {{ + :ipaddress_lo => '127.0.0.1' + }} + it { + should contain_exec('reload nomad service'). + with_command('nomad reload -rpc-addr=127.0.0.1:8400') + } + end + + context "When nomad is reloaded on a custom port" do + let (:params) {{ + :services => { + 'test_service1' => {} + }, + :config_hash => { + 'ports' => { + 'rpc' => '9999' + }, + 'addresses' => { + 'rpc' => 'nomad.example.com' + } + } + }} + it { + should contain_exec('reload nomad service'). + with_command('nomad reload -rpc-addr=nomad.example.com:9999') + } + end + + context "When nomad is reloaded with a default client_addr" do + let (:params) {{ + :services => { + 'test_service1' => {} + }, + :config_hash => { + 'client_addr' => '192.168.34.56', + } + }} + it { + should contain_exec('reload nomad service'). + with_command('nomad reload -rpc-addr=192.168.34.56:8400') + } + end + + context "When the user provides a hash of services" do + let (:params) {{ + :services => { + 'test_service1' => { + 'port' => '5' + } + } + }} + it { should contain_nomad__service('test_service1').with_port('5') } + it { should have_nomad__service_resource_count(1) } + it { should contain_exec('reload nomad service') } + end + + context "When using sysv" do + let (:params) {{ + :init_style => 'sysv' + }} + let (:facts) {{ + :ipaddress_lo => '127.0.0.1' + }} + it { should contain_class('nomad').with_init_style('sysv') } + it { + should contain_file('/etc/init.d/nomad'). + with_content(/-rpc-addr=127.0.0.1:8400/) + } + end + + context "When overriding default rpc port on sysv" do + let (:params) {{ + :init_style => 'sysv', + :config_hash => { + 'ports' => { + 'rpc' => '9999' + }, + 'addresses' => { + 'rpc' => 'nomad.example.com' + } + } + }} + it { should contain_class('nomad').with_init_style('sysv') } + it { + should contain_file('/etc/init.d/nomad'). + with_content(/-rpc-addr=nomad.example.com:9999/) + } + end + + context "When rpc_addr defaults to client_addr on sysv" do + let (:params) {{ + :init_style => 'sysv', + :config_hash => { + 'client_addr' => '192.168.34.56', + } + }} + it { should contain_class('nomad').with_init_style('sysv') } + it { + should contain_file('/etc/init.d/nomad'). + with_content(/-rpc-addr=192.168.34.56:8400/) + } + end + + context "When using debian" do + let (:params) {{ + :init_style => 'debian' + }} + let (:facts) {{ + :ipaddress_lo => '127.0.0.1' + }} + it { should contain_class('nomad').with_init_style('debian') } + it { + should contain_file('/etc/init.d/nomad'). + with_content(/-rpc-addr=127.0.0.1:8400/) + } + end + + context "When overriding default rpc port on debian" do + let (:params) {{ + :init_style => 'debian', + :config_hash => { + 'ports' => { + 'rpc' => '9999' + }, + 'addresses' => { + 'rpc' => 'nomad.example.com' + } + } + }} + it { should contain_class('nomad').with_init_style('debian') } + it { + should contain_file('/etc/init.d/nomad'). + with_content(/-rpc-addr=nomad.example.com:9999/) + } + end + + context "When using upstart" do + let (:params) {{ + :init_style => 'upstart' + }} + let (:facts) {{ + :ipaddress_lo => '127.0.0.1' + }} + it { should contain_class('nomad').with_init_style('upstart') } + it { + should contain_file('/etc/init/nomad.conf'). + with_content(/-rpc-addr=127.0.0.1:8400/) + } + end + + context "When overriding default rpc port on upstart" do + let (:params) {{ + :init_style => 'upstart', + :config_hash => { + 'ports' => { + 'rpc' => '9999' + }, + 'addresses' => { + 'rpc' => 'nomad.example.com' + } + } + }} + it { should contain_class('nomad').with_init_style('upstart') } + it { + should contain_file('/etc/init/nomad.conf'). + with_content(/-rpc-addr=nomad.example.com:9999/) + } + end + + context "On a redhat 6 based OS" do + let(:facts) {{ + :operatingsystem => 'CentOS', + :operatingsystemrelease => '6.5' + }} + + it { should contain_class('nomad').with_init_style('sysv') } + it { should contain_file('/etc/init.d/nomad').with_content(/daemon --user=nomad/) } + end + + context "On an Archlinux based OS" do + let(:facts) {{ + :operatingsystem => 'Archlinux', + }} + + it { should contain_class('nomad').with_init_style('systemd') } + it { should contain_file('/lib/systemd/system/nomad.service').with_content(/nomad agent/) } + end + + context "On an Amazon based OS" do + let(:facts) {{ + :operatingsystem => 'Amazon', + :operatingsystemrelease => '3.10.34-37.137.amzn1.x86_64' + }} + + it { should contain_class('nomad').with_init_style('sysv') } + it { should contain_file('/etc/init.d/nomad').with_content(/daemon --user=nomad/) } + end + + context "On a redhat 7 based OS" do + let(:facts) {{ + :operatingsystem => 'CentOS', + :operatingsystemrelease => '7.0' + }} + + it { should contain_class('nomad').with_init_style('systemd') } + it { should contain_file('/lib/systemd/system/nomad.service').with_content(/nomad agent/) } + end + + context "On a fedora 20 based OS" do + let(:facts) {{ + :operatingsystem => 'Fedora', + :operatingsystemrelease => '20' + }} + + it { should contain_class('nomad').with_init_style('systemd') } + it { should contain_file('/lib/systemd/system/nomad.service').with_content(/nomad agent/) } + end + + context "On hardy" do + let(:facts) {{ + :operatingsystem => 'Ubuntu', + :operatingsystemrelease => '8.04', + }} + + it { should contain_class('nomad').with_init_style('debian') } + it { + should contain_file('/etc/init.d/nomad') \ + .with_content(/start-stop-daemon .* \$DAEMON/) \ + .with_content(/DAEMON_ARGS="agent/) \ + .with_content(/--user \$USER/) + } + end + + context "On a Ubuntu Vivid 15.04 based OS" do + let(:facts) {{ + :operatingsystem => 'Ubuntu', + :operatingsystemrelease => '15.04' + }} + + it { should contain_class('nomad').with_init_style('systemd') } + it { should contain_file('/lib/systemd/system/nomad.service').with_content(/nomad agent/) } + end + + context "When asked not to manage the init_style" do + let(:params) {{ :init_style => false }} + it { should contain_class('nomad').with_init_style(false) } + it { should_not contain_file("/etc/init.d/nomad") } + it { should_not contain_file("/lib/systemd/system/nomad.service") } + end + + context "On squeeze" do + let(:facts) {{ + :operatingsystem => 'Debian', + :operatingsystemrelease => '7.1' + }} + + it { should contain_class('nomad').with_init_style('debian') } + end + + context "On opensuse" do + let(:facts) {{ + :operatingsystem => 'OpenSuSE', + :operatingsystemrelease => '13.1' + }} + + it { should contain_class('nomad').with_init_style('systemd') } + end + + context "On SLED" do + let(:facts) {{ + :operatingsystem => 'SLED', + :operatingsystemrelease => '11.4' + }} + + it { should contain_class('nomad').with_init_style('sles') } + end + + context "On SLES" do + let(:facts) {{ + :operatingsystem => 'SLES', + :operatingsystemrelease => '12.0' + }} + + it { should contain_class('nomad').with_init_style('systemd') } + end + + # Config Stuff + context "With extra_options" do + let(:params) {{ + :extra_options => '-some-extra-argument' + }} + it { should contain_file('/etc/init/nomad.conf').with_content(/\$CONSUL -S -- agent .*-some-extra-argument$/) } + end + # Service Stuff + +end diff --git a/spec/functions/nomad_sorted_json_spec.rb b/spec/functions/nomad_sorted_json_spec.rb new file mode 100644 index 0000000..2acfa39 --- /dev/null +++ b/spec/functions/nomad_sorted_json_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +RSpec.shared_examples 'handling_simple_types' do |pretty| + it 'handles nil' do + expect(subject.call([ {'key' => nil }],pretty)).to eql('{"key":null}') + end + it 'handles true' do + expect(subject.call([{'key' => true }],pretty)).to eql('{"key":true}') + end + it 'handles nil' do + expect(subject.call([{'key' => false }],pretty)).to eql('{"key":false}') + end + it 'handles positive integer' do + expect(subject.call([{'key' => 1 }],pretty)).to eql('{"key":1}') + end + it 'handles negative integer' do + expect(subject.call([{'key' => -1 }],pretty)).to eql('{"key":-1}') + end + it 'handles positive float' do + expect(subject.call([{'key' => 1.1 }],pretty)).to eql('{"key":1.1}') + end + it 'handles negative float' do + expect(subject.call([{'key' => -1.1 }],pretty)).to eql('{"key":-1.1}') + end + it 'handles integer in a string' do + expect(subject.call([{'key' => '1' }],pretty)).to eql('{"key":1}') + end + it 'handles negative integer in a string' do + expect(subject.call([{'key' => '-1' }],pretty)).to eql('{"key":-1}') + end + it 'handles simple string' do + expect(subject.call([{'key' => 'aString' }],pretty)).to eql("{\"key\":\"aString\"}") + end +end +describe 'nomad_sorted_json', :type => :puppet_function do + + let(:test_hash){ { 'z' => 3, 'a' => '1', 'p' => '2', 's' => '-7' } } + before do + @json = subject.call([test_hash, true]) + end + it "sorts keys" do + expect( @json.index('a') ).to be < @json.index('p') + expect( @json.index('p') ).to be < @json.index('s') + expect( @json.index('s') ).to be < @json.index('z') + end + + it "prints pretty json" do + expect(@json.split("\n").size).to eql(test_hash.size + 2) # +2 for { and } + end + + it "prints ugly json" do + json = subject.call([test_hash]) # pretty=false by default + expect(json.split("\n").size).to eql(1) + end + + it "validate ugly json" do + json = subject.call([test_hash]) # pretty=false by default + expect(json).to match("{\"a\":1,\"p\":2,\"s\":-7,\"z\":3}") + end + + context 'nesting' do + + let(:nested_test_hash){ { 'z' => [{'l' => 3, 'k' => '2', 'j'=> '1'}], + 'a' => {'z' => '3', 'x' => '1', 'y' => '2'}, + 'p' => [ '9','8','7'] } } + before do + @json = subject.call([nested_test_hash, true]) + end + + it "sorts nested hashes" do + expect( @json.index('x') ).to be < @json.index('y') + expect( @json.index('y') ).to be < @json.index('z') + end + + end + context 'test simple behavior' do + context 'sorted' do + include_examples 'handling_simple_types', false + end + context 'sorted pretty' do + include_examples 'handling_simple_types', true + end + end +end diff --git a/spec/functions/nomad_validate_checks_spec.rb b/spec/functions/nomad_validate_checks_spec.rb new file mode 100644 index 0000000..c0ed1d5 --- /dev/null +++ b/spec/functions/nomad_validate_checks_spec.rb @@ -0,0 +1,100 @@ +require 'spec_helper' + +describe 'nomad_validate_checks' do + + describe 'validate script and http' do + it {should run.with_params([ + { + 'http' => 'localhost', + 'script' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate script and tcp' do + it {should run.with_params([ + { + 'tcp' => 'localhost', + 'script' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate http and tcp' do + it {should run.with_params([ + { + 'tcp' => 'localhost', + 'http' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate script check' do + it {should run.with_params([ + { + 'interval' => '30s', + 'script' => 'true' + } + ])} + end + + describe 'validate script missing interval' do + it {should run.with_params([ + { + 'script' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate http missing interval' do + it {should run.with_params([ + { + 'http' => 'localhost' + } + ]).and_raise_error(Exception) } + end + + describe 'validate tcp missing interval' do + it {should run.with_params([ + { + 'tcp' => 'localhost' + } + ]).and_raise_error(Exception) } + end + + describe 'validate script and ttl' do + it {should run.with_params([ + { + 'script' => 'true', + 'ttl' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate http and ttl' do + it {should run.with_params([ + { + 'http' => 'localhost', + 'ttl' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate tcp and ttl' do + it {should run.with_params([ + { + 'tcp' => 'localhost', + 'ttl' => 'true' + } + ]).and_raise_error(Exception) } + end + + describe 'validate tcp check' do + it {should run.with_params([ + { + 'tcp' => 'localhost:80', + 'interval' => '30s', + } + ])} + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..0cfbc95 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,19 @@ +require 'puppetlabs_spec_helper/module_spec_helper' +require 'hiera-puppet-helper/rspec' +require 'hiera' +require 'puppet/indirector/hiera' + +# config hiera to work with let(:hiera_data) +def hiera_stub + config = Hiera::Config.load(hiera_config) + config[:logger] = 'puppet' + Hiera.new(:config => config) +end + +RSpec.configure do |c| + c.mock_framework = :rspec + c.before(:each) do + allow(Puppet::Indirector::Hiera).to receive(:hiera) { hiera_stub } + end + +end diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb new file mode 100644 index 0000000..2c53dc7 --- /dev/null +++ b/spec/spec_helper_acceptance.rb @@ -0,0 +1,23 @@ +require 'beaker-rspec/spec_helper' +require 'beaker-rspec/helpers/serverspec' +require 'beaker/puppet_install_helper' + +run_puppet_install_helper unless ENV['BEAKER_provision'] == 'no' + +RSpec.configure do |c| + # Project root + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + # Install module and dependencies + puppet_module_install(:source => proj_root, :module_name => 'nomad') + hosts.each do |host| + on host, puppet('module', 'install', 'puppetlabs-stdlib'), { :acceptable_exit_codes => [0,1] } + on host, puppet('module', 'install', 'nanliu/staging'), { :acceptable_exit_codes => [0,1] } + end + end +end diff --git a/spec/unit/facter/nomad_version_spec.rb b/spec/unit/facter/nomad_version_spec.rb new file mode 100644 index 0000000..16a495a --- /dev/null +++ b/spec/unit/facter/nomad_version_spec.rb @@ -0,0 +1,23 @@ +require "spec_helper" + +describe Facter::Util::Fact do + before { + Facter.clear + } + + describe "nomad_version" do + + context 'Returns nomad version on Linux' + it do + nomad_version_output = <<-EOS +Nomad v0.6.0 + EOS + allow(Facter.fact(:kernel)).to receive(:value).and_return("Linux") + allow(Facter::Util::Resolution).to receive(:exec).with('nomad --version 2> /dev/null'). + and_return(nomad_version_output) + expect(Facter.fact(:nomad_version).value).to match('0.6.0') + end + + end + +end diff --git a/templates/.gitkeep b/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/nomad.debian.erb b/templates/nomad.debian.erb new file mode 100644 index 0000000..fcf9f9d --- /dev/null +++ b/templates/nomad.debian.erb @@ -0,0 +1,178 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: nomad +# Required-Start: $local_fs $remote_fs $syslog $named $network +# Required-Stop: $local_fs $remote_fs $syslog $named $network +# Default-Start: 2 3 4 5 +# Default-Stop: S 0 1 6 +# Short-Description: Nomad job scheduling framework +# Description: Healthchecks local services and registers +# them in a central nomad database. +### END INIT INFO + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/usr/sbin:/usr/bin:/sbin:/bin:<%= scope.lookupvar('nomad::bin_dir') %> +DESC="Nomad job scheduling service" +NAME=nomad +DAEMON=<%= scope.lookupvar('nomad::bin_dir') %>/$NAME +PIDFILE=/var/run/$NAME/$NAME.pid +DAEMON_ARGS="agent -config-dir <%= scope.lookupvar('nomad::config_dir') %> <%= scope.lookupvar('nomad::extra_options') %>" +USER=<%= scope.lookupvar('nomad::user') %> +SCRIPTNAME=/etc/init.d/$NAME +RPC_ADDR=-rpc-addr=<%= scope.lookupvar('nomad::rpc_addr') %>:<%= scope.lookupvar('nomad::rpc_port') %> + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +[ -f /etc/default/rcS ] && . /etc/default/rcS + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function to create run directory +# +mkrundir() { + [ ! -d /var/run/nomad ] && mkdir -p /var/run/nomad + chown $USER /var/run/nomad +} + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + mkrundir + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --chuid $USER --background --make-pidfile --test > /dev/null \ + || return 1 + start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --chuid $USER --background --make-pidfile -- \ + $DAEMON_ARGS \ + || return 2 + + for i in `seq 1 30`; do + if ! start-stop-daemon --quiet --stop --test --pidfile $PIDFILE --exec $DAEMON --user $USER; then + RETVAL=2 + sleep 1 + continue + fi + if "$DAEMON" info ${RPC_ADDR} >/dev/null; then + return 0 + fi + done + return "$RETVAL" +} + + +# +# Function that stops the daemon/service +# +do_stop() +{ + # If nomad is not acting as a server, exit gracefully + if ("${DAEMON}" info ${RPC_ADDR} 2>/dev/null | grep -q 'server = false' 2>/dev/null) ; then + "$DAEMON" leave ${RPC_ADDR} + fi + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + status) + status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/templates/nomad.launchd.erb b/templates/nomad.launchd.erb new file mode 100644 index 0000000..68fb0ab --- /dev/null +++ b/templates/nomad.launchd.erb @@ -0,0 +1,27 @@ + + + + + Label io.nomad.daemon + UserName <%= scope.lookupvar('nomad::user') %> + GroupName <%= scope.lookupvar('nomad::group') %> +<% if scope.lookupvar('nomad::service_enable') %> + Disabled +<% else %> + Disabled> +<% end %> + RunAtLoad + KeepAlive + ProgramArguments + + <%= scope.lookupvar('nomad::bin_dir') %>/nomad + agent + -config-dir + <%= scope.lookupvar('nomad::config_dir') %> +<% require 'shellwords' %> +<% for extra_option in Shellwords.split(scope.lookupvar('nomad::extra_options')) %> + <%= extra_option %> +<% end %> + + + diff --git a/templates/nomad.sles.erb b/templates/nomad.sles.erb new file mode 100644 index 0000000..f63653e --- /dev/null +++ b/templates/nomad.sles.erb @@ -0,0 +1,100 @@ +#!/bin/bash +# +# /etc/rc.d/init.d/nomad +# +# Daemonize the nomad agent. +# +### BEGIN INIT INFO +# Provides: nomad +# Required-Start: network +# Should-Start: $null +# Required-Stop: $null +# Should-Stop: $null +# Default-Start: 3 5 +# Default-Stop: 0 1 2 6 +# Short-Description: Job scheduling framework +# Description: Job scheduling framework +### END INIT INFO + +. /etc/rc.status + +rc_reset + +CONSUL_BIN=<%= scope.lookupvar('nomad::bin_dir') %>/nomad +CONFIG_DIR=<%= scope.lookupvar('nomad::config_dir') %> +LOG_FILE=/var/log/nomad + + +# read settings like GOMAXPROCS from "/etc/sysconfig/nomad" +[ -e /etc/sysconfig/nomad ] && . /etc/sysconfig/nomad + +export GOMAXPROCS=${GOMAXPROCS:-2} + + +case "$1" in + start) + echo -n "Starting nomad " + ## Start daemon with startproc(8). If this fails + ## the return value is set appropriately by startproc. + startproc $CONSUL_BIN agent -config-dir "$CONFIG_DIR" <%= scope.lookupvar('nomad::extra_options') %> >> "$LOG_FILE" + + # Remember status and be verbose + rc_status -v + ;; + stop) + echo -n "Shutting down nomad " + ## Stop daemon with killproc(8) and if this fails + ## killproc sets the return value according to LSB. + + killproc -TERM $CONSUL_BIN + + # Remember status and be verbose + rc_status -v + ;; + restart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + $0 stop + $0 start + + # Remember status and be quiet + rc_status + ;; + reload) + # If it supports signaling: + echo -n "Reload service nomad " + killproc -HUP $CONSUL_BIN + #touch /var/run/nomad.pid + rc_status -v + + ## Otherwise if it does not support reload: + #rc_failed 3 + #rc_status -v + ;; + status) + echo -n "Checking for service nomad " + ## Check status with checkproc(8), if process is running + ## checkproc will return with exit status 0. + + # Return value is slightly different for the status command: + # 0 - service up and running + # 1 - service dead, but /var/run/ pid file exists + # 2 - service dead, but /var/lock/ lock file exists + # 3 - service not running (unused) + # 4 - service status unknown :-( + # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) + + # NOTE: checkproc returns LSB compliant status values. + checkproc $CONSUL_BIN + # NOTE: rc_status knows that we called this init script with + # "status" option and adapts its messages accordingly. + rc_status -v + ;; + *) + ## If no parameters are given, print which are avaiable. + echo "Usage: $0 {start|stop|status|restart|reload}" + exit 1 + ;; +esac + +rc_exit diff --git a/templates/nomad.systemd.erb b/templates/nomad.systemd.erb new file mode 100644 index 0000000..93728a8 --- /dev/null +++ b/templates/nomad.systemd.erb @@ -0,0 +1,18 @@ +[Unit] +Description=Nomad Agent +Wants=basic.target +After=basic.target network.target + +[Service] +User=<%= scope.lookupvar('nomad::user') %> +Group=<%= scope.lookupvar('nomad::group') %> +ExecStart=<%= scope.lookupvar('nomad::bin_dir') %>/nomad agent \ + -config-dir <%= scope.lookupvar('nomad::config_dir') %> <%= scope.lookupvar('nomad::extra_options') %> +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=on-failure +RestartSec=42s +LimitNOFILE=131072 + +[Install] +WantedBy=multi-user.target diff --git a/templates/nomad.sysv.erb b/templates/nomad.sysv.erb new file mode 100644 index 0000000..7457a98 --- /dev/null +++ b/templates/nomad.sysv.erb @@ -0,0 +1,132 @@ +#!/bin/bash +# +# /etc/rc.d/init.d/nomad +# +# Daemonize the nomad agent. +# +# chkconfig: 2345 95 20 +# description: Service discovery and configuration made easy. \ +# Distributed, highly available, and datacenter-aware. +# processname: nomad +# pidfile: /var/run/nomad/pidfile + +# Source function library. +. /etc/init.d/functions + +CONSUL=<%= scope.lookupvar('nomad::bin_dir') %>/nomad +CONFIG=<%= scope.lookupvar('nomad::config_dir') %> +PID_FILE=/var/run/nomad/nomad.pid +LOG_FILE=/var/log/nomad +RPC_ADDR=-rpc-addr=<%= scope.lookupvar('nomad::rpc_addr') %>:<%= scope.lookupvar('nomad::rpc_port') %> + +[ -e /etc/sysconfig/nomad ] && . /etc/sysconfig/nomad + +export GOMAXPROCS=${GOMAXPROCS:-2} + +# +# Create the /var/run/nomad directory, which can live on a tmpfs +# filesystem and be destroyed between reboots. +# +mkrundir() { + [ ! -d /var/run/nomad ] && mkdir -p /var/run/nomad + chown <%= scope.lookupvar('nomad::user') %> /var/run/nomad +} + +# +# Create a PID file if it doesn't already exist, for clean upgrades +# from previous init-script controlled daemons. +# +KILLPROC_OPT="-p ${PID_FILE}" +mkpidfile() { + # Create PID file if it didn't exist + mkrundir + [ ! -f $PID_FILE ] && pidofproc $CONSUL > $PID_FILE + chown <%= scope.lookupvar('nomad::user') %> /var/run/nomad + if [ $? -ne 0 ] ; then + rm $PID_FILE + KILLPROC_OPT="" + fi +} + +start() { + echo -n "Starting nomad: " + mkrundir + [ -f $PID_FILE ] && rm $PID_FILE + daemon --user=<%= scope.lookupvar('nomad::user') %> \ + --pidfile="$PID_FILE" \ + "$CONSUL" agent -pid-file "${PID_FILE}" -config-dir "$CONFIG" <%= scope.lookupvar('nomad::extra_options') %> >> "$LOG_FILE" & + retcode=$? + touch /var/lock/subsys/nomad + return $retcode +} + +stop() { + DELAY=5 # seconds maximum to wait for a leave + + echo -n "Shutting down nomad: " + mkpidfile + + # If nomad is not acting as a server, exit gracefully + # Use SIGINT to create a "leave" event, unless the user has explicitly + # changed that behavior in the Consul config. + if ("${CONSUL}" info ${RPC_ADDR} 2>/dev/null | grep -q 'server = false' 2>/dev/null) ; then + nomad_pid=$(cat $PID_FILE) + killproc $KILLPROC_OPT $CONSUL -INT + retcode=$? + + # We'll wait if necessary to make sure the leave works, and return + # early if we can. If not, escalate to harsher signals. + try=0 + while [ $try -lt $DELAY ]; do + if ! checkpid $nomad_pid ; then + rm -f /var/lock/subsys/nomad + return $retcode + fi + sleep 1 + let try+=1 + done + fi + + # If acting as a server, use a SIGTERM to avoid a leave. + # This behavior is also configurable. Avoid doing a "leave" because + # having servers missing is a bad thing that we want to notice. + # + # A SIGTERM will mark the node as "failed" until it rejoins. + # killproc with no arguments uses TERM, then escalates to KILL. + killproc $KILLPROC_OPT $CONSUL + retcode=$? + + rm -f /var/lock/subsys/nomad $PID_FILE + return $retcode +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + status) + "$CONSUL" info ${RPC_ADDR} + ;; + restart) + stop + start + ;; + reload) + mkpidfile + killproc $KILLPROC_OPT $CONSUL -HUP + ;; + condrestart) + [ -f /var/lock/subsys/nomad ] && restart || : + ;; + *) + echo "Usage: nomad {start|stop|status|reload|restart}" + exit 1 + ;; +esac +retcode=$? +# Don't let the [OK] get stomped on. +echo +exit $retcode diff --git a/templates/nomad.upstart.erb b/templates/nomad.upstart.erb new file mode 100644 index 0000000..c7cc437 --- /dev/null +++ b/templates/nomad.upstart.erb @@ -0,0 +1,40 @@ +# Consul Agent (Upstart unit) +description "Nomad Agent" +start on runlevel [2345] +stop on runlevel [06] + +env CONSUL=<%= scope.lookupvar('nomad::bin_dir') %>/nomad +env CONFIG=<%= scope.lookupvar('nomad::config_dir') %> +env USER=<%= scope.lookupvar('nomad::user') %> +env GROUP=<%= scope.lookupvar('nomad::group') %> +env DEFAULTS=/etc/default/nomad +env RUNDIR=/var/run/nomad +env PID_FILE=/var/run/nomad/nomad.pid +env RPC_ADDR=-rpc-addr=<%= scope.lookupvar('nomad::rpc_addr') %>:<%= scope.lookupvar('nomad::rpc_port') %> + +pre-start script + [ -e $DEFAULTS ] && . $DEFAULTS + + mkdir -p $RUNDIR || true + chmod 0750 $RUNDIR || true + chown $USER:$GROUP $RUNDIR || true +end script + +script + # read settings like GOMAXPROCS from "/etc/default/nomad", if available. + [ -e $DEFAULTS ] && . $DEFAULTS + + export GOMAXPROCS=${GOMAXPROCS:-2} + exec start-stop-daemon -c $USER -g $GROUP -p $PID_FILE -x $CONSUL -S -- agent -config-dir $CONFIG -pid-file $PID_FILE <%= scope.lookupvar('nomad::extra_options') %> +end script + +pre-stop script + # Only leave the cluster if running as an agent + if ("${CONSUL}" info ${RPC_ADDR} 2>/dev/null | grep -q 'server = false' 2>/dev/null) ; then + exec "$CONSUL" leave ${RPC_ADDR} + fi +end script + +respawn +respawn limit 10 10 +kill timeout 10 diff --git a/tests/init.pp b/tests/init.pp new file mode 100644 index 0000000..1817370 --- /dev/null +++ b/tests/init.pp @@ -0,0 +1,12 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation +# errors and view a log of events) or by fully applying the test in a virtual +# environment (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: +# http://docs.puppetlabs.com/guides/tests_smoke.html +# +include nomad