├── .fixtures.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── Rakefile ├── files └── initial_cron_run ├── lib └── puppet │ └── parser │ └── functions │ ├── cron_human_time_to_seconds.rb │ ├── cron_list.rb │ ├── expand_cron_seconds.rb │ └── validate_cron_numeric.rb ├── manifests ├── d.pp ├── file.pp ├── init.pp ├── job.pp └── staleness_check.pp ├── spec ├── classes │ └── init_spec.rb ├── defines │ ├── d_spec.rb │ ├── file_spec.rb │ └── staleness_check_spec.rb ├── fixtures │ └── manifests │ │ └── site.pp ├── functions │ ├── cron_list_spec.rb │ ├── expand_cron_seconds_spec.rb │ └── validate_cron_numeric_spec.rb ├── rspec-hiera-hotfix.rb └── spec_helper.rb └── templates ├── cron_staleness_check.erb ├── d.erb ├── job_cron.erb ├── job_script.erb └── job_upstart.erb /.fixtures.yml: -------------------------------------------------------------------------------- 1 | fixtures: 2 | repositories: 3 | stdlib: "https://github.com/puppetlabs/puppetlabs-stdlib.git" 4 | symlinks: 5 | cron: "#{source_dir}" 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | spec/fixtures/modules/ 2 | vendor/ 3 | *.swp 4 | .bundle/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: ruby 3 | bundler_args: --without development system_tests 4 | before_install: gem update -q --system && gem install -q bundler && bundle update puppet 5 | use_sudo: false 6 | script: bundle exec rake test 7 | 8 | rvm: 9 | - 1.9.3 10 | - 2.0.0 11 | - 2.1.0 12 | - 2.2.0 13 | - 2.3.0 14 | env: 15 | - PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes 16 | - PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 17 | - PUPPET_VERSION="~> 3.8.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 18 | - PUPPET_VERSION="~> 4.3.0" STRICT_VARIABLES=yes 19 | - PUPPET_VERSION="~> 4.5.0" STRICT_VARIABLES=yes 20 | matrix: 21 | exclude: 22 | - rvm: 2.2.0 23 | env: PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes 24 | - rvm: 2.2.0 25 | env: PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 26 | - rvm: 2.2.0 27 | env: PUPPET_VERSION="~> 3.8.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 28 | - rvm: 2.3.0 29 | env: PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes 30 | - rvm: 2.3.0 31 | env: PUPPET_VERSION="~> 3.7.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 32 | - rvm: 2.3.0 33 | env: PUPPET_VERSION="~> 3.8.0" STRICT_VARIABLES=yes FUTURE_PARSER=yes 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/Yelp/puppet-cron/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/Yelp/puppet-cron/compare/0.1.0...HEAD) 6 | 7 | **Closed issues:** 8 | 9 | - Can't handle lock with complex commands [\#14](https://github.com/Yelp/puppet-cron/issues/14) 10 | 11 | ## [0.1.0](https://github.com/Yelp/puppet-cron/tree/0.1.0) (2016-05-12) 12 | **Merged pull requests:** 13 | 14 | - Require a value for the mailto parameter beyond an empty string. [\#17](https://github.com/Yelp/puppet-cron/pull/17) ([vulpine](https://github.com/vulpine)) 15 | - Adds a hash of environment variables to the cronjob. [\#16](https://github.com/Yelp/puppet-cron/pull/16) ([yeled](https://github.com/yeled)) 16 | - force expand\_cron\_seconds argument into string [\#15](https://github.com/Yelp/puppet-cron/pull/15) ([keymone](https://github.com/keymone)) 17 | - Make the staleness\_check wrapper 'uptime-aware' [\#13](https://github.com/Yelp/puppet-cron/pull/13) ([solarkennedy](https://github.com/solarkennedy)) 18 | - add cron\_list function [\#11](https://github.com/Yelp/puppet-cron/pull/11) ([somic](https://github.com/somic)) 19 | - Parameterize the nagios script location for cron\_staleness\_check [\#10](https://github.com/Yelp/puppet-cron/pull/10) ([hashbrowncipher](https://github.com/hashbrowncipher)) 20 | - Wrap upstart jobs with success wrapper [\#7](https://github.com/Yelp/puppet-cron/pull/7) ([hashbrowncipher](https://github.com/hashbrowncipher)) 21 | - Add 'second' parameter to cron::job [\#6](https://github.com/Yelp/puppet-cron/pull/6) ([hashbrowncipher](https://github.com/hashbrowncipher)) 22 | - Ensure cron is installed and running. OPS-7295 [\#5](https://github.com/Yelp/puppet-cron/pull/5) ([solarkennedy](https://github.com/solarkennedy)) 23 | - Escape colons in crontab names for reporting [\#3](https://github.com/Yelp/puppet-cron/pull/3) ([dnephin](https://github.com/dnephin)) 24 | - Fix README link for monitoring\_check [\#2](https://github.com/Yelp/puppet-cron/pull/2) ([ymilki](https://github.com/ymilki)) 25 | - Added github boilerplate stuff [\#1](https://github.com/Yelp/puppet-cron/pull/1) ([solarkennedy](https://github.com/solarkennedy)) 26 | 27 | 28 | 29 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :test do 4 | gem "json", '~> 1.8.3' 5 | gem "json_pure", '~> 1.8.3' 6 | # Pin for 1.8.7 compatibility for now 7 | gem "rake", '< 11.0.0' 8 | gem "puppet", ENV['PUPPET_VERSION'] || '~> 3.7.5' 9 | gem "puppet-lint" 10 | 11 | # Pin for 1.8.7 compatibility for now 12 | gem "rspec", '< 3.2.0' 13 | gem "rspec-core", "3.1.7" 14 | gem "rspec-puppet", "~> 2.1" 15 | gem "rspec-puppet-utils" 16 | 17 | gem "puppet-syntax" 18 | gem "puppetlabs_spec_helper" 19 | end 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.6) 5 | diff-lcs (1.3) 6 | facter (2.5.1) 7 | CFPropertyList (~> 2.2) 8 | hiera (1.3.4) 9 | json_pure 10 | json (1.8.6) 11 | json_pure (1.8.6) 12 | metaclass (0.0.4) 13 | mocha (1.5.0) 14 | metaclass (~> 0.0.1) 15 | puppet (3.7.5) 16 | facter (> 1.6, < 3) 17 | hiera (~> 1.0) 18 | json_pure 19 | puppet-lint (2.3.5) 20 | puppet-syntax (2.4.1) 21 | rake 22 | puppetlabs_spec_helper (2.9.1) 23 | mocha (~> 1.0) 24 | puppet-lint (~> 2.0) 25 | puppet-syntax (~> 2.0) 26 | rspec-puppet (~> 2.0) 27 | rake (10.5.0) 28 | rspec (3.1.0) 29 | rspec-core (~> 3.1.0) 30 | rspec-expectations (~> 3.1.0) 31 | rspec-mocks (~> 3.1.0) 32 | rspec-core (3.1.7) 33 | rspec-support (~> 3.1.0) 34 | rspec-expectations (3.1.2) 35 | diff-lcs (>= 1.2.0, < 2.0) 36 | rspec-support (~> 3.1.0) 37 | rspec-mocks (3.1.3) 38 | rspec-support (~> 3.1.0) 39 | rspec-puppet (2.6.13) 40 | rspec 41 | rspec-puppet-utils (3.4.0) 42 | mocha 43 | puppet 44 | puppetlabs_spec_helper 45 | rspec 46 | rspec-puppet 47 | rspec-support (3.1.2) 48 | 49 | PLATFORMS 50 | ruby 51 | 52 | DEPENDENCIES 53 | json (~> 1.8.3) 54 | json_pure (~> 1.8.3) 55 | puppet (~> 3.7.5) 56 | puppet-lint 57 | puppet-syntax 58 | puppetlabs_spec_helper 59 | rake (< 11.0.0) 60 | rspec (< 3.2.0) 61 | rspec-core (= 3.1.7) 62 | rspec-puppet (~> 2.1) 63 | rspec-puppet-utils 64 | 65 | BUNDLED WITH 66 | 1.12.5 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Yelp Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## puppet-cron 2 | 3 | [![Build Status](https://travis-ci.org/Yelp/puppet-cron.svg?branch=master)](https://travis-ci.org/Yelp/puppet-cron) 4 | 5 | A super great cron Puppet module with timeouts, locking, monitoring, and more! 6 | 7 | **WARNING**: This module still has some "Yelpisms". Watch out! 8 | 9 | Please open an [issue](https://github.com/Yelp/puppet-cron/issues) if you encounter 10 | one. Bonus points for a PR!! :) 11 | 12 | ## Description 13 | 14 | Cron is the foundation of any great architecture. Don't let anyone else tell you differently :) 15 | 16 | If you are going to deploy cron jobs, do it right. 17 | 18 | There is, however, a fine line between using cron to do scheduled tasks, 19 | and abusing cron where a better system should be used. 20 | 21 | ## Examples 22 | 23 | Minimal job config: 24 | ```puppet 25 | cron::d { 'minimum_example': 26 | minute => '*', 27 | user => 'root', 28 | command => '/bin/true', 29 | } 30 | ``` 31 | 32 | Full example high-frequency job with all optional params: 33 | ```puppet 34 | cron::d { 'name_of_cronjob': 35 | second => '*/20', 36 | minute => '*', 37 | hour => '*', 38 | dom => '*', 39 | month => '*', 40 | dow => '*', 41 | user => 'bob', 42 | mailto => 'example@example.com', 43 | command => '/some/example/job', 44 | } 45 | ``` 46 | 47 | ## Monitoring Params 48 | 49 | This cron module optionally integrates with Yelp's [monitoring_check](https://github.com/Yelp/puppet-monitoring_check) 50 | module to add monitoring to your cron job. You can use the following params 51 | to tune this monitoring: 52 | 53 | * `staleness_threshold`: A human-readable time unit ('24h', '5m', '3s') representing 54 | how long it is acceptable for the cron job to be failing before sending an alert. 55 | * `staleness_check_params`: A hash of any parameter to [monitoring_check](https://github.com/Yelp/puppet-monitoring_check). 56 | 57 | This makes it very easy to get alerts for cron jobs when they start failing, 58 | while re-using any existing notification options that your Sensu infrastructure 59 | has. 60 | 61 | Example: An important cron job that makes tickets after failing for 24h hours: 62 | ```puppet 63 | cron::d { 'name_of_cronjob': 64 | minute => '0', 65 | hour => '4', 66 | user => 'root', 67 | command => 'fortune'; 68 | staleness_threshold => '24h', 69 | staleness_check_params => { 70 | 'team' => 'operations', 71 | 'ticket => true, 72 | 'project' => 'FORTUNE', 73 | }, 74 | } 75 | ``` 76 | 77 | ## License 78 | 79 | Apache 2. 80 | 81 | ## Contributing 82 | 83 | Open an [issue](https://github.com/Yelp/puppet-cron/issues) or 84 | [fork](https://github.com/Yelp/puppet-cron/fork) and open a 85 | [Pull Request](https://github.com/Yelp/puppet-cron/pulls) 86 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/rake_tasks' 2 | require 'puppet/version' 3 | require 'puppet/vendor/semantic/lib/semantic' unless Puppet.version.to_f < 3.6 4 | require 'puppet-lint/tasks/puppet-lint' 5 | require 'puppet-syntax/tasks/puppet-syntax' 6 | 7 | # These gems aren't always present, for instance 8 | # on Travis with --without development 9 | begin 10 | require 'puppet_blacksmith/rake_tasks' 11 | rescue LoadError 12 | end 13 | 14 | Rake::Task[:lint].clear 15 | 16 | PuppetLint.configuration.relative = true 17 | PuppetLint.configuration.send("disable_80chars") 18 | PuppetLint.configuration.log_format = "%{path}:%{linenumber}:%{check}:%{KIND}:%{message}" 19 | PuppetLint.configuration.fail_on_warnings = true 20 | 21 | # Forsake support for Puppet 2.6.2 for the benefit of cleaner code. 22 | # http://puppet-lint.com/checks/class_parameter_defaults/ 23 | PuppetLint.configuration.send('disable_class_parameter_defaults') 24 | # http://puppet-lint.com/checks/class_inherits_from_params_class/ 25 | PuppetLint.configuration.send('disable_class_inherits_from_params_class') 26 | 27 | exclude_paths = [ 28 | "bundle/**/*", 29 | "pkg/**/*", 30 | "vendor/**/*", 31 | "spec/**/*", 32 | ] 33 | PuppetLint.configuration.ignore_paths = exclude_paths 34 | PuppetSyntax.exclude_paths = exclude_paths 35 | 36 | desc "Run acceptance tests" 37 | RSpec::Core::RakeTask.new(:acceptance) do |t| 38 | t.pattern = 'spec/acceptance' 39 | end 40 | 41 | desc "Populate CONTRIBUTORS file" 42 | task :contributors do 43 | system("git log --format='%aN' | sort -u > CONTRIBUTORS") 44 | end 45 | 46 | task :metadata do 47 | sh "metadata-json-lint metadata.json" 48 | end 49 | 50 | desc "Run syntax, lint, and spec tests." 51 | task :test => [ 52 | :syntax, 53 | :lint, 54 | :spec, 55 | ] 56 | -------------------------------------------------------------------------------- /files/initial_cron_run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | # Puppet puts marker files in place with the current mtime. Our first task is 7 | # to set them forward in time so that we can easily distinguish cronjobs that 8 | # haven't yet completed successfully. We put them forward to Y2K38 9 | success_file="/nail/run/success_wrapper/${1}" 10 | [ -e "${success_file}" ] && touch -d '@2147483647' "/nail/run/success_wrapper/${1}" 11 | initctl emit --no-wait "${1}" 12 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/cron_human_time_to_seconds.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | newfunction( 3 | :cron_human_time_to_seconds, 4 | :type => :rvalue, 5 | :doc => <<-'ENDHEREDOC') do |args| 6 | Converts a human time of the form Xs, Xm or Xh into an integer number of seconds 7 | ENDHEREDOC 8 | if !(arg = args.first) || args.size > 1 9 | raise CronHumanTimeError, "wrong number of arguments (#{args.length}; must be 1)" 10 | end 11 | 12 | unless arg.respond_to? 'to_s' 13 | raise CronHumanTimeError, "#{arg.class}(#{arg.inspect}) does not respond to #to_s" 14 | end 15 | 16 | unless arg.to_s =~ /^(\d+)([hmsdw])?$/ 17 | raise CronHumanTimeError, "#{arg} is not of the form \\d+([hmsdw])?" 18 | end 19 | 20 | mult = {nil => 1, 's' => 1, 'm' => 60, 'h' => 3600, 'd' => 86400, 'w' => 604800 }[$2] 21 | time = $1.to_i * mult 22 | time.to_s 23 | end 24 | 25 | class CronHumanTimeError < Puppet::ParseError; end 26 | end 27 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/cron_list.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | newfunction(:cron_list, :type => :rvalue, :doc => <<-EOD 3 | Builds a custom list of numbers for crontab. 4 | Takes 3 args - start, end, step. Start must be non-negative integer, 5 | end and step must be positive integers. 6 | 7 | # run twice a day starting with hour 2 every 12 hours; 8 | # will return '2,14' 9 | cron::d { 'test1': 10 | hour => cron_list(2, 24, 12), 11 | ... 12 | } 13 | 14 | # run every 5 minutes starting with random minute; 15 | # will return something like '3,8,13,18,23,28,33,38,43,48,53,58' 16 | cron::d { 'test2': 17 | minute => cron_list(fqdn_rand(60, 'starting minute for test2'), 60, 5), 18 | ... 19 | } 20 | EOD 21 | ) do |args| 22 | raise(Puppet::ParseError, 'expected 3 args: start, end, step') if args.size != 3 23 | raise(Puppet::ParseError, 'all arguments must be integers') if args.any? {|x| !x.is_a?(Fixnum) } 24 | 25 | _start, _end, _step = args 26 | raise(Puppet::ParseError, 'start must be non-negative integer') if _start < 0 27 | raise(Puppet::ParseError, 'end and step args must be positive integers') if _end <= 0 || _step <= 0 28 | raise(Puppet::ParseError, 'start arg must be less than end arg') if _start >= _end 29 | 30 | (0..(_end/_step).to_i).map { |x| 31 | ( _start + x * _step ) % _end }.uniq.sort.join(',') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/expand_cron_seconds.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | newfunction(:expand_cron_seconds) do |args| 3 | unless args.length == 1 then 4 | raise Puppet::ParseError, ("expand_cron_seconds(): wrong number of arguments(#{args.length}; must be 1)") 5 | end 6 | 7 | arg = args[0].to_s 8 | 9 | times = [] 10 | 11 | arg.split(/,/).each do |chunk| 12 | pattern = %r{ 13 | # I want to use named capture groups, but ruby 1.8 doesnt support them. 14 | ^ 15 | # single number 16 | (\d+) 17 | $ 18 | | 19 | ^ 20 | ( 21 | # range 22 | \* 23 | | 24 | (\d+)-(\d+) 25 | ) 26 | (?: 27 | # optional step 28 | / 29 | ( 30 | \d+ 31 | ) 32 | )? 33 | $ 34 | }x 35 | 36 | m = pattern.match(chunk) 37 | if not m then 38 | raise Puppet::ParseError, ("#{chunk.inspect} doesn't look like a valid cron time string.") 39 | end 40 | m_single = m[1] 41 | m_range = m[2] 42 | m_rangebegin = m[3] 43 | m_rangeend = m[4] 44 | m_step = m[5] 45 | 46 | if m_single then 47 | times.push(m_single.to_i) 48 | elsif m_range 49 | if m_range == '*' then 50 | range = (0...60) 51 | else 52 | range = (m_rangebegin.to_i .. m_rangeend.to_i) 53 | end 54 | step = m_step ? m_step.to_i : 1 55 | times.concat(range.step(step).to_a) 56 | else 57 | raise Puppet::ParseError, ("") 58 | end 59 | end 60 | 61 | return times.sort.uniq.to_a 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/puppet/parser/functions/validate_cron_numeric.rb: -------------------------------------------------------------------------------- 1 | module Puppet::Parser::Functions 2 | 3 | newfunction(:validate_cron_numeric, :doc => <<-'ENDHEREDOC') do |args| 4 | Validate that all passed values are valid cron time strings. Abort catalog 5 | compilation if any value fails this check. 6 | 7 | The following values will pass: 8 | 9 | $example1 = "1" 10 | $example2 = "*" 11 | $example3 = "5-55/5" 12 | $example5 = "24,29" 13 | $example6 = "*/6" 14 | validate_cron_numeric($example1) 15 | validate_cron_numeric($example2) 16 | validate_cron_numeric($example3) 17 | validate_cron_numeric($example4) 18 | validate_cron_numeric($example5) 19 | validate_cron_numeric($example6) 20 | 21 | The following values will fail, causing compilation to abort: 22 | 23 | validate_cron_numeric(true) 24 | validate_cron_numeric([ 'some', 'array' ]) 25 | $undefined = undef 26 | validate_cron_numeric($undefined) 27 | validate_cron_numeric("foo") 28 | 29 | ENDHEREDOC 30 | 31 | unless args.length == 1 then 32 | raise Puppet::ParseError, ("validate_cron_numeric(): wrong number of arguments (#{args.length}; must be 1)") 33 | end 34 | arg = args[0] 35 | if arg.is_a?(Fixnum) 36 | arg = arg.to_s 37 | end 38 | unless arg.respond_to?('split') then 39 | raise Puppet::ParseError, ("#{arg.inspect} is not a cron time string. It looks to be a #{arg.class}") 40 | end 41 | 42 | arg.split(/[\/,-]/).each do |arg| 43 | if arg != '*' && ( !arg.respond_to?(:to_i) || !arg.to_i.is_a?(Integer) || arg.to_i.to_s != arg.to_s ) 44 | raise Puppet::ParseError, ("#{arg.inspect} is not a cron time string. It looks to be a #{arg.class}") 45 | end 46 | end 47 | 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /manifests/d.pp: -------------------------------------------------------------------------------- 1 | # == Define: cron::d 2 | # 3 | # Generates an /etc/cron.d file for you. 4 | # 5 | # == Reasoning 6 | # 7 | # Including a files makes it too easy to forget the MAILTO 8 | # or only put 4 *s, or any other stupid mistake. 9 | # Also, putting the cron command inline with the puppet code to generate 10 | # it generally involves less tail chasing when you're looking for a job 11 | # 12 | # == Examples 13 | # 14 | # This makes a job which runs every min, and emails systems+cron@yelp.com 15 | # if anything is sent to stdout 16 | # 17 | # ``` 18 | # cron::d { 'minimum_example': 19 | # minute => '*', 20 | # user => 'push', 21 | # command => '/nail/sys/bin/example_job | logger' 22 | # } 23 | # ``` 24 | # 25 | # Full example with all optional params: 26 | # ``` 27 | # cron::d { 'name_of_cronjob': 28 | # second => '*/20', 29 | # minute => '*', 30 | # hour => '*', 31 | # dom => '*', 32 | # month => '*', 33 | # dow => '*', 34 | # user => 'bob', 35 | # mailto => 'example@yelp.com', 36 | # command => '/some/example/job'; 37 | # } 38 | # ``` 39 | # 40 | # == Parameters 41 | # 42 | # [*lock*] 43 | # Boolean that defaults to false. If true, the command will be wrapped in a 44 | # a `flock` invocation to prevent the cron job from stacking. 45 | # 46 | # [*timeout*] 47 | # String giving the amount of time to wait before killing the supplied 48 | # command. All strings accepted by the gnu timeout utility are valid. 49 | # 50 | # [*second*] 51 | # String describing which seconds of the minute you want your job to execute 52 | # on. This is implemented by dropping multiple lines into the cron.d file, 53 | # prefixed with sleep commands. This supports lists (separated by commas), 54 | # ranges (e.g. 0-10), and ranges with steps (e.g. 0-10/2). An asterisk (*) 55 | # is equivalent to 0-59, and also supports steps (e.g. */20). 56 | # 57 | # [*env*] 58 | # Hash of VARIABLE=VALUE to export to the cronjob subshell 59 | # 60 | # [*staleness_threshold*]: A human-readable time unit ('24h', '5m', '3s') representing 61 | # how long it is acceptable for the cron job to be failing before sending an alert. 62 | # 63 | # [*staleness_check_params*]: A hash of any parameter to 64 | # [monitoring_check](https://github.com/Yelp/puppet-monitoring_check). 65 | # 66 | # [*show_diff*]: Whether to display differences when the file changes, defaulting to true. 67 | # 68 | define cron::d ( 69 | $minute, 70 | $command, 71 | $user, 72 | $second='0', 73 | $hour='*', 74 | $dom='*', 75 | $month='*', 76 | $dow='*', 77 | $mailto='""', 78 | $log_to_syslog=true, 79 | $staleness_threshold=undef, 80 | $staleness_check_params=undef, 81 | $lock=false, 82 | $timeout=undef, 83 | $normalize_path=hiera('cron::d::normalize_path', false), 84 | $comment='', 85 | $env={}, 86 | $show_diff=true, 87 | ) { 88 | validate_cron_numeric($second) 89 | validate_cron_numeric($minute) 90 | validate_cron_numeric($hour) 91 | validate_cron_numeric($dom) 92 | validate_cron_numeric($month) 93 | validate_cron_numeric($dow) 94 | 95 | validate_bool($log_to_syslog,$lock,$show_diff) 96 | 97 | if $mailto == '' { 98 | fail('You must provide a value for MAILTO. Did you mean mailto=\'""\'?') 99 | } 100 | 101 | $safe_name = regsubst($name, ':', '_', 'G') 102 | $reporting_name = "cron_${safe_name}" 103 | 104 | if $staleness_threshold { 105 | $actual_command = "/nail/sys/bin/success_wrapper '${reporting_name}' ${command}" 106 | 107 | cron::staleness_check { $reporting_name: 108 | threshold => $staleness_threshold, 109 | params => $staleness_check_params, 110 | user => $user, 111 | } 112 | } else { 113 | $actual_command = $command 114 | } 115 | 116 | # If both syslogging and mailing are requested, choose mailing over syslogging 117 | $actually_log_to_syslog = $log_to_syslog and $mailto=='""' 118 | 119 | # Ancient versions of `timeout` have a slightly different argument syntax 120 | # for what signal should be sent. 121 | $timeout_signal_arg = "${::lsbdistid}_${::lsbdistrelease}" ? { 122 | /(Ubuntu_10.04|CentOS_5.*)/ => '-9', 123 | default => '-s 9' 124 | } 125 | 126 | cron::file { $safe_name: 127 | file_params => { 128 | content => template('cron/d.erb'), 129 | show_diff => $show_diff, 130 | }, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /manifests/file.pp: -------------------------------------------------------------------------------- 1 | # puts a purge-safe cronjob in /etc/cron.d. 2 | # $file_params is a transparent wrapper around the File resource 3 | 4 | # if using $staleness_threshold, it is required that your cronjob 5 | # touch /nail/run/success_wrapper/cron_${name} whenever it succeeds 6 | # 7 | # 8 | define cron::file ( 9 | $file_params, 10 | ) { 11 | # We require that cron be declared somewhere. 12 | # This realize verifies that for us. 13 | realize Class['cron'] 14 | 15 | validate_re($title, '^[^/.]+$') 16 | 17 | validate_hash($file_params) 18 | $overrides = { 19 | owner => 'root', 20 | group => 'root', 21 | mode => '0444', 22 | ensure => 'file', 23 | } 24 | 25 | $nail_file = "/nail/etc/cron.d/${name}" 26 | $file_data = { "$nail_file" => merge($file_params, $overrides) } 27 | 28 | create_resources('file', $file_data) 29 | 30 | File[$nail_file] -> 31 | file { "/etc/cron.d/${name}": 32 | ensure => 'link', 33 | target => $nail_file, 34 | owner => 'root', 35 | group => 'root', 36 | } 37 | 38 | # We use an Exec together with the File resource, which ensures the mtime 39 | # on the symlink gets updated, which makes cron reload the job definition. 40 | File[$nail_file] ~> 41 | exec { "Symlink $nail_file at /etc/cron.d/${name}": 42 | command => "ln -nsf '$nail_file' '/etc/cron.d/${name}'", 43 | refreshonly => true, 44 | provider => 'shell', 45 | require => File['/nail/etc/cron.d/'], 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | # == Class cron 2 | # 3 | # Main class to get the cron infrastructure in place for the other 4 | # types. Enablings the cron job purging mechanism. 5 | # 6 | # === Parameters 7 | # 8 | # [*check_file_age_path*] 9 | # 10 | # path to a nagios-compatible check_file_age script 11 | # 12 | # [*service_ensure*] 13 | # 14 | # How Puppet should manage the service. Defaults to 'running'. 15 | # 16 | class cron ( 17 | $check_file_age_path = '/usr/lib/nagios/plugins/check_file_age', 18 | $conf_dir = '/nail/etc', 19 | $scripts_dir = '/nail/sys/bin', 20 | $purge_upstart_jobs = true, 21 | $package_ensure = 'latest', 22 | $service_ensure = 'running', 23 | ) { 24 | 25 | include nail 26 | 27 | $purged_directories = [ 28 | "${conf_dir}/init", 29 | "${conf_dir}/cron.d", 30 | "${conf_dir}/upstart_crons", 31 | ] 32 | 33 | $supported_upstart_oses = [ 34 | '10.04', 35 | '12.04', 36 | '14.04', 37 | ] 38 | 39 | file { $purged_directories: 40 | ensure => 'directory', 41 | mode => '0755', 42 | owner => 'root', 43 | group => 'root', 44 | recurse => true, 45 | purge => true, 46 | force => true, 47 | } 48 | 49 | # The cron::d wrapper uses symlinks to purge cron jobs 50 | # This purges the actual symlinks, puppet purges the targets. 51 | cron::d { 'delete_broken_cron_symlinks': 52 | user => 'root', 53 | minute => '0', 54 | hour => '*', 55 | command => '/usr/bin/find -L /etc/cron.d/ -type l -delete', 56 | } 57 | 58 | file_line { 'disable_cron_hourly_emails': 59 | line => 'MAILTO=""', 60 | path => '/etc/crontab', 61 | after => 'SHELL=/bin/sh', 62 | } 63 | 64 | if $purge_upstart_jobs and $::operatingsystemrelease in $supported_upstart_oses { 65 | cron::job { 'purge_cruft_upstart_jobs': 66 | user => 'root', 67 | command => "test -e '${conf_dir}/init' && comm -2 -3 <(grep -rl '^# FLAG: MANAGED BY PUPPET$' '/etc/init' | sort) <(find '/nail/etc/init' -mindepth 1 | sed -e 's#/nail##' | sort) | xargs -r rm", 68 | } 69 | } 70 | 71 | file { "${scripts_dir}/cron_staleness_check": 72 | mode => '0555', 73 | owner => 'root', 74 | group => 'root', 75 | content => template('cron/cron_staleness_check.erb'), 76 | } 77 | 78 | file { "${scripts_dir}/initial_cron_run": 79 | mode => '0555', 80 | owner => 'root', 81 | group => 'root', 82 | source => 'puppet:///modules/cron/initial_cron_run', 83 | } 84 | 85 | case $::osfamily { 86 | 'Debian': { $service_name = 'cron' } 87 | 'RedHat': { $service_name = 'crond' } 88 | } 89 | package { 'cron': 90 | ensure => $package_ensure, 91 | } -> 92 | service { 'cron': 93 | name => $service_name, 94 | ensure => $service_ensure, 95 | enable => true, 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /manifests/job.pp: -------------------------------------------------------------------------------- 1 | # == Define: cron::job 2 | # 3 | # Creates an upstart task and uses cron to periodically schedule it. 4 | # See also cron::d. 5 | # 6 | # Mutual exclusion is enforced by Upstart, and also using flock(1) 7 | # 8 | # The defaults result in a cronjob that runs hourly, splayed using 9 | # fqdn_rand. 10 | # 11 | # == Parameters 12 | # 13 | # [*sync*] 14 | # 15 | # Whether to force an initial run of the job when it is installed on the 16 | # system. 17 | # 18 | define cron::job ( 19 | $command, 20 | $user, 21 | $sync = false, 22 | $minute = fqdn_rand(60, $title), 23 | $second='0', 24 | $hour='*', 25 | $dom='*', 26 | $month='*', 27 | $dow='*', 28 | $d_params=undef, 29 | $staleness_threshold=undef, 30 | $staleness_check_params=undef, 31 | ) { 32 | # Deliberate copy here so we can add extra fancy options (like pipe stdout 33 | # to scribe) in additional parameters later 34 | validate_cron_numeric($second) 35 | validate_cron_numeric($minute) 36 | validate_cron_numeric($hour) 37 | validate_cron_numeric($dom) 38 | validate_cron_numeric($month) 39 | validate_cron_numeric($dow) 40 | 41 | include ::cron 42 | 43 | if ! ($::operatingsystemrelease in $::cron::supported_upstart_oses) { 44 | fail('I cannot install Upstart backed crons on machines without Upstart') 45 | } 46 | 47 | $reporting_name = "cron_${title}" 48 | 49 | if $staleness_threshold { 50 | $success_wrapper_command = "${::cron::scripts_dir}/success_wrapper '${reporting_name}' " 51 | 52 | cron::staleness_check { $reporting_name: 53 | threshold => $staleness_threshold, 54 | params => $staleness_check_params, 55 | user => $user, 56 | } 57 | } else { 58 | $success_wrapper_comand = "" 59 | } 60 | 61 | $job_ticket = "${::cron::conf_dir}/init/${reporting_name}.conf" 62 | $job_file = "/etc/init/${reporting_name}.conf" 63 | 64 | file { "${::cron::conf_dir}/upstart_crons/${title}": 65 | ensure => 'file', 66 | mode => '0555', 67 | content => template("${module_name}/job_script.erb"), 68 | before => File[$job_file], 69 | } 70 | 71 | # A mechanism for making these jobs purge-safe 72 | # Upstart's inotify based reload system basically ignores anything 73 | # that happens to a symlink (e.g. create, delete, change ctime). 74 | # Our options are to either: 75 | # 1) Use symlinks anyway, and do 'initctl reload-configuration' a 76 | # bunch. 77 | # 2) Create the jobs as regular files, and find some other way to purge 78 | # them. 79 | # We've opted for #2, so each job is associated with a marker file 80 | # that is its "ticket" to exist. If a job is present without a ticket, 81 | # a cleanup job is programmed to remove it. 82 | file { $job_ticket: 83 | ensure => 'file', 84 | } -> 85 | file { $job_file: 86 | ensure => 'file', 87 | content => template("${module_name}/job_upstart.erb"), 88 | before => Cron::File[$title], 89 | } 90 | 91 | if $sync { 92 | $sync_exec = "${::cron::scripts_dir}/initial_cron_run cron_${title}" 93 | exec { $sync_exec: 94 | subscribe => File[$job_file], 95 | refreshonly => true, 96 | } 97 | } 98 | 99 | if $sync and $staleness_threshold { 100 | Cron::Staleness_check[$reporting_name] -> 101 | Exec[$sync_exec] 102 | } 103 | 104 | cron::file { $title: 105 | file_params => { 106 | content => template("${module_name}/job_cron.erb"), 107 | }, 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /manifests/staleness_check.pp: -------------------------------------------------------------------------------- 1 | define cron::staleness_check( 2 | $threshold, 3 | $params, 4 | $user, 5 | ) { 6 | validate_hash($params) 7 | 8 | $threshold_s = cron_human_time_to_seconds($threshold) 9 | 10 | # Check whether we are fresh five times per threshold, not to exceed 1 hour 11 | if $threshold_s / 5 > 3600 { 12 | $check_every = 3600 13 | } else { 14 | $check_every = $threshold_s / 5 15 | } 16 | 17 | $check_title = "${name}_staleness" 18 | 19 | $overrides = { 20 | 'command' => "/nail/sys/bin/cron_staleness_check ${name} ${threshold_s}", 21 | 'check_every' => $check_every, 22 | 'needs_sudo' => true, 23 | 'alert_after' => '2m', 24 | } 25 | 26 | $check_data = { "$check_title" => 27 | merge( 28 | $params, 29 | $overrides 30 | ) 31 | } 32 | create_resources('monitoring_check', $check_data) 33 | 34 | file { "/nail/run/success_wrapper/${name}": 35 | ensure => 'file', 36 | owner => $user, 37 | mode => '640', 38 | } -> 39 | Monitoring_check[$check_title] 40 | } 41 | -------------------------------------------------------------------------------- /spec/classes/init_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'cron' do 4 | 5 | context 'by default' do 6 | let(:facts) {{ 7 | :lsbdistid => 'Ubuntu', 8 | :lsbdistrelease => '14.04', 9 | :operatingsystemrelease => '14.04', 10 | :osfamily => 'Debian', 11 | }} 12 | it { should compile } 13 | it { should contain_file('/nail/sys/bin/cron_staleness_check') } 14 | end 15 | 16 | context 'with service ensure stopped' do 17 | let(:facts) {{ 18 | :lsbdistid => 'Ubuntu', 19 | :lsbdistrelease => '16.04', 20 | :operatingsystemrelease => '16.04', 21 | :osfamily => 'Debian', 22 | }} 23 | let(:params) {{ 24 | :service_ensure => 'stopped' 25 | }} 26 | it { should compile } 27 | it { should contain_service('cron').with_ensure('stopped') } 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/defines/d_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'cron::d' do 4 | let(:title) { 'foobar' } 5 | let(:facts) {{ 6 | :operatingsystemrelease => '14.04', 7 | :osfamily => 'Debian', 8 | :lsbdistid => 'Ubuntu', 9 | :lsbdistrelease => '14.04', 10 | }} 11 | let(:pre_condition) { "class { 'cron': }" } 12 | let(:params) {{ 13 | :minute => 37, 14 | :user => 'somebody', 15 | :command => 'foobar | logger -t cleanup-srv-deploy -p daemon.info', 16 | }} 17 | 18 | it { 19 | should contain_exec('Symlink /nail/etc/cron.d/foobar at /etc/cron.d/foobar') \ 20 | .with_command("ln -nsf '/nail/etc/cron.d/foobar' '/etc/cron.d/foobar'") 21 | should contain_file('/nail/etc/cron.d/foobar').with_ensure('file') 22 | 23 | should_not contain_cron__staleness_check('cron_foobar') 24 | } 25 | 26 | [ 27 | /MAILTO=""/, 28 | /^37 \* \* \* \* somebody \(foobar/m 29 | ].each do |regex| 30 | it { 31 | should contain_file('/nail/etc/cron.d/foobar') \ 32 | .with_content(regex) 33 | } 34 | end 35 | 36 | context 'with staleness' do 37 | let(:params) {{ 38 | :minute => 37, 39 | :user => 'somebody', 40 | :command => 'foobar | logger -t cleanup-srv-deploy -p daemon.info', 41 | :staleness_threshold => '10m', 42 | :staleness_check_params => { 'runbook' => 'y/rb-foobar' }, 43 | }} 44 | 45 | it { 46 | should contain_file('/nail/etc/cron.d/foobar') \ 47 | .with_content(/success_wrapper 'cron_foobar'/) 48 | should contain_cron__staleness_check('cron_foobar') 49 | } 50 | end 51 | 52 | context 'with staleness and colon in the name' do 53 | let(:params) {{ 54 | :minute => 37, 55 | :user => 'somebody', 56 | :command => 'foobar | logger -t cleanup-srv-deploy -p daemon.info', 57 | :staleness_threshold => '10m', 58 | :staleness_check_params => {}, 59 | }} 60 | let(:title) { 'something:latest:foo' } 61 | 62 | it { 63 | should contain_file('/nail/etc/cron.d/something_latest_foo') \ 64 | .with_content(/success_wrapper 'cron_something_latest_foo'/) 65 | should contain_cron__staleness_check('cron_something_latest_foo') 66 | } 67 | end 68 | 69 | 70 | context 'when asked to lock' do 71 | let(:params) {{ 72 | :minute => 0, 73 | :user => 'somebody', 74 | :command => 'overrunning command', 75 | :lock => true, 76 | }} 77 | 78 | it { 79 | should contain_file('/nail/etc/cron.d/foobar') \ 80 | .with_content(/0 \* \* \* \* somebody \(flock -n "\/var\/lock\/cron_foobar\.lock" overrunning command\) 2>&1 \| logger -t cron_foobar \n/) 81 | } 82 | end 83 | 84 | context 'when asked to timeout' do 85 | let(:params) {{ 86 | :minute => 0, 87 | :user => 'somebody', 88 | :command => 'overrunning command', 89 | :timeout => '123s', 90 | }} 91 | 92 | it { 93 | should contain_file('/nail/etc/cron.d/foobar') \ 94 | .with_content(/0 \* \* \* \* somebody \(\/usr\/bin\/timeout -s 9 123s overrunning command\) 2>&1 \| logger -t cron_foobar \n/) 95 | } 96 | end 97 | 98 | context 'when asked to timeout without syslog' do 99 | let(:params) {{ 100 | :minute => 0, 101 | :user => 'somebody', 102 | :command => 'overrunning command', 103 | :timeout => '123s', 104 | :log_to_syslog => false, 105 | }} 106 | 107 | it { 108 | should contain_file('/nail/etc/cron.d/foobar') \ 109 | .with_content(/0 \* \* \* \* somebody \(\/usr\/bin\/timeout -s 9 123s overrunning command\)\n/) 110 | } 111 | end 112 | 113 | context 'when asked to timeout on an old OS' do 114 | let(:params) {{ 115 | :minute => 0, 116 | :user => 'somebody', 117 | :command => 'overrunning command', 118 | :timeout => '123s', 119 | :log_to_syslog => false, 120 | }} 121 | let(:facts) {{ 122 | :operatingsystemrelease => '10.04', 123 | :osfamily => 'Debian', 124 | :lsbdistid => 'Ubuntu', 125 | :lsbdistrelease => '10.04', 126 | }} 127 | 128 | it { 129 | should contain_file('/nail/etc/cron.d/foobar') \ 130 | .with_content(/0 \* \* \* \* somebody \(\/usr\/bin\/timeout -9 123s overrunning command\)\n/) 131 | } 132 | end 133 | 134 | context 'with second as */10' do 135 | let(:params) {{ 136 | :minute => '*', 137 | :command => 'echo hi', 138 | :user => 'nobody', 139 | :second => '*/10', 140 | }} 141 | 142 | it { 143 | should contain_file('/nail/etc/cron.d/foobar') \ 144 | .with_content(%r{ 145 | \*\ \*\ \*\ \*\ \*\ nobody\ \(echo\ hi\).*\n 146 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 10;\ echo\ hi\).*\n 147 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 20;\ echo\ hi\).*\n 148 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 30;\ echo\ hi\).*\n 149 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 40;\ echo\ hi\).*\n 150 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 50;\ echo\ hi\).*\Z 151 | }x) 152 | } 153 | end 154 | 155 | context 'with multiple seconds and locking and timeout' do 156 | let(:params) {{ 157 | :minute => '*', 158 | :command => 'echo hi', 159 | :user => 'nobody', 160 | :second => '*/20', 161 | :lock => true, 162 | :timeout => '2h', 163 | }} 164 | 165 | it { 166 | should contain_file('/nail/etc/cron.d/foobar') \ 167 | .with_content(%r{ 168 | \*\ \*\ \*\ \*\ \*\ nobody\ \(flock\ -n\ "/var/lock/cron_foobar.lock"\ \/usr\/bin\/timeout\ -s\ 9\ 2h\ echo\ hi\).*\n 169 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 20;\ flock\ -n\ "/var/lock/cron_foobar.lock"\ \/usr\/bin\/timeout\ -s\ 9\ 2h\ echo\ hi\).*\n 170 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 40;\ flock\ -n\ "/var/lock/cron_foobar.lock"\ \/usr\/bin\/timeout\ -s\ 9\ 2h\ echo\ hi\).*\n 171 | }x) 172 | } 173 | end 174 | 175 | context 'with complex seconds' do 176 | let(:params) {{ 177 | :minute => '*', 178 | :command => 'echo hi', 179 | :user => 'nobody', 180 | :second => '*/30,8,40-50/2', 181 | }} 182 | 183 | it { 184 | should contain_file('/nail/etc/cron.d/foobar') \ 185 | .with_content(%r{ 186 | \*\ \*\ \*\ \*\ \*\ nobody\ \(echo\ hi\).*\n 187 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 8;\ echo\ hi\).*\n 188 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 30;\ echo\ hi\).*\n 189 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 40;\ echo\ hi\).*\n 190 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 42;\ echo\ hi\).*\n 191 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 44;\ echo\ hi\).*\n 192 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 46;\ echo\ hi\).*\n 193 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 48;\ echo\ hi\).*\n 194 | \*\ \*\ \*\ \*\ \*\ nobody\ \(sleep\ 50;\ echo\ hi\).*\n 195 | }x) 196 | } 197 | end 198 | 199 | context 'when given an specific environment vars to use' do 200 | let(:params) {{ 201 | :minute => '*', 202 | :command => 'echo hi', 203 | :user => 'jeremy', 204 | :env => { 'foo' => 'bar', 'FIZZ' => 'buzz' } 205 | }} 206 | it { should contain_file('/nail/etc/cron.d/foobar').with_content(/FIZZ="buzz"\nfoo="bar"/) } 207 | end 208 | 209 | context 'when given show_diff=false' do 210 | let(:params) {{ 211 | :minute => '*', 212 | :command => 'echo hi', 213 | :user => 'jeremy', 214 | :env => { 'foo' => 'bar', 'FIZZ' => 'buzz' }, 215 | :show_diff => false, 216 | }} 217 | it { should contain_file('/nail/etc/cron.d/foobar').with_show_diff(false) } 218 | end 219 | 220 | context 'without specifying show_diff' do 221 | let(:params) {{ 222 | :minute => '*', 223 | :command => 'echo hi', 224 | :user => 'jeremy', 225 | :env => { 'foo' => 'bar', 'FIZZ' => 'buzz' }, 226 | }} 227 | it { should contain_file('/nail/etc/cron.d/foobar').with_show_diff(true) } 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /spec/defines/file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'cron::file' do 4 | let(:params) {{ 5 | :file_params => {} 6 | }} 7 | let(:facts) {{ 8 | :operatingsystemrelease => '14.04', 9 | :lsbdistid => 'Ubuntu', 10 | :osfamily => 'Debian', 11 | :lsbdistrelease => '14.04', 12 | }} 13 | let(:pre_condition) { "class { 'cron': }" } 14 | 15 | context 'with correct title' do 16 | let(:title) { 'foobar' } 17 | 18 | it { should_not raise_error } 19 | end 20 | 21 | context 'with a full path' do 22 | let(:title) { '/etc/cron.d/foobar' } 23 | 24 | it { should raise_error(/validate/) } 25 | end 26 | 27 | context 'with a dot' do 28 | let(:title) { 'foo.bar' } 29 | 30 | it { should raise_error(/validate/) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/defines/staleness_check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'cron::staleness_check' do 4 | let(:title) { 'cron_foobar' } 5 | 6 | context 'with 10m interval' do 7 | let(:params) {{ 8 | :threshold => '10m', 9 | :params => { 'team' => 'baz', 'runbook' => 'y/rb-foobar' }, 10 | :user => 'mary', 11 | }} 12 | 13 | it { 14 | should contain_monitoring_check('cron_foobar_staleness') \ 15 | .with_check_every('120') \ 16 | .with_command(/600$/) 17 | } 18 | end 19 | 20 | context 'with long interval' do 21 | let(:params) {{ 22 | :threshold => '240h', 23 | :params => { 'team' => 'ops', 'runbook' => 'y/rb-backups' }, 24 | :user => 'bob', 25 | }} 26 | 27 | it "should never have an interval over an hour" do 28 | should contain_monitoring_check('cron_foobar_staleness') \ 29 | .with_check_every('3600') 30 | end 31 | end 32 | 33 | context 'without runbook link' do 34 | let(:params) {{ 35 | :threshold => '10m', 36 | :params => { 'team' => 'baz', }, 37 | :user => 'mary', 38 | }} 39 | it { 40 | should contain_monitoring_check('cron_foobar_staleness') \ 41 | .with_check_every('120') \ 42 | } 43 | end 44 | 45 | context 'creates success wrapper' do 46 | let(:params) {{ 47 | :threshold => '10m', 48 | :params => { 'team' => 'baz', }, 49 | :user => 'mary', 50 | }} 51 | it { 52 | should contain_file('/nail/run/success_wrapper/cron_foobar') \ 53 | .with_owner('mary') \ 54 | .with_group(nil) \ 55 | } 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/fixtures/manifests/site.pp: -------------------------------------------------------------------------------- 1 | class nail {} 2 | include nail 3 | define monitoring_check ( 4 | $command, 5 | $runbook = 'y/rb-generic-alerts', 6 | $check_every = '1m', 7 | $alert_after = '0s', 8 | $realert_every = '-1', 9 | $irc_channels = undef, 10 | $notification_email = 'undef', 11 | $ticket = false, 12 | $project = false, 13 | $tip = false, 14 | $sla = 'No SLA defined.', 15 | $page = false, 16 | $needs_sudo = false, 17 | $sudo_user = 'root', 18 | $team = 'operations', 19 | $ensure = 'present', 20 | $dependencies = [], 21 | $use_sensu = pick($profile_sensu::enable, true), 22 | $use_consul = pick($profile_consul::enable, false), 23 | $use_nagios = false, 24 | $nagios_custom = {}, 25 | $low_flap_threshold = undef, 26 | $high_flap_threshold = undef, 27 | $aggregate = false, 28 | $sensu_custom = {}, 29 | ) {} 30 | class profile_sensu( 31 | $enable = true, 32 | ) {} 33 | class profile_consul( 34 | $enable = true, 35 | ) {} 36 | include profile_sensu 37 | include profile_consul 38 | -------------------------------------------------------------------------------- /spec/functions/cron_list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'cron_list' do 4 | 5 | context 'args validation' do 6 | it { 7 | expect { subject.call([]) }.to raise_error(Puppet::ParseError) 8 | expect { subject.call([1]) }.to raise_error(Puppet::ParseError) 9 | expect { subject.call([1,2]) }.to raise_error(Puppet::ParseError) 10 | 11 | expect { subject.call([1,-2,3]) }.to raise_error(Puppet::ParseError) 12 | expect { subject.call([1.0,2,3]) }.to raise_error(Puppet::ParseError) 13 | expect { subject.call([3,2,1,]) }.to raise_error(Puppet::ParseError) 14 | } 15 | end 16 | 17 | context 'default' do 18 | it { 19 | should run.with_params(1,2,3).and_return('1') 20 | should run.with_params(10,60,10).and_return('0,10,20,30,40,50') 21 | should run.with_params(2,24,12).and_return('2,14') 22 | should run.with_params(3,60,15).and_return('3,18,33,48') 23 | should run.with_params(48,60,15).and_return('3,18,33,48') 24 | } 25 | end 26 | 27 | end 28 | -------------------------------------------------------------------------------- /spec/functions/expand_cron_seconds_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'expand_cron_seconds' do 4 | 5 | context "stuff" do 6 | it { should run.with_params('*').and_return((0..59).step().to_a) } 7 | it { should run.with_params('*/30').and_return([0, 30]) } 8 | it { should run.with_params('1,10,30-40/2').and_return([1,10,30,32,34,36,38,40]) } 9 | it { should run.with_params('2,1').and_return([1,2]) } 10 | it { should run.with_params('*/35,*/23').and_return([0,23,35,46]) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/functions/validate_cron_numeric_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'validate_cron_numeric' do 4 | it 'should fail with foo' do 5 | expect { subject.call(['foo']) }.to raise_error(Puppet::ParseError) 6 | end 7 | it 'should fail with empry array' do 8 | expect { subject.call([[]]) }.to raise_error(Puppet::ParseError) 9 | end 10 | it 'should fail if given multiple values' do 11 | expect { subject.call(['1', '2']) }.to raise_error(Puppet::ParseError) 12 | end 13 | it 'works for 1' do 14 | expect { subject.call(['1']) }.not_to raise_error() 15 | end 16 | it 'works for *' do 17 | expect { subject.call(['*']) }.not_to raise_error() 18 | end 19 | it 'works for 1.to_s' do 20 | expect { subject.call([1.to_s]) }.not_to raise_error() 21 | end 22 | it 'works for 10,35,50' do 23 | expect { subject.call(['10,35,50']) }.not_to raise_error() 24 | end 25 | it 'works for */5' do 26 | expect { subject.call(['*/5']) }.not_to raise_error() 27 | end 28 | it 'works for 5-55/5' do 29 | expect { subject.call(['5-55/5']) }.not_to raise_error() 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/rspec-hiera-hotfix.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Monkey-patch rspec-puppet to hotfix https://github.com/rodjek/rspec-puppet/issues/137 3 | # This reverts commit https://github.com/rodjek/rspec-puppet/commit/b04e7c5e23ebb3f0a8293a29757bb649c89db262 4 | # 5 | module RSpec::Puppet 6 | module Support 7 | def setup_puppet 8 | vardir = Dir.mktmpdir 9 | Puppet[:vardir] = vardir 10 | 11 | [ 12 | [:modulepath, :module_path], 13 | [:manifestdir, :manifest_dir], 14 | [:manifest, :manifest], 15 | [:templatedir, :template_dir], 16 | [:config, :config], 17 | [:confdir, :confdir], 18 | [:hiera_config, :hiera_config], 19 | ].each do |a, b| 20 | if Puppet[a] 21 | if self.respond_to? b 22 | Puppet[a] = self.send(b) 23 | else 24 | Puppet[a] = RSpec.configuration.send(b) 25 | end 26 | end 27 | end 28 | 29 | Puppet[:libdir] = Dir["#{Puppet[:modulepath]}/*/lib"].entries.join(File::PATH_SEPARATOR) 30 | vardir 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'puppetlabs_spec_helper/module_spec_helper' 2 | require 'rspec/core/shared_context' 3 | 4 | RSpec.configure do |c| 5 | c.color = true 6 | c.profile_examples = true if $stdin.isatty && ENV['PROFILE'] 7 | c.module_path = 'spec/fixtures/modules' 8 | c.manifest_dir = 'spec/fixtures/manifests' 9 | end 10 | -------------------------------------------------------------------------------- /templates/cron_staleness_check.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Command to check how long ago a cron job successfully ran, and potentially 4 | # provide helpful context for the user as to why. 5 | # 6 | # Utilizes the check_file_age nagios plugin. 7 | set -u 8 | 9 | function get_context { 10 | # Scans syslog for a given logger tag where we might have additional 11 | # information about why a cron job failed. 12 | echo "Some potentially helpful context from /var/log/messages: (may be empty)" 13 | grep "${1}: " /var/log/messages | tail -c 1024 14 | } 15 | 16 | function uptime_less_than { 17 | local threshold=$1 18 | local uptime_seconds="$(cat /proc/uptime | grep -o '^[0-9]\+')" 19 | [[ $uptime_seconds -lt $threshold ]] 20 | } 21 | 22 | name=$1 23 | threshold_s=$2 24 | 25 | # If the server hasn't been up for long enough for the cron job to run in the 26 | # first place, we don't need to alert people. This suppresses alerts that come 27 | # from servers that are brought back up after reboots, downtime, or from being 28 | # recently launched/provisioned. 29 | if uptime_less_than $threshold_s; then 30 | echo "OK: Our uptime is less than $threshold_s seconds:" 31 | uptime 32 | echo "Assuming things will be OK once the script has had a chance to run." 33 | exit 0 34 | fi 35 | 36 | <%= @check_file_age_path %> "/nail/run/success_wrapper/${name}" -w ${threshold_s} -c ${threshold_s} >/dev/null 37 | return_code=$? 38 | 39 | last_success=$(stat -c %z "/nail/run/success_wrapper/${name}") 40 | 41 | if [ $return_code -ne 0 ]; then 42 | echo "${name} hasn't completed successfully since ${last_success}" 43 | get_context ${name} 44 | else 45 | echo "${name} last completed successfully ${last_success}" 46 | fi 47 | 48 | exit $return_code 49 | -------------------------------------------------------------------------------- /templates/d.erb: -------------------------------------------------------------------------------- 1 | # WARNING - this file is managed by puppet - do not edit! 2 | <% if @comment != '' -%> 3 | <% Array(@comment).each do |c| %># <%= c %><% end %> 4 | <% end %> 5 | MAILTO=<%= @mailto %> 6 | SHELL=/bin/bash 7 | <% if @normalize_path -%> 8 | PATH=/usr/local/bin:/usr/local/sbin:/nail/sys/bin:/usr/sbin:/usr/bin:/sbin:/bin 9 | <% else -%> 10 | PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin:/usr/local/sbin 11 | <% end -%> 12 | <% @env.keys.sort.each do |k| v=@env[k] -%> 13 | <%= k %>="<%= v %>" 14 | <% end -%> 15 | <% scope.function_expand_cron_seconds([@second]).each do |sec| -%> 16 | <%= @minute %> <%= @hour %> <%= @dom %> <%= @month %> <%= @dow %> <%= @user %> (<% if sec != 0 -%>sleep <%= sec %>; <% end %><% if @lock -%>flock -n "/var/lock/<%= @reporting_name -%>.lock" <% end %><% if @timeout -%>/usr/bin/timeout <%= @timeout_signal_arg -%> <%= @timeout -%> <% end %><%= @actual_command %>)<% if @actually_log_to_syslog -%> 2>&1 | logger -t <%= @reporting_name %> <% end %> 17 | <% end -%> 18 | -------------------------------------------------------------------------------- /templates/job_cron.erb: -------------------------------------------------------------------------------- 1 | <%# initctl should never output anything, but if it did there would be a 2 | torrent of mail. Let's not let that happen. -%> 3 | MAILTO="" 4 | <% scope.function_expand_cron_seconds([@second]).each do |sec| -%> 5 | <%= @minute %> <%= @hour %> <%= @dom %> <%= @month %> <%= @dow %> root (<% if sec != 0 -%>sleep <%= sec %>; <% end %>/sbin/initctl emit --no-wait cron_<%= @title %>) 6 | <% end -%> 7 | -------------------------------------------------------------------------------- /templates/job_script.erb: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -o errexit 3 | set -o nounset 4 | set -o pipefail 5 | 6 | exec &> >(logger -t '<%= @reporting_name %>') 7 | 8 | set -a 9 | source /etc/environment 10 | set +a 11 | 12 | exec 3> '/var/lock/<%= @reporting_name %>.lock' 13 | if ! flock -n 3; then 14 | echo "Failed to flock /var/lock/<%= @reporting_name %>.lock" 15 | exit 1 16 | fi 17 | 18 | <%= @command %> 19 | -------------------------------------------------------------------------------- /templates/job_upstart.erb: -------------------------------------------------------------------------------- 1 | # FLAG: MANAGED BY PUPPET 2 | start on <%= @reporting_name %> 3 | task 4 | exec setuidgid '<%= @user %>' <%= @success_wrapper_command %>'/nail/etc/upstart_crons/<%= @title %>' 5 | --------------------------------------------------------------------------------