baton 0.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +19 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +6 -0
  4. data/COPYING +8 -0
  5. data/Gemfile +10 -0
  6. data/Guardfile +6 -0
  7. data/README.md +77 -0
  8. data/Rakefile +10 -0
  9. data/baton.gemspec +30 -0
  10. data/bin/batonize +60 -0
  11. data/lib/baton.rb +13 -0
  12. data/lib/baton/api.rb +27 -0
  13. data/lib/baton/channel.rb +47 -0
  14. data/lib/baton/configuration.rb +63 -0
  15. data/lib/baton/consumer.rb +89 -0
  16. data/lib/baton/consumer_manager.rb +67 -0
  17. data/lib/baton/logging.rb +18 -0
  18. data/lib/baton/observer.rb +74 -0
  19. data/lib/baton/server.rb +68 -0
  20. data/lib/baton/service.rb +55 -0
  21. data/lib/baton/templates/gem/Gemfile.tt +5 -0
  22. data/lib/baton/templates/gem/LICENSE.tt +22 -0
  23. data/lib/baton/templates/gem/README.md.tt +29 -0
  24. data/lib/baton/templates/gem/Rakefile.tt +6 -0
  25. data/lib/baton/templates/gem/bin/gem-monitor.tt +10 -0
  26. data/lib/baton/templates/gem/bin/gem.tt +12 -0
  27. data/lib/baton/templates/gem/config/gem.cfg.tt +4 -0
  28. data/lib/baton/templates/gem/gem.gemspec.tt +22 -0
  29. data/lib/baton/templates/gem/gitignore.tt +17 -0
  30. data/lib/baton/templates/gem/lib/baton/gem.rb.tt +15 -0
  31. data/lib/baton/templates/gem/lib/baton/gem/gem-api.rb.tt +17 -0
  32. data/lib/baton/templates/gem/lib/baton/gem/gem-consumer.rb.tt +26 -0
  33. data/lib/baton/templates/gem/lib/baton/gem/gem-monitor.rb.tt +32 -0
  34. data/lib/baton/templates/gem/lib/baton/gem/version.rb.tt +7 -0
  35. data/lib/baton/version.rb +3 -0
  36. data/spec/baton/baton_spec.rb +13 -0
  37. data/spec/baton/configuration_spec.rb +54 -0
  38. data/spec/baton/consumer_manager_spec.rb +72 -0
  39. data/spec/baton/consumer_spec.rb +63 -0
  40. data/spec/baton/server_spec.rb +64 -0
  41. data/spec/fixtures/config.cfg +4 -0
  42. data/spec/fixtures/facts.json +1776 -0
  43. data/spec/fixtures/file +1 -0
  44. data/spec/fixtures/file.sum +1 -0
  45. data/spec/fixtures/invalid_facts.json +1774 -0
  46. data/spec/fixtures/invalid_file +1 -0
  47. data/spec/fixtures/invalid_file.sum +1 -0
  48. data/spec/spec_helper.rb +12 -0
  49. metadata +236 -0
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ log/*
19
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ bundler_args: --without=darwin
6
+ script: bundle exec rspec
data/COPYING ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2012 Macmillan Digital Science, Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'http://rubygems.org'
2
+ source 'http://gems.dsci.it'
3
+
4
+ gemspec
5
+
6
+ group :darwin do
7
+ gem "guard-rspec", "~> 0.5.8"
8
+ gem 'rb-fsevent', "~> 0.4.3.1"
9
+ gem 'growl_notify', "~> 0.0.3"
10
+ end
@@ -0,0 +1,6 @@
1
+ guard 'rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
6
+
@@ -0,0 +1,77 @@
1
+ # Baton - Server Orchestration Tool
2
+
3
+ [![Build Status](https://secure.travis-ci.org/digital-science/baton.png)](http://travis-ci.org/digital-science/baton)
4
+
5
+ ## Description
6
+
7
+ Baton is a general purpose server orchestration tool.
8
+
9
+ ## Getting Started
10
+
11
+ git clone git@github.com:digital-science/baton.git
12
+ cd baton
13
+ bundle install
14
+
15
+ ## Testing
16
+
17
+ bundle exec rspec
18
+
19
+ ## Install
20
+
21
+ You can either:
22
+
23
+ gem install baton
24
+
25
+ Or, in your Gemfile:
26
+
27
+ gem 'baton'
28
+
29
+ ## How to use
30
+
31
+ Please check an existing extension, e.g. [baton-ping](https://github.com/digital-science/baton-ping), for more information about how to use and extend baton.
32
+ Since baton was created as a base for other extensions, it doesn't do anything in particular by itself but provide the structure and a basic setup on top of RabbitMQ and EventMachine.
33
+
34
+ ## Details and Building Extensions
35
+
36
+ Baton relies on [EventMachine](http://rubyeventmachine.com/) and [AMQP](http://rubyamqp.info/) for message passing. The gem defines a basic set of classes operating on top of RabbitMQ. The initial configuration will setup an input exchange and an output exchange.
37
+
38
+ On the input exchange, baton will wait for meaningful messages to perform actions (described by each service) and it will output messages to the output exchange.
39
+
40
+ ### API
41
+
42
+ This is the entry point for input messages. One should extend the API class and add meaningful methods that ultimately use `publish` to publish messages to the input exchange. One example can be found [here](https://github.com/digital-science/baton-ping/blob/master/lib/baton/baton-ping/api.rb#L8)
43
+
44
+ ### Executable script
45
+
46
+ Any baton extension should have an executable script that will start the extension service. [Here](https://github.com/digital-science/baton-ping/blob/master/bin/baton-ping) is an example.
47
+
48
+ ### Service
49
+
50
+ The Service is the starting point of any baton extension. The idea of the service is to setup consumers for the input messages arriving from the API. By implementing `setup_consumers` one will allow the consumers to receive messages. [Here](https://github.com/digital-science/baton-ping/blob/master/lib/baton/baton-ping.rb) is an example.
51
+
52
+ ### Consumer Manager
53
+
54
+ This class is an orchestration class that attaches observers to the consumers (like logger, etc), binds the input queues to the correct exchanges, dispatches the received messages to the consumers and updates the observers on changes. One doesn't need to extend this class unless one wants to change it's behaviour.
55
+
56
+ ### Consumer
57
+
58
+ This class must be extended in order to process each received message. One should implement `process_message` at least, in order to give meaning to each received message. One can also override `routing_key` in order to listen to specific messages. [Here](https://github.com/digital-science/baton-ping/blob/master/lib/baton/baton-ping/ping_consumer.rb) is an example of an implementation.
59
+
60
+ ### Channel
61
+
62
+ Like the consumer manager, this class doesn't need to be extended. It provides functionality to setup the exchanges and add consumers.
63
+
64
+ ### Observer
65
+
66
+ The observer class provides methods to notify observers. It is by default included in the consumers so that the output exchange (and possibly loggers, etc) receive the output messages.
67
+
68
+
69
+ ## Minimal Extension
70
+
71
+ [baton-ping](https://github.com/digital-science/baton-ping) provides what we consider to be a minimal extension to baton. One should note that there is an extra class on `baton-ping` called [monitor](https://github.com/digital-science/baton-ping/blob/master/lib/baton/baton-ping/monitor.rb). This is a good example of what to do with output messages from a baton extension. Together with [baton-ping-monitor](https://github.com/digital-science/baton-ping/blob/master/bin/baton-ping-monitor) it provides a standard way of consuming output messages and do something relevant with them.
72
+
73
+ If you would like to create your own extension, simple install baton, as explained above, and run the following:
74
+
75
+ batonize gem GEMNAME -b
76
+
77
+ This will create a basic gem structure withe the necessary files to create a minimum viable baton extension.
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:test)
6
+ task :test => :spec
7
+
8
+ task :console do
9
+ sh "irb -rubygems -I lib -r baton.rb"
10
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/baton/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["John Griffin", "Carlos Vilhena"]
6
+ gem.email = ["johnog@gmail.com", "carlosvilhena@gmail.com"]
7
+ gem.description = "Baton"
8
+ gem.summary = "Baton"
9
+ gem.homepage = ""
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
+ gem.files = `git ls-files`.split("\n").reject! { |fn| fn.include? ".tgz" }
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").reject! { |fn| fn.include? ".tgz" }
14
+ gem.name = "baton"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Baton::VERSION
17
+
18
+ gem.add_runtime_dependency "amqp", "~> 0.8.4"
19
+ gem.add_runtime_dependency "eventmachine", "~> 1.0.0.beta.4"
20
+ gem.add_runtime_dependency "em-http-request", "1.0.0"
21
+ gem.add_runtime_dependency "bunny", "~> 0.7.8"
22
+ gem.add_runtime_dependency "thor"
23
+
24
+ gem.add_development_dependency "rspec", "~> 2.7"
25
+ gem.add_development_dependency "moqueue", "~> 0.1.4"
26
+ gem.add_development_dependency "fakefs", "~> 0.4.0"
27
+ gem.add_development_dependency "rake", "~> 0.9.2"
28
+ gem.add_development_dependency "webmock", "~> 1.7.7"
29
+ gem.add_development_dependency "minitar", "0.5.3"
30
+ end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'thor'
5
+
6
+ module Baton
7
+ class CLI < Thor
8
+ include Thor::Actions
9
+
10
+ desc "gem GEMNAME", "Creates a skeleton for creating a rubygem that extends Baton called baton-GEMNAME"
11
+
12
+ method_option :bin, :type => :boolean, :default => false, :aliases => '-b', :banner => "Generate a binary for your library."
13
+
14
+ def gem(name)
15
+ name = "baton-#{name.chomp("/")}"
16
+ target = File.join(Dir.pwd, name)
17
+ constant_name = name.split('_').map{|p| p[0..0].upcase + p[1..-1] }.join
18
+ constant_name = constant_name.split('-').map{|q| q[0..0].upcase + q[1..-1] }.join('::') if constant_name =~ /-/
19
+ constant_array = constant_name.split('::')
20
+ FileUtils.mkdir_p(File.join(target, 'config'))
21
+ FileUtils.mkdir_p(File.join(target, 'lib', "baton"))
22
+ FileUtils.mkdir_p(File.join(target, 'lib', 'baton', name))
23
+ git_user_name = `git config user.name`.chomp
24
+ git_user_email = `git config user.email`.chomp
25
+ opts = {
26
+ :name => name,
27
+ :constant_name => constant_name,
28
+ :constant_array => constant_array,
29
+ :author => git_user_name.empty? ? "TODO: Write your name" : git_user_name,
30
+ :email => git_user_email.empty? ? "TODO: Write your email address" : git_user_email
31
+ }
32
+
33
+ template(File.join("gem/Gemfile.tt"), File.join(target, "Gemfile"), opts)
34
+ template(File.join("gem/Rakefile.tt"), File.join(target, "Rakefile"), opts)
35
+ template(File.join("gem/LICENSE.tt"), File.join(target, "LICENSE"), opts)
36
+ template(File.join("gem/README.md.tt"), File.join(target, "README.md"), opts)
37
+ template(File.join("gem/gitignore.tt"), File.join(target, ".gitignore"), opts)
38
+ template(File.join("gem/gem.gemspec.tt"), File.join(target, "#{name}.gemspec"), opts)
39
+ template(File.join("gem/config/gem.cfg.tt"), File.join(target, "config/#{name}.cfg"), opts)
40
+ template(File.join("gem/lib/baton/gem.rb.tt"), File.join(target, "lib/baton/#{name}.rb"), opts)
41
+ template(File.join("gem/lib/baton/gem/version.rb.tt"), File.join(target, "lib/baton/#{name}/version.rb"), opts)
42
+ template(File.join("gem/lib/baton/gem/gem-consumer.rb.tt"), File.join(target, "lib/baton/#{name}/#{name}-consumer.rb"), opts)
43
+ template(File.join("gem/lib/baton/gem/gem-monitor.rb.tt"), File.join(target, "lib/baton/#{name}/#{name}-monitor.rb"), opts)
44
+ template(File.join("gem/lib/baton/gem/gem-api.rb.tt"), File.join(target, "lib/baton/#{name}/#{name}-api.rb"), opts)
45
+ if options[:bin]
46
+ template(File.join("gem/bin/gem.tt"), File.join(target, 'bin', name), opts)
47
+ template(File.join("gem/bin/gem-monitor.tt"), File.join(target, 'bin', "#{name}-monitor"), opts)
48
+ end
49
+
50
+ Dir.chdir(target) { `git init`; `git add .` }
51
+ end
52
+
53
+ def self.source_root
54
+ File.expand_path(File.join(File.dirname(__FILE__), '../lib/baton/templates'))
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+ Baton::CLI.start
@@ -0,0 +1,13 @@
1
+ require "baton/configuration"
2
+ require "baton/version"
3
+ require "baton/logging"
4
+
5
+ module Baton
6
+ def self.configuration
7
+ @configuration ||= Baton::Configuration.new
8
+ end
9
+
10
+ def self.configure
11
+ yield configuration if block_given?
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require "baton"
2
+ require "bunny"
3
+ require "json"
4
+
5
+ module Baton
6
+ class API
7
+
8
+ # Public: Method that publishes a message using Bunny to an exchange.
9
+ #
10
+ # message - a json object containing the message
11
+ # key - the routing key used to forward the message to the right queue(s)
12
+ #
13
+ # Examples
14
+ #
15
+ # publish("{\"message\":\"a message\",\"type\":\"a type\"}", "server.production")
16
+ #
17
+ # Returns nothing.
18
+ def self.publish(message, key)
19
+ b = Bunny.new(Baton.configuration.connection_opts)
20
+ b.start
21
+ e = b.exchange(Baton.configuration.exchange, :auto_delete => false)
22
+ e.publish(message, :key => key, :mandatory => true)
23
+ b.stop
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,47 @@
1
+ require "baton/consumer"
2
+ require "baton/logging"
3
+
4
+ module Baton
5
+ class Channel
6
+ include Baton::Logging
7
+
8
+ attr_accessor :channel, :exchange_in, :exchange_out, :connection
9
+
10
+ # Public: Initialize a Channel. It creates an AMQP connection, a channel,
11
+ # an input and an output exchange and finally attaches the handle_channel_exception
12
+ # callback to the on_error event on the channel.
13
+ def initialize
14
+ @connection = AMQP.connect(Baton.configuration.connection_opts)
15
+ @channel = AMQP::Channel.new(@connection)
16
+ @exchange_in = channel.direct(Baton.configuration.exchange)
17
+ @exchange_out = channel.direct(Baton.configuration.exchange_out)
18
+ @channel.on_error(&method(:handle_channel_exception))
19
+ end
20
+
21
+ # Public: creates a consumer manager with a consumer attached and starts
22
+ # listening to messages.
23
+ #
24
+ # consumer - An instance of Baton::Consumer. it will typically be a extension of
25
+ # Baton::Consumer (e.g. Baton::DeployConsumer).
26
+ #
27
+ # Examples
28
+ #
29
+ # add_consumer(consumer)
30
+ #
31
+ # Returns nothing.
32
+ def add_consumer(consumer)
33
+ Baton::ConsumerManager.new(consumer, channel, exchange_in, exchange_out).start
34
+ end
35
+
36
+ # Public: Callback to handle errors on an AMQP channel.
37
+ #
38
+ # channel - An AMQP channel
39
+ # channel_close -
40
+ #
41
+ # Returns nothing.
42
+ #
43
+ def handle_channel_exception(channel, channel_close)
44
+ logger.error "Channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ require "yaml"
2
+ require "baton/logging"
3
+
4
+ module Baton
5
+ class Configuration
6
+ include Baton::Logging
7
+
8
+ attr_accessor :exchange, :exchange_out, :ohai_file,
9
+ :host, :vhost, :user, :password,
10
+ :pusher_app_id, :pusher_key, :pusher_secret, :config_path
11
+
12
+ # Public: Loads the config file given as parameter and sets up RabbitMQ's options.
13
+ #
14
+ # path - A file path representing a config file
15
+ #
16
+ # Examples
17
+ #
18
+ # config_file = "/path/to/file"
19
+ #
20
+ # Returns nothing.
21
+ # Raises Errno::ENOENT if file cannot be found.
22
+ def config_path=(path)
23
+ config_file = YAML.load_file(path)
24
+ setup_rabbitmq_opts(config_file)
25
+ rescue Errno::ENOENT => e
26
+ self.host = "localhost"
27
+ logger.error "Could not find a baton configuration file at #{path}"
28
+ end
29
+
30
+ # Public: Setup RabbitMQ's options from a config file.
31
+ #
32
+ # config_file - A hash representing a config file
33
+ #
34
+ # Examples
35
+ #
36
+ # setup_rabbitmq_opts({
37
+ # "RABBIT_HOST" => "localhost",
38
+ # "RABBIT_VHOST" => "baton",
39
+ # "RABBIT_USER" => "baton",
40
+ # "RABBIT_PASS" => "password"
41
+ # })
42
+ #
43
+ # Returns nothing.
44
+ def setup_rabbitmq_opts(config_file)
45
+ self.host = config_file.fetch("RABBIT_HOST") {"localhost"}
46
+ self.vhost = config_file["RABBIT_VHOST"]
47
+ self.user = config_file["RABBIT_USER"]
48
+ self.password = config_file["RABBIT_PASS"]
49
+ end
50
+
51
+ # Public: Defines the connection options for RabbitMQ as a Hash.
52
+ #
53
+ # Examples
54
+ #
55
+ # connection_options
56
+ # # => {:host=>"localhost", :vhost=>"baton", :user=>"baton", :password=>"password"}
57
+ #
58
+ # Returns a hash of RabbitMQ connection options.
59
+ def connection_opts
60
+ {:host => host, :vhost => vhost, :user => user, :password => password, :pass => password}.delete_if{|k,v| v.nil?}
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,89 @@
1
+ require "baton/observer"
2
+
3
+ module Baton
4
+ class Consumer
5
+ include Baton::Observer
6
+ include Baton::Logging
7
+
8
+ attr_accessor :consumer_name, :server, :consumer_manager
9
+
10
+ # Public: Initialize a Consumer.
11
+ #
12
+ # consumer_name - A String naming the consumer.
13
+ # server - An instance of Baton::Server.
14
+ def initialize(consumer_name, server)
15
+ @consumer_name, @server = consumer_name, server
16
+ end
17
+
18
+ # Public: Defines the routing key for the consumer. Messages with the
19
+ # defined routing queue will be consumed by this consumer.
20
+ #
21
+ # Examples
22
+ #
23
+ # routing_key
24
+ # # => "central-apu.production"
25
+ #
26
+ # Returns the routing key.
27
+ def routing_key
28
+ "#{consumer_name}.#{server.environment}"
29
+ end
30
+
31
+ # Public: Method that wraps a block and notifies when errors occur.
32
+ #
33
+ # Examples
34
+ #
35
+ # exception_notifier do
36
+ # <your code>
37
+ # end
38
+ #
39
+ # Returns nothing.
40
+ def exception_notifier
41
+ yield
42
+ rescue Exception => e
43
+ notify_error(e.class, e.message)
44
+ end
45
+
46
+ # Public: Method that decodes a json message and passes it to process_message
47
+ # to be processed.
48
+ #
49
+ # payload - A message in json format
50
+ #
51
+ # Examples
52
+ #
53
+ # handle_message("{\"message\":\"a message\",\"type\":\"a type\"}")
54
+ #
55
+ # Returns nothing.
56
+ def handle_message(payload)
57
+ exception_notifier do
58
+ message = JSON.load(payload)
59
+ process_message(message)
60
+ end
61
+ end
62
+
63
+ # Public: Method that will be called when handle_message receives a message.
64
+ # It should be implemented by baton-like gems and it should add logic to
65
+ # process messages.
66
+ #
67
+ # message - A message in ruby-format
68
+ #
69
+ # Examples
70
+ #
71
+ # process_message({"type" => "current"})
72
+ #
73
+ # Returns Output depends on the implementation.
74
+ def process_message(message)
75
+ end
76
+
77
+ # Public: Method that provides an hash of attributes, if they are needed.
78
+ #
79
+ # Examples
80
+ #
81
+ # attributes
82
+ # # => {type: "pong"}.merge(server.attributes)
83
+ #
84
+ # Returns Output depends on the implementation.
85
+ def attributes
86
+ end
87
+
88
+ end
89
+ end