class Flapjack::CLI::Maintenance

Public Class Methods

new(global_options, options) click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 14
def initialize(global_options, options)
  @global_options = global_options
  @options = options

  if @global_options[:'force-utf8']
    Encoding.default_external = 'UTF-8'
    Encoding.default_internal = 'UTF-8'
  end

  config = Flapjack::Configuration.new
  config.load(global_options[:config])
  @config_env = config.all

  if @config_env.nil? || @config_env.empty?
    exit_now! "No config data found in '#{global_options[:config]}'"
  end

  Flapjack::RedisProxy.config = config.for_redis
  Zermelo.redis = Flapjack.redis
end

Private Class Methods

extract_duration_range(duration_in_words) click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 201
def self.extract_duration_range(duration_in_words)
  return nil if duration_in_words.nil?

  dur_words = duration_in_words.downcase

  case dur_words
  when /^(?:equal to|less than|more than)/
    cdur_words = dur_words.gsub(/^(equal to|less than|more than) /, '')
    parsed_dur = ChronicDuration.parse(cdur_words)
    case dur_words
    when /^equal to/
      [parsed_dur, parsed_dur + 1]
    when /^less than/
      [nil, parsed_dur]
    when /^more than/
      [parsed_dur, nil]
    end
  when /^between/
    first, second = dur_words.match(/between (.*) and (.*)/).captures

    # If the first time only contains only a single word, the unit (and past/future) is
    # most likely directly after the first word of the the second time
    # eg between 3 and 4 hours
    suffix = second.match(/\w (.*)/) ? second.match(/\w (.*)/).captures.first : ''
    first = "#{first} #{suffix}" unless / /.match(first)

    [ChronicDuration.parse(first), ChronicDuration.parse(second)].sort
  end
end
extract_time_range(time_period_in_words, base_time) click to toggle source

returns two timestamps, the start and end of the calculated time range. if either one is nil that indicates that the range is unbounded at that end. Start is inclusive of that second, end is exclusive.

# File lib/flapjack/cli/maintenance.rb, line 234
def self.extract_time_range(time_period_in_words, base_time)
  return nil if time_period_in_words.nil?

  time_words = time_period_in_words.downcase

  # Chronic can't parse timestamps for strings starting with before, after or in some cases, on.
  # Strip the before or after for the conversion only, but use it for the comparison later
  ctime_words = time_words.gsub(/^(on|before|after) /, '')

  case time_words
  # Between 3 and 4 hours ago translates to more than 3 hours ago, less than 4 hours ago
  when /^between/
    first, second = time_words.match(/between (.*) and (.*)/).captures

    # If the first time only contains only a single word, the unit (and past/future) is
    # most likely directly after the first word of the the second time
    # eg between 3 and 4 hours ago
    suffix = second.match(/\w (.*)/) ? second.match(/\w (.*)/).captures.first : ''
    first = "#{first} #{suffix}" unless / /.match(first)

    [Chronic.parse(first,  :now => base_time),
     Chronic.parse(second, :now => base_time)].sort
  when /^on/
    # e.g. On 1/1/15.  We use Chronic to work out the minimum and maximum timestamp.
    parsed = Chronic.parse(ctime, :guess => false, :now => base_time)
    [parsed.first, parsed.last]
  when /^(less than|more than|before|after)/
    input_time = Chronic.parse(ctime_words, :keep_zero => true, :now => base_time) ||
      Chronic.parse("#{ctime_words} from now", :keep_zero => true, :now => base_time)
    case time_words
    when /^less than .+ ago$/, /^more than/, /^after/
      [input_time, nil]
    when /^less than/, /^more than .+ ago$/, /^before/
      [nil, input_time]
    end
  end
end
filter_duration(maints, duration_range) click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 190
def self.filter_duration(maints, duration_range)
  maints.select do |maint|
    dur = maint.duration
    lower = duration_range.first
    upper = duration_range.last

    (lower.nil? && upper.nil?) || ((lower.nil? || (dur >= lower)) &&
                                   (upper.nil? || (dur <  upper)))
  end
end

Public Instance Methods

