ironfan 4.6.2 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ # v4.7.0:
2
+ (@nickmarden rocks the house again)
3
+ * Added support for "prepare" phase, prior to any machine-specific actions
4
+ * Move security group creation and authorization assurance to prepare phase (fixes #189)
5
+ * Allow user/group-style security group references (fixes #207)
6
+ * Move keypair creation to prepare phase
7
+
1
8
  # v4.6.2:
2
9
  * Added a -f/--with-facet option to knife cluster list
3
10
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.6.2
1
+ 4.7.0
data/ironfan.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "ironfan"
8
- s.version = "4.6.2"
8
+ s.version = "4.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Infochimps"]
12
- s.date = "2012-12-13"
12
+ s.date = "2012-12-14"
13
13
  s.description = "Ironfan allows you to orchestrate not just systems but clusters of machines. It includes a powerful layer on top of knife and a collection of cloud cookbooks."
14
14
  s.email = "coders@infochimps.com"
15
15
  s.extra_rdoc_files = [
@@ -75,6 +75,10 @@ class Chef
75
75
  confirm_or_exit("Are you absolutely certain that you want to delete #{delete_message}? (Type 'Yes' to confirm) ", 'Yes')
76
76
  end
77
77
 
78
+ def prepares?
79
+ false
80
+ end
81
+
78
82
  end
79
83
  end
80
84
  end
@@ -82,6 +82,11 @@ class Chef
82
82
  section("Syncing to chef")
83
83
  target.save :providers => :chef
84
84
 
85
+ unless target.empty?
86
+ ui.info "Preparing shared resources:"
87
+ all_computers(*@name_args).prepare
88
+ end
89
+
85
90
  # Launch computers
86
91
  ui.info("")
87
92
  section("Launching computers", :green)
@@ -119,6 +119,10 @@ class Chef
119
119
  }\n}
120
120
  end
121
121
 
122
+ def prepares?
123
+ false
124
+ end
125
+
122
126
  def aggregates?
123
127
  false
124
128
  end
@@ -62,6 +62,10 @@ class Chef
62
62
  else Chef::Log.debug("Skipping sync to cloud") ; end
63
63
  end
64
64
 
65
+ def prepares_on_noop?
66
+ true
67
+ end
68
+
65
69
  def aggregates_on_noop?
66
70
  true
67
71
  end
@@ -44,6 +44,11 @@ module Ironfan
44
44
 
45
45
  target = get_relevant_slice(* @name_args)
46
46
 
47
+ if prepares? and (prepares_on_noop? or not target.empty?)
48
+ ui.info "Preparing shared resources:"
49
+ all_computers(*@name_args).prepare
50
+ end
51
+
47
52
  unless target.empty?
