ironfan 3.1.0.rc1
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/.gitignore +51 -0
- data/.rspec +3 -0
- data/CHANGELOG.md +130 -0
- data/Gemfile +26 -0
- data/LICENSE.md +201 -0
- data/README.md +328 -0
- data/Rakefile +104 -0
- data/TODO.md +16 -0
- data/VERSION +1 -0
- data/chefignore +41 -0
- data/cluster_chef-knife.gemspec +123 -0
- data/cluster_chef.gemspec +111 -0
- data/config/client.rb +59 -0
- data/config/proxy.pac +12 -0
- data/config/ubuntu10.04-ironfan.erb +157 -0
- data/config/ubuntu11.10-ironfan.erb +145 -0
- data/ironfan.gemspec +121 -0
- data/lib/chef/knife/bootstrap/ubuntu10.04-ironfan.erb +157 -0
- data/lib/chef/knife/bootstrap/ubuntu11.10-ironfan.erb +145 -0
- data/lib/chef/knife/cluster_bootstrap.rb +74 -0
- data/lib/chef/knife/cluster_kick.rb +94 -0
- data/lib/chef/knife/cluster_kill.rb +73 -0
- data/lib/chef/knife/cluster_launch.rb +164 -0
- data/lib/chef/knife/cluster_list.rb +50 -0
- data/lib/chef/knife/cluster_proxy.rb +126 -0
- data/lib/chef/knife/cluster_show.rb +61 -0
- data/lib/chef/knife/cluster_ssh.rb +141 -0
- data/lib/chef/knife/cluster_start.rb +40 -0
- data/lib/chef/knife/cluster_stop.rb +43 -0
- data/lib/chef/knife/cluster_sync.rb +77 -0
- data/lib/chef/knife/generic_command.rb +66 -0
- data/lib/chef/knife/knife_common.rb +195 -0
- data/lib/ironfan.rb +143 -0
- data/lib/ironfan/chef_layer.rb +299 -0
- data/lib/ironfan/cloud.rb +412 -0
- data/lib/ironfan/cluster.rb +118 -0
- data/lib/ironfan/compute.rb +153 -0
- data/lib/ironfan/deprecated.rb +33 -0
- data/lib/ironfan/discovery.rb +177 -0
- data/lib/ironfan/dsl_object.rb +124 -0
- data/lib/ironfan/facet.rb +144 -0
- data/lib/ironfan/fog_layer.rb +150 -0
- data/lib/ironfan/private_key.rb +130 -0
- data/lib/ironfan/role_implications.rb +58 -0
- data/lib/ironfan/security_group.rb +119 -0
- data/lib/ironfan/server.rb +281 -0
- data/lib/ironfan/server_slice.rb +260 -0
- data/lib/ironfan/volume.rb +157 -0
- data/spec/ironfan/cluster_spec.rb +13 -0
- data/spec/ironfan/facet_spec.rb +69 -0
- data/spec/ironfan/server_slice_spec.rb +19 -0
- data/spec/ironfan/server_spec.rb +112 -0
- data/spec/ironfan_spec.rb +193 -0
- data/spec/spec_helper.rb +50 -0
- data/spec/spec_helper/dummy_chef.rb +25 -0
- data/spec/test_config.rb +20 -0
- data/tasks/chef_config.rake +38 -0
- data/tasks/jeweler_use_alt_branch.rake +53 -0
- metadata +217 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ironfan
|
2
|
+
ComputeBuilder.class_eval do
|
3
|
+
|
4
|
+
# organization-wide security group
|
5
|
+
role_implication "systemwide" do
|
6
|
+
self.cloud.security_group "systemwide" do
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# NFS server allows access from nfs_clients
|
11
|
+
role_implication "nfs_server" do
|
12
|
+
self.cloud.security_group "nfs_server" do
|
13
|
+
authorize_group "nfs_client"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
role_implication "nfs_client" do
|
18
|
+
self.cloud.security_group "nfs_client"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Opens port 22 to the world
|
22
|
+
role_implication "ssh" do
|
23
|
+
self.cloud.security_group 'ssh' do
|
24
|
+
authorize_port_range 22..22
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Open the Chef server API port (4000) and the webui (4040)
|
29
|
+
role_implication "chef_server" do
|
30
|
+
self.cloud.security_group "chef_server" do
|
31
|
+
authorize_port_range 4000..4000 # chef-server-api
|
32
|
+
authorize_port_range 4040..4040 # chef-server-webui
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# web server? add the group "web_server" to open the web holes
|
37
|
+
role_implication "web_server" do
|
38
|
+
self.cloud.security_group("#{cluster_name}-web_server") do
|
39
|
+
authorize_port_range 80..80
|
40
|
+
authorize_port_range 443..443
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# if you're a redis server, open the port and authorize redis clients in your group to talk to you
|
45
|
+
role_implication("redis_server") do
|
46
|
+
cluster_name = self.cluster_name # hack: put cluster_name is in scope
|
47
|
+
self.cloud.security_group("#{cluster_name}-redis_server") do
|
48
|
+
authorize_group("#{cluster_name}-redis_client")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# redis_clients gain rights to the redis_server
|
53
|
+
role_implication("redis_client") do
|
54
|
+
self.cloud.security_group("#{cluster_name}-redis_client")
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module Ironfan
|
2
|
+
module Cloud
|
3
|
+
|
4
|
+
class SecurityGroup < DslObject
|
5
|
+
has_keys :name, :description, :owner_id
|
6
|
+
attr_reader :group_authorizations
|
7
|
+
attr_reader :range_authorizations
|
8
|
+
|
9
|
+
def initialize cloud, group_name, group_description=nil, group_owner_id=nil
|
10
|
+
super()
|
11
|
+
set :name, group_name.to_s
|
12
|
+
description group_description || "ironfan generated group #{group_name}"
|
13
|
+
@cloud = cloud
|
14
|
+
@group_authorizations = []
|
15
|
+
@group_authorized_by = []
|
16
|
+
@range_authorizations = []
|
17
|
+
owner_id(group_owner_id || Chef::Config[:knife][:aws_account_id])
|
18
|
+
end
|
19
|
+
|
20
|
+
@@all = nil
|
21
|
+
def all
|
22
|
+
self.class.all
|
23
|
+
end
|
24
|
+
def self.all
|
25
|
+
return @@all if @@all
|
26
|
+
get_all
|
27
|
+
end
|
28
|
+
def self.get_all
|
29
|
+
groups_list = Ironfan.fog_connection.security_groups.all
|
30
|
+
@@all = groups_list.inject(Mash.new) do |hsh, fog_group|
|
31
|
+
hsh[fog_group.name] = fog_group ; hsh
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def get
|
36
|
+
all[name] || Ironfan.fog_connection.security_groups.get(name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.get_or_create(group_name, description)
|
40
|
+
# FIXME: the '|| Ironfan.fog' part is probably unnecessary
|
41
|
+
fog_group = all[group_name] || Ironfan.fog_connection.security_groups.get(group_name)
|
42
|
+
unless fog_group
|
43
|
+
self.step(group_name, "creating (#{description})", :green)
|
44
|
+
fog_group = all[group_name] = Ironfan.fog_connection.security_groups.new(:name => group_name, :description => description, :connection => Ironfan.fog_connection)
|
45
|
+
fog_group.save
|
46
|
+
end
|
47
|
+
fog_group
|
48
|
+
end
|
49
|
+
|
50
|
+
def authorize_group(group_name, owner_id=nil)
|
51
|
+
@group_authorizations << [group_name.to_s, owner_id]
|
52
|
+
end
|
53
|
+
|
54
|
+
def authorized_by_group(other_name)
|
55
|
+
@group_authorized_by << other_name.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def authorize_port_range(range, cidr_ip = '0.0.0.0/0', ip_protocol = 'tcp')
|
59
|
+
range = (range .. range) if range.is_a?(Integer)
|
60
|
+
@range_authorizations << [range, cidr_ip, ip_protocol]
|
61
|
+
end
|
62
|
+
|
63
|
+
def group_permission_already_set?(fog_group, other_name, authed_owner)
|
64
|
+
return false if fog_group.ip_permissions.nil?
|
65
|
+
fog_group.ip_permissions.any? do |existing_permission|
|
66
|
+
existing_permission["groups"].include?({"userId" => authed_owner, "groupName" => other_name}) &&
|
67
|
+
existing_permission["fromPort"] == 1 &&
|
68
|
+
existing_permission["toPort"] == 65535
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def range_permission_already_set?(fog_group, range, cidr_ip, ip_protocol)
|
73
|
+
return false if fog_group.ip_permissions.nil?
|
74
|
+
fog_group.ip_permissions.include?(
|
75
|
+
{ "groups"=>[], "ipRanges"=>[{"cidrIp"=>cidr_ip}],
|
76
|
+
"ipProtocol"=>ip_protocol, "fromPort"=>range.first, "toPort"=>range.last})
|
77
|
+
end
|
78
|
+
|
79
|
+
# FIXME: so if you're saying to yourself, "self, this is some soupy gooey
|
80
|
+
# code right here" then you and your self are correct. Much of this is to
|
81
|
+
# work around old limitations in the EC2 api. You can now treat range and
|
82
|
+
# group permissions the same, and we should.
|
83
|
+
|
84
|
+
def run
|
85
|
+
fog_group = self.class.get_or_create(name, description)
|
86
|
+
@group_authorizations.uniq.each do |other_name, authed_owner|
|
87
|
+
authed_owner ||= self.owner_id
|
88
|
+
next if group_permission_already_set?(fog_group, other_name, authed_owner)
|
89
|
+
step("authorizing access from all machines in #{other_name} to #{name}", :blue)
|
90
|
+
self.class.get_or_create(other_name, "Authorized to access #{name}")
|
91
|
+
begin fog_group.authorize_group_and_owner(other_name, authed_owner)
|
92
|
+
rescue StandardError => e ; ui.warn e ; end
|
93
|
+
end
|
94
|
+
@group_authorized_by.uniq.each do |other_name|
|
95
|
+
authed_owner = self.owner_id
|
96
|
+
other_group = self.class.get_or_create(other_name, "Authorized for access by #{self.name}")
|
97
|
+
next if group_permission_already_set?(other_group, self.name, authed_owner)
|
98
|
+
step("authorizing access to all machines in #{other_name} from #{name}", :blue)
|
99
|
+
begin other_group.authorize_group_and_owner(self.name, authed_owner)
|
100
|
+
rescue StandardError => e ; ui.warn e ; end
|
101
|
+
end
|
102
|
+
@range_authorizations.uniq.each do |range, cidr_ip, ip_protocol|
|
103
|
+
next if range_permission_already_set?(fog_group, range, cidr_ip, ip_protocol)
|
104
|
+
step("opening #{ip_protocol} ports #{range} to #{cidr_ip}", :blue)
|
105
|
+
begin fog_group.authorize_port_range(range, { :cidr_ip => cidr_ip, :ip_protocol => ip_protocol })
|
106
|
+
rescue StandardError => e ; ui.warn e ; end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.step(group_name, desc, *style)
|
111
|
+
ui.info(" group #{"%-15s" % (group_name+":")}\t#{ui.color(desc.to_s, *style)}")
|
112
|
+
end
|
113
|
+
def step(desc, *style)
|
114
|
+
self.class.step(self.name, desc, *style)
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,281 @@
|
|
1
|
+
module Ironfan
|
2
|
+
|
3
|
+
#
|
4
|
+
# A server is a specific (logical) member of a facet within a cluster.
|
5
|
+
#
|
6
|
+
# It may have extra attributes if it also exists in the Chef server,
|
7
|
+
# or if it exists in the real world (as revealed by Fog)
|
8
|
+
#
|
9
|
+
class Server < Ironfan::ComputeBuilder
|
10
|
+
attr_reader :cluster, :facet, :facet_index, :tags
|
11
|
+
attr_accessor :chef_node, :fog_server
|
12
|
+
|
13
|
+
@@all ||= Mash.new
|
14
|
+
|
15
|
+
def initialize facet, idx
|
16
|
+
@cluster = facet.cluster
|
17
|
+
@facet = facet
|
18
|
+
@facet_index = idx
|
19
|
+
@fullname = [cluster_name, facet_name, facet_index].join('-')
|
20
|
+
super(@fullname)
|
21
|
+
@tags = { "name" => name, "cluster" => cluster_name, "facet" => facet_name, "index" => facet_index, }
|
22
|
+
ui.warn("Duplicate server #{[self, facet.name, idx]} vs #{@@all[fullname]}") if @@all[fullname]
|
23
|
+
@@all[fullname] = self
|
24
|
+
end
|
25
|
+
|
26
|
+
def fullname fn=nil
|
27
|
+
@fullname = fn if fn
|
28
|
+
@fullname
|
29
|
+
end
|
30
|
+
|
31
|
+
def cluster_name
|
32
|
+
cluster.name
|
33
|
+
end
|
34
|
+
|
35
|
+
def facet_name
|
36
|
+
facet.name
|
37
|
+
end
|
38
|
+
|
39
|
+
def servers
|
40
|
+
Ironfan::ServerGroup.new(cluster, [self])
|
41
|
+
end
|
42
|
+
|
43
|
+
def bogosity val=nil
|
44
|
+
@settings[:bogosity] = val if not val.nil?
|
45
|
+
return @settings[:bogosity] if not @settings[:bogosity].nil?
|
46
|
+
return :bogus_facet if facet.bogus?
|
47
|
+
# return :out_of_range if (self.facet_index.to_i >= facet.instances)
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def in_cloud?
|
52
|
+
!! fog_server
|
53
|
+
end
|
54
|
+
|
55
|
+
def in_chef?
|
56
|
+
chef_node || chef_client
|
57
|
+
end
|
58
|
+
|
59
|
+
def has_cloud_state?(*states)
|
60
|
+
in_cloud? && states.flatten.include?(fog_server.state)
|
61
|
+
end
|
62
|
+
|
63
|
+
def exists?
|
64
|
+
created? || in_chef?
|
65
|
+
end
|
66
|
+
|
67
|
+
def created?
|
68
|
+
in_cloud? && (not ['terminated', 'shutting-down'].include?(fog_server.state))
|
69
|
+
end
|
70
|
+
|
71
|
+
def running?
|
72
|
+
has_cloud_state?('running')
|
73
|
+
end
|
74
|
+
|
75
|
+
def startable?
|
76
|
+
has_cloud_state?('stopped')
|
77
|
+
end
|
78
|
+
|
79
|
+
def launchable?
|
80
|
+
not created?
|
81
|
+
end
|
82
|
+
|
83
|
+
def sshable?
|
84
|
+
in_chef?
|
85
|
+
end
|
86
|
+
|
87
|
+
def killable?
|
88
|
+
in_chef? || created?
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
super[0..-3] + " chef: #{in_chef? && chef_node.name} fog: #{in_cloud? && fog_server.id}}>"
|
93
|
+
end
|
94
|
+
|
95
|
+
#
|
96
|
+
# Attributes
|
97
|
+
#
|
98
|
+
|
99
|
+
def tag key, value=nil
|
100
|
+
if value then @tags[key] = value ; end
|
101
|
+
@tags[key]
|
102
|
+
end
|
103
|
+
|
104
|
+
#
|
105
|
+
# Resolve:
|
106
|
+
#
|
107
|
+
def resolve!
|
108
|
+
reverse_merge!(facet)
|
109
|
+
reverse_merge!(cluster)
|
110
|
+
@settings[:run_list] = combined_run_list
|
111
|
+
#
|
112
|
+
cloud.reverse_merge!(facet.cloud)
|
113
|
+
cloud.reverse_merge!(cluster.cloud)
|
114
|
+
#
|
115
|
+
cloud.user_data({
|
116
|
+
:chef_server => Chef::Config.chef_server_url,
|
117
|
+
:validation_client_name => Chef::Config.validation_client_name,
|
118
|
+
#
|
119
|
+
:node_name => fullname,
|
120
|
+
:cluster_name => cluster_name,
|
121
|
+
:facet_name => facet_name,
|
122
|
+
:facet_index => facet_index,
|
123
|
+
#
|
124
|
+
:run_list => run_list,
|
125
|
+
})
|
126
|
+
#
|
127
|
+
if client_key.body then cloud.user_data({ :client_key => client_key.body, })
|
128
|
+
else cloud.user_data({ :validation_key => cloud.validation_key }) ; end
|
129
|
+
cloud.keypair(cluster_name) if cloud.keypair.nil?
|
130
|
+
#
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# Assembles the combined runlist.
|
136
|
+
#
|
137
|
+
# * run_list :first items -- cluster then facet then server
|
138
|
+
# * run_list :normal items -- cluster then facet then server
|
139
|
+
# * own roles: cluster_role then facet_role
|
140
|
+
# * run_list :last items -- cluster then facet then server
|
141
|
+
#
|
142
|
+
# Ironfan.cluster(:my_cluster) do
|
143
|
+
# role('f', :last)
|
144
|
+
# role('c')
|
145
|
+
# facet(:my_facet) do
|
146
|
+
# role('d')
|
147
|
+
# role('e')
|
148
|
+
# role('b', :first)
|
149
|
+
# role('h', :last)
|
150
|
+
# end
|
151
|
+
# role('a', :first)
|
152
|
+
# role('g', :last)
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# produces
|
156
|
+
# cluster list [a] [c] [cluster_role] [fg]
|
157
|
+
# facet list [b] [de] [facet_role] [h]
|
158
|
+
#
|
159
|
+
# yielding run_list
|
160
|
+
# ['a', 'b', 'c', 'd', 'e', 'cr', 'fr', 'f', 'g', 'h']
|
161
|
+
#
|
162
|
+
# Avoid duplicate conflicting declarations. If you say define things more
|
163
|
+
# than once, the *earliest encountered* one wins, even if it is elsewhere
|
164
|
+
# marked :last.
|
165
|
+
#
|
166
|
+
def combined_run_list
|
167
|
+
cg = @cluster.run_list_groups
|
168
|
+
fg = @facet.run_list_groups
|
169
|
+
sg = self.run_list_groups
|
170
|
+
[ cg[:first], fg[:first], sg[:first],
|
171
|
+
cg[:normal], fg[:normal], sg[:normal],
|
172
|
+
cg[:own], fg[:own],
|
173
|
+
cg[:last], fg[:last], sg[:last], ].flatten.compact.uniq
|
174
|
+
end
|
175
|
+
|
176
|
+
#
|
177
|
+
# This prepares a composited view of the volumes -- it shows the cluster
|
178
|
+
# definition overlaid by the facet definition overlaid by the server
|
179
|
+
# definition.
|
180
|
+
#
|
181
|
+
# This method *does* auto-vivify an empty volume declaration on the server,
|
182
|
+
# but doesn't modify it.
|
183
|
+
#
|
184
|
+
# This code is pretty smelly, but so is the resolve! behavior. advice welcome.
|
185
|
+
#
|
186
|
+
def composite_volumes
|
187
|
+
vols = {}
|
188
|
+
facet.volumes.each do |vol_name, vol|
|
189
|
+
self.volumes[vol_name] ||= Ironfan::Volume.new(:parent => self, :name => vol_name)
|
190
|
+
vols[vol_name] ||= self.volumes[vol_name].dup
|
191
|
+
vols[vol_name].reverse_merge!(vol)
|
192
|
+
end
|
193
|
+
cluster.volumes.each do |vol_name, vol|
|
194
|
+
self.volumes[vol_name] ||= Ironfan::Volume.new(:parent => self, :name => vol_name)
|
195
|
+
vols[vol_name] ||= self.volumes[vol_name].dup
|
196
|
+
vols[vol_name].reverse_merge!(vol)
|
197
|
+
end
|
198
|
+
vols.each{|vol_name, vol| vol.availability_zone self.default_availability_zone }
|
199
|
+
vols
|
200
|
+
end
|
201
|
+
|
202
|
+
# FIXME -- this will break on some edge case wehre a bogus node is
|
203
|
+
# discovered after everything is resolve!d
|
204
|
+
def default_availability_zone
|
205
|
+
cloud.default_availability_zone
|
206
|
+
end
|
207
|
+
|
208
|
+
#
|
209
|
+
# retrieval
|
210
|
+
#
|
211
|
+
def self.get(cluster_name, facet_name, facet_index)
|
212
|
+
cluster = Ironfan.cluster(cluster_name)
|
213
|
+
had_facet = cluster.has_facet?(facet_name)
|
214
|
+
facet = cluster.facet(facet_name)
|
215
|
+
facet.bogosity true unless had_facet
|
216
|
+
had_server = facet.has_server?( facet_index )
|
217
|
+
server = facet.server(facet_index)
|
218
|
+
server.bogosity :not_defined_in_facet unless had_server
|
219
|
+
return server
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.all
|
223
|
+
@@all
|
224
|
+
end
|
225
|
+
|
226
|
+
#
|
227
|
+
# Actions!
|
228
|
+
#
|
229
|
+
|
230
|
+
def sync_to_cloud
|
231
|
+
step "Syncing to cloud"
|
232
|
+
attach_volumes
|
233
|
+
create_tags
|
234
|
+
associate_public_ip
|
235
|
+
end
|
236
|
+
|
237
|
+
def sync_to_chef
|
238
|
+
step "Syncing to chef server"
|
239
|
+
sync_chef_node
|
240
|
+
true
|
241
|
+
end
|
242
|
+
|
243
|
+
# FIXME: a lot of AWS logic in here. This probably lives in the facet.cloud
|
244
|
+
# but for the one or two things that come from the facet
|
245
|
+
def create_server
|
246
|
+
return nil if created? # only create a server if it does not already exist
|
247
|
+
fog_create_server
|
248
|
+
end
|
249
|
+
|
250
|
+
def create_tags
|
251
|
+
return unless created?
|
252
|
+
step(" labeling servers and volumes")
|
253
|
+
fog_create_tags(fog_server, self.fullname, tags)
|
254
|
+
composite_volumes.each do |vol_name, vol|
|
255
|
+
if vol.fog_volume
|
256
|
+
fog_create_tags(vol.fog_volume, vol.desc,
|
257
|
+
{ "server" => self.fullname, "name" => "#{name}-#{vol.name}", "device" => vol.device, "mount_point" => vol.mount_point, "cluster" => cluster_name, "facet" => facet_name, "index" => facet_index, })
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def block_device_mapping
|
263
|
+
composite_volumes.values.map(&:block_device_mapping).compact
|
264
|
+
end
|
265
|
+
|
266
|
+
# ugh. non-dry below.
|
267
|
+
|
268
|
+
def announce_as_started
|
269
|
+
return unless chef_node
|
270
|
+
announce_state('start')
|
271
|
+
chef_node.save
|
272
|
+
end
|
273
|
+
|
274
|
+
def announce_as_stopped
|
275
|
+
return unless chef_node
|
276
|
+
announce_state('stop')
|
277
|
+
chef_node.save
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
end
|