class Synapse::Haproxy

Attributes

opts[R]

Public Class Methods

new(opts) click to toggle source
Calls superclass method
# File lib/synapse/haproxy.rb, line 507
def initialize(opts)
  super()

  %w{global defaults reload_command}.each do |req|
    raise ArgumentError, "haproxy requires a #{req} section" if !opts.has_key?(req)
  end

  req_pairs = {
    'do_writes' => 'config_file_path',
    'do_socket' => 'socket_file_path',
    'do_reloads' => 'reload_command'}

  req_pairs.each do |cond, req|
    if opts[cond]
      raise ArgumentError, "the `#{req}` option is required when `#{cond}` is true" unless opts[req]
    end
  end

  @opts = opts

  # how to restart haproxy
  @restart_interval = 2
  @restart_required = true
  @last_restart = Time.new(0)

  # a place to store the parsed haproxy config from each watcher
  @watcher_configs = {}
end

Public Instance Methods

construct_name(backend) click to toggle source

used to build unique, consistent haproxy names for backends

# File lib/synapse/haproxy.rb, line 788
def construct_name(backend)
  name = "#{backend['host']}:#{backend['port']}"
  if backend['name'] && !backend['name'].empty?
    name = "#{name}_#{backend['name']}"
  end

  return name
end
generate_backend_stanza(watcher, config) click to toggle source
# File lib/synapse/haproxy.rb, line 657
def generate_backend_stanza(watcher, config)
  if watcher.backends.empty?
    log.warn "synapse: no backends found for watcher #{watcher.name}"
  end

  stanza = [
    "\nbackend #{watcher.name}",
    config.map {|c| "\t#{c}"},
    watcher.backends.shuffle.map {|backend|
      backend_name = construct_name(backend)
      b = "\tserver #{backend_name} #{backend['host']}:#{backend['port']}"
      b = "#{b} cookie #{backend_name}" unless config.include?('mode tcp')
      b = "#{b} #{watcher.haproxy['server_options']}"
      b }
  ]
end
generate_base_config() click to toggle source

generates the global and defaults sections of the config file

# File lib/synapse/haproxy.rb, line 587
def generate_base_config
  base_config = ["# auto-generated by synapse at #{Time.now}\n"]

  %w{global defaults}.each do |section|
    base_config << "#{section}"
    @opts[section].each do |option|
      base_config << "\t#{option}"
    end
  end

  if @opts['extra_sections']
    @opts['extra_sections'].each do |title, section|
      base_config << "\n#{title}"
      section.each do |option|
        base_config << "\t#{option}"
      end
    end
  end

  return base_config
end
generate_config(watchers) click to toggle source

generates a new config based on the state of the watchers

# File lib/synapse/haproxy.rb, line 555
def generate_config(watchers)
  new_config = generate_base_config
  shared_frontend_lines = generate_shared_frontend

  watchers.each do |watcher|
    @watcher_configs[watcher.name] ||= parse_watcher_config(watcher)
    new_config << generate_frontend_stanza(watcher, @watcher_configs[watcher.name]['frontend'])
    new_config << generate_backend_stanza(watcher, @watcher_configs[watcher.name]['backend'])
    if watcher.haproxy.include?('shared_frontend')
      if @opts['shared_frontend'] == nil
        log.warn "synapse: service #{watcher.name} contains a shared frontend section but the base config does not! skipping."
      else
        shared_frontend_lines << validate_haproxy_stanza(watcher.haproxy['shared_frontend'].map{|l| "\t#{l}"}, "frontend", "shared frontend section for #{watcher.name}")
      end
    end
  end
  new_config << shared_frontend_lines.flatten if shared_frontend_lines

  log.debug "synapse: new haproxy config: #{new_config}"
  return new_config.flatten.join("\n")
end
generate_frontend_stanza(watcher, config) click to toggle source

generates an individual stanza for a particular watcher

# File lib/synapse/haproxy.rb, line 643
def generate_frontend_stanza(watcher, config)
  unless watcher.haproxy.has_key?("port")
    log.debug "synapse: not generating frontend stanza for watcher #{watcher.name} because it has no port defined"
    return []
  end

  stanza = [
    "\nfrontend #{watcher.name}",
    config.map {|c| "\t#{c}"},
    "\tbind #{@opts['bind_address'] || 'localhost'}:#{watcher.haproxy['port']}",
    "\tdefault_backend #{watcher.name}"
  ]
end
generate_shared_frontend() click to toggle source

pull out the shared frontend section if any

# File lib/synapse/haproxy.rb, line 578
def generate_shared_frontend
  return nil unless @opts.include?('shared_frontend')
  log.debug "synapse: found a shared frontend section"
  shared_frontend_lines = ["\nfrontend shared-frontend"]
  shared_frontend_lines << validate_haproxy_stanza(@opts['shared_frontend'].map{|l| "\t#{l}"}, "frontend", "shared frontend")
  return shared_frontend_lines
end
parse_watcher_config(watcher) click to toggle source

split the haproxy config in each watcher into fields applicable in frontend and backend sections

