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,110 @@
1
+ require 'eventmachine'
2
+
3
+ # Eventmachine polling event helper.
4
+ class Souffle::PollingEvent
5
+ # The node to run the polling event against.
6
+ attr_accessor :node
7
+
8
+ # The current state of the polling event.
9
+ attr_accessor :state
10
+
11
+ # The interval to run the periodic timer against the event_loop.
12
+ attr_reader :interval
13
+
14
+ # The timeout (in seconds) for the periodic timer.
15
+ attr_reader :timeout
16
+
17
+ # The proc to run prior to the periodic event loop.
18
+ attr_accessor :pre_event
19
+
20
+ # The event loop proc, should call complete on success.
21
+ attr_accessor :event_loop
22
+
23
+ # The proc to run when the timeout has occurred.
24
+ attr_accessor :error_handler
25
+
26
+ # Create a new polling even instance.
27
+ #
28
+ # @param [ Souffle::Node ] node The node to run the polling event against.
29
+ # @param [ Proc ] blk The block to evaluate in the instance context.
30
+ #
31
+ # @example
32
+ # node = Souffle::Node.new
33
+ # node.name = "example_node"
34
+ #
35
+ # EM.run do
36
+ # evt = PollingEvent.new(node) do
37
+ # interval 1
38
+ # timeout 5
39
+ # pre_event { puts "at the beginning" }
40
+ # event_loop { puts "inside of the event loop" }
41
+ # error_handler { puts "in error handler"; EM.stop }
42
+ # end
43
+ # end
44
+ #
45
+ def initialize(node, &blk)
46
+ @state = Hash.new
47
+ @node = node
48
+ instance_eval(&blk) if block_given?
49
+ initialize_defaults
50
+ initialize_state
51
+ start_event
52
+ end
53
+
54
+ # Changes or returns the setting for a parameter.
55
+ %w( interval timeout ).each do |setting|
56
+ class_eval %[
57
+ def #{setting}(value=nil)
58
+ return @#{setting} if value.nil?
59
+ @#{setting} = value unless @#{setting} == value
60
+ end
61
+ ]
62
+ end
63
+
64
+ # Sets the callback proc or runs the callback proc with the current state.
65
+ %w( pre_event event_loop error_handler ).each do |type|
66
+ class_eval %[
67
+ def #{type}(&blk)
68
+ if block_given?
69
+ @#{type} = blk
70
+ else
71
+ @#{type}.call(@state)
72
+ end
73
+ end
74
+ ]
75
+ end
76
+
77
+ # Begin the polling event.
78
+ def start_event
79
+ pre_event
80
+ @event_timer = EM.add_periodic_timer(interval) { event_loop }
81
+ @timeout_timer = EM::Timer.new(timeout) do
82
+ @event_timer.cancel
83
+ error_handler
84
+ end
85
+ end
86
+
87
+ # Helper for the event block to set notify the
88
+ def event_complete
89
+ @event_timer.cancel
90
+ @timeout_timer.cancel
91
+ end
92
+
93
+ private
94
+
95
+ # Initialize default values for the event.
96
+ def initialize_defaults
97
+ @timeout ||= 100
98
+ @interval ||= 2
99
+ @pre_event ||= Proc.new { |state| nil }
100
+ @event_loop ||= Proc.new { |state| nil }
101
+ @error_handler ||= Proc.new { |state| nil }
102
+ end
103
+
104
+ # Initialize the default values for the state of the event.
105
+ def initialize_state
106
+ @state[:node] = @node
107
+ @state[:interval] = interval
108
+ @state[:timeout] = timeout
109
+ end
110
+ end
@@ -1,27 +1,235 @@
1
- # The souffle cloud provider class.
2
- class Souffle::Provider
3
-
4
- # The setup method for the provider. Intended to be overridden.
5
- #
6
- # @raise [Souffle::Exceptions::Provider] This definition must be overridden.
7
- def setup
8
- error_msg = "#{self.to_s}: you must override setup"
9
- raise Souffle::Exceptions::Provider, error_msg
10
- end
1
+ require 'fileutils'
2
+ require 'tmpdir'
11
3
 
