Puppet: System Administration Automated

Support

Update from Subversion Repository Recipe

This is a script/daemon that updates a set of puppetmaster directories(manifests,files,etc) from a subversion repository, and will push those updates to additional puppetmasters via rsync+ssh.

It expects the subversion repository to setup in a trunk/tag format(though that's not necessary here, it's just the way that I use it). For instance:

/puppet
/puppet/trunk
/puppet/trunk/files
/puppet/trunk/manifests
....
/puppet/tags
/puppet/tags/prod-YYYYMMDDHHmmss

For the script below to work, /puppet-dev and /puppet-prod must have an initial check done ahead of time.

It'll do a "lite" query every 5 seconds, and perform updates if it detects something.

This script is most useful if you can't trigger an update via a commit hook.

puprepod

#!/usr/bin/env ruby

CHECKFREQ  = 5
USER       = "puppet"
PASSWORD   = "secret"

PUPPETDIRS = {
  "/puppet-dev" => nil,
  "/puppet-prod" => "http://svnserver/repo/puppet/tags",
}
PUPPETMASTERS = [
  "puppetmaster2",
  "puppetmaster3",
  "puppetmaster4",
]

require 'syslog'
require 'logger'

module UpdateLogger
  @@log = nil
  debug = false
  if debug
    @@log = Logger.new(STDOUT)
    @@log.level = Logger::INFO
  else
    @@log = Syslog.open("puprepod", Syslog::LOG_PID | Syslog::LOG_NDELAY)
  end

  def UpdateLogger::log
    @@log
  end

end

class RunCmd
  attr_reader :status, :output, :cmd

  def initialize(cmd)
    @cmd    = cmd
    @status = nil
  end

  def run
    @output = `#{@cmd} 2>&1`
    @status = $?
  end
end

class RepoError < StandardError; end
class RepoCheckError < RepoError; end
class RepoUpdateError < RepoError; end
class RepoChecker
  attr_reader :workingcopy

  def initialize(workingcopy)
    @workingcopy    = workingcopy
  end

  def get_repo_version
    UpdateLogger.log.debug("#{@workingcopy}: Getting repo version")
    cmd = RunCmd.new("/usr/bin/svn stat -Nu --username #{USER} --password #{PASSWORD} #{@workingcopy}")
    cmd.run
    raise RepoCheckError.new(cmd.output) if cmd.status.exitstatus != 0
    m = /^Status against revision:\s+(\d+)\n/.match(cmd.output)
    raise RepoCheckError.new("Unable to determine repository version: #{cmd.output}") if m.nil?
    UpdateLogger.log.debug("#{@workingcopy}: repo version: #{m[1]}")
    m[1].to_i
  end

  def get_working_copy_version
    UpdateLogger.log.debug("#{@workingcopy}: Getting workingcopy version")
    cmd = RunCmd.new("/usr/bin/svn info #{@workingcopy}")
    cmd.run
    raise RepoCheckError.new(cmd.output) if cmd.status.exitstatus != 0
    m = /^Revision:\s+(\d+)$/.match(cmd.output)
    raise RepoCheckError.new("Unable to determine workingcopy version: #{cmd.output}") if m.nil?
    UpdateLogger.log.debug("#{@workingcopy}: workingcopy version: #{m[1]}")
    m[1].to_i
  end

  def uptodate?
    get_working_copy_version == get_repo_version
  end

  def update
    UpdateLogger.log.debug("#{@workingcopy}: Updating")
    cmd = RunCmd.new("/usr/bin/svn update --username #{USER} --password #{PASSWORD} #{@workingcopy}")
    cmd.run
    raise RepoUpdateError.new(cmd.output) if cmd.status.exitstatus != 0
    UpdateLogger.log.debug("/usr/bin/svn update --username #{USER} --password ******** #{@workingcopy} output:\n  #{cmd.output.gsub("\n", "\n  ")}")
    UpdateLogger.log.info("#{@workingcopy}: updated to #{get_working_copy_version}")
    cmd.output
  end

end

class TagError < StandardError; end
class TagCheckError < TagError; end
class TagUpdateError < TagError; end
class TagFollower
  attr_reader :workingcopy, :tagrepodir

  def initialize(workingcopy, tagrepodir)
    @workingcopy = workingcopy
    @tagrepodir  = tagrepodir
  end

  def get_repo_tag
    UpdateLogger.log.debug("#{@workingcopy}: Getting repo tag")
    cmd = RunCmd.new("/usr/bin/svn ls -v --username #{USER} --password #{PASSWORD} #{@tagrepodir} | latesttag")
    cmd.run
    raise TagCheckError.new(cmd.output) if cmd.status.exitstatus != 0
    cmd.output.strip
  end

  def get_working_copy_tag
    UpdateLogger.log.debug("#{@workingcopy}: Getting workingcopy tag")
    cmd = RunCmd.new("/usr/bin/svn info #{@workingcopy}")
    cmd.run
    raise TagCheckError.new(cmd.output) if cmd.status.exitstatus != 0
    m = /^URL: (\S+)$/.match(cmd.output)
    raise TagCheckError.new("Unable to determine tag directory: #{cmd.output}") if m.nil?
    raise TagCheckError.new("Tag repo does not match @tagrepodir: #{cmd.output}") if m[1][0,@tagrepodir.size] != @tagrepodir
    tag = m[1][@tagrepodir.size+1 .. -1] + "/"
    UpdateLogger.log.debug("#{@workingcopy}: workingcopy tag: #{tag}")
    tag
  end

  def uptodate?
    get_working_copy_tag == get_repo_tag
  end

  def update
    UpdateLogger.log.debug("#{@workingcopy}: Updating")
    cmd = RunCmd.new("/usr/bin/svn switch --username #{USER} --password #{PASSWORD} #{@tagrepodir}/#{get_repo_tag} #{@workingcopy}")
    cmd.run
    raise TagUpdateError.new(cmd.output) if cmd.status.exitstatus != 0
    UpdateLogger.log.debug("/usr/bin/svn switch --username #{USER} --password ******** #{@tagrepodir}/#{get_repo_tag} #{@workingcopy}")
    UpdateLogger.log.info("#{@workingcopy}: updated to #{get_working_copy_tag}")
    cmd.output
  end

