Initial Naily implementation

This commit is contained in:
Maxim Kulkin 2012-10-02 17:31:46 +04:00
parent b03db72118
commit 81972e9da7
6 changed files with 255 additions and 0 deletions

67
naily/bin/nailyd Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/env ruby
require 'naily/server'
require 'naily/version'
require 'ostruct'
require 'optparse'
options = OpenStruct.new
options.show_version = false
options.daemonize = false
options.config_path = nil
options.log_path = nil
options.log_level = :error
OptionParser.new do |opts|
opts.banner = 'Usage: nailyd [options]'
opts.separator ''
opts.separator 'Options:'
opts.on('-v', '--version', 'Show version') { |flag| options.show_version = true }
opts.on('-d', '--[no-]deamonize', 'Daemonize server') { |flag| options.daemonize = flag }
opts.on('-c', '--config PATH', 'Use custom config file') do |path|
unless File.exists?(path)
puts "Error: config file #{options.config_path} was not found"
exit
end
options.config_path = path
end
opts.on('-l', '--logfile PATH' 'Log file path') { |path| options.log_path = path }
opts.on('--loglevel LEVEL', [:fatal, :error, :warn, :info, :debug], 'Logging level (fatal, error, warn, info, debug)') { |level| option.log_level = level }
opts.on_tail('-h', '--help', 'Show this message') do
puts opts
exit
end
opts.on_tail('-v', '--version', 'Show version') do
puts Naily::VERSION
exit
end
end.parse!
config_path = options.config_path || '/etc/naily/nailyd.conf'
config = Naily::Config.default
config.update(Naily::Config.load(config_path)) if File.exists?(config_path)
if options.daemonize
# After daemonize we can't log to STDOUT, pick a default log file
options.log_path ||= "#{Dir.pwd}/naily.log"
require 'daemons'
Daemons.daemonize
end
logger = options.log_path ? Logger.new(options.log_path) : Logger.new(STDOUT)
logger.level = Logger.const_get(options.log_level.upcase)
server = Naily::Server.new(config)
server.logger = logger
server.run

55
naily/lib/naily/config.rb Normal file
View File

@ -0,0 +1,55 @@
module Naily
class ConfigError < StandardError; end
class UnknownOptionError < ConfigError
attr_reader :name
def initialize(name)
super("Unknown config option #{name}")
@name = name
end
end
class ParseError < ConfigError
attr_reader :line
def initialize(message, line)
super(message)
@line = line
end
end
class Config
def self.load(path)
config = new
File.open(path) do |f|
f.each_line do |line|
line.gsub!(/#.*$/, '').trim! # remove comments
next if line == ''
unless /(\S+)\s*=\s*(\S.*)/ ~= line
raise ConfigParseErorr.new("Syntax error in line #{line}", line)
end
name, value = $1, $2
# TODO: validate config option
# raise UnknownOptionError.new(name) unless config.respond_to?(:"#{name}=")
config[name.to_sym] = value
end
end
config
end
def self.default
config = {}
config[:host] = 'localhost'
config[:port] = 5672
config[:username] = 'guest'
config[:password] = 'guest'
config[:queue] = 'naily'
config
end
end
end

View File

@ -0,0 +1,8 @@
module Naily
class Dispatcher
def echo(*args)
args
end
end
end

100
naily/lib/naily/server.rb Normal file
View File

@ -0,0 +1,100 @@
require 'eventmachine'
require 'amqp'
require 'json'
require 'logger'
require 'naily/dispatcher'
module Naily
class Server
attr_reader :options, :delegate
def initialize(config)
@options = options.dup
# TODO: validate options
@config = config
@options.freeze
@delegate = options[:delegate] || Dispatcher.new
end
def run
EM.run do
AMQP.logging = true
@connection = AMQP.connect(connection_options)
@channel = AMQP::Channel.new(@connection)
@channel.on_error do |ch, error|
logger.fatal "Channel error #{error}"
stop { exit }
end
queue = @channel.queue(options[:queue], :durable => true)
queue.subscribe do |header, payload|
dispatch payload
end
Signal.trap('INT') { stop }
Signal.trap('TERM') { stop }
puts "Server started"
end
end
def stop(&block)
if @connection
@connection.close { EM.stop(&block) }
else
EM.stop(&block)
end
end
def logger
@logger ||= ::Logger.new(STDOUT)
end
attr_writer :logger
private
def dispatch(payload)
logger.debug "Got message with payload #{payload.inspect}"
begin
data = JSON.load(payload)
rescue
logger.error "Error deserializing payload: #{$!}"
return
end
unless delegate.respond_to?(data['method'])
logger.error "Unsupported RPC call #{data['method']}"
return
end
logger.info "Processing RPC call #{data['method']}"
begin
result = delegate.send(data['method'], *data['args'])
rescue
logger.error "Error running RPC method #{data['method']}: #{$!}"
# TODO: send error response in case of RPC call
return
end
if data['msg_id']
logger.info "Sending RPC call result #{result.inspect} for rpc call #{data['method']}"
@channel.default_exchange.publish(JSON.dump(result), :routing_key => data['msg_id'])
end
end
def connection_options
{
:host => options[:host],
:port => options[:port],
:username => options[:username],
:password => options[:password],
}.reject { |k, v| v.nil? }
end
end
end

View File

@ -0,0 +1,3 @@
module Naily
VERSION = '0.1'
end

22
naily/naily.gemspec Normal file
View File

@ -0,0 +1,22 @@
$:.unshift File.expand_path('lib', File.dirname(__FILE__))
require 'naily/version'
Gem::Specification.new do |s|
s.name = 'naily'
s.version = Naily::VERSION
s.summary = 'Backend server for Nailgun'
s.description = 'Nailgun deployment job server'
s.authors = ['Maxim Kulkin']
s.email = ['mkulkin@mirantis.com']
s.add_dependency 'daemons'
s.add_dependency 'amqp'
s.add_dependency 'mcollective-client'
s.files = Dir.glob("{bin,lib}/**/*")
s.executables = ['nailyd']
s.require_path = 'lib'
end