ironfan 3.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.gitignore +51 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +130 -0
  4. data/Gemfile +26 -0
  5. data/LICENSE.md +201 -0
  6. data/README.md +328 -0
  7. data/Rakefile +104 -0
  8. data/TODO.md +16 -0
  9. data/VERSION +1 -0
  10. data/chefignore +41 -0
  11. data/cluster_chef-knife.gemspec +123 -0
  12. data/cluster_chef.gemspec +111 -0
  13. data/config/client.rb +59 -0
  14. data/config/proxy.pac +12 -0
  15. data/config/ubuntu10.04-ironfan.erb +157 -0
  16. data/config/ubuntu11.10-ironfan.erb +145 -0
  17. data/ironfan.gemspec +121 -0
  18. data/lib/chef/knife/bootstrap/ubuntu10.04-ironfan.erb +157 -0
  19. data/lib/chef/knife/bootstrap/ubuntu11.10-ironfan.erb +145 -0
  20. data/lib/chef/knife/cluster_bootstrap.rb +74 -0
  21. data/lib/chef/knife/cluster_kick.rb +94 -0
  22. data/lib/chef/knife/cluster_kill.rb +73 -0
  23. data/lib/chef/knife/cluster_launch.rb +164 -0
  24. data/lib/chef/knife/cluster_list.rb +50 -0
  25. data/lib/chef/knife/cluster_proxy.rb +126 -0
  26. data/lib/chef/knife/cluster_show.rb +61 -0
  27. data/lib/chef/knife/cluster_ssh.rb +141 -0
  28. data/lib/chef/knife/cluster_start.rb +40 -0
  29. data/lib/chef/knife/cluster_stop.rb +43 -0
  30. data/lib/chef/knife/cluster_sync.rb +77 -0
  31. data/lib/chef/knife/generic_command.rb +66 -0
  32. data/lib/chef/knife/knife_common.rb +195 -0
  33. data/lib/ironfan.rb +143 -0
  34. data/lib/ironfan/chef_layer.rb +299 -0
  35. data/lib/ironfan/cloud.rb +412 -0
  36. data/lib/ironfan/cluster.rb +118 -0
  37. data/lib/ironfan/compute.rb +153 -0
  38. data/lib/ironfan/deprecated.rb +33 -0
  39. data/lib/ironfan/discovery.rb +177 -0
  40. data/lib/ironfan/dsl_object.rb +124 -0
  41. data/lib/ironfan/facet.rb +144 -0
  42. data/lib/ironfan/fog_layer.rb +150 -0
  43. data/lib/ironfan/private_key.rb +130 -0
  44. data/lib/ironfan/role_implications.rb +58 -0
  45. data/lib/ironfan/security_group.rb +119 -0
  46. data/lib/ironfan/server.rb +281 -0
  47. data/lib/ironfan/server_slice.rb +260 -0
  48. data/lib/ironfan/volume.rb +157 -0
  49. data/spec/ironfan/cluster_spec.rb +13 -0
  50. data/spec/ironfan/facet_spec.rb +69 -0
  51. data/spec/ironfan/server_slice_spec.rb +19 -0
  52. data/spec/ironfan/server_spec.rb +112 -0
  53. data/spec/ironfan_spec.rb +193 -0
  54. data/spec/spec_helper.rb +50 -0
  55. data/spec/spec_helper/dummy_chef.rb +25 -0
  56. data/spec/test_config.rb +20 -0
  57. data/tasks/chef_config.rake +38 -0
  58. data/tasks/jeweler_use_alt_branch.rake +53 -0
  59. metadata +217 -0
