souffle 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -3
- data/README.md +6 -0
- data/bin/{souffle-server → souffle} +0 -0
- data/lib/souffle.rb +8 -8
- data/lib/souffle/application.rb +15 -10
- data/lib/souffle/application/souffle-server.rb +90 -5
- data/lib/souffle/config.rb +88 -59
- data/lib/souffle/daemon.rb +156 -0
- data/lib/souffle/exceptions.rb +29 -17
- data/lib/souffle/http.rb +43 -0
- data/lib/souffle/log.rb +11 -14
- data/lib/souffle/node.rb +91 -53
- data/lib/souffle/node/runlist.rb +16 -18
- data/lib/souffle/node/runlist_item.rb +43 -36
- data/lib/souffle/node/runlist_parser.rb +60 -62
- data/lib/souffle/polling_event.rb +110 -0
- data/lib/souffle/provider.rb +231 -23
- data/lib/souffle/provider/aws.rb +654 -7
- data/lib/souffle/provider/vagrant.rb +42 -5
- data/lib/souffle/provisioner.rb +55 -0
- data/lib/souffle/provisioner/node.rb +157 -0
- data/lib/souffle/provisioner/system.rb +195 -0
- data/lib/souffle/redis_client.rb +8 -0
- data/lib/souffle/redis_mixin.rb +40 -0
- data/lib/souffle/server.rb +42 -8
- data/lib/souffle/ssh_monkey.rb +8 -0
- data/lib/souffle/state.rb +16 -0
- data/lib/souffle/system.rb +139 -37
- data/lib/souffle/template.rb +30 -0
- data/lib/souffle/templates/Vagrantfile.erb +41 -0
- data/lib/souffle/version.rb +6 -0
- data/spec/config_spec.rb +20 -0
- data/spec/log_spec.rb +24 -0
- data/spec/{runlist_parser_spec.rb → node/runlist_parser_spec.rb} +1 -1
- data/spec/{runlist_spec.rb → node/runlist_spec.rb} +1 -1
- data/spec/node_spec.rb +43 -8
- data/spec/provider_spec.rb +56 -0
- data/spec/providers/aws_provider_spec.rb +114 -0
- data/spec/providers/vagrant_provider_spec.rb +22 -0
- data/spec/provisioner_spec.rb +47 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/system_spec.rb +242 -13
- data/spec/template_spec.rb +20 -0
- data/spec/templates/example_template.erb +1 -0
- metadata +125 -30
- data/bin/souffle-worker +0 -7
- data/lib/souffle/application/souffle-worker.rb +0 -46
- data/lib/souffle/providers.rb +0 -2
- data/lib/souffle/worker.rb +0 -14
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'redis'
|
3
|
+
|
4
|
+
# A singleton mixin adapter similar to mixlib/log.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
#
|
8
|
+
# require 'souffle/redis_mixin'
|
9
|
+
#
|
10
|
+
# class MyRedis
|
11
|
+
# extend Souffle::RedisMixin
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# MyRedis.set('awesome', 'cool')
|
15
|
+
# MyRedis.get('awesome')
|
16
|
+
#
|
17
|
+
module Souffle::RedisMixin
|
18
|
+
attr_reader :redis
|
19
|
+
|
20
|
+
# Initializes the redis client (uses synchrony if Eventmachine is running).
|
21
|
+
def init(*opts)
|
22
|
+
if EM.reactor_running?
|
23
|
+
@redis ||= Redis.new({ :driver => :synchrony }.merge(*opts))
|
24
|
+
else
|
25
|
+
@redis ||= Redis.new(*opts)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# The singleton redis object, initializes if it doesn't exist.
|
30
|
+
def redis
|
31
|
+
@redis || init
|
32
|
+
end
|
33
|
+
|
34
|
+
# Pass any other method calls to the underlying redis object created with
|
35
|
+
# init. If this method is hit before the call to Souffle::RedisMixin.init
|
36
|
+
# has been made, it will call Souffle::RedisMixin.init() with no arguments.
|
37
|
+
def method_missing(method_symbol, *args, &blk)
|
38
|
+
redis.send(method_symbol, *args, &blk)
|
39
|
+
end
|
40
|
+
end
|
data/lib/souffle/server.rb
CHANGED
@@ -1,14 +1,48 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'thin'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'souffle/http'
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
# The souffle server and management daemon.
|
7
|
+
class Souffle::Server
|
8
|
+
|
9
|
+
# Creates a new souffle server.
|
10
|
+
def initialize
|
11
|
+
end
|
12
|
+
|
13
|
+
# Runs the server.
|
14
|
+
def run
|
15
|
+
if Souffle::Config[:server]
|
16
|
+
EM.synchrony do
|
17
|
+
@app = Rack::Builder.new do
|
18
|
+
use Rack::Lint
|
19
|
+
use Rack::ShowExceptions
|
20
|
+
run Rack::Cascade.new([Souffle::Http])
|
21
|
+
end.to_app
|
22
|
+
|
23
|
+
Rack::Handler.get(:thin).run(@app, rack_options)
|
24
|
+
end
|
7
25
|
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Gets the rack options from the configuration.
|
29
|
+
#
|
30
|
+
# @return [ Hash ] The rack options from Souffle::Config.
|
31
|
+
def rack_options
|
32
|
+
opts = Hash.new
|
33
|
+
Souffle::Config.configuration.each do |k,v|
|
34
|
+
if /^rack/ =~ k.to_s
|
35
|
+
param = k.to_s.gsub('rack_', '')
|
8
36
|
|
9
|
-
|
10
|
-
|
37
|
+
case param
|
38
|
+
when "environment"
|
39
|
+
opts[param.to_sym] = v
|
40
|
+
else
|
41
|
+
opts[param.capitalize.to_sym] = v
|
42
|
+
end
|
43
|
+
end
|
11
44
|
end
|
12
|
-
|
45
|
+
opts
|
13
46
|
end
|
47
|
+
|
14
48
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'souffle/redis_client'
|
2
|
+
|
3
|
+
class Souffle::State
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# The Souffle::State prefix for Redis.
|
7
|
+
def prefix
|
8
|
+
"souffle_state_"
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns the current system states.
|
12
|
+
def status
|
13
|
+
Souffle::Redis.get("#{Souffle::State.prefix}status")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/souffle/system.rb
CHANGED
@@ -1,51 +1,153 @@
|
|
1
|
-
|
2
|
-
# A system description with nodes and the statemachine to manage them.
|
3
|
-
class System
|
4
|
-
attr_reader :nodes, :root, :provider
|
5
|
-
|
6
|
-
state_machine :state, :initial => :uninitialized do
|
7
|
-
before_transition :uninitialized => any - :uninitialized,
|
8
|
-
:do => :initialize_provider
|
9
|
-
|
10
|
-
around_transition do |system, transition, block|
|
11
|
-
start = Time.now
|
12
|
-
block.call
|
13
|
-
system.time_used += Time.now - start
|
14
|
-
end
|
15
|
-
end
|
1
|
+
require 'set'
|
16
2
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
3
|
+
# A system description with nodes and the statemachine to manage them.
|
4
|
+
class Souffle::System
|
5
|
+
attr_reader :nodes
|
6
|
+
attr_accessor :options, :provisioner
|
7
|
+
|
8
|
+
# Creates a new souffle system.
|
9
|
+
def initialize
|
10
|
+
@nodes = []
|
11
|
+
@options = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds a node to the system tree.
|
15
|
+
#
|
16
|
+
# @param [ Souffle::Node ] node The node to add into the tree.
|
17
|
+
def add(node)
|
18
|
+
node.system = self
|
19
|
+
@nodes << node
|
20
|
+
end
|
21
|
+
|
22
|
+
# Checks node dependencies and rebalances them accordingly.
|
23
|
+
#
|
24
|
+
# If a node has no dependencies, it's a root node!
|
25
|
+
# If a node has depdendencies, setup the node's parents.
|
26
|
+
def rebalance_nodes
|
27
|
+
clear_node_heirarchy
|
28
|
+
dependent_nodes.each { |n| setup_node_parents(n) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Clears all parents and children from nodes to prepare to rebalancing.
|
32
|
+
def clear_node_heirarchy
|
33
|
+
@nodes.each { |n| n.parents = []; n.children = [] }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Finds all of a nodes parent dependencies and setup the parents.
|
37
|
+
#
|
38
|
+
# @param [ Souffle::Node ] node The node to check and configure.
|
39
|
+
def setup_node_parents(node)
|
40
|
+
optimal_deps = optimized_node_dependencies(node)
|
41
|
+
optimal_deps.each { |n, node_deps| n.add_child(node) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Gets all of the node dependencies on the system.
|
45
|
+
#
|
46
|
+
# @param [ Souffle::Node ] node The node to retrieve dependencies for.
|
47
|
+
#
|
48
|
+
# @return [ Array ] The tuple of [ node, dependency_list ] for the node.
|
49
|
+
def dependencies_on_system(node)
|
50
|
+
node_dependencies = []
|
51
|
+
nodes_except(node).each do |n|
|
52
|
+
is_dependant, dep_list = node.depends_on?(n)
|
53
|
+
node_dependencies << [n, dep_list] if is_dependant
|
23
54
|
end
|
55
|
+
node_dependencies
|
56
|
+
end
|
24
57
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
58
|
+
# Returns a dependency to node list mapping.
|
59
|
+
#
|
60
|
+
# @return [ Hash ] The mapping of depdencies to nodes.
|
61
|
+
def dependency_mapping(node)
|
62
|
+
mapping = {}
|
63
|
+
dependencies_on_system(node).each do |n, deps|
|
64
|
+
deps.each do |dep|
|
65
|
+
mapping[dep] ||= []; mapping[dep] << n
|
66
|
+
end
|
30
67
|
end
|
68
|
+
mapping
|
69
|
+
end
|
31
70
|
|
32
|
-
|
33
|
-
|
34
|
-
|
71
|
+
# The optimized the node dependencies for the system.
|
72
|
+
#
|
73
|
+
# @param [ Souffle::Node ] node The node that you want to optimize.
|
74
|
+
def optimized_node_dependencies(node)
|
75
|
+
deps = Set.new
|
76
|
+
dependency_mapping(node).each do |dep, nodes|
|
77
|
+
deps << nodes.sort_by { |n| n.weight }.first
|
35
78
|
end
|
79
|
+
deps.to_a
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns the list of all nodes except the given node.
|
83
|
+
#
|
84
|
+
# @return [ Array ] The list of all nodes except the given node.
|
85
|
+
def nodes_except(node)
|
86
|
+
@nodes.select { |n| n != node }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns the list of all root nodes.
|
90
|
+
#
|
91
|
+
# @note We use dependencies here to validate whether a node is root here
|
92
|
+
# because the parents are only determined after a rebalance is run.
|
93
|
+
#
|
94
|
+
# @return [ Array ] The list of all root nodes. (Nodes without parents).
|
95
|
+
def roots
|
96
|
+
@nodes.select { |n| n.dependencies.empty? }
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns the list of all dependent nodes.
|
100
|
+
#
|
101
|
+
# @return [ Array ] The list of all dependant nodes.
|
102
|
+
def dependent_nodes
|
103
|
+
@nodes.select { |n| n.dependencies.any? }
|
104
|
+
end
|
36
105
|
|
37
|
-
|
106
|
+
# Tries to fetch an option parameter otherwise it grabs it from config.
|
107
|
+
#
|
108
|
+
# @param [ Symbol ] opt The option to try and fetch.
|
109
|
+
#
|
110
|
+
# @return [ String ] The option return value.
|
111
|
+
def try_opt(opt)
|
112
|
+
options.fetch(opt, Souffle::Config[opt])
|
113
|
+
rescue
|
114
|
+
nil
|
115
|
+
end
|
116
|
+
|
117
|
+
class << self
|
118
|
+
# Creates a new system from a given hash.
|
38
119
|
#
|
39
|
-
# @param [
|
40
|
-
def
|
41
|
-
|
120
|
+
# @param [ Hash ] system_hash The hash representation of the system.
|
121
|
+
def from_hash(system_hash)
|
122
|
+
guarentee_valid_hash(system_hash)
|
123
|
+
system_hash[:options] ||= {}
|
124
|
+
|
125
|
+
sys = Souffle::System.new
|
126
|
+
system_hash[:nodes].each do |n|
|
127
|
+
n[:options] ||= Hash.new
|
128
|
+
|
129
|
+
node = Souffle::Node.new
|
130
|
+
node.name = n[:name]
|
131
|
+
Array(n[:run_list]).each { |rl| node.run_list << rl }
|
132
|
+
Array(n[:dependencies]).each { |dep| node.dependencies << dep }
|
133
|
+
node.options = system_hash[:options].merge(n[:options])
|
134
|
+
node.options[:attributes] ||= Hash.new
|
135
|
+
sys.add(node)
|
136
|
+
end
|
137
|
+
sys
|
42
138
|
end
|
43
139
|
|
44
|
-
|
140
|
+
private
|
141
|
+
|
142
|
+
# Guarentee that the system hash that was passed in is valid.
|
45
143
|
#
|
46
|
-
# @param [
|
47
|
-
def
|
144
|
+
# @param [ Hash ] system_hash The hash representation of the system.
|
145
|
+
def guarentee_valid_hash(system_hash)
|
146
|
+
if system_hash.nil? or !system_hash.has_key?(:nodes)
|
147
|
+
raise Souffle::Exceptions::InvalidSystemHash,
|
148
|
+
"The system hash must have a nodes key with a list of nodes."
|
149
|
+
end
|
48
150
|
end
|
49
|
-
|
50
151
|
end
|
152
|
+
|
51
153
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'tilt'
|
3
|
+
|
4
|
+
# Template wrapper around the Tilt Template Abstraction Library.
|
5
|
+
class Souffle::Template
|
6
|
+
|
7
|
+
# Creates a new template.
|
8
|
+
#
|
9
|
+
# @param [ String ] template The name of the template to render.
|
10
|
+
def initialize(template)
|
11
|
+
@template = Tilt.new(
|
12
|
+
File.expand_path("#{Souffle::Template.template_path}/#{template}"))
|
13
|
+
end
|
14
|
+
|
15
|
+
# Renders the template with the given binding.
|
16
|
+
#
|
17
|
+
# @param [ Object ] binding The binding object for the template.
|
18
|
+
#
|
19
|
+
# @return [ String ] The rendered template.
|
20
|
+
def render(binding)
|
21
|
+
@template.render(binding)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Helper pointing to the default templates path.
|
25
|
+
#
|
26
|
+
# @return [ String ] The path to the Souffle templates.
|
27
|
+
def self.template_path
|
28
|
+
File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# This file is generated by Souffle (<%= version %>)
|
2
|
+
|
3
|
+
require 'mixlib/config'
|
4
|
+
require 'yajl'
|
5
|
+
|
6
|
+
class SouffleCfg
|
7
|
+
extend Mixlib::Config
|
8
|
+
|
9
|
+
def self.from_file(filename, parser="ruby")
|
10
|
+
send("from_file_#{parser}".to_sym, filename)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.from_file_ruby(filename)
|
14
|
+
self.instance_eval(IO.read(filename), filename, 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.from_file_json(filename)
|
18
|
+
self.from_stream_json(IO.read(filename))
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.from_stream_json(input)
|
22
|
+
parser = Yajl::Parser.new(:symbolize_keys => true)
|
23
|
+
configuration.merge!(parser.parse(input))
|
24
|
+
end
|
25
|
+
|
26
|
+
nodes = []
|
27
|
+
end
|
28
|
+
|
29
|
+
cfg = File.join(File.dirname(__FILE__), 'souffle.json')
|
30
|
+
SouffleCfg.from_file(cfg, "json") if File.exists? cfg
|
31
|
+
|
32
|
+
SouffleCfg[:nodes] = Array(SouffleCfg[:nodes])
|
33
|
+
|
34
|
+
Vagrant::Config.run do |config|
|
35
|
+
SouffleCfg[:nodes].each do |node|
|
36
|
+
config.vm_box = config[:vm_box]
|
37
|
+
|
38
|
+
node.vm.provision :chef_solo do |chef|
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/spec/config_spec.rb
CHANGED
@@ -1,6 +1,26 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
3
|
describe "Souffle::Config" do
|
4
|
+
after(:each) do
|
5
|
+
Souffle::Config.configure { |c| c.delete(:random_something) }
|
6
|
+
Souffle::Config[:aws_access_key] = ""
|
7
|
+
Souffle::Config[:aws_access_secret] = ""
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have default values for aws, rack and vagrant" do
|
11
|
+
%w{ log_level log_location provider
|
12
|
+
user group umask aws_access_key aws_access_secret
|
13
|
+
rack_host rack_port rack_environment vagrant_dir }.each do |cfg|
|
14
|
+
cfg = cfg.to_sym
|
15
|
+
Souffle::Config.configuration.keys.include?(cfg).should eql(true)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should have a proper default vagrant directory" do
|
20
|
+
vagrant_dir = "#{ENV['HOME']}/vagrant/vms"
|
21
|
+
Souffle::Config[:vagrant_dir].should eql(vagrant_dir)
|
22
|
+
end
|
23
|
+
|
4
24
|
it "should be able to read from a ruby config file" do
|
5
25
|
config = File.join(File.dirname(__FILE__), 'config', 'example.rb')
|
6
26
|
Souffle::Config.from_file(config)
|
data/spec/log_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'tempfile'
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
4
|
+
|
5
|
+
describe "Souffle::Log" do
|
6
|
+
before(:each) do
|
7
|
+
Souffle::Log.reset!
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should ave the ability to write directly to the log" do
|
11
|
+
io = StringIO.new
|
12
|
+
Souffle::Log.init(io)
|
13
|
+
Souffle::Log << "Example Log"
|
14
|
+
io.string.should eql("Example Log")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should have the ability to write to a specific log level" do
|
18
|
+
io = StringIO.new
|
19
|
+
Souffle::Log.init(io)
|
20
|
+
Souffle::Log.level(:info)
|
21
|
+
Souffle::Log.info "Awesome"
|
22
|
+
(/INFO: Awesome/ =~ io.string).should_not eql(nil)
|
23
|
+
end
|
24
|
+
end
|