12
- # The name of the given provider. Intended to be overridden.
13
- #
14
- # @raise [Souffle::Exceptions::Provider] This definition must be overridden.
15
- def name
16
- error_msg = "#{self.to_s}: you must override name"
17
- raise Souffle::Exceptions::Provider, error_msg
18
- end
4
+ # A metal provider module (Describes AWS, Softlayer, etc).
5
+ module Souffle::Provider
6
+ # The souffle cloud provider class.
7
+ class Base
8
+ attr_accessor :system
9
+
10
+ # Initialize a new provider for a given system.
11
+ #
12
+ # @param [ Souffle::System ] system The system to provision.
13
+ def initialize(system=Souffle::System.new)
14
+ @system ||= system
15
+ create_ssh_dir_if_missing
16
+ end
17
+
18
+ # The name of the given provider.
19
+ #
20
+ # @return [ String ] The name of the given provider.
21
+ def name
22
+ self.class.name.split('::').last
23
+ end
24
+
25
+ # Wait until ssh is available for the node and then connect.
26
+ def boot(node, retries=50)
27
+ end
28
+
29
+ # Creates a system for a given provider. Intended to be overridden.
30
+ #
31
+ # @raise [Souffle::Exceptions::Provider] This definition must be
32
+ # overrridden.
33
+ #
34
+ # @param [ Souffle::System ] system The system to instantiate.
35
+ # @param [ String ] tag The tag to use for the system.
36
+ def create_system(system, tag="souffle")
37
+ error_msg = "#{self.class.to_s}: you must override create_system"
38
+ raise Souffle::Exceptions::Provider, error_msg
39
+ end
40
+
41
+ # Takes a node definition and begins the provisioning process.
42
+ #
43
+ # @param [ Souffle::Node ] node The node to instantiate.
44
+ # @param [ String ] tag The tag to use for the node.
45
+ def create_node(node, tag=nil)
46
+ error_msg = "#{self.class.to_s}: you must override create_node"
47
+ raise Souffle::Exceptions::Provider, error_msg
48
+ end
49
+
50
+ # Creates a raid array for a given provider. Intended to be overridden.
51
+ #
52
+ # @raise [Souffle::Exceptions::Provider] This definition must be
53
+ # overridden.
54
+ def create_raid
55
+ error_msg = "#{self.class.to_s}: you must override create_raid"
56
+ raise Souffle::Exceptions::Provider, error_msg
57
+ end
58
+
59
+ # Generates the json required for chef-solo to run on a node.
60
+ #
61
+ # @param [ Souffle::Node ] node The node to generate chef-solo json for.
62
+ #
63
+ # @return [ String ] The chef-solo json for the particular node.
64
+ def generate_chef_json(node)
65
+ json_info = Hash.new
66
+ json_info[:domain] = "souffle"
67
+ json_info.merge!(node.options[:attributes])
68
+ json_info[:run_list] = node.run_list
69
+ JSON.pretty_generate(json_info)
70
+ end
71
+
72
+ # Waits for ssh to be accessible for a node for the initial connection and
73
+ # yields an ssh object to manage the commands naturally from there.
74
+ #
75
+ # @param [ String ] address The address of the machine to connect to.
76
+ # @param [ String ] user The user to connect as.
77
+ # @param [ String, NilClass ] pass By default publickey and password auth
78
+ # will be attempted.
79
+ # @param [ Hash ] opts The options hash.
80
+ # @param [ Fixnum ] timeout The timeout for ssh boot.
81
+ # @option opts [ Hash ] :net_ssh Options to pass to Net::SSH,
82
+ # see Net::SSH.start
83
+ # @option opts [ Hash ] :timeout (TIMEOUT) default timeout for all
84
+ # #wait_for and #send_wait calls.
85
+ # @option opts [ Boolean ] :reconnect When disconnected reconnect.
86
+ #
87
+ # @yield [ Eventmachine::Ssh:Session ] The ssh session.
88
+ def wait_for_boot(address, user="root", pass=nil, opts={},
89
+ timeout=200)
90
+ Souffle::Log.info "Waiting for ssh for #{address}..."
91
+ is_booted = false
92
+ timer = EM::PeriodicTimer.new(EM::Ssh::Connection::TIMEOUT) do
93
+ opts[:password] = pass unless pass.nil?
94
+ opts[:paranoid] = false
95
+ EM::Ssh.start(address, user, opts) do |connection|
96
+ connection.errback { |err| nil }
97
+ connection.callback do |ssh|
98
+ is_booted = true
99
+ yield(ssh) if block_given?
100
+ ssh.close
101
+ end
102
+ end
103
+ end
104
+
105
+ EM::Timer.new(timeout) do
106
+ unless is_booted
107
+ Souffle::Log.error "SSH Boot timeout for #{address}..."
108
+ timer.cancel
109
+ end
110
+ end
111
+ end
19
112
 
