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.
Files changed (49) hide show
  1. data/Gemfile +9 -3
  2. data/README.md +6 -0
  3. data/bin/{souffle-server → souffle} +0 -0
  4. data/lib/souffle.rb +8 -8
  5. data/lib/souffle/application.rb +15 -10
  6. data/lib/souffle/application/souffle-server.rb +90 -5
  7. data/lib/souffle/config.rb +88 -59
  8. data/lib/souffle/daemon.rb +156 -0
  9. data/lib/souffle/exceptions.rb +29 -17
  10. data/lib/souffle/http.rb +43 -0
  11. data/lib/souffle/log.rb +11 -14
  12. data/lib/souffle/node.rb +91 -53
  13. data/lib/souffle/node/runlist.rb +16 -18
  14. data/lib/souffle/node/runlist_item.rb +43 -36
  15. data/lib/souffle/node/runlist_parser.rb +60 -62
  16. data/lib/souffle/polling_event.rb +110 -0
  17. data/lib/souffle/provider.rb +231 -23
  18. data/lib/souffle/provider/aws.rb +654 -7
  19. data/lib/souffle/provider/vagrant.rb +42 -5
  20. data/lib/souffle/provisioner.rb +55 -0
  21. data/lib/souffle/provisioner/node.rb +157 -0
  22. data/lib/souffle/provisioner/system.rb +195 -0
  23. data/lib/souffle/redis_client.rb +8 -0
  24. data/lib/souffle/redis_mixin.rb +40 -0
  25. data/lib/souffle/server.rb +42 -8
  26. data/lib/souffle/ssh_monkey.rb +8 -0
  27. data/lib/souffle/state.rb +16 -0
  28. data/lib/souffle/system.rb +139 -37
  29. data/lib/souffle/template.rb +30 -0
  30. data/lib/souffle/templates/Vagrantfile.erb +41 -0
  31. data/lib/souffle/version.rb +6 -0
  32. data/spec/config_spec.rb +20 -0
  33. data/spec/log_spec.rb +24 -0
  34. data/spec/{runlist_parser_spec.rb → node/runlist_parser_spec.rb} +1 -1
  35. data/spec/{runlist_spec.rb → node/runlist_spec.rb} +1 -1
  36. data/spec/node_spec.rb +43 -8
  37. data/spec/provider_spec.rb +56 -0
  38. data/spec/providers/aws_provider_spec.rb +114 -0
  39. data/spec/providers/vagrant_provider_spec.rb +22 -0
  40. data/spec/provisioner_spec.rb +47 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/system_spec.rb +242 -13
  43. data/spec/template_spec.rb +20 -0
  44. data/spec/templates/example_template.erb +1 -0
  45. metadata +125 -30
  46. data/bin/souffle-worker +0 -7
  47. data/lib/souffle/application/souffle-worker.rb +0 -46
  48. data/lib/souffle/providers.rb +0 -2
  49. data/lib/souffle/worker.rb +0 -14
@@ -0,0 +1,8 @@
1
+ require 'souffle/redis_mixin'
2
+
3
+ class Souffle::Redis
4
+ extend Souffle::RedisMixin
5
+
6
+ # Force initialization of the redis client (@redis).
7
+ init
8
+ end
@@ -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
@@ -1,14 +1,48 @@
1
- module Souffle
2
- # The souffle server and management daemon.
3
- class Server
1
+ require 'thin'
2
+ require 'eventmachine'
3
+ require 'em-synchrony'
4
+ require 'souffle/http'
4
5
 
5
- # Creates a new souffle server.
6
- def initialize
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
- # Runs the server.
10
- def run
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,8 @@
1
+ require 'em-ssh'
2
+
3
+ # Monkeypatching connect on ssh so it doesn't spam the log.
4
+ class Net::SSH::Authentication::KeyManager
5
+ def use_agent?
6
+ false
7
+ end
8
+ 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
@@ -1,51 +1,153 @@
1
- module Souffle
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
- # Creates a new souffle system, defaulting to using Vagrant as a provider.
18
- #
19
- # @param [ String ] provider The provider to use for the given system.
20
- def initialize(provider="Vagrant")
21
- initialize_provider(provider)
22
- super() # NOTE: This is here to initialize state_machine.
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
- def initialize_provider(provider)
26
- @provider = Souffle::Provider.const_get(provider.to_sym).new
27
- rescue
28
- raise Souffle::Exceptions::InvalidProvider,
29
- "The provider Souffle::Provider::#{provider} does not exist."
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
- # Proxy to the provider setup routine.
33
- def setup_provider
34
- @provider.setup
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
- # Adds the root node to the system.
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 [ Souffle::Node ] node The node to become to root node.
40
- def root=(node)
41
- @root = node
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
- # Adds a node to the system tree.
140
+ private
141
+
142
+ # Guarentee that the system hash that was passed in is valid.
45
143
  #
46
- # @param [ Souffle::Node ] node The node to add into the tree.
47
- def add(node)
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
@@ -0,0 +1,6 @@
1
+ # An orchestrator for setting up isolated chef-managed systems.
2
+ module Souffle
3
+ # The current souffle version.
4
+ VERSION = "0.0.2"
5
+ end
6
+
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