traut 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@
2
2
  .bundle
3
3
  Gemfile.lock
4
4
  pkg/*
5
+ logs/
data/README.md CHANGED
@@ -1,28 +1,14 @@
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
23
- [supervisord](http://supervisord.org/) to daemonize Traut; the code
24
- needed to achieve self-daemonization is outside of the core focus of
25
- this program.
1
+ Traut -- a cron-like for AMQP
2
+ =============================
3
+
4
+ Unix cron's an ancient program that runs a script dependent on the passage
5
+ time. Traut's the same way, except it keys off AMQP events instead. Use it to
6
+ update your Puppet machines or log-rotate if nagios sends a disk-full
7
+ warning. Significant number of possibilities.
8
+
9
+ Traut cannot daemonize itself. Use [supervisord](http://supervisord.org/) or
10
+ similar to daemonize Traut; the code needed to achieve self-daemonization is
11
+ outside of the core focus of this program.
26
12
 
27
13
  Installation
28
14
  ------------
@@ -37,9 +23,31 @@ files. See the contents of etc/ for an example traut.conf. Note that
37
23
  response to `com.carepilot.event.code.review.app` events, making its
38
24
  full path `/tmp/traut/scripts/deploy/app`.
39
25
 
40
- Known Issues
41
- ------------
26
+ Configuration
27
+ -------------
28
+
29
+ The source distribution has an [etc/](etc/) directory with example
30
+ configuration.
31
+
32
+ Use
33
+ ---
34
+
35
+ Run traut from the root of the project like this:
36
+
37
+ bundle exec bin/traut -C etc/traut.conf
38
+
39
+ Now, using [hare](https://github.com/blt/hare):
40
+
41
+ $ hare --exchange_name traut --exchange_type topic --route_key whatthesum
42
+ --producer "that wasn't so bad"
43
+
44
+ You should see nothing. Open up another terminal and
45
+
46
+ $ hare --exchange_name traut --exchange_type topic --route_key 'whatthesum.exited'
47
+
48
+ Note that the route key is the same as before, save that '.exited' has been
49
+ appended to it. Listen to this channel if you need notification of
50
+ success. Run both hare commands in the order presented, notice the second print
51
+ a json hash.
42
52
 
43
- * traut has no ability to reload its configuration or restart. It must
44
- be killed and started.
45
- * traut doesn't do log rotation. Run logrotate on your system.
53
+ See the sample configuration file for more details.
data/bin/traut CHANGED
@@ -1,6 +1,33 @@
1
1
  #!/usr/bin/env ruby
2
- # Traut command line interface script.
3
- # Run <tt>traut -h</tt> to get more usage.
2
+
3
+ require 'optparse'
4
+ require 'logger'
4
5
 
5
6
  require 'traut'
6
- Traut::Runner.new(ARGV).run!
7
+
8
+ # Default options
9
+ options = Traut.defaults
10
+
11
+ p = OptionParser.new do |opts|
12
+ opts.banner = "Usage: traut [options]"
13
+ opts.on("-C", "--config FILE", "Load options from config file") {
14
+ |file| options['config'] = file
15
+ }
16
+ opts.on_tail("-h", "--help", "Show this message.") do
17
+ puts opts; exit
18
+ end
19
+ opts.on_tail("-v", "--version", "Show version") {
20
+ puts Traut::VERSION; exit
21
+ }
22
+ end
23
+ p.parse! ARGV
24
+
25
+ @immortal = true
26
+ trap('INT') { @immortal = false; EM.stop }
27
+ trap('HUP') { EM.stop }
28
+
29
+ while @immortal do
30
+ EventMachine.run do
31
+ Traut::Application.new(:options => options).run
32
+ end
33
+ end
@@ -0,0 +1,7 @@
1
+ - event: whatthesum
2
+ command: /usr/bin/sha512sum
3
+ user: blt
4
+ group: blt
5
+
6
+ - event: passiton
7
+ command: cat > /tmp/bleh.txt
data/etc/traut.conf ADDED
@@ -0,0 +1,16 @@
1
+ amqp:
2
+ host: localhost
3
+ port: 5672
4
+ vhost: '/'
5
+ username: 'guest'
6
+ password: 'guest'
7
+ exchange: 'traut'
8
+ queue: ''
9
+
10
+ debug: true
11
+ logdir: './logs/'
12
+ include: './events.d'
13
+
14
+ events:
15
+ - event: need.listing
16
+ command: /bin/ls
@@ -0,0 +1,65 @@
1
+ require 'yaml'
2
+ require 'amqp'
3
+
4
+ module Traut
5
+
6
+ class Application
7
+ def initialize(params)
8
+ @options = params[:options] || raise('parameter :options required')
9
+ end
10
+
11
+ # Parse the arguments and run the program. Exit on error.
12
+ def run
13
+ load_options
14
+
15
+ @logger = Logger.new File.join( File.absolute_path(@options['logdir']), 'traut.log')
16
+ @logger.level = boolean(@options['debug']) ? Logger::DEBUG : Logger::INFO
17
+
18
+ ## NOTE: Have to start AMQP connection out here.
19
+ amqp = @options['amqp']
20
+
21
+ AMQP.connect(:host => amqp['host'], :port => amqp['port'], :vhost => amqp['vhost'],
22
+ :username => amqp['username'], :password => amqp['password']) do |connection|
23
+ @logger.info "Traut #{Traut::VERSION} started"
24
+ channel = AMQP::Channel.new(connection)
25
+ exchange = channel.topic(amqp['exchange'] || 'traut')
26
+
27
+ Traut::Server.new(:channel => channel, :exchange => exchange,
28
+ :events => @options['events'], :log => @logger).run
29
+ end
30
+ end
31
+
32
+ private
33
+ def boolean(string)
34
+ return true if string== true || string =~ (/(true|t|yes|y|1)$/i)
35
+ return false
36
+ end
37
+
38
+ def abs(p)
39
+ File.absolute_path(p)
40
+ end
41
+
42
+ def abs?(p)
43
+ abs(p) == p
44
+ end
45
+
46
+ def mung_config_path(includedir, config)
47
+ # if includedir is absolute do nothing else
48
+ includedir if abs?(includedir)
49
+ # else take abs-dirname of config and append includedir
50
+ File.join( abs(File.dirname(config)), includedir )
51
+ end
52
+
53
+ def load_options
54
+ YAML.load_file(@options['config']).each {
55
+ |key, value| @options[key] = value
56
+ }
57
+ includedir = mung_config_path(@options['include'], @options['config'])
58
+ Dir.open(includedir).each do |f|
59
+ ff = File.join(includedir, f)
60
+ @options['events'] += YAML.load_file(ff) if File.file?(ff)
61
+ end
62
+ end
63
+
64
+ end
65
+ end
data/lib/traut/server.rb CHANGED
@@ -1,41 +1,54 @@
1
- require 'amqp'
1
+ require 'json'
2
2
  require 'systemu'
3
3
 
4
4
  module Traut
5
5
 
6
6
  class Server
7
- def initialize(amqp, actions, log)
8
- @amqp = amqp
9
- @actions = actions
10
- @log = log
7
+ def initialize(params)
8
+ @channel = params[:channel] || raise('parameter :channel required')
9
+ @exchange = params[:exchange] || raise('parameter :exchange required')
10
+ @events = params[:events] || raise('parameter :events required')
11
+ @log = params[:log] || raise('parameter :log required')
11
12
  end
12
13
 
13
- def loop
14
- EventMachine.run do
15
- AMQP.connect(:host => @amqp[:host], :port => @amqp[:port]) do |connection|
16
- @log.info "Connected to AMQP at #{@amqp[:host]}:#{@amqp[:port]}"
17
- channel = AMQP::Channel.new(connection)
18
- exchange = channel.topic('traut')
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
14
+ # :: () -> ()
15
+ def run
16
+ subscribe('#') do |headers, payload|
17
+ @log.debug("Noted the reception of message with route '#{headers.routing_key}'.")
37
18
  end
19
+
20
+ @events.each do |event|
21
+ route, script, user, group = event['event'], event['command'], event['user'], event['group']
22
+ @log.debug("Registering #{script} to run as #{user}:#{group} for event #{route}")
23
+
24
+ subscribe(route) do |headers, payload|
25
+ Traut.spawn(:user => user, :group => group, :command => script,
26
+ :payload => payload, :logger => @log) do |status, stdout, stderr|
27
+ condition = 0 == status.exitstatus ? :debug : :error
28
+ result = {:exitstatus => status.exitstatus, :stdout => stdout.strip, :stderr => stderr.strip}
29
+ @log.send(condition, "[#{script}] #{result}")
30
+ publish(result.to_json, headers.routing_key)
31
+ end
32
+ end # channel.queue
33
+
34
+ end # eventmap.each
35
+ end # run
36
+
37
+ private
38
+
39
+ # :: string -> string
40
+ def finished_route(key)
41
+ [key, 'exited'].join('.')
38
42
  end
39
43
 
40
- end
44
+ def publish(msg, route)
45
+ @exchange.publish msg, :routing_key => finished_route(route)
46
+ end
47
+
48
+ def subscribe(route, &block)
49
+ @channel.queue('').bind(@exchange, :routing_key => route).subscribe(&block)
50
+ end
51
+
52
+ end # Server
53
+
41
54
  end
@@ -0,0 +1,55 @@
1
+ require 'systemu'
2
+
3
+ module Traut
4
+ def self.spawn(params, &block)
5
+ uid = params[:user].nil? ? Process::UID.eid : Etc::getpwnam(params[:user])[:uid]
6
+ gid = params[:group].nil? ? Process::GID.eid : Etc::getgrnam(params[:group])[:gid]
7
+ command = params[:command] || require('parameter :command is required')
8
+ payload = params[:payload]
9
+
10
+ s = Spawn.new(params[:logger])
11
+ s.spawn(uid, gid, command, payload, block)
12
+ end
13
+
14
+ class Spawn
15
+ def initialize(log)
16
+ @log = log
17
+ end
18
+
19
+ def spawn(uid, gid, command, payload, block)
20
+ runas(uid, gid) do
21
+ # Why do I use systemu? Have a look at this:
22
+ ## http://stackoverflow.com/questions/8998097/how-do-i-close-eventmachine-systems-side-of-an-stdin-pipe
23
+ status, stdout, stderr = systemu command, 0=>payload
24
+ block.call(status, stdout, stderr)
25
+
26
+ # If you have an answer for that, consider enabling the following code,
27
+ # keeping in mind that you need to figure out a way to get stderr back
28
+ # as well.
29
+
30
+ # EM.system command, proc{ |p| msg(p, payload) } do |stdout,status|
31
+ # @log.debug("#{stdout} :: #{status}")
32
+ # end
33
+ end
34
+ end
35
+
36
+ private
37
+ def msg(process, m)
38
+ process.send_data(m + "\n")
39
+ end
40
+
41
+ def runas(uid, gid, &block)
42
+ cur_uid = Process::UID.eid
43
+ cur_gid = Process::GID.eid
44
+
45
+ begin
46
+ Process::UID.change_privilege(uid)
47
+ Process::GID.change_privilege(gid)
48
+ block.call
49
+ ensure
50
+ Process::UID.change_privilege(cur_uid)
51
+ Process::GID.change_privilege(cur_gid)
52
+ end
53
+ end
54
+ end
55
+ end
data/lib/traut/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Traut
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/traut.rb CHANGED
@@ -1,11 +1,23 @@
1
- require "traut/version"
1
+ require 'traut/version'
2
+ require 'ostruct'
3
+ require 'eventmachine'
2
4
 
3
5
  module Traut
4
6
  ROOT = File.expand_path(File.dirname(__FILE__))
5
7
 
6
- autoload :Runner, "#{ROOT}/traut/runner"
7
- autoload :Server, "#{ROOT}/traut/server"
8
- autoload :Daemon, "#{ROOT}/traut/daemon"
8
+ autoload :Server, "#{ROOT}/traut/server"
9
+ autoload :Application, "#{ROOT}/traut/application"
10
+
11
+ # Provide the base option sets for all Textme daemons and their
12
+ # defaults.
13
+ def self.defaults
14
+ {
15
+ 'config' => './traut.conf',
16
+ 'logdir' => './logs/',
17
+ 'debug' => true
18
+ }
19
+ end
9
20
  end
10
21
 
11
22
  require "#{Traut::ROOT}/traut/version"
23
+ require "#{Traut::ROOT}/traut/spawn"
data/traut.gemspec CHANGED
@@ -6,10 +6,10 @@ Gem::Specification.new do |s|
6
6
  s.name = "traut"
7
7
  s.version = Traut::VERSION
8
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.}
9
+ s.email = ["brian@troutwine.us"]
10
+ s.homepage = "https://github.com/blt/traut"
11
+ s.summary = %q{Traut is like cron for AMQP events.}
12
+ s.description = %q{Unix cron is a venerable program that turns the passage of time into program invokation. Traut does the same, but using AMQP events to trigger execution. AMQP message payloads are written to the stdin of invoked commands.}
13
13
 
14
14
  s.rubyforge_project = "traut"
15
15
 
@@ -18,8 +18,14 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_runtime_dependency "daemons", '~> 1.1'
21
+ ## Someday...
22
+ # s.add_development_dependency "rspec", '~> 2.8.0'
23
+ # s.add_development_dependency 'guard'
24
+ # s.add_development_dependency 'guard-rspec'
25
+ # s.add_development_dependency 'simplecov'
26
+
22
27
  s.add_runtime_dependency "amqp", '>= 0.8.0'
23
28
  s.add_runtime_dependency "systemu", '~> 2.4'
29
+ s.add_runtime_dependency 'json', '~> 1.6.5'
24
30
 
25
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: traut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,48 +9,46 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-14 00:00:00.000000000Z
12
+ date: 2012-01-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: daemons
16
- requirement: &71562050 !ruby/object:Gem::Requirement
15
+ name: amqp
16
+ requirement: &75597390 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
- - - ~>
19
+ - - ! '>='
20
20
  - !ruby/object:Gem::Version
21
- version: '1.1'
21
+ version: 0.8.0
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *71562050
24
+ version_requirements: *75597390
25
25
  - !ruby/object:Gem::Dependency
26
- name: amqp
27
- requirement: &71561500 !ruby/object:Gem::Requirement
26
+ name: systemu
27
+ requirement: &75597140 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
- - - ! '>='
30
+ - - ~>
31
31
  - !ruby/object:Gem::Version
32
- version: 0.8.0
32
+ version: '2.4'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *71561500
35
+ version_requirements: *75597140
36
36
  - !ruby/object:Gem::Dependency
37
- name: systemu
38
- requirement: &71560720 !ruby/object:Gem::Requirement
37
+ name: json
38
+ requirement: &75596790 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ~>
42
42
  - !ruby/object:Gem::Version
43
- version: '2.4'
43
+ version: 1.6.5
44
44
  type: :runtime
45
45
  prerelease: false
46
- version_requirements: *71560720
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.
46
+ version_requirements: *75596790
47
+ description: Unix cron is a venerable program that turns the passage of time into
48
+ program invokation. Traut does the same, but using AMQP events to trigger execution.
49
+ AMQP message payloads are written to the stdin of invoked commands.
52
50
  email:
53
- - brian.troutwine@carepilot.com
51
+ - brian@troutwine.us
54
52
  executables:
55
53
  - traut
56
54
  extensions: []
@@ -62,15 +60,16 @@ files:
62
60
  - README.md
63
61
  - Rakefile
64
62
  - bin/traut
65
- - etc/actions/app.action
66
- - etc/traut.conf.sample
63
+ - etc/events.d/summer.yml
64
+ - etc/traut.conf
67
65
  - lib/traut.rb
68
- - lib/traut/runner.rb
66
+ - lib/traut/application.rb
69
67
  - lib/traut/server.rb
68
+ - lib/traut/spawn.rb
70
69
  - lib/traut/version.rb
71
70
  - samples/kili
72
71
  - traut.gemspec
73
- homepage: https://github.com/CarePilot/Traut
72
+ homepage: https://github.com/blt/traut
74
73
  licenses: []
75
74
  post_install_message:
76
75
  rdoc_options: []
@@ -93,5 +92,5 @@ rubyforge_project: traut
93
92
  rubygems_version: 1.8.10
94
93
  signing_key:
95
94
  specification_version: 3
96
- summary: Turns AMQP events to system command execution
95
+ summary: Traut is like cron for AMQP events.
97
96
  test_files: []
@@ -1 +0,0 @@
1
- com.carepilot.event.code.review.app: deploy/app
@@ -1,7 +0,0 @@
1
- amqp:
2
- :host: localhost
3
- :port: 5672
4
- logs: /tmp/traut.log
5
- pid: /tmp/traut.pid
6
- scripts: /tmp/traut/scripts
7
- action_dir: /tmp/traut/actions
data/lib/traut/runner.rb DELETED
@@ -1,124 +0,0 @@
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
- :action_dir => '/etc/traut/actions/',
26
- :scripts => '/usr/local/bin/traut',
27
- :debug => false,
28
- :actions => nil,
29
- }
30
-
31
- parse!
32
- end
33
-
34
- def parser
35
- @parser ||= OptionParser.new do |opts|
36
- opts.banner = "Usage: traut [options]"
37
- opts.separator ""
38
- opts.on("-A", "--amqp HOST", "The AMQP server host") {
39
- |host| @options[:amqp][:host] = host
40
- }
41
- opts.on("-P", "--amqp_port PORT", "The AMQP server host port") {
42
- |port| @options[:amqp][:port] = port
43
- }
44
- opts.on("-C", "--config FILE", "Load options from config file") {
45
- |file| @options[:config] = file
46
- }
47
- opts.on("-s", "--scripts DIR", "Location of traut scripts directory"){
48
- |scripts| @options[:scripts] = scripts
49
- }
50
- opts.on("-a", "--actions DIR", "Location of traut actions directory"){
51
- |acts| @options[:action_dir] = acts
52
- }
53
- opts.on('-l', '--logs LOG', "Location of log directory location") {
54
- |log| @options[:logs] = log
55
- }
56
- opts.on('--debug', 'Enable debug logging') {
57
- |debug| @options[:debug] = true
58
- }
59
- opts.on_tail("-h", "--help", "Show this message.") do
60
- puts opts
61
- exit
62
- end
63
- opts.on_tail("-v", "--version", "Show version") {
64
- puts Traut::VERSION; exit
65
- }
66
- end
67
- end
68
-
69
- # Parse command options out of @argv
70
- def parse!
71
- parser.parse! @argv
72
- @command = @argv.shift
73
- @arguments = @argv
74
- end
75
-
76
- # Parse the arguments and run the program. Exit on error.
77
- def run!
78
- load_options_from_config_file!
79
-
80
- log = Logger.new(@options[:logs])
81
- log.level = @options[:debug] ? Logger::DEBUG : Logger::INFO
82
-
83
- actions = @options[:actions]
84
- actions = actions.merge(actions) { |k,v|
85
- File.join(@options[:scripts], v)
86
- }
87
-
88
- actions.each { |route, script|
89
- if ! File.exists?(script)
90
- log.error("#{script} does not exist on disk")
91
- exit 1
92
- elsif ! File.executable?(script)
93
- log.error("#{script} exists on disk but is not executable.")
94
- exit 1
95
- else
96
- log.info("#{script} recognized on disk and executable.")
97
- end
98
- }
99
-
100
- server = Server.new(@options[:amqp], actions, log)
101
-
102
- server.loop
103
- end
104
-
105
- private
106
-
107
- def load_options_from_config_file!
108
- if file = @options.delete(:config)
109
- YAML.load_file(file).each {
110
- |key, value| @options[key.to_sym] = value
111
- }
112
- dir = Dir.open(@options[:action_dir])
113
- @options[:actions] = {}
114
- dir.each do |f|
115
- af = File.join(@options[:action_dir], f)
116
- if ! File.directory? af
117
- YAML.load_file(af).each { |k,v| @options[:actions][k] = v }
118
- end
119
- end
120
- end
121
- end
122
-
123
- end
124
- end