20
- # Creates a raid array for a given provider. Intended to be overridden.
21
- #
22
- # @raise [Souffle::Exceptions::Provider] This definition must be overridden.
23
- def create_raid
24
- error_msg = "#{self.to_s}: you must override create_raid"
25
- raise Souffle::Exceptions::Provider, error_msg
113
+ # Yields an ssh object to manage the commands naturally from there.
114
+ #
115
+ # @param [ String ] address The address of the machine to connect to.
116
+ # @param [ String ] user The user to connect as.
117
+ # @param [ String, NilClass ] pass By default publickey and password auth
118
+ # will be attempted.
119
+ # @param [ Hash ] opts The options hash.
120
+ # @option opts [ Hash ] :net_ssh Options to pass to Net::SSH,
121
+ # see Net::SSH.start
122
+ # @option opts [ Hash ] :timeout (TIMEOUT) default timeout for all
123
+ # #wait_for and #send_wait calls.
124
+ # @option opts [ Boolean ] :reconnect When disconnected reconnect.
125
+ #
126
+ # @yield [ EventMachine::Ssh::Session ] The ssh session.
127
+ def ssh_block(address, user="root", pass=nil, opts={})
128
+ opts[:password] = pass unless pass.nil?
129
+ opts[:paranoid] = false
130
+ EM::Ssh.start(address, user, opts) do |connection|
131
+ connection.errback do |err|
132
+ Souffle::Log.error "SSH Error: #{err} (#{err.class})"
133
+ end
134
+ connection.callback { |ssh| yield(ssh) if block_given?; ssh.close }
135
+ end
136
+ end
137
+
138
+ # The path to the ssh key with the given name.
139
+ #
140
+ # @param [ String ] key_name The name fo the ssh key to lookup.
141
+ #
142
+ # @return [ String ] The path to the ssh key with the given name.
143
+ def ssh_key(key_name)
144
+ "#{ssh_key_path}/#{key_name}"
145
+ end
146
+
147
+ # Grabs an ssh key for a given aws node.
148
+ #
149
+ # @param [ String ] key_name The name fo the ssh key to lookup.
150
+ #
151
+ # @return [ Boolean ] Whether or not the ssh_key exists
152
+ # for the node.
153
+ def ssh_key_exists?(key_name)
154
+ File.exists? ssh_key(key_name)
155
+ end
156
+
157
+ # Creates the ssh directory for a given provider if it does not exist.
158
+ def create_ssh_dir_if_missing
159
+ FileUtils.mkdir_p(ssh_key_path) unless Dir.exists?(ssh_key_path)
160
+ rescue
161
+ error_msg = "The ssh key directory does not have write permissions: "
162
+ error_msg << ssh_key_path
163
+ raise PermissionErrorSshKeys, error_msg
164
+ end
165
+
166
+ # The path to the ssh keys for the provider.
167
+ #
168
+ # @return [ String ] The path to the ssh keys for the provider.
169
+ def ssh_key_path
170
+ File.join(File.dirname(
171
+ Souffle::Config[:config_file]), "ssh", name.downcase)
172
+ end
173
+
174
+ # Rsync's a file to a remote node.
175
+ #
176
+ # @param [ String ] ipaddress The ipaddress of the node to connect to.
177
+ # @param [ String ] file The file to rsync.
178
+ # @param [ String ] path The remote path to rsync.
179
+ def rsync_file(ipaddress, file, path='.')
180
+ ssh_command = "ssh -o UserKnownHostsFile=/dev/null "
181
+ ssh_command << "-o StrictHostKeyChecking=no -o LogLevel=quiet"
182
+ rsync_command = "rsync -qar -e \"#{ssh_command}\" "
183
+ rsync_command << "#{file} root@#{ipaddress}:#{path}"
184
+ if EM.reactor_running?
185
+ EM.system(rsync_command)
186
+ else
187
+ IO.popen(rsync_command)
188
+ end
189
+ end
190
+
191
+ # The list of cookbooks and their full paths.
192
+ #
193
+ # @return [ Array ] The list of cookbooks and their full paths.
194
+ def cookbook_paths
195
+ Array(Souffle::Config[:chef_cookbook_path]).inject([]) do |_paths, path|
196
+ Dir.glob("#{File.expand_path(path)}/*").each do |cb|
197
+ _paths << cb if File.directory? cb
198
+ end
199
+ _paths
200
+ end
201
+ end
202
+
203
+ # Creates a new cookbook tarball for the deployment.
204
+ #
205
+ # @return [ String ] The path to the created tarball.
206
+ def create_cookbooks_tarball
207
+ tarball_name = "cookbooks-latest.tar.gz"
208
+ temp_dir = File.join(Dir.tmpdir, "chef-cookbooks-latest")
209
+ temp_cookbook_dir = File.join(temp_dir, "cookbooks")
210
+ tarball_dir = "#{File.dirname(Souffle::Config[:config_file])}/tarballs"
211
+ tarball_path = File.join(tarball_dir, tarball_name)
212
+
213
+ FileUtils.mkdir_p(tarball_dir) unless File.exists?(tarball_dir)
214
+ FileUtils.mkdir_p(temp_dir) unless File.exists?(temp_dir)
215
+ FileUtils.mkdir(temp_cookbook_dir) unless File.exists?(temp_cookbook_dir)
216
+ cookbook_paths.each { |pkg| FileUtils.cp_r(pkg, temp_cookbook_dir) }
217
+
218
+ tar_command = "tar -C #{temp_dir} -czf #{tarball_path} ./cookbooks"
219
+ if EM.reactor_running?
220
+ EM::DeferrableChildProcess.open(tar_command) do
221
+ FileUtils.rm_rf temp_dir
222
+ end
223
+ else
224
+ Kernel.system(tar_command)
225
+ FileUtils.rm_rf temp_dir
226
+ end
227
+ tarball_path
228
+ end
26
229
  end
27
230
  end
231
+
232
+ _provider_dir = File.join(File.dirname(__FILE__), "provider")
233
+ Dir.glob("#{_provider_dir}/*").each do |s|
234
+ require "souffle/provider/#{File.basename(s)}"
235
+ end
@@ -1,16 +1,663 @@
1
- require 'souffle/provider'
1
+ require 'right_aws'
2
+ require 'securerandom'
3
+
4
+ require 'souffle/polling_event'
5
+
6
+ # Monkeypatch RightAws to support EBS delete on termination.
7
+ class RightAws::Ec2
8
+ def modify_block_device_delete_on_termination_attribute(instance_id,
9
+ device_name, delete_on_termination)
10
+ request_hash = {'InstanceId' => instance_id}
11
+ prefix = "BlockDeviceMapping.1"
12
+ request_hash["#{prefix}.DeviceName"] = device_name
13
+ request_hash["#{prefix}.Ebs.DeleteOnTermination"] = delete_on_termination
14
+ link = generate_request('ModifyInstanceAttribute', request_hash)
15
+ request_info(link, RightAws::RightBoolResponseParser.new(
16
+ :logger => @logger))
17
+ rescue Exception
18
+ on_exception
19
+ end
20
+ end
2
21
 
3
22
  # The AWS souffle provider.
4
- class Souffle::Provider::AWS < Souffle::Provider
23
+ class Souffle::Provider::AWS < Souffle::Provider::Base
24
+ attr_reader :access_key, :access_secret
5
25
 
6
26
  # Setup the internal AWS configuration and object.
