traut 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in traut.gemspec
4
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # Traut command line interface script.
3
+ # Run <tt>traut -h</tt> to get more usage.
4
+
5
+ require 'traut'
6
+ Traut::Runner.new(ARGV).run!
@@ -0,0 +1,10 @@
1
+ amqp:
2
+ :host: localhost
3
+ :port: 5672
4
+ logs: /tmp/traut.log
5
+ pid: /tmp/traut.pid
6
+ scripts: /tmp/traut/scripts
7
+ daemon: true
8
+
9
+ actions:
10
+ com.carepilot.event.code.review.app: deploy/app
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Traut
2
+ VERSION = "0.0.1"
3
+ 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: []