ironfan 4.6.2 → 4.7.0

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/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"