7
- def setup
27
+ def initialize
28
+ super()
29
+ @access_key = @system.try_opt(:aws_access_key)
30
+ @access_secret = @system.try_opt(:aws_access_secret)
31
+ @newest_cookbooks = create_cookbooks_tarball
32
+
33
+ if Souffle::Config[:debug]
34
+ logger = Souffle::Log.logger
35
+ else
36
+ logger = Logger.new('/dev/null')
37
+ end
38
+
39
+ @ec2 = RightAws::Ec2.new(
40
+ @access_key, @access_secret,
41
+ :region => @system.try_opt(:aws_region),
42
+ :logger => logger)
43
+ rescue
44
+ raise Souffle::Exceptions::InvalidAwsKeys,
45
+ "AWS access keys are required to operate on EC2"
46
+ end
47
+
48
+ # Generates a prefixed unique tag.
49
+ #
50
+ # @param [ String ] tag_prefix The tag prefix to use.
51
+ #
52
+ # @return [ String ] The unique tag with prefix.
53
+ def generate_tag(tag_prefix="souffle")
54
+ "#{tag_prefix}-#{SecureRandom.hex(6)}"
55
+ end
56
+
57
+ # Creates a system using aws as the provider.
58
+ #
59
+ # @param [ Souffle::System ] system The system to instantiate.
60
+ # @param [ String ] tag_prefix The tag prefix to use for the system.
61
+ def create_system(system, tag_prefix="souffle")
62
+ system.options[:tag] = generate_tag(tag_prefix)
63
+ system.provisioner = Souffle::Provisioner::System.new(system, self)
64
+ system.provisioner.initialized
65
+ end
66
+
67
+ # Takes a list of nodes and returns the list of their aws instance_ids.
68
+ #
69
+ # @param [ Array ] nodes The list of nodes to get instance_id's from.
70
+ def instance_id_list(nodes)
71
+ Array(nodes).map { |n| n.options[:aws_instance_id] }
72
+ end
73
+
74
+ # Takes a node definition and begins the provisioning process.
75
+ #
76
+ # @param [ Souffle::Node ] node The node to instantiate.
77
+ # @param [ String ] tag The tag to use for the node.
78
+ def create_node(node, tag=nil)
79
+ opts = prepare_node_options(node)
80
+ node.options[:tag] = tag unless tag.nil?
81
+
82
+ create_ebs(node)
83
+ instance_info = @ec2.launch_instances(
84
+ node.try_opt(:aws_image_id), opts).first
85
+
86
+ node.options[:aws_instance_id] = instance_info[:aws_instance_id]
87
+ wait_until_node_running(node) { tag_node(node, node.try_opt(:tag)) }
88
+ end
89
+
90
+ # Tags a node and it's volumes.
91
+ #
92
+ # @param [ Souffle::Node ] node The node to tag.
93
+ # @param [ String ] tag The tag to use for the node.
94
+ def tag_node(node, tag="")
95
+ @ec2.create_tags(Array(node.options[:aws_instance_id]), {
96
+ :Name => node.name,
97
+ :souffle => tag
98
+ })
99
+ volume_ids = node.options[:volumes].map { |vol| vol[:aws_id] }
100
+ @ec2.create_tags(Array(volume_ids), {
101
+ :instance_id => node.options[:aws_instance_id],
102
+ :souffle => tag
103
+ }) unless Array(volume_ids).empty?
104
+ end
105
+
106
+ # Takes a list of nodes an stops the instances.
107
+ #
108
+ # @param [ Souffle::Node, Array ] nodes The list of nodes to stop.
109
+ def stop_nodes(nodes)
110
+ @ec2.stop_instances(instance_id_list(nodes))
111
+ end
112
+
113
+ # Stops all nodes in a given system.
114
+ #
115
+ # @param [ Souffle::System ] system The system to stop.
116
+ def stop_system(system)
117
+ stop_nodes(system.nodes)
118
+ end
119
+
120
+ # Takes a list of nodes and kills them. (Haha)
121
+ #
122
+ # @param [ Souffle::Node ] nodes The list of nodes to terminate.
123
+ def kill(nodes)
124
+ @ec2.terminate_instances(instance_id_list(nodes))
125
+ end
126
+
127
+ # Takes a list of nodes kills them and then recreates them.
128
+ #
129
+ # @param [ Souffle::Node ] nodes The list of nodes to kill and recreate.
130
+ def kill_and_recreate(nodes)
131
+ kill(nodes)
132
+ @provisioner.reclaimed
8
133
  end
9
-
10
- # The name of the given provider.
11
- def name; "AWS"; end
12
134
 
13
135
  # Creates a raid array with the given requirements.
