class Minicron::Cron

Used to interact with the crontab on hosts over an ssh connection

Public Class Methods

new(ssh) click to toggle source

Initialise the cron class

@param ssh [Minicron::Transport::SSH] instance

# File lib/minicron/cron.rb, line 10
def initialize(ssh)
  @ssh = ssh
end

Public Instance Methods

add_schedule(job, schedule, conn = nil) click to toggle source

Add the schedule for this job to the crontab

@param job [Minicron::Hub::Job] an instance of a job model @param schedule [String] the job schedule as a string @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 135
def add_schedule(job, schedule, conn = nil)
  # Open an SSH connection
  conn ||= @ssh.open

  # Prepare the line we are going to write to the crontab
  line = build_minicron_command(schedule, job.user, job.command)
  escaped_line = line.shellescape
  echo_line = "echo #{escaped_line} >> #{Minicron.config['server']['cron_file']}"

  # Append it to the end of the crontab
  conn.exec!(echo_line).to_s.strip

  # Check the line is there
  tail = conn.exec!("tail -n 1 #{Minicron.config['server']['cron_file']}").to_s.strip

  # Throw an exception if we can't see our new line at the end of the file
  if tail != line
    fail Exception, "Expected to find '#{line}' at EOF but found '#{tail}'"
  end
end
build_minicron_command(schedule, user, command) click to toggle source

Build the minicron command to be used in the crontab

@param schedule [String] @param user [String] @param command [String] @return [String]

# File lib/minicron/cron.rb, line 20
def build_minicron_command(schedule, user, command)
  # Escape the command so it will work in bourne shells
  command = Escape.shell_command(['minicron', 'run', command])
  cron_command = Escape.shell_command(['/bin/bash', '-l', '-c', command])

  "#{schedule} #{user} #{cron_command}"
end
delete_host(host, conn = nil) click to toggle source

Delete a host and all it's jobs from the crontab

@param job [Minicron::Hub::Job] a job instance with it's schedules @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 237
def delete_host(host, conn = nil)
  conn ||= @ssh.open

  # Loop through each job and delete them one by one
  # TODO: what if one job removal fails but others don't? Should
  # we try and rollback somehow or just return the host with half its
  # jobs deleted?
  host.jobs.each do |job|
    delete_job(job, conn)
  end
end
delete_job(job, conn = nil) click to toggle source

Delete a job and all it's schedules from the crontab

@param job [Minicron::Hub::Job] a job instance with it's schedules @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 221
def delete_job(job, conn = nil)
  conn ||= @ssh.open

  # Loop through each schedule and delete them one by one
  # TODO: what if one schedule removal fails but others don't? Should
  # we try and rollback somehow or just return the job with half its
  # schedules deleted?
  job.schedules.each do |schedule|
    delete_schedule(job, schedule.formatted, conn)
  end
end
delete_schedule(job, schedule, conn = nil) click to toggle source

Remove the schedule for this job from the crontab

@param job [Minicron::Hub::Job] an instance of a job model @param schedule [String] the job schedule as a string @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 206
def delete_schedule(job, schedule, conn = nil)
  # Open an SSH connection
  conn ||= @ssh.open

  # We are looking for the current value of the schedule
  find = build_minicron_command(schedule, job.user, job.command)

  # Replace the old schedule with nothing i.e deleting it
  find_and_replace(conn, find, '')
end
find_and_replace(conn, find, replace) click to toggle source

Used to find a string and replace it with another in the crontab by using the sed command

@param conn an instance of an open ssh connection @param find [String] @param replace [String]

