traut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +44 -0
- data/Rakefile +1 -0
- data/bin/traut +6 -0
- data/etc/traut.conf.sample +10 -0
- data/lib/traut/runner.rb +115 -0
- data/lib/traut/server.rb +41 -0
- data/lib/traut/version.rb +3 -0
- data/lib/traut.rb +11 -0
- data/samples/kili +106 -0
- data/traut.gemspec +25 -0
- metadata +96 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Brian L. Troutwine
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
Traut -- a real-time notification automaton
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
It's not uncommon that an event will happen in a computer cluster for
|
5
|
+
which a system administrator is needed to execute a program or two: a
|
6
|
+
database goes down and it's time to switch to a hot-standby or there's
|
7
|
+
a vital configuration fix that needs to go out faster than the ususal
|
8
|
+
Puppet update interval. Maybe you have a specific human process that's
|
9
|
+
kicked off when a 5xx HTTP error gets recorded by your
|
10
|
+
httpd. Heretofore someone scans the logs periodically, though it could
|
11
|
+
be done by a machine. In fact, all of this could be done by a machine.
|
12
|
+
|
13
|
+
Traut is a program which listens to an AMQP queue and executes scripts
|
14
|
+
found in local `/usr/local/bin/traut/` based on the message route. It
|
15
|
+
is presumed that traut will be supported by a small legion of log
|
16
|
+
watchers, daemon prodders and otherwise. Here at CarePilot, we have a
|
17
|
+
daemon hooked up to Gerrit's ssh stream-events so that we might turn
|
18
|
+
'change-merged' events into immediate project deployments, for
|
19
|
+
example. See `samples/kili`. All payloads are delivered to the
|
20
|
+
scripts' stdin.
|
21
|
+
|
22
|
+
Traut cannot daemonize itself. We use [supervisord](http://supervisord.org/) to daemonize Traut;
|
23
|
+
the code needed to achieve self-daemonization is outside of the core
|
24
|
+
focus of this program.
|
25
|
+
|
26
|
+
Installation
|
27
|
+
------------
|
28
|
+
|
29
|
+
Traut is distributed through the RubyGems
|
30
|
+
|
31
|
+
gem install traut
|
32
|
+
|
33
|
+
You're tasked with providing your own init scripts and configuration
|
34
|
+
files. See the contents of etc/ for an example traut.conf. Note that
|
35
|
+
`traut.conf.sample` defines a script `deploy/app` to be run in
|
36
|
+
response to `com.carepilot.event.code.review.app` events, making its
|
37
|
+
full path `/tmp/traut/scripts/deploy/app`.
|
38
|
+
|
39
|
+
Known Issues
|
40
|
+
------------
|
41
|
+
|
42
|
+
* traut has no ability to reload its configuration or restart. It must
|
43
|
+
be killed and started.
|
44
|
+
* traut doesn't do log rotation. Run logrotate on your system.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/traut
ADDED
data/lib/traut/runner.rb
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Traut
|
6
|
+
trap(:INT) { puts; exit }
|
7
|
+
|
8
|
+
# CLI runner. Parse options, run program.
|
9
|
+
class Runner
|
10
|
+
COMMANDS = %w(start stop restart status)
|
11
|
+
|
12
|
+
attr_accessor :options # parsed options
|
13
|
+
|
14
|
+
def initialize(argv)
|
15
|
+
@argv = argv
|
16
|
+
|
17
|
+
# Default options
|
18
|
+
@options = {
|
19
|
+
:amqp => {
|
20
|
+
:host => 'localhost',
|
21
|
+
:port => '5672'
|
22
|
+
},
|
23
|
+
:logs => '/var/log/traut.log',
|
24
|
+
:config => '/etc/traut/traut.conf',
|
25
|
+
:scripts => '/usr/local/bin/traut',
|
26
|
+
:debug => false,
|
27
|
+
:actions => nil,
|
28
|
+
}
|
29
|
+
|
30
|
+
parse!
|
31
|
+
end
|
32
|
+
|
33
|
+
def parser
|
34
|
+
@parser ||= OptionParser.new do |opts|
|
35
|
+
opts.banner = "Usage: traut [options]"
|
36
|
+
opts.separator ""
|
37
|
+
opts.on("-A", "--amqp [HOST]", "The AMQP server host") {
|
38
|
+
|host| @options[:amqp_host] = host
|
39
|
+
}
|
40
|
+
opts.on("-P", "--amqp_port [PORT]", "The AMQP server host port") {
|
41
|
+
|port| @options[:amqp_host] = port
|
42
|
+
}
|
43
|
+
opts.on("-S", "--subscriptions", "The server AMQP subscriptions.") {
|
44
|
+
|subscriptions| @options[:subscriptions] = subscriptions || '*'
|
45
|
+
}
|
46
|
+
opts.on("-C", "--config [FILE]", "Load options from config file") {
|
47
|
+
|file| @options[:config] = file
|
48
|
+
}
|
49
|
+
opts.on("-s", "--scripts", "Location of traut scripts directory") {
|
50
|
+
|scripts| @options[:scripts] = scripts
|
51
|
+
}
|
52
|
+
opts.on('-l', '--logs [LOG]', "Location of log directory location") {
|
53
|
+
|log| @options[:logs] = log
|
54
|
+
}
|
55
|
+
opts.on('--debug', 'Enable debug logging') {
|
56
|
+
|debug| @options[:debug] = true
|
57
|
+
}
|
58
|
+
opts.on_tail("-h", "--help", "Show this message.") do
|
59
|
+
puts opts
|
60
|
+
exit
|
61
|
+
end
|
62
|
+
opts.on_tail("-v", "--version", "Show version") {
|
63
|
+
puts Traut::VERSION; exit
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parse command options out of @argv
|
69
|
+
def parse!
|
70
|
+
parser.parse! @argv
|
71
|
+
@command = @argv.shift
|
72
|
+
@arguments = @argv
|
73
|
+
end
|
74
|
+
|
75
|
+
# Parse the arguments and run the program. Exit on error.
|
76
|
+
def run!
|
77
|
+
load_options_from_config_file!
|
78
|
+
|
79
|
+
log = Logger.new(@options[:logs])
|
80
|
+
log.level = @options[:debug] ? Logger::DEBUG : Logger::INFO
|
81
|
+
|
82
|
+
actions = @options[:actions]
|
83
|
+
actions = actions.merge(actions) { |k,v|
|
84
|
+
File.join(@options[:scripts], v)
|
85
|
+
}
|
86
|
+
|
87
|
+
actions.each { |route, script|
|
88
|
+
if ! File.exists?(script)
|
89
|
+
log.error("#{script} does not exist on disk")
|
90
|
+
exit 1
|
91
|
+
elsif ! File.executable?(script)
|
92
|
+
log.error("#{script} exists on disk but is not executable.")
|
93
|
+
exit 1
|
94
|
+
else
|
95
|
+
log.info("#{script} recognized on disk and executable.")
|
96
|
+
end
|
97
|
+
}
|
98
|
+
|
99
|
+
server = Server.new(@options[:amqp], actions, log)
|
100
|
+
|
101
|
+
server.loop
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def load_options_from_config_file!
|
107
|
+
if file = @options.delete(:config)
|
108
|
+
YAML.load_file(file).each {
|
109
|
+
|key, value| @options[key.to_sym] = value
|
110
|
+
}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
data/lib/traut/server.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'amqp'
|
2
|
+
require 'systemu'
|
3
|
+
|
4
|
+
module Traut
|
5
|
+
|
6
|
+
class Server
|
7
|
+
def initialize(amqp, actions, log)
|
8
|
+
@amqp = amqp
|
9
|
+
@actions = actions
|
10
|
+
@log = log
|
11
|
+
end
|
12
|
+
|
13
|
+
def loop
|
14
|
+
EventMachine.run do
|
15
|
+
AMQP.connect(:host => @amqp[:host]) do |connection|
|
16
|
+
@log.info "Connected to AMQP at #{@amqp[:host]}:#{@amqp[:port]}"
|
17
|
+
channel = AMQP::Channel.new(connection)
|
18
|
+
exchange = channel.topic("traut", :auto_delete => true)
|
19
|
+
|
20
|
+
@actions.each { |route, script|
|
21
|
+
@log.info("Registering #{script} for route #{route}")
|
22
|
+
channel.queue("").bind(exchange, :routing_key => route).subscribe do |headers, payload|
|
23
|
+
status, stdout, stderr = systemu script, 'stdin' => payload
|
24
|
+
if status.exitstatus != 0
|
25
|
+
@log.error("[#{script}] exit status: #{status.exitstatus}")
|
26
|
+
@log.error("[#{script}] stdout: #{stdout.strip}")
|
27
|
+
@log.error("[#{script}] stderr: #{stderr.strip}")
|
28
|
+
else
|
29
|
+
@log.info("[#{script}] exit status: #{status.exitstatus}")
|
30
|
+
@log.info("[#{script}] stdout: #{stdout.strip}")
|
31
|
+
@log.info("[#{script}] stderr: #{stderr.strip}")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
}
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
data/lib/traut.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require "traut/version"
|
2
|
+
|
3
|
+
module Traut
|
4
|
+
ROOT = File.expand_path(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
autoload :Runner, "#{ROOT}/traut/runner"
|
7
|
+
autoload :Server, "#{ROOT}/traut/server"
|
8
|
+
autoload :Daemon, "#{ROOT}/traut/daemon"
|
9
|
+
end
|
10
|
+
|
11
|
+
require "#{Traut::ROOT}/traut/version"
|
data/samples/kili
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'optparse'
|
5
|
+
require 'net/ssh'
|
6
|
+
require 'json'
|
7
|
+
require 'yaml'
|
8
|
+
require 'amqp'
|
9
|
+
require 'logger'
|
10
|
+
|
11
|
+
trap(:INT) { puts; exit }
|
12
|
+
|
13
|
+
options = {
|
14
|
+
:logs => 'kili.log',
|
15
|
+
:amqp => {
|
16
|
+
:host => 'localhost',
|
17
|
+
:port => '5672',
|
18
|
+
},
|
19
|
+
:ssh => {
|
20
|
+
:host => 'localhost',
|
21
|
+
:port => '22',
|
22
|
+
:user => 'nobody',
|
23
|
+
:keys => '~/.ssh/id_rsa',
|
24
|
+
}
|
25
|
+
}
|
26
|
+
optparse = OptionParser.new do|opts|
|
27
|
+
opts.banner = "Usage: kili [options]"
|
28
|
+
opts.on( '--amqp_host HOST', 'The AMQP host kili will connect to.') do |a|
|
29
|
+
options[:amqp][:host] = a
|
30
|
+
end
|
31
|
+
opts.on( '--amqp_port PORT', 'The port for the AMQP host.') do |ap|
|
32
|
+
options[:amqp][:port] = ap
|
33
|
+
end
|
34
|
+
opts.on( '--ssh_host HOST', 'The SSH host kili will connect to.') do |s|
|
35
|
+
options[:ssh][:host] = s
|
36
|
+
end
|
37
|
+
opts.on( '--ssh_port PORT', 'The SSH port kili will connect on.') do |sp|
|
38
|
+
options[:ssh][:port] = sp
|
39
|
+
end
|
40
|
+
opts.on( '--ssh_keys KEYS', 'Comma delimeted SSH keys for user.') do |sk|
|
41
|
+
options[:ssh][:keys] = sk
|
42
|
+
end
|
43
|
+
opts.on( '--ssh_user USER', 'SSH user for host.') do |su|
|
44
|
+
options[:ssh][:user] = su
|
45
|
+
end
|
46
|
+
opts.on( '-l', '--log', 'The log location of Kili') do |log|
|
47
|
+
options[:logs] = log
|
48
|
+
end
|
49
|
+
opts.on( '-h', '--help', 'Display this screen' ) do
|
50
|
+
puts opts
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
optparse.parse!
|
56
|
+
log = Logger.new(options[:logs])
|
57
|
+
log.level = Logger::INFO
|
58
|
+
|
59
|
+
amqp = options[:amqp]
|
60
|
+
sshd = options[:ssh]
|
61
|
+
|
62
|
+
loop do
|
63
|
+
begin
|
64
|
+
EventMachine.run do
|
65
|
+
AMQP.connect(:host => amqp[:host]) do |connection|
|
66
|
+
log.info "Connected to AMQP at #{amqp[:host]}:#{amqp[:port]}"
|
67
|
+
channel = AMQP::Channel.new(connection)
|
68
|
+
exchange = channel.topic("traut", :auto_delete => true)
|
69
|
+
|
70
|
+
Net::SSH.start(sshd[:host], sshd[:user],
|
71
|
+
:port => sshd[:port], :keys => sshd[:keys].split(',')) do |ssh|
|
72
|
+
channel = ssh.open_channel do |ch|
|
73
|
+
ch.exec "gerrit stream-events" do |ch, success|
|
74
|
+
abort "could not stream gerrit events" unless success
|
75
|
+
|
76
|
+
# "on_data" is called when the process writes something to
|
77
|
+
# stdout
|
78
|
+
ch.on_data do |c, data|
|
79
|
+
json = JSON.parse(data)
|
80
|
+
if json['type'] == 'change-merged'
|
81
|
+
log.info("Received change-merged event.")
|
82
|
+
project = json['change']['project']
|
83
|
+
exchange.publish(json, "com.carepilot.event.code.review.#{project}")
|
84
|
+
else
|
85
|
+
log.info("Ignoring event of type #{json['type']}")
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# "on_extended_data" is called when the process writes
|
90
|
+
# something to stderr
|
91
|
+
ch.on_extended_data do |c, type, data|
|
92
|
+
log.error(data)
|
93
|
+
end
|
94
|
+
|
95
|
+
ch.on_close { log.info('Connection closed') }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
channel.wait
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
rescue Net::SSH::Exception => e
|
104
|
+
log.error("Connection died with exception #{e}. Restarting...")
|
105
|
+
end
|
106
|
+
end
|
data/traut.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "traut/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "traut"
|
7
|
+
s.version = Traut::VERSION
|
8
|
+
s.authors = ["Brian L. Troutwine"]
|
9
|
+
s.email = ["brian.troutwine@carepilot.com"]
|
10
|
+
s.homepage = "https://github.com/CarePilot/Traut"
|
11
|
+
s.summary = %q{Turns AMQP events to system command execution}
|
12
|
+
s.description = %q{Traut is a configurable daemon for running localhost commands in response to events generated elsewhere. AMQP is used as the interchange. Traut can make application deployments in response to code checkins, automate database failover and anything else that can be scripted. It needs only companions to pump events through the 'traut' exchange.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "traut"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_runtime_dependency "daemons", '~> 1.1'
|
22
|
+
s.add_runtime_dependency "amqp", '>= 0.8.0'
|
23
|
+
s.add_runtime_dependency "systemu", '~> 2.4'
|
24
|
+
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: traut
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Brian L. Troutwine
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-27 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: daemons
|
16
|
+
requirement: &87052460 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.1'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *87052460
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: amqp
|
27
|
+
requirement: &87052200 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *87052200
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: systemu
|
38
|
+
requirement: &87051970 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.4'
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *87051970
|
47
|
+
description: Traut is a configurable daemon for running localhost commands in response
|
48
|
+
to events generated elsewhere. AMQP is used as the interchange. Traut can make application
|
49
|
+
deployments in response to code checkins, automate database failover and anything
|
50
|
+
else that can be scripted. It needs only companions to pump events through the 'traut'
|
51
|
+
exchange.
|
52
|
+
email:
|
53
|
+
- brian.troutwine@carepilot.com
|
54
|
+
executables:
|
55
|
+
- traut
|
56
|
+
extensions: []
|
57
|
+
extra_rdoc_files: []
|
58
|
+
files:
|
59
|
+
- .gitignore
|
60
|
+
- Gemfile
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- Rakefile
|
64
|
+
- bin/traut
|
65
|
+
- etc/traut.conf.sample
|
66
|
+
- lib/traut.rb
|
67
|
+
- lib/traut/runner.rb
|
68
|
+
- lib/traut/server.rb
|
69
|
+
- lib/traut/version.rb
|
70
|
+
- samples/kili
|
71
|
+
- traut.gemspec
|
72
|
+
homepage: https://github.com/CarePilot/Traut
|
73
|
+
licenses: []
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ! '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project: traut
|
92
|
+
rubygems_version: 1.8.7
|
93
|
+
signing_key:
|
94
|
+
specification_version: 3
|
95
|
+
summary: Turns AMQP events to system command execution
|
96
|
+
test_files: []
|