14
- def create_raid
136
+ #
137
+ # @param [ Souffle::Node ] node The node to the raid for.
138
+ # @param [ Array ] devices The list of devices to use for the raid.
139
+ # @param [ Fixnum ] md_device The md device number.
140
+ # @param [ Fixnum ] chunk The chunk size in kilobytes.
141
+ # @param [ String ] level The raid level to use.
142
+ # options are: linear, raid0, 0, stipe, raid1, 1, mirror,
143
+ # raid4, 4, raid5, 5, raid6, 6, multipath, mp
144
+ def create_raid(node, devices=[], md_device=0, chunk=64, level="raid0")
145
+ dev_list = devices.map { |s| "#{s}1" }
146
+ mdadm_string = "/sbin/mdadm --create /dev/md#{md_device} "
147
+ mdadm_string << "--chunk=#{chunk} --level=#{level} "
148
+ mdadm_string << "--raid-devices=#{devices.size} #{dev_list.join(' ')}"
149
+
150
+ export_mdadm = "/sbin/mdadm --detail --scan > /etc/mdadm.conf"
151
+
152
+ ssh_block(node) do |ssh|
153
+ ssh.exec!(mdadm_string)
154
+ ssh.exec!(export_mdadm)
155
+ yield if block_given?
156
+ end
157
+ end
158
+
159
+ # Wait for the machine to boot up.
160
+ #
161
+ # @parameter [ Souffle::Node ] The node to boot up.
162
+ def boot(node)
163
+ wait_for_boot(node)
164
+ end
165
+
166
+ # Formats all of the devices on a given node for the provisioner interface.
167
+ #
168
+ # @param [ Souffle::Node ] node The node to format it's new partitions.
169
+ def format_device(node)
170
+ partition_device(node, "/dev/md0", "8e") do
171
+ _format_device(node, "/dev/md0p1")
172
+ end
173
+ end
174
+
175
+ # Formats a device on a given node with the provided filesystem.
176
+ #
177
+ # @param [ Souffle::Node ] node The node to format a device on.
178
+ # @param [ String ] device The device to format.
179
+ # @param [ String ] filesystem The filesystem to use when formatting.
180
+ def _format_device(node, device, filesystem="ext4")
181
+ return if node.options[:volumes].nil?
182
+ setup_lvm(node)
183
+ ssh_block(node) do |ssh|
184
+ ssh.exec!("#{fs_formatter(filesystem)} #{device}")
185
+ mount_lvm(node) { node.provisioner.device_formatted }
186
+ end
187
+ end
188
+
189
+ # Partition each of the volumes with raid for the node.
190
+ #
191
+ # @param [ Souffle::Node ] node The node to partition the volumes on.
192
+ # @param [ Fixnum ] iteration The current retry iteration.
193
+ def partition(node, iteration=0)
194
+ return node.provisioner.error_occurred if iteration == 3
195
+ Souffle::PollingEvent.new(node) do
196
+ timeout 30
197
+
198
+ pre_event do
199
+ @partitions = 0
200
+ @provider = node.provisioner.provider
201
+ node.options[:volumes].each_with_index do |volume, index|
202
+ @provider.partition_device(
203
+ node, @provider.volume_id_to_device(index)) do |count|
204
+ @partitions += count
205
+ end
206
+ end
207
+ end
208
+
209
+ event_loop do
210
+ if @partitions == node.options[:volumes].size
211
+ event_complete
212
+ node.provisioner.partitioned_device
213
+ end
214
+ end
215
+
216
+ error_handler do
217
+ error_msg = "#{node.log_prefix} Timeout during partitioning..."
218
+ Souffle::Log.error error_msg
219
+ @provider.partition(node, iteration+1)
220
+ end
221
+ end
222
+ end
223
+
224
+ # Partitions a device on a given node with the given partition_type.
225
+ #
226
+ # @note Currently this is a naive implementation and uses the full disk.
227
+ #
228
+ # @param [ Souffle::Node ] node The node to partition a device on.
229
+ # @param [ String ] device The device to partition.
230
+ # @param [ String ] partition_type The type of partition to create.
231
+ def partition_device(node, device, partition_type="fd")
232
+ partition_cmd = "echo \",,#{partition_type}\""
233
+ partition_cmd << "| /sbin/sfdisk #{device}"
234
+ ssh_block(node) do |ssh|
235
+ ssh.exec!("#{partition_cmd}")
236
+ yield(1) if block_given?
237
+ end
238
+ end
239
+
240
+ # Sets up the lvm partition for the raid devices.
241
+ #
242
+ # @param [ Souffle::Node ] node The node to setup lvm on.
243
+ def setup_lvm(node)
244
+ return if node.options[:volumes].nil?
245
+ ssh_block(node) do |ssh|
246
+ ssh.exec!("pvcreate /dev/md0p1")
247
+ ssh.exec!("vgcreate VolGroup00 /dev/md0p1")
248
+ ssh.exec!("lvcreate -l 100%vg VolGroup00 -n data")
249
+ end
250
+ end
251
+
252
+ # Mounts the newly created lvm configuration and adds it to fstab.
253
+ #
254
+ # @param [ Souffle::Node ] node The node to mount lvm on.
255
+ def mount_lvm(node)
256
+ fstab_str = "/dev/md0p1 /data"
257
+ fstab_str << " ext4 noatime,nodiratime 1 1"
258
+
259
+ mount_str = "mount -o rw,noatime,nodiratime"
260
+ mount_str << " /dev/mapper/VolGroup00-data /data"
261
+ ssh_block(node) do |ssh|
262
+ ssh.exec!("mkdir /data")
263
+ ssh.exec!(mount_str)
264
+ ssh.exec!("echo #{fstab_str} >> /etc/fstab")
265
+ ssh.exec!("echo #{fstab_str} >> /etc/mtab")
266
+ yield if block_given?
267
+ end
268
+ end
269
+
270
+ # Installs mdadm (multiple device administration) to manage raid.
271
+ #
272
+ # @param [ Souffle::Node ] node The node to install mdadm on.
273
+ def setup_mdadm(node)
274
+ ssh_block(node) do |ssh|
275
+ ssh.exec!("/usr/bin/yum install -y mdadm")
276
+ end
277
+ node.provisioner.mdadm_installed
278
+ end
279
+
280
+ # Sets up software raid for the given node.
281
+ #
282
+ # @param [ Souffle::Node ] node The node setup raid for.
283
+ def setup_raid(node)
284
+ volume_list = []
285
+ node.options[:volumes].each_with_index do |volume, index|
286
+ volume_list << volume_id_to_device(index)
287
+ end
288
+ create_raid(node, volume_list) { node.provisioner.raid_initialized }
289
+ end
290
+
291
+ # Creates ebs volumes for the given node.
292
+ #
293
+ # @param [ Souffle::Node ] node The node to create ebs volumes for.
294
+ #
295
+ # @return [ Array ] The list of created ebs volumes.
296
+ def create_ebs(node)
297
+ volumes = Array.new
298
+ node.options.fetch(:volume_count, 0).times do
299
+ volumes << @ec2.create_volume(
300
+ node.try_opt(:aws_snapshot_id),
301
+ node.try_opt(:aws_ebs_size),
302
+ node.try_opt(:aws_availability_zone) )
303
+ end
304
+ node.options[:volumes] = volumes
305
+ volumes
306
+ end
307
+
308
+ # Polls the EC2 instance information until it is in the running state.
309
+ #
310
+ # @param [ Souffle::Node ] node The node to wait until running on.
311
+ # @param [ Fixnum ] poll_timeout The maximum number of seconds to wait.
312
+ # @param [ Fixnum ] poll_interval The interval in seconds to poll EC2.
313
+ def wait_until_node_running(node, poll_timeout=100, poll_interval=2, &blk)
314
+ ec2 = @ec2; Souffle::PollingEvent.new(node) do
315
+ timeout poll_timeout
316
+ interval poll_interval
317
+
318
+ pre_event do
319
+ Souffle::Log.info "#{node.log_prefix} Waiting for node running..."
320
+ @provider = node.provisioner.provider
321
+ @blk = blk
322
+ end
323
+
324
+ event_loop do
325
+ instance = ec2.describe_instances(
326
+ node.options[:aws_instance_id]).first
327
+ if instance[:aws_state].downcase == "running"
328
+ event_complete
329
+ @blk.call unless @blk.nil?
330
+ @provider.wait_until_ebs_ready(node)
331
+ end
332
+ end
333
+
334
+ error_handler do
335
+ error_msg = "#{node.log_prefix} Wait for node running timeout..."
336
+ Souffle::Log.error error_msg
337
+ node.provisioner.error_occurred
338
+ end
339
+ end
340
+ end
341
+
342
+ # Polls the EBS volume status until they're ready then runs the given block.
343
+ #
344
+ # @param [ Souffle::Node ] node The node to wait for EBS on.
345
+ # @param [ Fixnum ] poll_timeout The maximum number of seconds to wait.
346
+ # @param [ Fixnum ] poll_interval The interval in seconds to poll EC2.
347
+ def wait_until_ebs_ready(node, poll_timeout=100, poll_interval=2)
348
+ ec2 = @ec2; Souffle::PollingEvent.new(node) do
349
+ timeout poll_timeout
350
+ interval poll_interval
351
+
352
+ pre_event do
353
+ Souffle::Log.info "#{node.log_prefix} Waiting for EBS to be ready..."
354
+ @provider = node.provisioner.provider
355
+ @volume_ids = node.options[:volumes].map { |v| v[:aws_id] }
356
+ end
357
+
358
+ event_loop do
359
+ vol_status = ec2.describe_volumes(@volume_ids)
360
+ avail = Array(vol_status).select { |v| v[:aws_status] == "available" }
361
+ if avail.size == vol_status.size
362
+ event_complete
363
+ @provider.attach_ebs(node)
364
+ node.provisioner.created
365
+ end
366
+ end
367
+
368
+ error_handler do
369
+ error_msg = "#{node.log_prefix} Waiting for EBS Timed out..."
370
+ Souffle::Log.error error_msg
371
+ node.provisioner.error_occurred
372
+ end
373
+ end
374
+ end
375
+
376
+ # Attaches ebs volumes to the given node.
377
+ #
378
+ # @param [ Souffle::Node ] node The node to attach ebs volumes onto.
379
+ def attach_ebs(node)
380
+ Souffle::Log.info "#{node.log_prefix} Attaching EBS..."
381
+ node.options[:volumes].each_with_index do |volume, index|
382
+ @ec2.attach_volume(
383
+ volume[:aws_id],
384
+ node.options[:aws_instance_id],
385
+ volume_id_to_aws_device(index) )
386
+ @ec2.modify_block_device_delete_on_termination_attribute(
387
+ node.options[:aws_instance_id],
388
+ volume_id_to_aws_device(index),
389
+ node.try_opt(:delete_on_termination) )
390
+ end
391
+ end
392
+
393
+ # Detach and delete all volumes from a given node.
394
+ #
395
+ # @param [ Souffle::Node ] node The node to destroy ebs volumes from.
396
+ def detach_and_delete_ebs(node)
397
+ detach_ebs(node, force=true)
398
+ delete_ebs(node)
399
+ end
400
+
401
+ # Detaches all ebs volumes from a given node.
402
+ #
403
+ # @param [ Souffle::Node ] node The node to detach volumes from.
404
+ # @param [ Boolean ] force Whether or not to force the
405
+ # detachment.
406
+ def detach_ebs(node, force=false)
407
+ node.options[:volumes].each_with_index do |volume, index|
408
+ @ec2.detach_volume(
409
+ volume[:aws_id],
410
+ node.options[:aws_instance_id],
411
+ volume_id_to_aws_device(index),
412
+ force)
413
+ end
414
+ end
415
+
416
+ # Deletes the ebs volumes from a given node.
417
+ #
418
+ # @param [ Souffle::Node ] node The node to delete volumes from.
419
+ def delete_ebs(node)
420
+ node.options[:volumes].each do |volume|
421
+ @ec2.delete_volume(volume[:aws_id])
422
+ end
423
+ end
424
+
425
+ # Whether or not to use a vpc instance and subnet for provisioning.
426
+ #
427
+ # @param [ Souffle::Node ] node The node to check vpc information for.
428
+ # @return [ Boolean ] Whether to use a vpc instance and
429
+ # specific subnet.
430
+ def using_vpc?(node)
431
+ !!node.try_opt(:aws_vpc_id) and
432
+ !!node.try_opt(:aws_subnet_id)
433
+ end
434
+
435
+ # Checks whether or not the vpc and subnet are setup proeprly.
436
+ #
437
+ # @param [ Souffle::Node ] node The node to check vpc information for.
438
+ #
439
+ # @return [ Boolean ] Whether or not the vpc is setup.
440
+ def vpc_setup?(node)
441
+ vpc_exists? and subnet_exists?
442
+ end
443
+
444
+ # Checks whether or not the vpc currently exists.
445
+ #
446
+ # @param [ Souffle::Node ] node The node to check vpc information for.
447
+ #
448
+ # @return [ Boolean ] Whether or not the vpc exists.
449
+ def vpc_exists?(node)
450
+ @ec2.describe_vpcs({:filters =>
451
+ { 'vpc-id' => node.try_opt(:aws_vpc_id) } }).any?
452
+ end
453
+
454
+ # Checks whether or not the subnet currently exists.
455
+ #
456
+ # @param [ Souffle::Node ] node The node to check vpc information for.
457
+ #
458
+ # @return [ Boolean ] Whether or not the subnet exists.
459
+ def subnet_exists?(node)
460
+ @ec2.describe_subnets({:filters =>
461
+ { 'subnet-id' => node.try_opt(:aws_subnet_id) } }).any?
462
+ end
463
+
464
+ # Provisions a node with the chef/chef-solo configuration.
465
+ #
466
+ # @todo Setup the chef/chef-solo tar gzip and ssh connections.
467
+ def provision(node)
468
+ if node.try_opt(:chef_provisioner) == :solo
469
+ provision_chef_solo(node, generate_chef_json(node))
470
+ else
471
+ provision_chef_client(node)
472
+ end
473
+ node.provisioner.provisioned
474
+ end
475
+
476
+ # Waits for ssh to be accessible for a node for the initial connection and
477
+ # yields an ssh object to manage the commands naturally from there.
478
+ #
479
+ # @param [ Souffle::Node ] node The node to run commands against.
480
+ # @param [ String ] user The user to connect as.
481
+ # @param [ String, NilClass ] pass By default publickey and password auth
482
+ # will be attempted.
483
+ # @param [ Hash ] opts The options hash.
484
+ # @param [ Fixnum ] poll_timeout The maximum number of seconds to wait.
485
+ # @param [ Fixnum ] iteration The current retry iteration.
486
+ #
487
+ # @option opts [ Hash ] :net_ssh Options to pass to Net::SSH,
488
+ # see Net::SSH.start
489
+ # @option opts [ Hash ] :timeout (TIMEOUT) default timeout for all #wait_for
490
+ # and #send_wait calls.
491
+ # @option opts [ Boolean ] :reconnect When disconnected reconnect.
492
+ #
493
+ # @yield [ Eventmachine::Ssh:Session ] The ssh session.
494
+ def wait_for_boot(node, user="root", pass=nil, opts={},
495
+ poll_timeout=100, iteration=0, &blk)
496
+ return node.provisioner.error_occurred if iteration == 3
497
+
498
+ ec2 = @ec2; Souffle::PollingEvent.new(node) do
499
+ timeout poll_timeout
500
+ interval EM::Ssh::Connection::TIMEOUT
501
+
502
+ pre_event do
503
+ Souffle::Log.info "#{node.log_prefix} Waiting for ssh..."
504
+ @provider = node.provisioner.provider
505
+ @blk = blk
506
+ end
507
+
508
+ event_loop do
509
+ n = ec2.describe_instances(node.options[:aws_instance_id]).first
510
+ unless n.nil?
511
+ key = n[:ssh_key_name]
512
+ if @provider.ssh_key_exists?(key)
513
+ opts[:keys] = @provider.ssh_key(key)
514
+ end
515
+ opts[:password] = pass unless pass.nil?
516
+ opts[:paranoid] = false
517
+ address = n[:private_ip_address]
518
+
519
+ EM::Ssh.start(address, user, opts) do |connection|
520
+ connection.errback { |err| nil }
521
+ connection.callback do |ssh|
522
+ event_complete
523
+ node.provisioner.booted
524
+ @blk.call(ssh) unless @blk.nil?
525
+ ssh.close
526
+ end
527
+ end
528
+ end
529
+ end
530
+
531
+ error_handler do
532
+ Souffle::Log.error "#{node.log_prefix} SSH Boot timeout..."
533
+ @provider.wait_for_boot(node, user, pass, opts,
534
+ poll_timeout, iteration+1, &blk)
535
+ end
536
+ end
537
+ end
538
+
539
+ # Provisions a box using the chef_solo provisioner.
540
+ #
541
+ # @param [ String ] ipaddress The ip address of the node to provision.
542
+ # @param [ String ] solo_json The chef solo json string to use.
543
+ def provision_chef_solo(node, solo_json)
544
+ rsync_file(node, @newest_cookbooks, "/tmp")
545
+ solo_config = "node_name \"#{node.name}.souffle\"\n"
546
+ solo_config << 'cookbook_path "/tmp/cookbooks"'
547
+ ssh_block(node) do |ssh|
548
+ ssh.exec!("sleep 2; tar -zxf /tmp/cookbooks-latest.tar.gz -C /tmp")
549
+ ssh.exec!("echo '#{solo_config}' >/tmp/solo.rb")
550
+ ssh.exec!("echo '#{solo_json}' >/tmp/solo.json")
551
+ ssh.exec!("chef-solo -c /tmp/solo.rb -j /tmp/solo.json")
552
+ rm_files = "/tmp/cookbooks /tmp/cookbooks-latest.tar.gz"
553
+ rm_files << " /tmp/solo.rb /tmp/solo.json > /tmp/chef_bootstrap"
554
+ ssh.exec!("rm -rf #{rm_files}")
555
+ end
556
+ end
557
+
558
+ # Provisions a box using the chef_client provisioner.
559
+ #
560
+ # @todo Chef client provisioner needs to be completed.
561
+ def provision_chef_client(node)
562
+ ssh_block(node) do |ssh|
563
+ ssh.exec!("chef-client")
564
+ end
565
+ end
566
+
567
+ # Rsync's a file to a remote node.
568
+ #
569
+ # @param [ Souffle::Node ] node The node to connect to.
570
+ # @param [ Souffle::Node ] file The file to rsync.
571
+ # @param [ Souffle::Node ] path The remote path to rsync.
572
+ def rsync_file(node, file, path='.')
573
+ n = @ec2.describe_instances(node.options[:aws_instance_id]).first
574
+ super(n[:private_ip_address], file, path)
575
+ end
576
+
577
+ # Yields an ssh object to manage the commands naturally from there.
578
+ #
579
+ # @param [ Souffle::Node ] node The node to run commands against.
580
+ # @param [ String ] user The user to connect as.
581
+ # @param [ String, NilClass ] pass By default publickey and password auth
582
+ # will be attempted.
583
+ # @param [ Hash ] opts The options hash.
584
+ # @option opts [ Hash ] :net_ssh Options to pass to Net::SSH,
585
+ # see Net::SSH.start
586
+ # @option opts [ Hash ] :timeout (TIMEOUT) default timeout for all #wait_for
587
+ # and #send_wait calls.
588
+ # @option opts [ Boolean ] :reconnect When disconnected reconnect.
589
+ #
590
+ # @yield [ EventMachine::Ssh::Session ] The ssh session.
591
+ def ssh_block(node, user="root", pass=nil, opts={})
592
+ n = @ec2.describe_instances(node.options[:aws_instance_id]).first
593
+ if n.nil?
594
+ raise AwsInstanceDoesNotExist,
595
+ "The AWS instance (#{node.options[:aws_instance_id]}) does not exist."
596
+ else
597
+ key = n[:ssh_key_name]
598
+ opts[:keys] = ssh_key(key) if ssh_key_exists?(key)
599
+ super(n[:private_ip_address], user, pass, opts)
600
+ end
601
+ end
602
+
603
+ # Prepares the node options using the system or global defaults.
604
+ #
605
+ # @param [ Souffle::Node ] node The node you wish to prepare options for.
606
+ #
607
+ # @return [ Hash ] The options hash to pass into ec2 launch instance.
608
+ def prepare_node_options(node)
609
+ opts = Hash.new
610
+ opts[:instance_type] = node.try_opt(:aws_instance_type)
611
+ opts[:min_count] = 1
612
+ opts[:max_count] = 1
613
+ if using_vpc?(node)
614
+ opts[:subnet_id] = node.try_opt(:aws_subnet_id)
615
+ opts[:aws_subnet_id] = node.try_opt(:aws_subnet_id)
616
+ opts[:aws_vpc_id] = Array(node.try_opt(:aws_vpc_id))
617
+ opts[:group_ids] = Array(node.try_opt(:group_ids))
618
+ else
619
+ opts[:group_names] = node.try_opt(:group_names)
620
+ end
621
+ opts[:key_name] = node.try_opt(:key_name)
622
+ opts
623
+ end
624
+
625
+ # Takes the volume count in the array and converts it to a device name.
626
+ #
627
+ # @note This starts at /dev/xvda and goes to /dev/xvdb, etc.
628
+ # And due to the special case on AWS, skips /dev/xvde.
629
+ #
630
+ # @param [ Fixnum ] volume_id The count in the array for the volume id.
631
+ #
632
+ # @return [ String ] The device string to mount to.
633
+ def volume_id_to_device(volume_id)
634
+ if volume_id >= 4
635
+ volume_id += 1
636
+ end
637
+ "/dev/xvd#{(volume_id + "a".ord).chr}"
638
+ end
639
+
640
+ # Takes the volume count in the array and converts it to a device name.
641
+ #
642
+ # @note This starts at /dev/xvda and goes to /dev/xvdb, etc.
643
+ # And due to the special case on AWS, skips /dev/xvde.
644
+ #
645
+ # @param [ Fixnum ] volume_id The count in the array for the volume id.
646
+ #
647
+ # @return [ String ] The device string to mount to.
648
+ def volume_id_to_aws_device(volume_id)
649
+ if volume_id >= 4
650
+ volume_id += 1
651
+ end
652
+ "/dev/hd#{(volume_id + "a".ord).chr}"
653
+ end
654
+
655
+ # Chooses the appropriate formatter for the given filesystem.
656
+ #
657
+ # @param [ String ] filesystem The filessytem you intend to use.
658
+ #
659
+ # @param [ String ] The filesystem formatter.
660
+ def fs_formatter(filesystem)
661
+ "mkfs.#{filesystem}"
15
662
  end
16
663
  end