@@ -0,0 +1,260 @@
1
+ module Ironfan
2
+ #
3
+ # A server group is a set of actual or implied servers.
4
+ #
5
+ # The idea is we want to be able to smoothly roll up settings
6
+ #
7
+ #
8
+ class ServerSlice < Ironfan::DslObject
9
+ attr_reader :name, :servers, :cluster
10
+
11
+ def initialize cluster, servers
12
+ super()
13
+ @name = "#{cluster.name} slice"
14
+ @cluster = cluster
15
+ @servers = servers
16
+ end
17
+
18
+ #
19
+ # Enumerable
20
+ #
21
+ include Enumerable
22
+ def each(&block)
23
+ @servers.each(&block)
24
+ end
25
+ def length
26
+ @servers.length
27
+ end
28
+ def empty?
29
+ length == 0
30
+ end
31
+ [:select, :find_all, :reject, :detect, :find, :drop_while].each do |method|
32
+ define_method(method) do |*args, &block|
33
+ ServerSlice.new cluster, @servers.send(method, *args, &block)
34
+ end
35
+ end
36
+
37
+ # Return the collection of servers that are not yet 'created'
38
+ def uncreated_servers
39
+ select{|svr| not svr.created? }
40
+ end
41
+
42
+ def bogus_servers
43
+ select(&:bogus?)
44
+ end
45
+
46
+ #
47
+ # Info!
48
+ #
49
+
50
+ def chef_nodes
51
+ servers.map(&:chef_node).compact
52
+ end
53
+
54
+ def fog_servers
55
+ servers.map(&:fog_server).compact
56
+ end
57
+
58
+ def security_groups
59
+ sg = {}
60
+ servers.each{|svr| sg.merge!(svr.cloud.security_groups) }
61
+ sg
62
+ end
63
+
64
+ def facets
65
+ servers.map(&:facet)
66
+ end
67
+
68
+ def chef_roles
69
+ [ cluster.chef_roles, facets.map(&:chef_roles) ].flatten.compact.uniq
70
+ end
71
+
72
+ # hack -- take the ssh_identity_file from the first server.
73
+ def ssh_identity_file
74
+ return if servers.empty?
75
+ servers.first.cloud.ssh_identity_file
76
+ end
77
+
78
+ #
79
+ # Actions!
80
+ #
81
+
82
+ def start
83
+ delegate_to_fog_servers( :start )
84
+ delegate_to_fog_servers( :reload )
85
+ end
86
+
87
+ def stop
88
+ delegate_to_fog_servers( :stop )
89
+ delegate_to_fog_servers( :reload )
90
+ end
91
+
92
+ def destroy
93
+ delegate_to_fog_servers( :destroy )
94
+ delegate_to_fog_servers( :reload )
95
+ end
96
+
97
+ def reload
98
+ delegate_to_fog_servers( :reload )
99
+ end
100
+
101
+ def create_servers
102
+ delegate_to_servers( :create_server )
103
+ end
104
+
105
+ def delete_chef
106
+ delegate_to_servers( :delete_chef, true )
107
+ end
108
+
109
+ def sync_to_cloud
110
+ sync_keypairs
111
+ sync_security_groups
112
+ delegate_to_servers( :sync_to_cloud )
113
+ end
114
+
115
+ def sync_to_chef
116
+ sync_roles
117
+ delegate_to_servers( :sync_to_chef )
118
+ end
119
+
120
+ #
121
+ # Display!
122
+ #
123
+
124
+ # FIXME: this is a jumble. we need to pass it in some other way.
125
+
126
+ MINIMAL_HEADINGS = ["Name", "Chef?", "State", "InstanceID", "Public IP", "Private IP", "Created At"].to_set.freeze
127
+ DEFAULT_HEADINGS = (MINIMAL_HEADINGS + ['Flavor', 'AZ', 'Env']).freeze
128
+ EXPANDED_HEADINGS = DEFAULT_HEADINGS + ['Image', 'Volumes', 'Elastic IP', 'SSH Key']
129
+
130
+ MACHINE_STATE_COLORS = {
131
+ 'running' => :green,
132
+ 'pending' => :yellow,
133
+ 'stopping' => :magenta,
134
+ 'shutting-down' => :magenta,
135
+ 'stopped' => :cyan,
136
+ 'terminated' => :blue,
137
+ 'not running' => :blue,
138
+ }
139
+
140
+ #
141
+ # This is a generic display routine for cluster-like sets of nodes. If you
142
+ # call it with no args, you get the basic table that knife cluster show
143
+ # draws. If you give it an array of strings, you can override the order and
144
+ # headings displayed. If you also give it a block you can add your own logic
145
+ # for generating content. The block is given a Ironfan::Server instance
146
+ # for each item in the collection and should return a hash of Name,Value
147
+ # pairs to merge into the minimal fields.
148
+ #
149
+ def display hh = :default
150
+ headings =
151
+ case hh
152
+ when :minimal then MINIMAL_HEADINGS
153
+ when :default then DEFAULT_HEADINGS
154
+ when :expanded then EXPANDED_HEADINGS
155
+ else hh.to_set end
156
+ headings += ["Bogus"] if servers.any?(&:bogus?)
157
+ # probably not necessary any more
158
+ # servers = servers.sort{ |a,b| (a.facet_name <=> b.facet_name) *9 + (a.facet_index.to_i <=> b.facet_index.to_i)*3 + (a.facet_index <=> b.facet_index) }
159
+ defined_data = servers.map do |svr|
160
+ hsh = {
161
+ "Name" => svr.fullname,
162
+ "Facet" => svr.facet_name,
163
+ "Index" => svr.facet_index,
164
+ "Chef?" => (svr.chef_node? ? "yes" : "[red]no[reset]"),
165
+ "Bogus" => (svr.bogus? ? "[red]#{svr.bogosity}[reset]" : ''),
166
+ "Env" => svr.environment,
167
+ }
168
+ # if (cs = svr.chef_server)
169
+ # hsh.merge!(
170
+ # "Env" => cs.environment,
171
+ # )
172
+ # end
173
+ if (fs = svr.fog_server)
174
+ hsh.merge!(
175
+ "InstanceID" => (fs.id && fs.id.length > 0) ? fs.id : "???",
176
+ "Flavor" => fs.flavor_id,
177
+ "Image" => fs.image_id,
178
+ "AZ" => fs.availability_zone,
179
+ "SSH Key" => fs.key_name,
180
+ "State" => "[#{MACHINE_STATE_COLORS[fs.state] || 'white'}]#{fs.state}[reset]",
181
+ "Public IP" => fs.public_ip_address,
182
+ "Private IP" => fs.private_ip_address,
183
+ "Created At" => fs.created_at.strftime("%Y%m%d-%H%M%S")
184
+ )
185
+ else
186
+ hsh["State"] = "not running"
187
+ end
188
+ hsh['Volumes'] = []
189
+ svr.composite_volumes.each do |name, vol|
190
+ if vol.ephemeral_device? then next
191
+ elsif vol.volume_id then hsh['Volumes'] << vol.volume_id
192
+ elsif vol.create_at_launch? then hsh['Volumes'] << vol.snapshot_id
193
+ end
194
+ end
195
+ hsh['Volumes'] = hsh['Volumes'].join(',')
196
+ hsh['Elastic IP'] = svr.cloud.public_ip if svr.cloud.public_ip
197
+ if block_given?
198
+ extra_info = yield(svr)
199
+ hsh.merge!(extra_info)
200
+ headings += extra_info.keys
201
+ end
202
+ hsh
203
+ end
204
+ if defined_data.empty?
205
+ ui.info "Nothing to report"
206
+ else
207
+ Formatador.display_compact_table(defined_data, headings.to_a)
208
+ end
209
+ end
210
+
211
+ def to_s
212
+ str = super
213
+ str[0..-2] + " #{@servers.map(&:fullname)}>"
214
+ end
215
+
216
+ def joined_names
217
+ map(&:name).join(", ").gsub(/, ([^,]*)$/, ' and \1')
218
+ end
219
+
220
+ # Calls block on each server in parallel, each in its own thread
221
+ #
222
+ # @example
223
+ # target = Ironfan::Cluster.slice('web_server')
224
+ # target.parallelize{|svr| svr.launch }
225
+ #
226
+ # @yield each server, in turn
227
+ #
228
+ # @return array (in same order as servers) of each block's result
229
+ def parallelize
230
+ servers.map do |svr|
231
+ sleep(0.1) # avoid hammering with simultaneous requests
232
+ Thread.new(svr){|svr| yield(svr) }
233
+ end
234
+ end
235
+
236
+ protected
237
+
238
+ # Helper methods for iterating through the servers to do things
239
+ #
240
+ # @param [Symbol] method -- method to call on each server
241
+ # @param [Boolean] threaded -- execute each call in own thread
242
+ #
243
+ # @return array (in same order as servers) of results for that method
244
+ def delegate_to_servers method, threaded = true
245
+ if threaded # Call in threads
246
+ threads = parallelize{|svr| svr.send(method) }
247
+ threads.map{|t| t.join.value } # Wait, returning array of results
248
+ else # Call the method for each server sequentially
249
+ servers.map{|svr| svr.send(method) }
250
+ end
251
+ end
252
+
253
+ def delegate_to_fog_servers method
254
+ fog_servers.compact.map do |fs|
255
+ fs.send(method)
256
+ end
257
+ end
258
+
259
+ end
260
+ end
@@ -0,0 +1,157 @@
1
+ module Ironfan
2
+ #
3
+ # Internal or external storage
4
+ #
5
+ class Volume < Ironfan::DslObject
6
+ attr_reader :parent
7
+ attr_accessor :fog_volume
8
+ has_keys(
9
+ :name,
10
+ # mountable volume attributes
11
+ :device, :mount_point, :mount_options, :fstype, :mount_dump, :mount_pass,
12
+ :mountable, :formattable, :resizable, :in_raid,
13
+ # cloud volume attributes
14
+ :attachable, :create_at_launch, :volume_id, :snapshot_id, :size, :keep, :availability_zone,
15
+ # arbitrary tags
16
+ :tags
17
+ )
18
+
19
+ VOLUME_DEFAULTS = {
20
+ :fstype => 'xfs',
21
+ :mount_options => 'defaults,nouuid,noatime',
22
+ :keep => true,
23
+ :attachable => :ebs,
24
+ :create_at_launch => false,
25
+ #
26
+ :mountable => true,
27
+ :resizable => false,
28
+ :formattable => false,
29
+ :in_raid => false,
30
+ }
31
+
32
+ # Snapshot for snapshot_name method.
33
+ # Set your own by adding
34
+ #
35
+ # VOLUME_IDS = Mash.new unless defined?(VOLUME_IDS)
36
+ # VOLUME_IDS.merge!({ :your_id => 'snap-whatever' })
37
+ #
38
+ # to your organization's knife.rb
39
+ #
40
+ VOLUME_IDS = Mash.new unless defined?(VOLUME_IDS)
41
+ VOLUME_IDS.merge!({
42
+ :blank_xfs => 'snap-d9c1edb1',
43
+ })
44
+
45
+ # Describes a volume
46
+ #
47
+ # @example
48
+ # Ironfan::Volume.new( :name => 'redis',
49
+ # :device => '/dev/sdp', :mount_point => '/data/redis', :fstype => 'xfs', :mount_options => 'defaults,nouuid,noatime'
50
+ # :size => 1024, :snapshot_id => 'snap-66494a08', :volume_id => 'vol-12312',
51
+ # :tags => {}, :keep => true )
52
+ #
53
+ def initialize attrs={}
54
+ @parent = attrs.delete(:parent)
55
+ super(attrs)
56
+ @settings[:tags] ||= {}
57
+ end
58
+
59
+ # human-readable description for logging messages and such
60
+ def desc
61
+ "#{name} on #{parent.fullname} (#{volume_id} @ #{device})"
62
+ end
63
+
64
+ def defaults
65
+ self.configure(VOLUME_DEFAULTS)
66
+ end
67
+
68
+ def ephemeral_device?
69
+ volume_id =~ /^ephemeral/
70
+ end
71
+
72
+ # Named snapshots, as defined in Ironfan::Volume::VOLUME_IDS
73
+ def snapshot_name(name)
74
+ snap_id = VOLUME_IDS[name.to_sym]
75
+ raise "Unknown snapshot name #{name} - is it defined in Ironfan::Volume::VOLUME_IDS?" unless snap_id
76
+ self.snapshot_id(snap_id)
77
+ end
78
+
79
+ # With snapshot specified but volume missing, have it auto-created at launch
80
+ #
81
+ # Be careful with this -- you can end up with multiple volumes claiming to
82
+ # be the same thing.
83
+ #
84
+ def create_at_launch?
85
+ volume_id.blank? && self.create_at_launch
86
+ end
87
+
88
+ def in_cloud?
89
+ !! fog_volume
90
+ end
91
+
92
+ def has_server?
93
+ in_cloud? && fog_volume.server_id.present?
94
+ end
95
+
96
+ def reverse_merge!(other_hsh)
97
+ super(other_hsh)
98
+ self.tags.reverse_merge!(other_hsh.tags) if other_hsh.respond_to?(:tags) && other_hsh.tags.present?
99
+ self
100
+ end
101
+
102
+ # An array of hashes with dorky-looking keys, just like Fog wants it.
103
+ def block_device_mapping
104
+ hsh = { 'DeviceName' => device }
105
+ if ephemeral_device?
106
+ hsh['VirtualName'] = volume_id
107
+ elsif create_at_launch?
108
+ raise "Must specify a size or a snapshot ID for #{self}" if snapshot_id.blank? && size.blank?
109
+ hsh['Ebs.SnapshotId'] = snapshot_id if snapshot_id.present?
110
+ hsh['Ebs.VolumeSize'] = size.to_s if size.present?
111
+ hsh['Ebs.DeleteOnTermination'] = (! keep).to_s
112
+ else
113
+ return
114
+ end
115
+ hsh
116
+ end
117
+
118
+ end
119
+
120
+
121
+ #
122
+ # Consider raising the chunk size to 256 and setting read_ahead 65536 if you are raid'ing EBS volumes
123
+ #
124
+ # * http://victortrac.com/EC2_Ephemeral_Disks_vs_EBS_Volumes
125
+ # * http://orion.heroku.com/past/2009/7/29/io_performance_on_ebs/
126
+ # * http://tech.blog.greplin.com/aws-best-practices-and-benchmarks
127
+ # * http://stu.mp/2009/12/disk-io-and-throughput-benchmarks-on-amazons-ec2.html
128
+ #
129
+ class RaidGroup < Volume
130
+ has_keys(
131
+ :sub_volumes, # volumes that comprise this raid group
132
+ :level, # RAID level (http://en.wikipedia.org/wiki/RAID#Standard_levels)
133
+ :chunk, # Raid chunk size (https://raid.wiki.kernel.org/articles/r/a/i/RAID_setup_cbb2.html)
134
+ :read_ahead, # read-ahead buffer
135
+ )
136
+
137
+ def desc
138
+ "#{name} on #{parent.fullname} (#{volume_id} @ #{device} from #{sub_volumes.join(',')})"
139
+ end
140
+
141
+ def defaults()
142
+ super
143
+ fstype 'xfs'
144
+ mount_options "defaults,nobootwait,noatime,nouuid,comment=ironfan"
145
+ attachable false
146
+ create_at_launch false
147
+ #
148
+ mountable true
149
+ resizable false
150
+ formattable true
151
+ #
152
+ in_raid false
153
+ #
154
+ sub_volumes []
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,13 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ require IRONFAN_DIR("lib/ironfan")
4
+
5
+ describe Ironfan::Cluster do
6
+ describe 'discover!' do
7
+ let(:cluster){ get_example_cluster(:monkeyballs) }
8
+
9
+ it 'enumerates chef nodes' do
10
+ cluster.discover!
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,69 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ require IRONFAN_DIR("lib/ironfan")
4
+
5
+ describe Ironfan::Facet do
6
+ let(:cluster){ Ironfan.cluster(:gibbon) }
7
+ let(:facet){
8
+ cluster.facet(:namenode) do
9
+ instances 5
10
+ end
11
+ }
12
+
13
+ describe 'slicing' do
14
+ it 'has servers' do
15
+ facet.indexes.should == [0, 1, 2, 3, 4]
16
+ facet.valid_indexes.should == [0, 1, 2, 3, 4]
17
+ facet.server(3){ name(:bob) }
18
+ svrs = facet.servers
19
+ svrs.length.should == 5
20
+ svrs.map{|svr| svr.name }.should == ["gibbon-namenode-0", "gibbon-namenode-1", "gibbon-namenode-2", :bob, "gibbon-namenode-4"]
21
+ end
22
+
23
+ it 'servers have bogosity if out of range' do
24
+ facet.server(69).should be_bogus
25
+ facet.servers.select(&:bogus?).map(&:facet_index).should == [69]
26
+ facet.indexes.should == [0, 1, 2, 3, 4, 69]
27
+ facet.valid_indexes.should == [0, 1, 2, 3, 4]
28
+ end
29
+
30
+ it 'returns all on nil or "", but [] means none' do
31
+ facet.server(69)
32
+ facet.slice('' ).map(&:facet_index).should == [0, 1, 2, 3, 4, 69]
33
+ facet.slice(nil).map(&:facet_index).should == [0, 1, 2, 3, 4, 69]
34
+ facet.slice([] ).map(&:facet_index).should == []
35
+ end
36
+
37
+ it 'slice returns all by default' do
38
+ facet.server(69)
39
+ facet.slice().map(&:facet_index).should == [0, 1, 2, 3, 4, 69]
40
+ end
41
+
42
+ it 'with an array returns specified indexes (bogus or not) in sorted order' do
43
+ facet.server(69)
44
+ facet.slice( [3, 1, 0] ).map(&:facet_index).should == [0, 1, 3]
45
+ facet.slice( [3, 1, 69, 0] ).map(&:facet_index).should == [0, 1, 3, 69]
46
+ end
47
+
48
+ it 'with an array does not create new dummy servers' do
49
+ facet.server(69)
50
+ facet.slice( [3, 1, 69, 0, 75, 123] ).map(&:facet_index).should == [0, 1, 3, 69]
51
+ facet.has_server?(75).should be_false
52
+ facet.has_server?(69).should be_true
53
+ end
54
+
55
+ it 'with a string, converts to intervals' do
56
+ facet.slice('1' ).map(&:facet_index).should == [1]
57
+ facet.slice('5' ).map(&:facet_index).should == []
58
+ facet.slice('1-1' ).map(&:facet_index).should == [1]
59
+ facet.slice('0-1' ).map(&:facet_index).should == [0,1]
60
+ facet.slice('0-1,3-4').map(&:facet_index).should == [0,1,3,4]
61
+ facet.slice('0-1,69' ).map(&:facet_index).should == [0,1,69]
62
+ facet.slice('0-2,1-3').map(&:facet_index).should == [0,1,2,3]
63
+ facet.slice('3-1' ).map(&:facet_index).should == []
64
+ facet.slice('2-5' ).map(&:facet_index).should == [2,3,4]
65
+ facet.slice(1).map(&:facet_index).should == [1]
66
+ end
67
+
68
+ end
69
+ end