end

class RsyncError < StandardError; end
class Rsyncer

  def initialize(source, target)
    @source = source
    @target = target
  end

  def run
    UpdateLogger.log.debug("#{@source}: #{@target} syncing")
    cmd = RunCmd.new("rsync -v -aHz --delete --exclude .svn #{@source}/ root@#{@target}:#{@source}/")
    cmd.run
    raise RsyncError.new(cmd.output) if cmd.status.exitstatus != 0
    UpdateLogger.log.debug("rsync -v -aHz --delete --exclude .svn #{@source}/ root@#{@target}:#{@source}/ output:\n  #{cmd.output.gsub("\n", "\n  ")}")
    UpdateLogger.log.info("#{@source}: #{@target} synced")
  end

end

class WorkHorse

  def initialize
    @repos      = PUPPETDIRS
    @repochecks = {}
    @rsyncs     = {}

    @repos.keys.each do |d|
      if @repos[d].nil?
        @repochecks[d] = RepoChecker.new(d)
      else
        @repochecks[d] = TagFollower.new(d,@repos[d])
      end
    end
    @repos.keys.each do |d|
      @rsyncs[d] ||= []
      PUPPETMASTERS.each do |m|
        @rsyncs[d] << Rsyncer.new(d,m)
      end
    end

    UpdateLogger.log.info("Refreshing everything")
    @repos.keys.each do |d|
      begin
        @repochecks[d].update
        @rsyncs[d].each do |rs|
          begin
            rs.run
          rescue RsyncError => e
            UpdateLogger.log.err("Unable to rsync:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
          end
        end
      rescue RepoUpdateError => e
        UpdateLogger.log.err("Unable to run update:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
      rescue TagUpdateError => e
        UpdateLogger.log.error("Unable to run update:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
      end
    end
  end

  def main
    loop do
      repos_to_rsync = []
      UpdateLogger.log.debug("Main Loop")

      @repos.keys.each do |d|
        rc = @repochecks[d]
        begin
          if not rc.uptodate?
            UpdateLogger.log.debug("#{d}: Not up to date.  Updating.")
            res = rc.update
            if res[0,12] != "At revision "
              UpdateLogger.log.debug("#{d}: Has updates.  Rsync scheduled.")
              repos_to_rsync << d
            else
              UpdateLogger.log.info("#{d}: No new updates.")
            end
          end
        rescue RepoCheckError => e
          UpdateLogger.log.err("Unable to run check:\n  %s"%(e.to_s.gsub("\n","\n  ")))
        rescue RepoUpdateError => e
          UpdateLogger.log.err("Unable to run update:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
        rescue TagCheckError => e
          UpdateLogger.log.err("Unable to run check:\n  %s"%(e.to_s.gsub("\n","\n  ")))
        rescue TagUpdateError => e
          UpdateLogger.log.err("Unable to run update:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
        end
      end

      repos_to_rsync.each do |d|
        UpdateLogger.log.debug("#{d}: Rsyncs starting")
        @rsyncs[d].each do |rs|
          begin
            rs.run
          rescue RsyncError => e
            UpdateLogger.log.err("Unable to rsync:\n  %s"%(e.to_s.gsub("\n", "\n  ")))
          end
        end
        UpdateLogger.log.debug("#{d}: Rsyncs done")
      end

      UpdateLogger.log.debug("Sleeping for #{CHECKFREQ} seconds...")
      sleep(CHECKFREQ)
    end
  end

  def spawn
    pid = fork do
      Signal.trap('HUP', 'IGNORE')
      main
    end
    Process.detach(pid)
  end

end

a = WorkHorse.new()
a.spawn()

latesttag

#!/usr/bin/env ruby

def get_latest_tag(svn_output)
  tag = nil
  svn_output.each_line do |l|
    newtag = [l.split()[0].to_i, l.split()[5]]
    next if newtag[1] !~ /^prod-(\d{14})\/$/
    if tag.nil?
      tag = newtag
    else
      tag = newtag if newtag[0] > tag[0]
    end
  end
  return tag[1]
end

puts get_latest_tag(STDIN)
exit(0)