/
security_group.rb
261 lines (226 loc) · 11.3 KB
/
security_group.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
module Ironfan
class Provider
class Ec2
class SecurityGroup < Ironfan::Provider::Resource
WIDE_OPEN = (-1..-1)
delegate :_dump, :authorize_group_and_owner, :authorize_port_range,
:collection, :collection=, :connection, :connection=, :description,
:description=, :destroy, :group_id, :group_id=, :identity,
:identity=, :ip_permissions, :ip_permissions=, :name, :name=,
:new_record?, :owner_id, :owner_id=, :reload, :requires,
:requires_one, :revoke_group_and_owner, :revoke_port_range, :save,
:symbolize_keys, :vpc_id, :vpc_id=, :wait_for,
:to => :adaptee
def self.shared?() true; end
def self.multiple?() true; end
def self.resource_type() :security_group; end
def self.expected_ids(computer)
return unless computer.server
ec2 = computer.server.cloud(:ec2)
server_groups = computer.server.security_groups
cloud_groups = ec2.security_groups
result = []
[server_groups, cloud_groups].each do |container|
container.keys.each { |name| result.push( group_name_with_vpc(name,ec2.vpc) )}
end
return result.uniq
end
def name()
self.class.group_name_with_vpc(adaptee.name, adaptee.vpc_id)
end
#
# Discovery
#
def self.load!(cluster=nil)
Ec2.connection.security_groups.reject { |raw| raw.blank? }.each do |raw|
sg = SecurityGroup.new(:adaptee => raw)
remember sg
remember(sg, :id => sg.name.gsub( /^vpc-[^:]+:/, '') )
end
end
def receive_adaptee(obj)
obj = Ec2.connection.security_groups.new(obj) if obj.is_a?(Hash)
super
end
def to_s
if ip_permissions.present?
perm_str = ip_permissions.map{|perm|
"%s:%s-%s (%s | %s)" % [
perm['ipProtocol'], perm['fromPort'], perm['toPort'],
perm['groups' ].map{|el| el['groupName'] }.join(','),
perm['ipRanges'].map{|el| el['cidrIp'] }.join(','),
]
}
return "<%-15s %-12s %-25s %s>" % [ self.class.handle, group_id, name, perm_str]
else
return "<%-15s %-12s %s>" % [ self.class.handle, group_id, name ]
end
end
#
# Manipulation
#
def self.prepare!(computers)
# Create any groups that don't yet exist, and ensure any authorizations
# that are required for those groups
cluster_name = nil
groups_to_create = [ ]
authorizations_to_ensure = [ ]
computers.each{|comp| ensure_groups(comp) if Ec2.applicable(comp) } # Add facet and cluster security groups for the computer
# First, deduce the list of all groups to which at least one instance belongs
# We'll use this later to decide whether to create groups, or authorize access,
# using a VPC security group or an EC2 security group.
groups_that_should_exist = computers.map{|comp| expected_ids(comp) }.flatten.compact.sort.uniq
groups_to_create << groups_that_should_exist
computers.select { |computer| Ec2.applicable computer }.each do |computer|
cloud = computer.server.cloud(:ec2)
cluster_name = computer.server.cluster_name
# Iterate over all of the security group information, keeping track of
# any groups that must exist and any authorizations that must be ensured
[computer.server.security_groups, cloud.security_groups].each do |container|
container.values.each do |dsl_group|
groups_to_create << dsl_group.name
groups_to_create << dsl_group.group_authorized.map do |other_group|
most_appropriate_group_name(other_group, cloud.vpc)
end
groups_to_create << dsl_group.group_authorized_by.map do |other_group|
most_appropriate_group_name(other_group, cloud.vpc)
end
authorizations_to_ensure << dsl_group.group_authorized.map do |other_group|
{
:grantor => most_appropriate_group_name(dsl_group.name, cloud.vpc),
:grantee => most_appropriate_group_name(other_group, cloud.vpc),
:grantee_type => :group,
:range => WIDE_OPEN,
}
end
authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group|
{
:grantor => most_appropriate_group_name(other_group, cloud.vpc),
:grantee => most_appropriate_group_name(dsl_group.name, cloud.vpc),
:grantee_type => :group,
:range => WIDE_OPEN,
}
end
authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth|
range, cidr, protocol = range_auth
{
:grantor => group_name_with_vpc(dsl_group.name, cloud.vpc),
:grantee => { :cidr_ip => cidr, :ip_protocol => protocol },
:grantee_type => :cidr,
:range => range,
}
end
end
end
end
groups_to_create = groups_to_create.flatten.uniq.reject { |group| recall? group.to_s }.sort
authorizations_to_ensure = authorizations_to_ensure.flatten.uniq.sort { |a,b| a[:grantor] <=> b[:grantor] }
Ironfan.step(cluster_name, "creating security groups", :blue) unless groups_to_create.empty?
groups_to_create.each do |group|
if group =~ /\//
Ironfan.step(group, " assuming that owner/group pair #{group} already exists", :blue)
else
Ironfan.step(group, " creating #{group} security group", :blue)
begin
tokens = group.to_s.split(':')
group_id = tokens.pop
vpc_id = tokens.pop
Ec2.connection.create_security_group(group_id,"Ironfan created group #{group_id}",vpc_id)
rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
case e.message
when /SecurityGroupLimitExceeded/
raise e
else
Chef::Log.info("ignoring security group error: #{e}")
end
end
end
end
# Re-load everything so that we have a @@known list of security groups to manipulate
load! unless groups_to_create.empty?
# Now make sure that all required authorizations are present
Ironfan.step(cluster_name, "ensuring security group permissions", :blue) unless authorizations_to_ensure.empty?
authorizations_to_ensure.each do |auth|
grantor_fog = recall(auth[:grantor])
if :group == auth[:grantee_type]
if fog_grantee = recall(auth[:grantee])
options = { :group => fog_grantee.group_id }
elsif auth[:grantee] =~ /\//
options = { :group_alias => auth[:grantee] }
else
raise "Don't know what to do with authorization grantee #{auth[:grantee]}"
end
message = " ensuring access from #{auth[:grantee]} to #{auth[:grantor]}"
else
options = auth[:grantee]
message = " ensuring #{auth[:grantee][:ip_protocol]} access from #{auth[:grantee][:cidr_ip]} to #{auth[:range]}"
end
Ironfan.step(auth[:grantor], message, :blue)
safely_authorize(grantor_fog, auth[:range], options)
end
end
def self.group_name_with_vpc(name,vpc_id=nil)
vpc_id.nil? ? name.to_s : "#{vpc_id}:#{name.to_s}"
end
def self.most_appropriate_group_name(group, vpc_id)
vpc_id.present? ? group_name_with_vpc(group, vpc_id) : group
end
#
# Utility
#
def self.ensure_groups computer
return unless Ec2.applicable computer
# Ensure the security_groups include those for cluster & facet
# FIXME: This violates the DSL's immutability; it should be
# something calculated from within the DSL construction
Ironfan.todo("CODE SMELL: violation of DSL immutability: #{caller}")
server = computer.server
cluster_name = "#{computer.server.realm_name}-#{computer.server.cluster_name}"
server.security_group computer.server.realm_name
realm_group = server.security_group cluster_name
realm_group.authorized_by_group realm_group.name
end
# Try an authorization, ignoring duplicates (this is easier than correlating).
# Do so for both TCP and UDP, unless only one is specified
def self.safely_authorize(fog_group,range,options)
if options[:group_alias]
# In this case, we must first extract the group name
# before recursively calling this function with it.
owner, group = options.delete(:group_alias).split(/\//)
Chef::Log.debug("authorizing group alias #{options[:group_alias].inspect} to group #{fog_group.name}")
group_id = Ec2.connection.security_groups.get(group).group_id
safely_authorize(fog_group, range, options.merge(group: group_id))
elsif options[:ip_protocol]
# In this case, we've received the ip_protocol. With or
# without a group name, we have enough information to open
# the security group.
Chef::Log.debug("authorizing to #{fog_group.name} with options #{options.inspect}")
self.patiently(fog_group.name, Fog::Compute::AWS::Error, :ignore => Proc.new { |e| e.message =~ /Duplicate/ }) do
fog_group.authorize_port_range(range,options)
end
else
# Without an IP protocol, we'll open all of the relevant
# ones. On non-VPC, that means tcp, udp, and icmp. On VPC,
# that means -1 for all protocols.
Chef::Log.debug([
"didn't receive ip_protocol for authorization to #{fog_group.name} ",
"with options #{options.inspect}. assuming all protocols"
].join)
if fog_group.vpc_id.nil?
# Non-VPC does not support -1 for all protocols, so
# we'll need to do each protocol indendently. If we
# haven't received an ip_protocol, we'll assume the user
# meant to open everything.
safely_authorize(fog_group, 1..65535, options.merge(:ip_protocol => 'tcp'))
safely_authorize(fog_group, 1..65535, options.merge(:ip_protocol => 'udp'))
safely_authorize(fog_group, -1..-1, options.merge(:ip_protocol => 'icmp'))
else
# In VPC, we should use only one rule to conserve rules.
safely_authorize(fog_group,range,options.merge(:ip_protocol => -1))
end
end
end
end
end
end
end