souffle 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|