# File lib/synapse/haproxy.rb, line 611
def parse_watcher_config(watcher)
  config = {}
  %w{frontend backend}.each do |section|
    config[section] = watcher.haproxy[section] || []

    # copy over the settings from the 'listen' section that pertain to section
    config[section].concat(
      watcher.haproxy['listen'].select {|setting|
        parsed_setting = setting.strip.gsub(/\s+/, ' ').downcase
        @@section_fields[section].any? {|field| parsed_setting.start_with?(field)}
      })

    # pick only those fields that are valid and warn about the invalid ones
    config[section] = validate_haproxy_stanza(config[section], section, watcher.name)
  end

  return config
end
restart() click to toggle source

restarts haproxy

# File lib/synapse/haproxy.rb, line 773
def restart
  # sleep if we restarted too recently
  delay = (@last_restart - Time.now) + @restart_interval
  sleep(delay) if delay > 0

  # do the actual restart
  res = `#{opts['reload_command']}`.chomp
  raise "failed to reload haproxy via #{opts['reload_command']}: #{res}" unless $?.success?
  log.info "synapse: restarted haproxy"

  @last_restart = Time.now()
  @restart_required = false
end
update_backends(watchers) click to toggle source

tries to set active backends via haproxy's stats socket because we can't add backends via the socket, we might still need to restart haproxy

# File lib/synapse/haproxy.rb, line 676
def update_backends(watchers)
  # first, get a list of existing servers for various backends
  begin
    s = UNIXSocket.new(@opts['socket_file_path'])
    s.write("show stat\n")
    info = s.read()
  rescue StandardError => e
    log.warn "synapse: unhandled error reading stats socket: #{e.inspect}"
    @restart_required = true
    return
  end

  # parse the stats output to get current backends
  cur_backends = {}
  info.split("\n").each do |line|
    next if line[0] == '#'

    parts = line.split(',')
    next if ['FRONTEND', 'BACKEND'].include?(parts[1])

    cur_backends[parts[0]] ||= []
    cur_backends[parts[0]] << parts[1]
  end

  # build a list of backends that should be enabled
  enabled_backends = {}
  watchers.each do |watcher|
    enabled_backends[watcher.name] = []
    next if watcher.backends.empty?

    unless cur_backends.include? watcher.name
      log.debug "synapse: restart required because we added new section #{watcher.name}"
      @restart_required = true
      return
    end

    watcher.backends.each do |backend|
      backend_name = construct_name(backend)
      unless cur_backends[watcher.name].include? backend_name
        log.debug "synapse: restart required because we have a new backend #{watcher.name}/#{backend_name}"
        @restart_required = true
        return
      end

      enabled_backends[watcher.name] << backend_name
    end
  end

  # actually enable the enabled backends, and disable the disabled ones
  cur_backends.each do |section, backends|
    backends.each do |backend|
      if enabled_backends[section].include? backend
        command = "enable server #{section}/#{backend}\n"
      else
        command = "disable server #{section}/#{backend}\n"
      end

      # actually write the command to the socket
      begin
        s = UNIXSocket.new(@opts['socket_file_path'])
        s.write(command)
        output = s.read()
      rescue StandardError => e
        log.warn "synapse: unknown error writing to socket"
        @restart_required = true
        return
      else
        unless output == "\n"
          log.warn "synapse: socket command #{command} failed: #{output}"
          @restart_required = true
          return
        end
      end
    end
  end

  log.info "synapse: reconfigured haproxy"
end
update_config(watchers) click to toggle source
# File lib/synapse/haproxy.rb, line 536
def update_config(watchers)
  # if we support updating backends, try that whenever possible
  if @opts['do_socket']
    update_backends(watchers) unless @restart_required
  else
    @restart_required = true
  end

  # generate a new config
  new_config = generate_config(watchers)

  # if we write config files, lets do that and then possibly restart
  if @opts['do_writes']
    write_config(new_config)
    restart if @opts['do_reloads'] && @restart_required
  end
end
validate_haproxy_stanza(stanza, stanza_type, service_name) click to toggle source
# File lib/synapse/haproxy.rb, line 630
def validate_haproxy_stanza(stanza, stanza_type, service_name)
  return stanza.select {|setting|
    parsed_setting = setting.strip.gsub(/\s+/, ' ').downcase
    if @@section_fields[stanza_type].any? {|field| parsed_setting.start_with?(field)}
      true
    else
      log.warn "synapse: service #{service_name} contains invalid #{stanza_type} setting: '#{setting}', discarding"
      false
    end
  }
end
write_config(new_config) click to toggle source

writes the config

# File lib/synapse/haproxy.rb, line 756
def write_config(new_config)
  begin
    old_config = File.read(@opts['config_file_path'])
  rescue Errno::ENOENT => e
    log.info "synapse: could not open haproxy config file at #{@opts['config_file_path']}"
    old_config = ""
  end

  if old_config == new_config
    return false
  else
    File.open(@opts['config_file_path'],'w') {|f| f.write(new_config)}
    return true
  end
end