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