diff options
author | Jorie Tappa <jorie@jorietappa.com> | 2018-07-31 14:56:46 -0500 |
---|---|---|
committer | Jorie Tappa <jorie@jorietappa.com> | 2018-07-31 16:07:37 -0500 |
commit | a2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b (patch) | |
tree | 6a90efd716578c7449ec69051279150308884414 /lib | |
parent | e74dce11298298889a40879aad1e2fcc27fa0559 (diff) | |
download | puppet-cron_core-a2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b.tar.gz puppet-cron_core-a2af7dd0b9713f279724d2c7e6f17bfd8ce2d95b.tar.bz2 |
Initial cron import from puppet 7a4c5f07bdf61a7bc7aa32a50e99489a604eac52
Diffstat (limited to 'lib')
-rw-r--r-- | lib/puppet/provider/cron/crontab.rb | 297 | ||||
-rw-r--r-- | lib/puppet/type/cron.rb | 480 |
2 files changed, 777 insertions, 0 deletions
diff --git a/lib/puppet/provider/cron/crontab.rb b/lib/puppet/provider/cron/crontab.rb new file mode 100644 index 0000000..b24ed14 --- /dev/null +++ b/lib/puppet/provider/cron/crontab.rb @@ -0,0 +1,297 @@ +require 'puppet/provider/parsedfile' + +Puppet::Type.type(:cron).provide(:crontab, :parent => Puppet::Provider::ParsedFile, :default_target => ENV["USER"] || "root") do + commands :crontab => "crontab" + + text_line :comment, :match => %r{^\s*#}, :post_parse => proc { |record| + record[:name] = $1 if record[:line] =~ /Puppet Name: (.+)\s*$/ + } + + text_line :blank, :match => %r{^\s*$} + + text_line :environment, :match => %r{^\s*\w+\s*=} + + def self.filetype + tabname = case Facter.value(:osfamily) + when "Solaris" + :suntab + when "AIX" + :aixtab + else + :crontab + end + + Puppet::Util::FileType.filetype(tabname) + end + + self::TIME_FIELDS = [:minute, :hour, :monthday, :month, :weekday] + + record_line :crontab, + :fields => %w{time command}, + :match => %r{^\s*(@\w+|\S+\s+\S+\s+\S+\s+\S+\s+\S+)\s+(.+)$}, + :absent => '*', + :block_eval => :instance do + + def post_parse(record) + time = record.delete(:time) + if match = /@(\S+)/.match(time) + # is there another way to access the constant? + Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each { |f| record[f] = :absent } + record[:special] = match.captures[0] + elsif match = /(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)/.match(time) + record[:special] = :absent + Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.zip(match.captures).each do |field,value| + if value == self.absent + record[field] = :absent + else + record[field] = value.split(",") + end + end + else + raise Puppet::Error, _("Line got parsed as a crontab entry but cannot be handled. Please file a bug with the contents of your crontab") + end + record + end + + def pre_gen(record) + if record[:special] and record[:special] != :absent + record[:special] = "@#{record[:special]}" + end + + Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS.each do |field| + if vals = record[field] and vals.is_a?(Array) + record[field] = vals.join(",") + end + end + record + end + + def to_line(record) + str = "" + record[:name] = nil if record[:unmanaged] + str = "# Puppet Name: #{record[:name]}\n" if record[:name] + if record[:environment] and record[:environment] != :absent + str += record[:environment].map {|line| "#{line}\n"}.join('') + end + if record[:special] and record[:special] != :absent + fields = [:special, :command] + else + fields = Puppet::Type::Cron::ProviderCrontab::TIME_FIELDS + [:command] + end + str += record.values_at(*fields).map do |field| + if field.nil? or field == :absent + self.absent + else + field + end + end.join(self.joiner) + str + end + end + + def create + if resource.should(:command) then + super + else + resource.err _("no command specified, cannot create") + end + end + + # Look up a resource with a given name whose user matches a record target + # + # @api private + # + # @note This overrides the ParsedFile method for finding resources by name, + # so that only records for a given user are matched to resources of the + # same user so that orphaned records in other crontabs don't get falsely + # matched (#2251) + # + # @param [Hash<Symbol, Object>] record + # @param [Array<Puppet::Resource>] resources + # + # @return [Puppet::Resource, nil] The resource if found, else nil + def self.resource_for_record(record, resources) + resource = super + + if resource + target = resource[:target] || resource[:user] + if record[:target] == target + resource + end + end + end + + # Return the header placed at the top of each generated file, warning + # users that modifying this file manually is probably a bad idea. + def self.header +%{# HEADER: This file was autogenerated at #{Time.now} by puppet. +# HEADER: While it can still be managed manually, it is definitely not recommended. +# HEADER: Note particularly that the comments starting with 'Puppet Name' should +# HEADER: not be deleted, as doing so could cause duplicate cron jobs.\n} + end + + # Regex for finding one vixie cron header. + def self.native_header_regex + /# DO NOT EDIT THIS FILE.*?Cron version.*?vixie.*?\n/m + end + + # If a vixie cron header is found, it should be dropped, cron will insert + # a new one in any case, so we need to avoid duplicates. + def self.drop_native_header + true + end + + # See if we can match the record against an existing cron job. + def self.match(record, resources) + # if the record is named, do not even bother (#19876) + # except the resource name was implicitly generated (#3220) + return false if record[:name] and !record[:unmanaged] + resources.each do |name, resource| + # Match the command first, since it's the most important one. + next unless record[:target] == resource[:target] + next unless record[:command] == resource.value(:command) + + # Now check the time fields + compare_fields = self::TIME_FIELDS + [:special] + + matched = true + compare_fields.each do |field| + # If the resource does not manage a property (say monthday) it should + # always match. If it is the other way around (e.g. resource defines + # a should value for :special but the record does not have it, we do + # not match + next unless resource[field] + unless record.include?(field) + matched = false + break + end + + if record_value = record[field] and resource_value = resource.value(field) + # The record translates '*' into absent in the post_parse hook and + # the resource type does exactly the opposite (alias :absent to *) + next if resource_value == '*' and record_value == :absent + next if resource_value == record_value + end + matched =false + break + end + return resource if matched + end + false + end + + @name_index = 0 + + # Collapse name and env records. + def self.prefetch_hook(records) + name = nil + envs = nil + result = records.each { |record| + case record[:record_type] + when :comment + if record[:name] + name = record[:name] + record[:skip] = true + + # Start collecting env values + envs = [] + end + when :environment + # If we're collecting env values (meaning we're in a named cronjob), + # store the line and skip the record. + if envs + envs << record[:line] + record[:skip] = true + end + when :blank + # nothing + else + if name + record[:name] = name + name = nil + else + cmd_string = record[:command].gsub(/\s+/, "_") + index = ( @name_index += 1 ) + record[:name] = "unmanaged:#{cmd_string}-#{ index.to_s }" + record[:unmanaged] = true + end + if envs.nil? or envs.empty? + record[:environment] = :absent + else + # Collect all of the environment lines, and mark the records to be skipped, + # since their data is included in our crontab record. + record[:environment] = envs + + # And turn off env collection again + envs = nil + end + end + }.reject { |record| record[:skip] } + result + end + + def self.to_file(records) + text = super + # Apparently Freebsd will "helpfully" add a new TZ line to every + # single cron line, but not in all cases (e.g., it doesn't do it + # on my machine). This is my attempt to fix it so the TZ lines don't + # multiply. + if text =~ /(^TZ=.+\n)/ + tz = $1 + text.sub!(tz, '') + text = tz + text + end + text + end + + def user=(user) + # we have to mark the target as modified first, to make sure that if + # we move a cronjob from userA to userB, userA's crontab will also + # be rewritten + mark_target_modified + @property_hash[:user] = user + @property_hash[:target] = user + end + + def user + @property_hash[:user] || @property_hash[:target] + end + + CRONTAB_DIR = case Facter.value("osfamily") + when "Debian", "HP-UX" + "/var/spool/cron/crontabs" + when /BSD/ + "/var/cron/tabs" + when "Darwin" + "/usr/lib/cron/tabs/" + else + "/var/spool/cron" + end + + # Yield the names of all crontab files stored on the local system. + # + # @note Ignores files that are not writable for the puppet process. + # + # @api private + def self.enumerate_crontabs + Puppet.debug "looking for crontabs in #{CRONTAB_DIR}" + return unless File.readable?(CRONTAB_DIR) + Dir.foreach(CRONTAB_DIR) do |file| + path = "#{CRONTAB_DIR}/#{file}" + yield(file) if File.file?(path) and File.writable?(path) + end + end + + + # Include all plausible crontab files on the system + # in the list of targets (#11383 / PUP-1381) + def self.targets(resources = nil) + targets = super(resources) + enumerate_crontabs do |target| + targets << target + end + targets.uniq + end + +end + diff --git a/lib/puppet/type/cron.rb b/lib/puppet/type/cron.rb new file mode 100644 index 0000000..a4f1f91 --- /dev/null +++ b/lib/puppet/type/cron.rb @@ -0,0 +1,480 @@ +require 'etc' +require 'facter' +require 'puppet/util/filetype' + +Puppet::Type.newtype(:cron) do + @doc = <<-'EOT' + Installs and manages cron jobs. Every cron resource created by Puppet + requires a command and at least one periodic attribute (hour, minute, + month, monthday, weekday, or special). While the name of the cron job is + not part of the actual job, the name is stored in a comment beginning with + `# Puppet Name: `. These comments are used to match crontab entries created + by Puppet with cron resources. + + If an existing crontab entry happens to match the scheduling and command of a + cron resource that has never been synced, Puppet will defer to the existing + crontab entry and will not create a new entry tagged with the `# Puppet Name: ` + comment. + + Example: + + cron { 'logrotate': + command => '/usr/sbin/logrotate', + user => 'root', + hour => 2, + minute => 0, + } + + Note that all periodic attributes can be specified as an array of values: + + cron { 'logrotate': + command => '/usr/sbin/logrotate', + user => 'root', + hour => [2, 4], + } + + ...or using ranges or the step syntax `*/2` (although there's no guarantee + that your `cron` daemon supports these): + + cron { 'logrotate': + command => '/usr/sbin/logrotate', + user => 'root', + hour => ['2-4'], + minute => '*/10', + } + + An important note: _the Cron type will not reset parameters that are + removed from a manifest_. For example, removing a `minute => 10` parameter + will not reset the minute component of the associated cronjob to `*`. + These changes must be expressed by setting the parameter to + `minute => absent` because Puppet only manages parameters that are out of + sync with manifest entries. + + **Autorequires:** If Puppet is managing the user account specified by the + `user` property of a cron resource, then the cron resource will autorequire + that user. + EOT + ensurable + + # A base class for all of the Cron parameters, since they all have + # similar argument checking going on. + class CronParam < Puppet::Property + class << self + attr_accessor :boundaries, :default + end + + # We have to override the parent method, because we consume the entire + # "should" array + def insync?(is) + self.is_to_s(is) == self.should_to_s + end + + # A method used to do parameter input handling. Converts integers + # in string form to actual integers, and returns the value if it's + # an integer or false if it's just a normal string. + def numfix(num) + if num =~ /^\d+$/ + return num.to_i + elsif num.is_a?(Integer) + return num + else + return false + end + end + + # Verify that a number is within the specified limits. Return the + # number if it is, or false if it is not. + def limitcheck(num, lower, upper) + (num >= lower and num <= upper) && num + end + + # Verify that a value falls within the specified array. Does case + # insensitive matching, and supports matching either the entire word + # or the first three letters of the word. + def alphacheck(value, ary) + tmp = value.downcase + + # If they specified a shortened version of the name, then see + # if we can lengthen it (e.g., mon => monday). + if tmp.length == 3 + ary.each_with_index { |name, index| + if tmp.upcase == name[0..2].upcase + return index + end + } + else + return ary.index(tmp) if ary.include?(tmp) + end + + false + end + + def should_to_s(value = @should) + if value + if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol)) + value = value[0] + end + super(value) + else + nil + end + end + + def is_to_s(value = @is) + if value + if value.is_a?(Array) && (name == :command || value[0].is_a?(Symbol)) + value = value[0] + end + super(value) + else + nil + end + end + + def should + if @should and @should[0] == :absent + :absent + else + @should + end + end + + def should=(ary) + super + @should.flatten! + end + + # The method that does all of the actual parameter value + # checking; called by all of the +param<name>=+ methods. + # Requires the value, type, and bounds, and optionally supports + # a boolean of whether to do alpha checking, and if so requires + # the ary against which to do the checking. + munge do |value| + # Support 'absent' as a value, so that they can remove + # a value + if value == "absent" or value == :absent + return :absent + end + + # Allow the */2 syntax + if value =~ /^\*\/[0-9]+$/ + return value + end + + # Allow ranges + if value =~ /^[0-9]+-[0-9]+$/ + return value + end + + # Allow ranges + */2 + if value =~ /^[0-9]+-[0-9]+\/[0-9]+$/ + return value + end + + if value == "*" + return :absent + end + + return value unless self.class.boundaries + lower, upper = self.class.boundaries + retval = nil + if num = numfix(value) + retval = limitcheck(num, lower, upper) + elsif respond_to?(:alpha) + # If it has an alpha method defined, then we check + # to see if our value is in that list and if so we turn + # it into a number + retval = alphacheck(value, alpha) + end + + if retval + return retval.to_s + else + self.fail _("%{value} is not a valid %{name}") % { value: value, name: self.class.name } + end + end + end + + # Somewhat uniquely, this property does not actually change anything -- it + # just calls +@resource.sync+, which writes out the whole cron tab for + # the user in question. There is no real way to change individual cron + # jobs without rewriting the entire cron file. + # + # Note that this means that managing many cron jobs for a given user + # could currently result in multiple write sessions for that user. + newproperty(:command, :parent => CronParam) do + desc "The command to execute in the cron job. The environment + provided to the command varies by local system rules, and it is + best to always provide a fully qualified command. The user's + profile is not sourced when the command is run, so if the + user's environment is desired it should be sourced manually. + + All cron parameters support `absent` as a value; this will + remove any existing values for that field." + + def retrieve + return_value = super + return_value = return_value[0] if return_value && return_value.is_a?(Array) + + return_value + end + + def should + if @should + if @should.is_a? Array + @should[0] + else + devfail "command is not an array" + end + else + nil + end + end + + def munge(value) + value.strip + end + end + + newproperty(:special) do + desc "A special value such as 'reboot' or 'annually'. + Only available on supported systems such as Vixie Cron. + Overrides more specific time of day/week settings. + Set to 'absent' to make puppet revert to a plain numeric schedule." + + def specials + %w{reboot yearly annually monthly weekly daily midnight hourly absent} + + [ :absent ] + end + + validate do |value| + raise ArgumentError, _("Invalid special schedule %{value}") % { value: value.inspect } unless specials.include?(value) + end + + def munge(value) + # Support value absent so that a schedule can be + # forced to change to numeric. + if value == "absent" or value == :absent + return :absent + end + value + end + end + + newproperty(:minute, :parent => CronParam) do + self.boundaries = [0, 59] + desc "The minute at which to run the cron job. + Optional; if specified, must be between 0 and 59, inclusive." + end + + newproperty(:hour, :parent => CronParam) do + self.boundaries = [0, 23] + desc "The hour at which to run the cron job. Optional; + if specified, must be between 0 and 23, inclusive." + end + + newproperty(:weekday, :parent => CronParam) do + def alpha + %w{sunday monday tuesday wednesday thursday friday saturday} + end + self.boundaries = [0, 7] + desc "The weekday on which to run the command. Optional; if specified, + must be either: + + - A number between 0 and 7, inclusive, with 0 or 7 being Sunday + - The name of the day, such as 'Tuesday'." + end + + newproperty(:month, :parent => CronParam) do + def alpha + # The ___placeholder accounts for the fact that month is unique among + # "nameable" crontab entries in that it does not use 0-based indexing. + # Padding the array with a placeholder introduces the appropriate shift + # in indices. + %w{___placeholder january february march april may june july + august september october november december} + end + self.boundaries = [1, 12] + desc "The month of the year. Optional; if specified, + must be either: + + - A number between 1 and 12, inclusive, with 1 being January + - The name of the month, such as 'December'." + end + + newproperty(:monthday, :parent => CronParam) do + self.boundaries = [1, 31] + desc "The day of the month on which to run the + command. Optional; if specified, must be between 1 and 31." + end + + newproperty(:environment) do + desc "Any environment settings associated with this cron job. They + will be stored between the header and the job in the crontab. There + can be no guarantees that other, earlier settings will not also + affect a given cron job. + + + Also, Puppet cannot automatically determine whether an existing, + unmanaged environment setting is associated with a given cron + job. If you already have cron jobs with environment settings, + then Puppet will keep those settings in the same place in the file, + but will not associate them with a specific job. + + Settings should be specified exactly as they should appear in + the crontab, like `PATH=/bin:/usr/bin:/usr/sbin`." + + validate do |value| + unless value =~ /^\s*(\w+)\s*=\s*(.*)\s*$/ or value == :absent or value == "absent" + raise ArgumentError, _("Invalid environment setting %{value}") % { value: value.inspect } + end + end + + def insync?(is) + if is.is_a? Array + return is.sort == @should.sort + else + return is == @should + end + end + + def should + @should + end + + def should_to_s(newvalue = @should) + if newvalue + newvalue.join(",") + else + nil + end + end + end + + newparam(:name) do + desc "The symbolic name of the cron job. This name + is used for human reference only and is generated automatically + for cron jobs found on the system. This generally won't + matter, as Puppet will do its best to match existing cron jobs + against specified jobs (and Puppet adds a comment to cron jobs it adds), + but it is at least possible that converting from unmanaged jobs to + managed jobs might require manual intervention." + + isnamevar + end + + newproperty(:user) do + desc "The user who owns the cron job. This user must + be allowed to run cron jobs, which is not currently checked by + Puppet. + + This property defaults to the user running Puppet or `root`. + + The default crontab provider executes the system `crontab` using + the user account specified by this property." + + defaultto { + if not provider.is_a?(@resource.class.provider(:crontab)) + struct = Etc.getpwuid(Process.uid) + struct.respond_to?(:name) && struct.name or 'root' + end + } + end + + # Autorequire the owner of the crontab entry. + autorequire(:user) do + self[:user] + end + + newproperty(:target) do + desc "The name of the crontab file in which the cron job should be stored. + + This property defaults to the value of the `user` property if set, the + user running Puppet or `root`. + + For the default crontab provider, this property is functionally + equivalent to the `user` property and should be avoided. In particular, + setting both `user` and `target` to different values will result in + undefined behavior." + + defaultto { + if provider.is_a?(@resource.class.provider(:crontab)) + if val = @resource.should(:user) + val + else + struct = Etc.getpwuid(Process.uid) + struct.respond_to?(:name) && struct.name or 'root' + end + elsif provider.class.ancestors.include?(Puppet::Provider::ParsedFile) + provider.class.default_target + else + nil + end + } + end + + validate do + return true unless self[:special] + return true if self[:special] == :absent + # there is a special schedule in @should, so we don't want to see + # any numeric should values + [ :minute, :hour, :weekday, :monthday, :month ].each do |field| + next unless self[field] + next if self[field] == :absent + raise ArgumentError, _("%{cron} cannot specify both a special schedule and a value for %{field}") % { cron: self.ref, field: field } + end + end + + # We have to reorder things so that :provide is before :target + + attr_accessor :uid + + # Marks the resource as "being purged". + # + # @api public + # + # @note This overrides the Puppet::Type method in order to handle + # an edge case that has so far been observed during testing only. + # Without forcing the should-value for the user property to be + # identical to the original cron file, purging from a fixture + # will not work, because the user property defaults to the user + # running the test. It is not clear whether this scenario can apply + # during normal operation. + # + # @note Also, when not forcing the should-value for the target + # property, unpurged file content (such as comments) can end up + # being written to the default target (i.e. the current login name). + def purging + self[:target] = provider.target + self[:user] = provider.target + super + end + + def value(name) + name = name.intern + ret = nil + if obj = @parameters[name] + ret = obj.should + + ret ||= obj.retrieve + + if ret == :absent + ret = nil + end + end + + unless ret + case name + when :command + when :special + # nothing + else + #ret = (self.class.validproperty?(name).default || "*").to_s + ret = "*" + end + end + + ret + end +end + |