create() click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 141
def create
  errors = {}

  check_names  = @options[:check].is_a?(String) ? @options[:check].split(',') : @options[:check]

  started = Chronic.parse(@options[:started])
  raise "Failed to parse start time #{@options[:started]}" if started.nil?

  duration = ChronicDuration.parse(@options[:duration])
  raise "Failed to parse duration #{@options[:duration]}" if duration.nil?

  check_names.each do |check_name|
    check = Flapjack::Data::Check.intersect(:name => check_names).all.first

    if check.nil?
      # Create the check if it doesn't exist, so we can schedule maintenance against it
      check = Flapjack::Data::Check.new(:name => check_name)
      check.save
    end

    success = case @options[:type]
    when 'scheduled'

      sched_maint = Flapjack::Data::ScheduledMaintenance.new(:start_time => started,
        :end_time => started + duration, :summary => @options[:reason])
      sched_maint.save

      check.scheduled_maintenances << sched_maint
    when 'unscheduled'
      unsched_maint = Flapjack::Data::UnscheduledMaintenance.new(:start_time => started,
        :end_time => started + duration, :summary => @options[:reason])
      unsched_maint.save

      check.set_unscheduled_maintenance(unsched_maint)
    end
    identifier = "#{check_name}:#{started}"
    errors[identifier] = "The following check failed to create: #{identifier}" unless success
  end

  if errors.empty?
    puts "The maintenances specified have been created"
  else
    puts(errors.map {|k, v| "#{k}: #{v}" }.join("\n"))
    exit_now!('Failed to create maintenances')
  end
end
delete() click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 106
def delete
  base_time = Time.now

  maintenances = show(base_time)

  unless @options[:apply]
    exit_now!('The following maintenances would be deleted. Run this ' +
      'command again with --apply true to remove them.')
  end

  errors = {}
  maintenances.each do |maint|
    check = maint.check
    identifier = "#{check.name}:#{check.start}"
    if maint.end_time < base_time
      errors[identifier] = "Maintenance can't be deleted as it finished in the past"
    else
      success = case @options[:type]
      when 'scheduled'
        check.end_scheduled_maintenance(maint, base_time)
      when 'unscheduled'
        check.clear_unscheduled_maintenance(base_time)
      end
      errors[identifier] = "The following maintenance failed to delete: #{entry}" unless success
    end
  end

  if errors.empty?
    puts "The maintenances above have been deleted"
  else
    puts(errors.map {|k, v| "#{k}: #{v}" }.join("\n"))
    exit_now!('Failed to delete maintenances')
  end
end
show(base_time = Time.now) click to toggle source
# File lib/flapjack/cli/maintenance.rb, line 35
def show(base_time = Time.now)
  state = @options[:state]

  checks = if @options[:check]
    Flapjack::Data::Check.intersect(:name => Regexp.new(@options[:check])).all
  else
    Flapjack::Data::Check.all
  end

  start_time_begin, start_time_end = self.class.extract_time_range(
    @options[:started], base_time)

  start_time_begin = start_time_begin.to_i unless start_time_begin.nil?
  start_time_end = start_time_end.to_i unless start_time_end.nil?

  finish_time_begin, finish_time_end = self.class.extract_time_range(
    @options[:finishing], base_time)

  finish_time_begin = finish_time_begin.to_i unless finish_time_begin.nil?
  finish_time_end = finish_time_end.to_i unless finish_time_end.nil?

  duration_range = @options[:duration] ? self.class.extract_duration_range(
    @options[:duration]) : nil

  start_time_range = Zermelo::Filters::IndexRange.new(start_time_begin,
    start_time_end, :by_score => true)
  finish_time_range = Zermelo::Filters::IndexRange.new(finish_time_begin,
    finish_time_end, :by_score => true)

  maintenances = case @options[:type].downcase
  when 'scheduled'
    checks.inject([]) do |memo, check|

      maints = check.scheduled_maintenances.
        intersect(:start_time => start_time_range, :end_time => finish_time_range).all

      if !maints.empty?
        filtered = duration_range.nil? ? maints :
          self.class.filter_duration(maints, duration_range)
        memo += filtered
      end

      memo
    end
  when 'unscheduled'
    checks.inject([]) do |memo, check|

      maints = check.unscheduled_maintenances.
        intersect(:start_time => start_time_range, :end_time => finish_time_range).all

      if !maints.empty?
        filtered = duration_range.nil? ? maints :
          self.class.filter_duration(maints, duration_range)
        memo += filtered
      end

      memo
    end
  end

  rows = maintenances.collect do |maint|
    [maint.check.name,
     Time.at(maint.start_time), maint.end_time - maint.start_time,
     maint.summary, Time.at(maint.end_time)]
  end
  puts Terminal::Table.new :headings => ['Check',
    'Start', 'Duration (s)', 'Reason', 'End'], :rows => rows
  puts "Found #{rows.count} maintenances"
  maintenances
end