48
53
  ui.info(["\n",
49
54
  ui.color("Running #{sub_command}", :cyan),
@@ -76,6 +81,14 @@ module Ironfan
76
81
  target.send(sub_command)
77
82
  end
78
83
 
84
+ def prepares?
85
+ true
86
+ end
87
+
88
+ def prepares_on_noop?
89
+ false
90
+ end
91
+
79
92
  def aggregates?
80
93
  true
81
94
  end
@@ -298,11 +298,19 @@ module Ironfan
298
298
  values.map {|c| c.providers.values}.flatten.uniq.each {|p| p.validate computers }
299
299
  end
300
300
 
301
- def aggregate
301
+ def group_action(verb)
302
302
  computers = self
303
- provider_keys = values.map {|c| c.chosen_providers({ :providers => :iaas})}.flatten.uniq
303
+ provider_keys = values.map {|c| c.chosen_providers({ :providers => :iaas })}.flatten.uniq
304
304
  providers = provider_keys.map { |pk| values.map { |c| c.providers[pk] } }.flatten.compact.uniq
305
- providers.each { |p| p.aggregate! computers }
305
+ providers.each { |p| p.send(verb, computers) }
306
+ end
307
+
308
+ def prepare
309
+ group_action(:prepare!)
310
+ end
311
+
312
+ def aggregate
313
+ group_action(:aggregate!)
306
314
  end
307
315
 
308
316
  #
@@ -42,6 +42,12 @@ module Ironfan
42
42
  resources.each {|r| r.validate_resources! computers }
43
43
  end
44
44
 
45
+ def self.prepare!(computers)
46
+ resources.each do |r|
47
+ r.prepare!(computers) if r.shared?
48
+ end
49
+ end
50
+
45
51
  def self.aggregate!(computers)
46
52
  resources.each do |r|
47
53
  r.aggregate!(computers) if r.shared?
@@ -87,14 +93,15 @@ module Ironfan
87
93
  #
88
94
  def self.create!(*p) Ironfan.noop(self,__method__,*p); end
89
95
  def self.save!(*p) Ironfan.noop(self,__method__,*p); end
96
+ def self.prepare!(*p) Ironfan.noop(self,__method__,*p); end
90
97
  def self.aggregate!(*p) Ironfan.noop(self,__method__,*p); end
91
98
  def self.destroy!(*p) Ironfan.noop(self,__method__,*p); end
92
99
 
93
100
  #
94
101
  # Utilities
95
102
  #
96
- [:shared?, :multiple?, :load!,:validate_computer!,
97
- :validate_resources!,:create!,:save!,:aggregate!,:destroy!].each do |method_name|
103
+ [:shared?, :multiple?, :load!,:validate_computer!, :validate_resources!,
104
+ :create!, :save!, :prepare!, :aggregate!, :destroy!].each do |method_name|
98
105
  define_method(method_name) {|*p| self.class.send(method_name,*p) }
99
106
  end
100
107
 
@@ -50,8 +50,9 @@ module Ironfan
50
50
  # Manipulation
51
51
  #
52
52
 
53
- def self.create!(computer)
54
- name = computer.server.cluster_name
53
+ def self.prepare!(computers)
54
+ return if computers.empty?
55
+ name = computers.values[0].server.cluster_name
55
56
  return if recall? name
56
57
  Ironfan.step(name, "creating key pair for #{name}", :blue)
57
58
  result = Ec2.connection.create_key_pair(name)
@@ -20,22 +20,18 @@ module Ironfan
20
20
  def self.resource_type() :security_group; end
21
21
  def self.expected_ids(computer)
22
22
  ec2 = computer.server.cloud(:ec2)
23
- ec2.security_groups.keys.map do |name|
24
- ec2.vpc ? "#{ec2.vpc}:#{name.to_s}" : name.to_s
25
- end.uniq
23
+ ec2.security_groups.keys.map { |name| group_name_with_vpc(name,ec2.vpc) }.uniq
26
24
  end
27
25
 
28
26
  def name()
29
- return adaptee.name if adaptee.vpc_id.nil?
30
- "#{adaptee.vpc_id}:#{adaptee.name}"
27
+ self.class.group_name_with_vpc(adaptee.name, adaptee.vpc_id)
31
28
  end
32
29
 
33
30
  #
34
31
  # Discovery
35
32
  #
36
33
  def self.load!(cluster=nil)
37
- Ec2.connection.security_groups.each do |raw|
38
- next if raw.blank?
34
+ Ec2.connection.security_groups.reject { |raw| raw.blank? }.each do |raw|
39
35
  sg = SecurityGroup.new(:adaptee => raw)
40
36
  remember(sg)
41
37
  Chef::Log.debug("Loaded #{sg}: #{sg.inspect}")
@@ -66,75 +62,119 @@ module Ironfan
66
62
  # Manipulation
67
63
  #
68
64
 
69
- def self.create!(computer)
70
- return unless Ec2.applicable computer
65
+ def self.prepare!(computers)
66
+
67
+ # Create any groups that don't yet exist, and ensure any authorizations
68
+ # that are required for those groups
69
+ cluster_name = nil
70
+ groups_to_create = [ ]
71
+ authorizations_to_ensure = [ ]
72
+
73
+ # First, deduce the list of all groups to which at least one instance belongs
74
+ # We'll use this later to decide whether to create groups, or authorize access,
75
+ # using a VPC security group or an EC2 security group.
76
+ groups_that_should_exist = computers.map { |c| expected_ids(c) }.flatten.sort.uniq
77
+ groups_to_create << groups_that_should_exist
78
+
79
+ computers.select { |computer| Ec2.applicable computer }.each do |computer|
80
+ ensure_groups(computer) # Add facet and cluster security groups for the computer
81
+ cloud = computer.server.cloud(:ec2)
82
+ cluster_name = computer.server.cluster_name
83
+
84
+ # Iterate over all of the security group information, keeping track of
85
+ # any groups that must exist and any authorizations that must be ensured
86
+ cloud.security_groups.values.each do |dsl_group|
87
+
88
+ groups_to_create << dsl_group.name
89
+
90
+ groups_to_create << dsl_group.group_authorized.map do |other_group|
91
+ most_appropriate_group_name(other_group, cloud.vpc, groups_that_should_exist)
92
+ end
93
+
94
+ groups_to_create << dsl_group.group_authorized_by.map do |other_group|
95
+ most_appropriate_group_name(other_group, cloud.vpc, groups_that_should_exist)
96
+ end
97
+
98
+ authorizations_to_ensure << dsl_group.group_authorized.map do |other_group|
99
+ {
100
+ :grantor => most_appropriate_group_name(dsl_group.name, cloud.vpc, groups_that_should_exist),
101
+ :grantee => most_appropriate_group_name(other_group, cloud.vpc, groups_that_should_exist),
102
+ :grantee_type => :group,
103
+ :range => WIDE_OPEN,
104
+ }
105
+ end
106
+
107
+ authorizations_to_ensure << dsl_group.group_authorized_by.map do |other_group|
108
+ {
109
+ :grantor => most_appropriate_group_name(other_group, cloud.vpc, groups_that_should_exist),
110
+ :grantee => most_appropriate_group_name(dsl_group.name, cloud.vpc, groups_that_should_exist),
111
+ :grantee_type => :group,
112
+ :range => WIDE_OPEN,
113
+ }
114
+ end
115
+
116
+ authorizations_to_ensure << dsl_group.range_authorizations.map do |range_auth|
117
+ range, cidr, protocol = range_auth
118
+ {
119
+ :grantor => group_name_with_vpc(dsl_group.name, cloud.vpc),
120
+ :grantee => { :cidr_ip => cidr, :ip_protocol => protocol },
121
+ :grantee_type => :cidr,
122
+ :range => range,
123
+ }
124
+ end
125
+ end
126
+ end
127
+ groups_to_create = groups_to_create.flatten.uniq.reject { |group| recall? group.to_s }.sort
128
+ authorizations_to_ensure = authorizations_to_ensure.flatten.uniq.sort { |a,b| a[:grantor] <=> b[:grantor] }
129
+
130
+ Ironfan.step(cluster_name, "creating security groups", :blue) unless groups_to_create.empty?
131
+ groups_to_create.each do |group|
132
+ if group =~ /\//
133
+ Ironfan.step(group, " assuming that owner/group pair #{group} already exists", :blue)
134
+ else
135
+ Ironfan.step(group, " creating #{group} security group", :blue)
136
+ begin
137
+ tokens = group.to_s.split(':')
138
+ group_id = tokens.pop
139
+ vpc_id = tokens.pop
140
+ Ec2.connection.create_security_group(group_id,"Ironfan created group #{group_id}",vpc_id)
141
+ rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
142
+ Chef::Log.info("ignoring security group error: #{e}")
143
+ end
144
+ end
145
+ end
71
146
 
72
- ensure_groups(computer)
73
- groups = self.expected_ids(computer)
74
- # Only handle groups that don't already exist
75
- groups.delete_if {|group| recall? group.to_s }
76
- return if groups.empty?
77
-
78
- Ironfan.step(computer.server.cluster_name, "creating security groups", :blue)
79
- groups.each do |group|
80
- Ironfan.step(group, " creating #{group} security group", :blue)
81
- begin
82
- tokens = group.to_s.split(':')
83
- group_id = tokens.pop
84
- vpc_id = tokens.pop
85
- Ec2.connection.create_security_group(group_id,"Ironfan created group #{group_id}",vpc_id)
86
- rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
87
- Chef::Log.info("ignoring security group error: #{e}")
88
- sleep 0.5 # quit racing so hard
147
+ # Re-load everything so that we have a @@known list of security groups to manipulate
148
+ load! unless groups_to_create.empty?
149
+
150
+ # Now make sure that all required authorizations are present
151
+ Ironfan.step(cluster_name, "ensuring security group permissions", :blue) unless authorizations_to_ensure.empty?
152
+ authorizations_to_ensure.each do |auth|
153
+ grantor_fog = recall(auth[:grantor])
154
+ if :group == auth[:grantee_type]
155
+ if fog_grantee = recall(auth[:grantee])
156
+ options = { :group => fog_grantee.group_id }
157
+ elsif auth[:grantee] =~ /\//
158
+ options = { :group_alias => auth[:grantee] }
159
+ else
160
+ raise "Don't know what to do with authorization grantee #{auth[:grantee]}"
161
+ end
162
+ message = " ensuring access from #{auth[:grantee]} to #{auth[:grantor]}"
163
+ else
164
+ options = auth[:grantee]
165
+ message = " ensuring #{auth[:grantee][:ip_protocol]} access from #{auth[:grantee][:cidr_ip]} to #{auth[:range]}"
89
166
  end
167
+ Ironfan.step(auth[:grantor], message, :blue)
168
+ safely_authorize(grantor_fog, auth[:range], options)
90
169
  end
91
- load! # Get the native groups via reload
92
170
  end
93
171
 
94
- def self.recall_with_vpc(name,vpc_id=nil)
95
- group_name = vpc_id.nil? ? name : "#{vpc_id}:#{name}"
96
- recall(group_name)
172
+ def self.group_name_with_vpc(name,vpc_id=nil)
173
+ vpc_id.nil? ? name.to_s : "#{vpc_id}:#{name.to_s}"
97
174
  end
98
175
 
99
- def self.save!(computer)
100
- return unless Ec2.applicable computer
101
- cloud = computer.server.cloud(:ec2)
102
-
103
- create!(computer) # Make sure the security groups exist
104
- security_groups = cloud.security_groups.values
105
- dsl_groups = security_groups.select do |dsl_group|
106
- not (recall_with_vpc(dsl_group,cloud.vpc)) and \
107
- not (dsl_group.range_authorizations +
108
- dsl_group.group_authorized_by +
109
- dsl_group.group_authorized).empty?
110
- end.compact
111
- return if dsl_groups.empty?
112
-
113
- Ironfan.step(computer.server.cluster_name, "ensuring security group permissions", :blue)
114
- dsl_groups.each do |dsl_group|
115
- dsl_group_fog = recall_with_vpc(dsl_group.name,cloud.vpc)
116
- dsl_group.group_authorized.each do |other_group|
117
- other_group_fog = recall_with_vpc(other_group,cloud.vpc)
118
- Ironfan.step(dsl_group.name, " ensuring access from #{other_group}", :blue)
119
- options = {:group => other_group_fog.group_id}
120
- safely_authorize(dsl_group_fog, WIDE_OPEN, options)
121
- end
122
-
123
- dsl_group.group_authorized_by.each do |other_group|
124
- other_group_fog = recall_with_vpc(other_group,cloud.vpc)
125
- Ironfan.step(dsl_group.name, " ensuring access to #{other_group}", :blue)
126
- options = {:group => dsl_group_fog.group_id}
127
- safely_authorize(other_group_fog, WIDE_OPEN, options)
128
- end
129
-
130
- dsl_group.range_authorizations.each do |range_auth|
131
- range, cidr, protocol = range_auth
132
- step_message = " ensuring #{protocol} access from #{cidr} to #{range}"
133
- Ironfan.step(dsl_group.name, step_message, :blue)
134
- options = {:cidr_ip => cidr, :ip_protocol => protocol}
135
- safely_authorize(dsl_group_fog, range, options)
136
- end
137
- end
176
+ def self.most_appropriate_group_name(group, vpc_id, all_valid_groups)
177
+ all_valid_groups.include?(group_name_with_vpc(group, vpc_id)) ? group_name_with_vpc(group, vpc_id) : group
138
178
  end
139
179
 
140
180
  #
@@ -156,20 +196,27 @@ module Ironfan
156
196
  # Try an authorization, ignoring duplicates (this is easier than correlating).
157
197
  # Do so for both TCP and UDP, unless only one is specified
158
198
  def self.safely_authorize(fog_group,range,options)
159
- unless options[:ip_protocol]
199
+ if options[:group_alias]
200
+ owner, group = options[:group_alias].split(/\//)
201
+ self.patiently(fog_group.name, Fog::Compute::AWS::Error, :ignore => Proc.new { |e| e.message =~ /InvalidPermission\.Duplicate/ }) do
202
+ Ec2.connection.authorize_security_group_ingress(
203
+ 'GroupName' => fog_group.name,
204
+ 'SourceSecurityGroupName' => group,
205
+ 'SourceSecurityGroupOwnerId' => owner
206
+ )
207
+ end
208
+ elsif options[:ip_protocol]
209
+ self.patiently(fog_group.name, Fog::Compute::AWS::Error, :ignore => Proc.new { |e| e.message =~ /InvalidPermission\.Duplicate/ }) do
210
+ fog_group.authorize_port_range(range,options)
211
+ end
212
+ else
160
213
  safely_authorize(fog_group,range,options.merge(:ip_protocol => 'tcp'))
161
214
  safely_authorize(fog_group,range,options.merge(:ip_protocol => 'udp'))
162
215
  safely_authorize(fog_group,Range.new(-1,-1),options.merge(:ip_protocol => 'icmp')) if(range == WIDE_OPEN)
163
216
  return
164
217
  end
165
-
166
- self.patiently(fog_group.name, Fog::Compute::AWS::Error, :ignore => Proc.new { |e| e.message =~ /InvalidPermission\.Duplicate/ }) do
167
- fog_group.authorize_port_range(range,options)
168
- end
169
-
170
218
  end
171
219
  end
172
-
173
220
  end
174
221
  end
175
222
  end
@@ -17,6 +17,10 @@ Ironfan.cluster "simple" do
17
17
 
18
18
  facet :web do
19
19
  instances 1
20
+ cloud(:ec2).security_group(:web) do
21
+ authorize_group :web_clients
22
+ authorize_group 'amazon-elb/amazon-elb-sg'
23
+ end
20
24
  end
21
25
 
22
26
  facet :db do
@@ -35,7 +39,7 @@ launch_cluster 'simple' do |cluster, computers|
35
39
 
36
40
  describe "the web facet security groups" do
37
41
  subject { cluster.facets[:web].server(0).cloud(:ec2).security_groups.keys.map(&:to_s).sort }
38
- it { should == %w[ simple simple-web ssh systemwide ] }
42
+ it { should == %w[ simple simple-web ssh systemwide web ] }
39
43
  end
40
44
 
41
45
  describe "the db facet security groups" do
@@ -43,6 +47,12 @@ launch_cluster 'simple' do |cluster, computers|
43
47
  it { should == %w[ simple simple-db ssh systemwide ] }
44
48
  end
45
49
 
50
+ describe "the passively created security groups" do
51
+ it "should include the :web_clients group" do
52
+ Ironfan::Provider::Ec2::SecurityGroup.recall('web_clients').should_not be_nil
53
+ end
54
+ end
55
+
46
56
  describe "the cluster-wide security group" do
47
57
  before :each do
48
58
  @sg = Ironfan::Provider::Ec2::SecurityGroup.recall('simple')
@@ -76,7 +86,17 @@ launch_cluster 'simple' do |cluster, computers|
76
86
  @ordered_ipp['icmp']['fromPort'].to_i.should == -1
77
87
  @ordered_ipp['icmp']['toPort'].to_i.should == -1
78
88
  end
89
+ end
90
+
91
+ describe "the web security group" do
92
+ before :each do
93
+ @sg = Ironfan::Provider::Ec2::SecurityGroup.recall('web')
94
+ @ordered_ipp = Hash[ @sg.ip_permissions.map { |s| [ s['ipProtocol'], s ] } ]
95
+ end
79
96
 
97
+ it "allows TCP connections to web_clients and to amazon-elb-sg" do
98
+ @ordered_ipp['tcp']['groups'].map { |g| g['groupName'] }.sort.should == %w[ amazon-elb-sg web_clients ]
99
+ end
80
100
  end
81
101
  end
82
102
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: ironfan
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 4.6.2
5
+ version: 4.7.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Infochimps
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2012-12-13 00:00:00 Z
13
+ date: 2012-12-14 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: chef
@@ -282,7 +282,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
282
282
  requirements:
283
283
  - - ">="
284
284
  - !ruby/object:Gem::Version
285
- hash: 4084772978397561372
285
+ hash: -2214564911911745674
286
286
  segments:
287
287
  - 0
288
288
  version: "0"