# File lib/minicron/cron.rb, line 67
def find_and_replace(conn, find, replace)
  begin
    # Test the SSH connection first
    test = test_host_permissions(conn)
  rescue Exception => e
    raise Exception, "Error connecting to host, reason: #{e.message}"
  end

  # Check the connection worked
  raise Exception, "Unable to connect to host, reason: unknown" if !test[:connect]

  # Check the crontab is readable
  raise Exception, "Insufficient permissions to read from the cron file" if !test[:crontab][:read]

  # Check the crontab is writeable
  raise Exception, "Insufficient permissions to write to the cron file" if !test[:crontab][:write]

  # Check /etc is writeable
  raise Exception, "/etc is not writeable by the current user" if !test[:etc][:write]

  # Check /etc is executable
  raise Exception, "/etc is not executable by the current user" if !test[:etc][:execute]

  # Get the full crontab
  crontab = conn.exec!("cat #{Minicron.config['server']['cron_file']}").to_s.strip

  # Replace the full string with the replacement string
  begin
    crontab[find] = replace
  rescue Exception => e
    raise Exception, "Unable to replace '#{find}' with '#{replace}' in the cron file, reason: #{e}"
  end

  # Echo the crontab back to the tmp crontab
  conn.exec!("echo #{crontab.shellescape} > /tmp/minicron_crontab").to_s.strip

  # If it's a delete
  if replace == ''
    # Check the original line is no longer there
    grep = conn.exec!("grep -F #{find.shellescape} /tmp/minicron_crontab").to_s.strip

    # Throw an exception if we can't see our new line at the end of the file
    if grep != replace
      fail Exception, "Expected to find nothing when grepping the cron file but found #{grep}"
    end
  else
    # Check the updated line is there
    grep = conn.exec!("grep -F #{replace.shellescape} /tmp/minicron_crontab").to_s.strip

    # Throw an exception if we can't see our new line at the end of the file
    if grep != replace
      fail Exception, "Expected to find '#{replace}' when grepping the cron file but found #{grep}"
    end
  end

  # And finally replace the crontab with the new one now we now the change worked
  move = conn.exec!("/bin/sh -c 'mv /tmp/minicron_crontab #{Minicron.config['server']['cron_file']} && echo \"y\" || echo \"n\"'").to_s.strip

  if move != 'y'
    fail Exception, "Unable to move /tmp/minicron_crontab to #{Minicron.config['server']['cron_file']}, check the permissions?"
  end
end
test_host_permissions(conn = nil) click to toggle source

Test an SSH connection and the permissions for the crontab

@param conn an instance of an open ssh connection @return [Hash]

# File lib/minicron/cron.rb, line 32
def test_host_permissions(conn = nil)
  # Open an SSH connection
  conn ||= @ssh.open

  # Check if /etc is writeable
  etc_write = conn.exec!("/bin/sh -c 'test -w /etc && echo \"y\" || echo \"n\"'").strip

  # Check if /etc is executable
  etc_execute = conn.exec!("/bin/sh -c 'test -x /etc && echo \"y\" || echo \"n\"'").strip

  # Check if the crontab is readable
  crontab_read = conn.exec!("/bin/sh -c 'test -r #{Minicron.config['server']['cron_file']} && echo \"y\" || echo \"n\"'").strip

  # Check if the crontab is writeable
  crontab_write = conn.exec!("/bin/sh -c 'test -w #{Minicron.config['server']['cron_file']} && echo \"y\" || echo \"n\"'").strip

  {
    :connect => true,
    :etc => {
      :write => etc_write == 'y',
      :execute => etc_execute == 'y'
    },
    :crontab => {
      :read => crontab_read == 'y',
      :write => crontab_write == 'y'
    }
  }
end
update_schedule(job, old_schedule, new_schedule, conn = nil) click to toggle source

Update the schedule for this job in the crontab

@param job [Minicron::Hub::Job] an instance of a job model @param old_schedule [String] the old job schedule as a string @param new_schedule [String] the new job schedule as a string @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 162
def update_schedule(job, old_schedule, new_schedule, conn = nil)
  # Open an SSH connection
  conn ||= @ssh.open

  # We are looking for the current value of the schedule
  find = build_minicron_command(old_schedule, job.user, job.command)

  # And replacing it with the updated value
  replace = build_minicron_command(new_schedule, job.user, job.command)

  # Replace the old schedule with the new schedule
  find_and_replace(conn, find, replace)
end
update_user(job, old_user, new_user, conn = nil) click to toggle source

Update the user for a job and all its schedules

@param job [Minicron::Hub::Job] an instance of a job model including the schedule relation @param old_user [String] the old job user as a string @param new_user [String] the new job user as a string @param conn an instance of an open ssh connection

# File lib/minicron/cron.rb, line 182
def update_user(job, old_user, new_user, conn = nil)
  conn ||= @ssh.open

  # Loop through each schedule and delete them one by one
  # TODO: what if one schedule update fails but others don't? Should
  # we try and rollback somehow or just return the job with half its
  # schedules deleted?
  job.schedules.each do |schedule|
    # We are looking for the current value of the schedule
    find = build_minicron_command(schedule.formatted, old_user, job.command)

    # And replacing it with the updated value
    replace = build_minicron_command(schedule.formatted, new_user, job.command)

    # Replace the old schedule with the new schedule
    find_and_replace(conn, find, replace)
  end
end