ironfan 4.2.1 → 4.2.2

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,5 +1,14 @@
1
+ # v4.2.2: @mrflip rocks the house
2
+ * Terminated machines are not bogus (fixes #165)
3
+ * Ignore deleting, deleted, or errored volumes in discovery
4
+ * Rescue on duplicate security groups; don't die logging on full 'load!'
5
+ * Changed key_pair to keypair, as per the DSL
6
+ * Specs for scripts (but not really)
7
+ * Model cleanup -- now mostly round-trip to JSON
8
+ * knife cluster bootstrap sets Chef::Config[:environment] (fixes #148)
9
+
1
10
  # v4.2.1: @nickmarden rocks the house
2
- * More correct merging of cluster and facet objects, with specs
11
+ * Correct merging of cluster and facet objects, with specs (fixes #158)
3
12
  * Circumvent memory bloat by resolving just once
4
13
 
5
14
  # v4.2.0:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.2.1
1
+ 4.2.2
data/ironfan.gemspec CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "ironfan"
8
- s.version = "4.2.1"
8
+ s.version = "4.2.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Infochimps"]
@@ -77,14 +77,18 @@ Gem::Specification.new do |s|
77
77
  "lib/ironfan/provider/ec2.rb",
78
78
  "lib/ironfan/provider/ec2/ebs_volume.rb",
79
79
  "lib/ironfan/provider/ec2/elastic_ip.rb",
80
- "lib/ironfan/provider/ec2/key_pair.rb",
80
+ "lib/ironfan/provider/ec2/keypair.rb",
81
81
  "lib/ironfan/provider/ec2/machine.rb",
82
82
  "lib/ironfan/provider/ec2/placement_group.rb",
83
83
  "lib/ironfan/provider/ec2/security_group.rb",
84
84
  "lib/ironfan/provider/virtualbox.rb",
85
85
  "lib/ironfan/provider/virtualbox/machine.rb",
86
86
  "lib/ironfan/requirements.rb",
87
+ "spec/chef/cluster_bootstrap_spec.rb",
88
+ "spec/fixtures/gunbai.rb",
89
+ "spec/fixtures/gunbai_slice.json",
87
90
  "spec/ironfan/cluster_spec.rb",
91
+ "spec/ironfan/ec2/cloud_provider.rb",
88
92
  "spec/ironfan/ec2/cloud_provider_spec.rb",
89
93
  "spec/ironfan/ec2/security_group_spec.rb",
90
94
  "spec/spec_helper.rb",
@@ -97,7 +101,7 @@ Gem::Specification.new do |s|
97
101
  s.require_paths = ["lib"]
98
102
  s.rubygems_version = "1.8.24"
99
103
  s.summary = "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."
100
- s.test_files = ["spec/spec_helper/dummy_chef.rb", "spec/ironfan/cluster_spec.rb", "spec/ironfan/ec2/cloud_provider_spec.rb", "spec/ironfan/ec2/security_group_spec.rb", "spec/spec_helper.rb", "spec/test_config.rb"]
104
+ s.test_files = ["spec/spec_helper/dummy_chef.rb", "spec/ironfan/cluster_spec.rb", "spec/ironfan/ec2/cloud_provider_spec.rb", "spec/ironfan/ec2/security_group_spec.rb", "spec/ironfan/ec2/cloud_provider.rb", "spec/chef/cluster_bootstrap_spec.rb", "spec/fixtures/gunbai_slice.json", "spec/fixtures/gunbai.rb", "spec/spec_helper.rb", "spec/test_config.rb"]
101
105
 
102
106
  if s.respond_to? :specification_version then
103
107
  s.specification_version = 3
@@ -59,6 +59,7 @@ class Chef
59
59
  end
60
60
 
61
61
  def perform_execution(target)
62
+ reconcile_chef_config(target)
62
63
  # Execute across all servers in parallel
63
64
  Ironfan.parallel(target.values) {|computer| run_bootstrap(computer)}
64
65
  # threads = target.servers.map{ |server| Thread.new(server) { |svr| run_bootstrap(svr, svr.public_hostname) } }
@@ -69,6 +70,18 @@ class Chef
69
70
  confirm_or_exit("Are you absolutely certain that you want to perform this action? (Type 'Yes' to confirm) ", 'Yes')
70
71
  end
71
72
 
73
+ protected
74
+
75
+ def reconcile_chef_config(target)
76
+ environments = target.environments
77
+ if environments.length > 1
78
+ ui.error "You cannot bootstrap machines in multiple chef environments: got #{environments.inspect} from #{target.map(&:name)}"
79
+ ui.error "Re-run this command on each subgroup of machines that share an environment"
80
+ raise StandardError, "Cannot bootstrap multiple chef environments"
81
+ end
82
+ Chef::Config[:environment] = environments.first
83
+ end
84
+
72
85
  end
73
86
  end
74
87
  end
@@ -45,9 +45,12 @@ class Chef
45
45
  cluster = target.cluster
46
46
 
47
47
  ui.info("")
48
- ui.info("You are in a cluster. There is a sign overhead reading '#{ui.color(@name_args.first, :yellow)}'.")
49
- ui.info("Next to you a burly man in a greasy apron sharpens his cleaver, and a lissom princess performs treacherous origami.")
50
- ui.info("It is Pitch Dark. You are likely to be eaten by a grue.") if target.select(&:running?).empty?
48
+ ui.info([
49
+ ui.color("You are in a cluster. There is a sign overhead reading '", :magenta),
50
+ ui.color(@name_args.first, :yellow, :bold),
51
+ ui.color("'.\nNext to you a burly man in a greasy apron sharpens his cleaver, \nand a lissom princess performs treacherous origami.", :magenta)
52
+ ].join)
53
+ ui.info(ui.color("It is Pitch Dark. You are likely to be eaten by a grue.", :black, :bold)) if target.select(&:running?).empty?
51
54
 
52
55
  # Commands to try:
53
56
  # nn = Chef::Node.load('node-name')
@@ -35,6 +35,7 @@ class Chef
35
35
  :boolean => true
36
36
 
37
37
  def run
38
+ with_verbosity(1){ config[:include_terminated] }
38
39
  load_ironfan
39
40
  die(banner) if @name_args.empty?
40
41
  configure_dry_run
@@ -4,7 +4,7 @@ require 'gorillib/hash/deep_merge'
4
4
 
5
5
  module Gorillib
6
6
 
7
- # Make a clean deep-copy of the value, via gorillib semantics if
7
+ # Make a clean deep-copy of the value, via gorillib semantics if
8
8
  # possible, otherwise via marshalling
9
9
  def self.deep_copy(value)
10
10
  case
@@ -36,7 +36,7 @@ module Gorillib
36
36
  else
37
37
  write_attribute(collection_field_name, coll)
38
38
  end
39
- rescue StandardError => err ; err.polish("#{self.class} #{collection_field_name} collection on #{args}'") rescue nil ; raise ; end
39
+ rescue StandardError => err ; err.polish("#{self.class} #{collection_field_name} collection on #{coll}'") rescue nil ; raise ; end
40
40
  end
41
41
  end
42
42
  end
@@ -44,13 +44,13 @@ module Gorillib
44
44
  end
45
45
 
46
46
  # The attribute :underlay provides an object (preferably another
47
- # Gorillib::Model or the like) that will resolve stacked
48
- # defaults. If fields are declared with a :resolver, it will
47
+ # Gorillib::Model or the like) that will resolve stacked
48
+ # defaults. If fields are declared with a :resolver, it will
49
49
  # apply that call in preference the default rules (self.field
50
50
  # -> underlay.field -> self.field.default )
51
51
  #
52
- # To provide resolve cleanly without read-write loops destroying
53
- # the separation of concerns, the resolve mechanism has been
52
+ # To provide resolve cleanly without read-write loops destroying
53
+ # the separation of concerns, the resolve mechanism has been
54
54
  # broken from the regular read-write accessors.
55
55
  #
56
56
  module Resolution
@@ -59,9 +59,9 @@ module Gorillib
59
59
  attr_accessor :underlay
60
60
 
61
61
  # Return a fully-resolved copy of this object. All objects
62
- # referenced will be clean deep_copies, and will lack the
62
+ # referenced will be clean deep_copies, and will lack the
63
63
  # :underlay accessor. This is by design, to prevent self-
64
- # referential loops (parent->collection->child->owner)
64
+ # referential loops (parent->collection->child->owner)
65
65
  # when deep_coping.
66
66
  def resolve
67
67
  result = self.class.new
@@ -91,7 +91,7 @@ module Gorillib
91
91
  end
92
92
  result
93
93
  end
94
-
94
+
95
95
  def resolve_value(value)
96
96
  return if value.nil?
97
97
  return value.resolve if value.respond_to? :resolve
@@ -4,9 +4,9 @@ module Ironfan
4
4
  class Computer < Builder
5
5
 
6
6
  field :server, Ironfan::Dsl::Server
7
- collection :resources, Whatever
8
- collection :drives, Ironfan::Broker::Drive
9
- collection :providers, Ironfan::Provider
7
+ collection :resources, Ironfan::Provider::Resource, :key_method => :name
8
+ collection :drives, Ironfan::Broker::Drive, :key_method => :name
9
+ collection :providers, Whatever, :key_method => :name
10
10
  delegate :[], :[]=, :include?, :delete, :to => :resources
11
11
 
12
12
  # Only used for bogus servers
@@ -21,6 +21,7 @@ module Ironfan
21
21
  volumes = server.volumes.values
22
22
  volumes += server.implied_volumes
23
23
  volumes.each { |v| self.drive v.name, :volume => v }
24
+ rescue StandardError => err ; err.polish("#{self.class} on #{args}'") rescue nil ; raise
24
25
  end
25
26
 
26
27
  def name
@@ -128,10 +129,6 @@ module Ironfan
128
129
  @chef_client_script_content = Ironfan.safely{ File.read(script_filename) }
129
130
  end
130
131
 
131
- def ssh_identity_file
132
- resources[:key_pair].key_filename
133
- end
134
-
135
132
  # def ensure_resource(type)
136
133
  # if type.multiple?
137
134
  # existing = resources[type.resource_id]
@@ -182,6 +179,8 @@ module Ironfan
182
179
  self[:node]= value
183
180
  end
184
181
 
182
+ def environment ; server && server.environment ; end
183
+
185
184
  #
186
185
  # client
187
186
  #
@@ -200,9 +199,21 @@ module Ironfan
200
199
  self[:machine] = value
201
200
  end
202
201
  def dns_name ; machine? ? machine.dns_name : nil ; end
203
- def ssh_user ; (server && server.selected_cloud) ? server.selected_cloud.ssh_user : nil ; end
204
202
  def bootstrap_distro ; (server && server.selected_cloud) ? server.selected_cloud.bootstrap_distro : nil ; end
205
203
 
204
+ def keypair
205
+ resources[:keypair]
206
+ end
207
+
208
+ def ssh_user ; (server && server.selected_cloud) ? server.selected_cloud.ssh_user : nil ; end
209
+
210
+ def ssh_identity_file
211
+ if keypair then keypair.key_filename
212
+ elsif server && server.selected_cloud then server.selected_cloud.ssh_identity_file(self)
213
+ else nil
214
+ end
215
+ end
216
+
206
217
  #
207
218
  # Status flags
208
219
  #
@@ -248,6 +259,14 @@ module Ironfan
248
259
  running? || node?
249
260
  end
250
261
 
262
+ def receive_providers(objs)
263
+ objs = objs.map do |obj|
264
+ if obj.is_a?(String) then obj = Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(obj.gsub(/\./, '/'))) ; end
265
+ obj
266
+ end
267
+ super(objs)
268
+ end
269
+
251
270
  def to_s
252
271
  "<#{self.class}(server=#{server}, resources=#{resources && resources.inspect_compact}, providers=#{providers && providers.inspect_compact})>"
253
272
  end
@@ -300,6 +319,10 @@ module Ironfan
300
319
  Ironfan.parallel(values) {|c| c.stop }
301
320
  end
302
321
 
322
+ def environments
323
+ map{|comp| comp.environment }.uniq
324
+ end
325
+
303
326
  #
304
327
  # Utility
305
328
  #
@@ -8,10 +8,11 @@ module Ironfan
8
8
  end
9
9
 
10
10
  class Broker < Builder
11
- # Take in a Dsl::Cluster, return Computers populated with
12
- # all discovered resources that correlate, plus bogus computers
13
- # corresponding to
11
+ # Take in a Dsl::Cluster; return Computers populated with all discovered
12
+ # resources that correlate; computers corresponding to partial or
13
+ # unrecognizable resources are labeled as bogus.
14
14
  def discover!(cluster)
15
+
15
16
  # Get fully resolved servers, and build Computers using them
16
17
  computers = Computers.new(:cluster => cluster)
17
18
  #
@@ -22,6 +23,7 @@ module Ironfan
22
23
  end
23
24
  #
24
25
  Ironfan.step cluster.name, "Reconciling DSL and provider information", :cyan
26
+
25
27
  computers.correlate
26
28
  computers.validate
27
29
  #
@@ -39,5 +41,4 @@ module Ironfan
39
41
  end
40
42
 
41
43
  end
42
-
43
44
  end
@@ -4,13 +4,17 @@ module Ironfan
4
4
  class Cloud < Ironfan::Dsl
5
5
  magic :default_cloud, :boolean, :default => false
6
6
 
7
- # Factory out to subclasses
8
- def self.receive(obj,&block)
9
- obj[:_type] = case obj[:name]
10
- when :ec2; Ec2
11
- when :virtualbox; VirtualBox
12
- else; raise "Unsupported cloud #{obj[:name]}"
13
- end unless native?(obj)
7
+ # Factory out to subclasses
8
+ def self.receive(obj, &block)
9
+ if obj.is_a?(Hash)
10
+ obj = obj.symbolize_keys
11
+ obj[:_type] ||=
12
+ case obj[:name]
13
+ when :ec2 then Ec2
14
+ when :virtualbox then VirtualBox
15
+ else raise "Unsupported cloud #{obj[:name]}"
16
+ end
17
+ end
14
18
  super
15
19
  end
16
20
 
@@ -18,4 +22,4 @@ module Ironfan
18
22
  end
19
23
 
20
24
  end
21
- end
25
+ end
@@ -1,14 +1,24 @@
1
1
  module Ironfan
2
2
  class Dsl
3
3
 
4
+ class RunListItem < Hash
5
+ Gorillib::Factory.register_factory(self, [self])
6
+ def name
7
+ self[:name]
8
+ end
9
+ def self.receive(hsh)
10
+ new.merge!(hsh.symbolize_keys)
11
+ end
12
+ end
13
+
4
14
  class Compute < Ironfan::Dsl
5
15
  @@run_list_rank = 0
6
16
  field :name, String
7
17
 
8
18
  # Resolve each of the following as a merge of their container's attributes and theirs
9
- collection :run_list_items, Hash, :resolver => :merge_resolve
10
- collection :clouds, Ironfan::Dsl::Cloud, :resolver => :merge_resolve
11
- collection :volumes, Ironfan::Dsl::Volume, :resolver => :merge_resolve
19
+ collection :run_list_items, RunListItem, :resolver => :merge_resolve, :key_method => :name
20
+ collection :clouds, Ironfan::Dsl::Cloud, :resolver => :merge_resolve, :key_method => :name
21
+ collection :volumes, Ironfan::Dsl::Volume, :resolver => :merge_resolve, :key_method => :name
12
22
 
13
23
  # Resolve these normally (overriding on each layer)
14
24
  magic :environment, Symbol, :default => :_default
@@ -81,4 +91,4 @@ module Ironfan
81
91
  end
82
92
 
83
93
  end
84
- end
94
+ end
@@ -15,17 +15,17 @@ module Ironfan
15
15
  magic :flavor, String, :default => 't1.micro'
16
16
  magic :image_id, String
17
17
  magic :image_name, String
18
- magic :keypair, Whatever
18
+ magic :keypair, String
19
19
  magic :monitoring, String
20
20
  magic :mount_ephemerals, Hash, :default => {}
21
21
  magic :permanent, :boolean, :default => false
22
22
  magic :placement_group, String
23
- magic :provider, Ironfan::Provider, :default => Ironfan::Provider::Ec2
23
+ magic :provider, Whatever, :default => Ironfan::Provider::Ec2
24
24
  magic :public_ip, String
25
25
  magic :region, String, :default => ->{ default_region }
26
26
  magic :ssh_user, String, :default => ->{ image_info[:ssh_user] }
27
27
  magic :ssh_identity_dir, String, :default => ->{ Chef::Config.ec2_key_dir }
28
- collection :security_groups, Ironfan::Dsl::Ec2::SecurityGroup
28
+ collection :security_groups, Ironfan::Dsl::Ec2::SecurityGroup, :key_method => :name
29
29
  magic :subnet, String
30
30
  magic :validation_key, String, :default => ->{ IO.read(Chef::Config.validation_key) rescue '' }
31
31
  magic :vpc, String
@@ -102,6 +102,14 @@ module Ironfan
102
102
  result
103
103
  end
104
104
 
105
+ def receive_provider(obj)
106
+ if obj.is_a?(String)
107
+ write_attribute :provider, Gorillib::Inflector.constantize(Gorillib::Inflector.camelize(obj.gsub(/\./, '/')))
108
+ else
109
+ super(obj)
110
+ end
111
+ end
112
+
105
113
  class SecurityGroup < Ironfan::Dsl
106
114
  field :name, String
107
115
  field :group_authorized, Array, :default => []
@@ -1,8 +1,8 @@
1
1
  module Ironfan
2
- module Dsl
2
+ class Dsl
3
3
 
4
4
  class VirtualBox < Cloud
5
5
  end
6
6
 
7
7
  end
8
- end
8
+ end
@@ -46,7 +46,7 @@ module Ironfan
46
46
  class EbsVolume < Ironfan::Provider::Resource; end
47
47
  class ElasticIp < Ironfan::Provider::Resource; end
48
48
  class Machine < Ironfan::IaasProvider::Machine; end
49
- class KeyPair < Ironfan::Provider::Resource; end
49
+ class Keypair < Ironfan::Provider::Resource; end
50
50
  class PlacementGroup < Ironfan::Provider::Resource; end
51
51
  class SecurityGroup < Ironfan::Provider::Resource; end
52
52
  end
@@ -51,6 +51,7 @@ module Ironfan
51
51
  def self.load!(cluster=nil)
52
52
  Ec2.connection.volumes.each do |vol|
53
53
  next if vol.blank?
54
+ next if %w[deleting deleted error].include?(vol.state.to_s)
54
55
  ebs = EbsVolume.new(:adaptee => vol)
55
56
  # Already have a volume by this name
56
57
  if recall? ebs.name
@@ -64,6 +65,11 @@ module Ironfan
64
65
  end
65
66
  end
66
67
 
68
+ def receive_adaptee(obj)
69
+ obj = Ec2.connection.volumes.new(obj) if obj.is_a?(Hash)
70
+ super
71
+ end
72
+
67
73
  def on_correlate(computer)
68
74
  drive = computer.drive(drivename)
69
75
  drive.disk = self
@@ -3,8 +3,7 @@ module Ironfan
3
3
  class Ec2
4
4
 
5
5
  class ElasticIp < Ironfan::Provider::Resource
6
-
7
6
  end
8
7
  end
9
8
  end
10
- end
9
+ end
@@ -2,20 +2,22 @@ module Ironfan
2
2
  class Provider
3
3
  class Ec2
4
4
 
5
- class KeyPair < Ironfan::Provider::Resource
5
+ class Keypair < Ironfan::Provider::Resource
6
6
  delegate :_dump, :collection, :collection=, :connection,
7
7
  :connection=, :destroy, :fingerprint, :fingerprint=, :identity,
8
8
  :identity=, :name, :name=, :new_record?, :public_key,
9
9
  :public_key=, :reload, :requires, :requires_one, :save,
10
10
  :symbolize_keys, :wait_for, :writable?, :write,
11
11
  :to => :adaptee
12
- field :key_filename, String,
13
- :default => ->{ "#{KeyPair.key_dir}/#{name}.pem" }
14
12
 
15
- def self.shared?() true; end
16
- def self.multiple?() false; end
17
- def self.resource_type() :key_pair; end
18
- def self.expected_ids(computer) [computer.server.cluster_name]; end
13
+ field :key_filename, String, :default => ->{ "#{Keypair.key_dir}/#{name}.pem" }
14
+
15
+ def self.shared? ; true ; end
16
+ def self.multiple? ; false ; end
17
+ def self.resource_type ; :keypair ; end
18
+ def self.expected_ids(computer)
19
+ [computer.server.cluster_name]
20
+ end
19
21
 
20
22
  def private_key
21
23
  File.open(key_filename, "rb").read
@@ -39,6 +41,11 @@ module Ironfan
39
41
  end
40
42
  end
41
43
 
44
+ def receive_adaptee(obj)
45
+ obj = Ec2.connection.key_pairs.new(obj) if obj.is_a?(Hash)
46
+ super
47
+ end
48
+
42
49
  #
43
50
  # Manipulation
44
51
  #
@@ -63,6 +70,7 @@ module Ironfan
63
70
  warn "Please set 'ec2_key_dir' in your knife.rb. Will use #{dir} as a default"
64
71
  dir
65
72
  end
73
+
66
74
  end
67
75
 
68
76
  end
@@ -34,7 +34,6 @@ module Ironfan
34
34
  :vpc_id=, :wait_for,
35
35
  :to => :adaptee
36
36
 
37
-
38
37
  def self.shared?() false; end
39
38
  def self.multiple?() false; end
40
39
  # def self.resource_type() Ironfan::IaasProvider::Machine; end
@@ -46,7 +45,8 @@ module Ironfan
46
45
  tags["Name"] || tags["name"] || id
47
46
  end
48
47
 
49
- def public_hostname() dns_name; end
48
+ def public_hostname ; dns_name ; end
49
+ def keypair ; key_pair ; end
50
50
 
51
51
  def created?
52
52
  not ['terminated', 'shutting-down'].include? state
@@ -104,20 +104,26 @@ module Ironfan
104
104
  def self.load!(cluster=nil)
105
105
  Ec2.connection.servers.each do |fs|
106
106
  machine = new(:adaptee => fs)
107
- if recall? machine.name
107
+ if (not machine.created?)
108
+ next unless Ironfan.chef_config[:include_terminated]
109
+ remember machine, :append_id => "terminated:#{machine.id}"
110
+ elsif recall? machine.name
108
111
  raise 'duplicate'
109
112
  machine.bogus << :duplicate_machines
110
113
  recall(machine.name).bogus << :duplicate_machines
111
114
  remember machine, :append_id => "duplicate:#{machine.id}"
112
- elsif machine.created?
115
+ else # never seen it
113
116
  remember machine
114
- else
115
- remember machine, :append_id => "terminated:#{machine.id}"
116
117
  end
117
118
  Chef::Log.debug("Loaded #{machine}")
118
119
  end
119
120
  end
120
121
 
122
+ def receive_adaptee(obj)
123
+ obj = Ec2.connection.servers.new(obj) if obj.is_a?(Hash)
124
+ super
125
+ end
126
+
121
127
  # Find active machines that haven't matched, but should have,
122
128
  # make sure all bogus machines have a computer to attach to
123
129
  # for display purposes
@@ -167,7 +173,7 @@ module Ironfan
167
173
  'name' => computer.name,
168
174
  'Name' => computer.name,
169
175
  }
170
- Ec2.ensure_tags(tags,computer.machine)
176
+ Ec2.ensure_tags(tags, computer.machine)
171
177
 
172
178
  # register the new volumes for later save!, and tag appropriately
173
179
  computer.machine.volumes.each do |v|
@@ -32,6 +32,11 @@ module Ironfan
32
32
  end
33
33
  end
34
34
 
35
+ def receive_adaptee(obj)
36
+ obj = Ec2.connection.security_groups.new(obj) if obj.is_a?(Hash)
37
+ super
38
+ end
39
+
35
40
  def to_s
36
41
  if ip_permissions.present?
37
42
  perm_str = ip_permissions.map{|perm|
@@ -63,7 +68,12 @@ module Ironfan
63
68
  Ironfan.step(computer.server.cluster_name, "creating security groups", :blue)
64
69
  groups.each do |group|
65
70
  Ironfan.step(group, " creating #{group} security group", :blue)
66
- Ec2.connection.create_security_group(group.to_s,"Ironfan created group #{group}")
71
+ begin
72
+ Ec2.connection.create_security_group(group.to_s,"Ironfan created group #{group}")
73
+ rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
74
+ Chef::Log.info("ignoring security group error: #{e}")
75
+ sleep 0.5 # quit racing so hard
76
+ end
67
77
  end
68
78
  load! # Get the native groups via reload
69
79
  end
@@ -134,7 +144,7 @@ module Ironfan
134
144
  begin
135
145
  fog_group.authorize_port_range(range,options)
136
146
  rescue Fog::Compute::AWS::Error => e # InvalidPermission.Duplicate
137
- Chef::Log.debug("ignoring #{e}")
147
+ Chef::Log.info("ignoring #{e}")
138
148
  end
139
149
  end
140
150
  end
@@ -5,7 +5,7 @@ module Ironfan
5
5
  self.handle = :ec2
6
6
 
7
7
  def self.resources
8
- [ Machine, EbsVolume, KeyPair, SecurityGroup ]
8
+ [ Machine, EbsVolume, Keypair, SecurityGroup ]
9
9
  end
10
10
 
11
11
  #
@@ -6,13 +6,17 @@ module Ironfan
6
6
  class Provider < Builder
7
7
  class_attribute :handle
8
8
 
9
- def self.receive(obj,&block)
10
- obj[:_type] = case obj[:name]
11
- when :chef; Chef
12
- when :ec2; Ec2
13
- when :virtualbox; VirtualBox
14
- else; raise "Unsupported provider #{obj[:name]}"
15
- end unless native?(obj)
9
+ def self.receive(obj, &block)
10
+ if obj.is_a?(Hash)
11
+ obj = obj.symbolize_keys
12
+ obj[:_type] =
13
+ case obj[:name]
14
+ when :chef then Chef
15
+ when :ec2 then Ec2
16
+ when :virtualbox then VirtualBox
17
+ else raise "Unsupported provider #{obj[:name]}"
18
+ end
19
+ end
16
20
  super
17
21
  end
18
22
 
@@ -50,6 +54,11 @@ module Ironfan
50
54
 
51
55
  def self.handle ; name.to_s.gsub(/.*::/,'').to_sym ; end
52
56
 
57
+ def self.receive(obj)
58
+ obj = obj.symbolize_keys if obj.is_a?(Hash)
59
+ super(obj)
60
+ end
61
+
53
62
  #
54
63
  # Flags
55
64
  #
@@ -2,11 +2,9 @@
2
2
  require 'gorillib/builder'
3
3
  require 'gorillib/resolution'
4
4
 
5
-
6
5
  # Pre-declaration of class hierarchy
7
6
  require 'ironfan/headers'
8
7
 
9
-
10
8
  # DSL for cluster descriptions
11
9
  require 'ironfan/dsl'
12
10
  require 'ironfan/builder'
@@ -34,7 +32,7 @@ require 'ironfan/provider/chef/role'
34
32
  require 'ironfan/provider/ec2'
35
33
  require 'ironfan/provider/ec2/ebs_volume'
36
34
  require 'ironfan/provider/ec2/machine'
37
- require 'ironfan/provider/ec2/key_pair'
35
+ require 'ironfan/provider/ec2/keypair'
38
36
  require 'ironfan/provider/ec2/placement_group'
39
37
  require 'ironfan/provider/ec2/security_group'
40
38
 
data/lib/ironfan.rb CHANGED
@@ -29,7 +29,7 @@ module Ironfan
29
29
  raise 'missing block' unless block_given?
30
30
  results = []
31
31
  [targets].flatten.each_with_index.map do |target, idx|
32
- sleep(0.1) # avoid hammering with simultaneous requests
32
+ sleep(0.25) # avoid hammering with simultaneous requests
33
33
  Thread.new(target) do |target|
34
34
  results[idx] = safely(target.inspect) do
35
35
  yield target
@@ -80,19 +80,19 @@ module Ironfan
80
80
  # doesn't define the requested cluster.
81
81
  #
82
82
  # @return [Ironfan::Cluster] the requested cluster
83
- def self.load_cluster(cluster_name)
84
- cluster = cluster_name.to_sym
85
- raise ArgumentError, "Please supply a cluster name" if cluster_name.to_s.empty?
86
- return @@clusters[cluster] if @@clusters[cluster]
83
+ def self.load_cluster(name)
84
+ name = name.to_sym
85
+ raise ArgumentError, "Please supply a cluster name" if name.to_s.empty?
86
+ return @@clusters[name] if @@clusters[name]
87
87
 
88
- cluster_file = cluster_filenames[cluster_name] or die("Couldn't find a definition for #{cluster_name} in cluster_path: #{cluster_path.inspect}")
88
+ cluster_file = cluster_filenames[name] or raise("Couldn't find a definition for #{name} in cluster_path: #{cluster_path.inspect}")
89
89
 
90
90
  Chef::Log.info("Loading cluster #{cluster_file}")
91
91
 
92
92
  require cluster_file
93
- unless @@clusters[cluster] then die("#{cluster_file} was supposed to have the definition for the #{cluster_name} cluster, but didn't") end
93
+ unless @@clusters[name] then die("#{cluster_file} was supposed to have the definition for the #{name} cluster, but didn't") end
94
94
 
95
- @@clusters[cluster]
95
+ @@clusters[name]
96
96
  end
97
97
 
98
98
  #
@@ -105,7 +105,7 @@ module Ironfan
105
105
  cluster_path.each do |cp_dir|
106
106
  Dir[ File.join(cp_dir, '*.rb') ].each do |filename|
107
107
  cluster_name = File.basename(filename).gsub(/\.rb$/, '')
108
- @cluster_filenames[cluster_name] ||= filename
108
+ @cluster_filenames[cluster_name.to_sym] ||= filename
109
109
  end
110
110
  end
111
111
  @cluster_filenames
@@ -149,7 +149,11 @@ module Ironfan
149
149
  end
150
150
 
151
151
  def self.substep(name, desc)
152
- step(name, " - #{desc}", :gray) if chef_config[:verbosity] >= 1
152
+ step(name, " - #{desc}", :gray) if verbosity >= 1
153
+ end
154
+
155
+ def self.verbosity
156
+ chef_config[:verbosity].to_i
153
157
  end
154
158
 
155
159
  # Output a TODO to the logs if you've switched on pestering
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+
3
+ ironfan_go!
4
+ require 'chef/knife/cluster_bootstrap'
5
+
6
+ describe Chef::Knife::ClusterBootstrap do
7
+ let(:cluster) do
8
+ Ironfan.load_cluster(:gunbai)
9
+ end
10
+
11
+ let(:target) do
12
+ Ironfan.broker.discover!(cluster)
13
+ end
14
+
15
+ let(:computers) do
16
+ Ironfan::Broker::Computers.receive(
17
+ MultiJson.load(
18
+ File.open(Pathname.path_to(:fixtures, 'gunbai_slice.json'))))
19
+ end
20
+
21
+ subject do
22
+ described_class.new(slice)
23
+ end
24
+
25
+ context 'getting slice' do
26
+ before do
27
+ subject.stub(:relevant?){ true }
28
+ subject.stub(:run_bootstrap)
29
+ subject.config[:yes] = true
30
+ end
31
+ context 'full slice' do
32
+ let(:slice){ ['gunbai'] }
33
+ it 'fails if there are multiple environments' do
34
+ expect{ subject.run }.to raise_error("Cannot bootstrap multiple chef environments")
35
+ end
36
+ end
37
+ context 'partial slice' do
38
+ let(:slice){ ['gunbai-hub'] }
39
+ it 'runs' do
40
+ subject.should_receive(:run_bootstrap).once
41
+ subject.run
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ # it 'loads computers from json' do
48
+ # computers.length.should == 2
49
+ # computers.first.server.fullname.should == 'gunbai-hub-0'
50
+ # end
51
+ end
@@ -0,0 +1,24 @@
1
+ Ironfan.cluster 'gunbai' do
2
+ cloud(:ec2) do
3
+ permanent false
4
+ availability_zones ['us-east-1d']
5
+ flavor 't1.micro'
6
+ backing 'ebs'
7
+ image_name 'natty'
8
+ bootstrap_distro 'ubuntu10.04-ironfan'
9
+ chef_client_script 'client.rb'
10
+ mount_ephemerals
11
+ end
12
+
13
+ environment :dev
14
+
15
+ role :ssh
16
+ cloud(:ec2).security_group(:ssh).authorize_port_range(22..22)
17
+
18
+ facet :hub do
19
+ end
20
+
21
+ facet :spoke do
22
+ environment :other
23
+ end
24
+ end
@@ -0,0 +1,330 @@
1
+ [
2
+ {
3
+ "server": {
4
+ "name": "0",
5
+ "run_list_items": [
6
+ {
7
+ "name": "role[ssh]",
8
+ "rank": 1,
9
+ "placement": "normal"
10
+ },
11
+ {
12
+ "name": "role[gunbai_cluster]",
13
+ "rank": 2,
14
+ "placement": "last"
15
+ },
16
+ {
17
+ "name": "role[gunbai_hub]",
18
+ "rank": 3,
19
+ "placement": "last"
20
+ }
21
+ ],
22
+ "clouds": [
23
+ {
24
+ "name": "ec2",
25
+ "default_cloud": false,
26
+ "availability_zones": [
27
+ "us-east-1d"
28
+ ],
29
+ "backing": "ebs",
30
+ "bits": 64,
31
+ "bootstrap_distro": "ubuntu10.04-ironfan",
32
+ "chef_client_script": "client.rb",
33
+ "default_availability_zone": "us-east-1d",
34
+ "flavor": "t1.micro",
35
+ "image_id": null,
36
+ "image_name": "natty",
37
+ "keypair": null,
38
+ "monitoring": null,
39
+ "mount_ephemerals": {
40
+
41
+ },
42
+ "permanent": false,
43
+ "placement_group": null,
44
+ "provider": "Ironfan::Provider::Ec2",
45
+ "public_ip": null,
46
+ "region": "us-east-1",
47
+ "ssh_user": "ubuntu",
48
+ "ssh_identity_dir": "/Users/flip/ics/sysadmin/enterprise-homebase/knife/credentials/ec2_keys",
49
+ "security_groups": [
50
+ {
51
+ "name": "ssh",
52
+ "group_authorized": [
53
+
54
+ ],
55
+ "group_authorized_by": [
56
+
57
+ ],
58
+ "range_authorizations": [
59
+ [
60
+ "22..22",
61
+ "0.0.0.0/0",
62
+ "tcp"
63
+ ]
64
+ ],
65
+ "_type": "ironfan.dsl.ec2.security_group"
66
+ }
67
+ ],
68
+ "subnet": null,
69
+ "validation_key": "-----BEGIN RSA PRIVATE KEY-----\nSNIP\n-----END RSA PRIVATE KEY-----\n",
70
+ "vpc": null,
71
+ "_type": "ironfan.dsl.ec2"
72
+ }
73
+ ],
74
+ "volumes": [
75
+
76
+ ],
77
+ "environment": "dev",
78
+ "use_cloud": null,
79
+ "cluster_role": {
80
+ "name": "gunbai_cluster",
81
+ "override_attributes": {
82
+
83
+ },
84
+ "default_attributes": {
85
+
86
+ },
87
+ "_type": "ironfan.dsl.role"
88
+ },
89
+ "facet_role": {
90
+ "name": "gunbai_hub",
91
+ "override_attributes": {
92
+
93
+ },
94
+ "default_attributes": {
95
+
96
+ },
97
+ "_type": "ironfan.dsl.role"
98
+ },
99
+ "cluster_name": "gunbai",
100
+ "facet_name": "hub",
101
+ "_type": "ironfan.dsl.server"
102
+ },
103
+ "resources": [
104
+ {
105
+ "adaptee": {"name":"gunbai","fingerprint":"a2:9a:15:b3:bc:95:68:99:2c:91:43:9c:3b:6b:99:00:11:e9:b2:5e"},
106
+ "bogus": [
107
+
108
+ ],
109
+ "key_filename": "/Users/flip/ics/sysadmin/enterprise-homebase/knife/credentials/ec2_keys/gunbai.pem",
110
+ "_type": "ironfan.provider.ec2.keypair"
111
+ },
112
+ {
113
+ "adaptee": {"ip_permissions":[{"groups":[],"ipRanges":[{"cidrIp":"0.0.0.0/0"}],"ipProtocol":"tcp","fromPort":22,"toPort":22}],"ip_permissions_egress":[],"owner_id":"689981200839","group_id":"sg-a0f174c9","name":"ssh","description":"cluster_chef+generated+group+ssh"},
114
+ "bogus": [
115
+
116
+ ],
117
+ "ensured": false,
118
+ "_type": "ironfan.provider.ec2.security_group"
119
+ }
120
+ ],
121
+ "drives": [
122
+ {
123
+ "node": {
124
+
125
+ },
126
+ "disk": null,
127
+ "volume": {
128
+ "name": "root",
129
+ "attachable": "ebs",
130
+ "availability_zone": null,
131
+ "create_at_launch": false,
132
+ "device": "/dev/sda1",
133
+ "formattable": false,
134
+ "fstype": "ext4",
135
+ "in_raid": false,
136
+ "keep": false,
137
+ "mount_dump": null,
138
+ "mount_pass": null,
139
+ "mount_options": "defaults,nouuid,noatime",
140
+ "mount_point": "/",
141
+ "mountable": true,
142
+ "size": null,
143
+ "volume_id": null,
144
+ "resizable": false,
145
+ "snapshot_id": null,
146
+ "snapshot_name": null,
147
+ "tags": {
148
+
149
+ },
150
+ "_type": "ironfan.dsl.volume"
151
+ },
152
+ "name": "root",
153
+ "_type": "ironfan.broker.drive"
154
+ }
155
+ ],
156
+ "providers": [
157
+ "Ironfan::Provider::ChefServer",
158
+ "Ironfan::Provider::Ec2"
159
+ ],
160
+ "name": null,
161
+ "bogus": [
162
+
163
+ ],
164
+ "_type": "ironfan.broker.computer"
165
+ },
166
+ {
167
+ "server": {
168
+ "name": "0",
169
+ "run_list_items": [
170
+ {
171
+ "name": "role[ssh]",
172
+ "rank": 1,
173
+ "placement": "normal"
174
+ },
175
+ {
176
+ "name": "role[gunbai_cluster]",
177
+ "rank": 4,
178
+ "placement": "last"
179
+ },
180
+ {
181
+ "name": "role[gunbai_spoke]",
182
+ "rank": 5,
183
+ "placement": "last"
184
+ }
185
+ ],
186
+ "clouds": [
187
+ {
188
+ "name": "ec2",
189
+ "default_cloud": false,
190
+ "availability_zones": [
191
+ "us-east-1d"
192
+ ],
193
+ "backing": "ebs",
194
+ "bits": 64,
195
+ "bootstrap_distro": "ubuntu10.04-ironfan",
196
+ "chef_client_script": "client.rb",
197
+ "default_availability_zone": "us-east-1d",
198
+ "flavor": "t1.micro",
199
+ "image_id": null,
200
+ "image_name": "natty",
201
+ "keypair": null,
202
+ "monitoring": null,
203
+ "mount_ephemerals": {
204
+
205
+ },
206
+ "permanent": false,
207
+ "placement_group": null,
208
+ "provider": "Ironfan::Provider::Ec2",
209
+ "public_ip": null,
210
+ "region": "us-east-1",
211
+ "ssh_user": "ubuntu",
212
+ "ssh_identity_dir": "/Users/flip/ics/sysadmin/enterprise-homebase/knife/credentials/ec2_keys",
213
+ "security_groups": [
214
+ {
215
+ "name": "ssh",
216
+ "group_authorized": [
217
+
218
+ ],
219
+ "group_authorized_by": [
220
+
221
+ ],
222
+ "range_authorizations": [
223
+ [
224
+ "22..22",
225
+ "0.0.0.0/0",
226
+ "tcp"
227
+ ]
228
+ ],
229
+ "_type": "ironfan.dsl.ec2.security_group"
230
+ }
231
+ ],
232
+ "subnet": null,
233
+ "validation_key": "-----BEGIN RSA PRIVATE KEY-----\nSNIP\n-----END RSA PRIVATE KEY-----\n",
234
+ "vpc": null,
235
+ "_type": "ironfan.dsl.ec2"
236
+ }
237
+ ],
238
+ "volumes": [
239
+
240
+ ],
241
+ "environment": "dev",
242
+ "use_cloud": null,
243
+ "cluster_role": {
244
+ "name": "gunbai_cluster",
245
+ "override_attributes": {
246
+
247
+ },
248
+ "default_attributes": {
249
+
250
+ },
251
+ "_type": "ironfan.dsl.role"
252
+ },
253
+ "facet_role": {
254
+ "name": "gunbai_spoke",
255
+ "override_attributes": {
256
+
257
+ },
258
+ "default_attributes": {
259
+
260
+ },
261
+ "_type": "ironfan.dsl.role"
262
+ },
263
+ "cluster_name": "gunbai",
264
+ "facet_name": "spoke",
265
+ "_type": "ironfan.dsl.server"
266
+ },
267
+ "resources": [
268
+ {
269
+ "adaptee": {"name":"gunbai","fingerprint":"a2:3b:9a:15:b3:bc:95:68:99:9e:2c:91:43:9c:6b:a5:6f:e9:b2:5e"},
270
+ "bogus": [
271
+
272
+ ],
273
+ "key_filename": "/Users/flip/ics/sysadmin/enterprise-homebase/knife/credentials/ec2_keys/gunbai.pem",
274
+ "_type": "ironfan.provider.ec2.keypair"
275
+ },
276
+ {
277
+ "adaptee": {"ip_permissions":[{"groups":[],"ipRanges":[{"cidrIp":"0.0.0.0/0"}],"ipProtocol":"tcp","fromPort":22,"toPort":22}],"ip_permissions_egress":[],"owner_id":"689981200839","group_id":"sg-a0f174c9","name":"ssh","description":"cluster_chef+generated+group+ssh"},
278
+ "bogus": [
279
+
280
+ ],
281
+ "ensured": false,
282
+ "_type": "ironfan.provider.ec2.security_group"
283
+ }
284
+ ],
285
+ "drives": [
286
+ {
287
+ "node": {
288
+
289
+ },
290
+ "disk": null,
291
+ "volume": {
292
+ "name": "root",
293
+ "attachable": "ebs",
294
+ "availability_zone": null,
295
+ "create_at_launch": false,
296
+ "device": "/dev/sda1",
297
+ "formattable": false,
298
+ "fstype": "ext4",
299
+ "in_raid": false,
300
+ "keep": false,
301
+ "mount_dump": null,
302
+ "mount_pass": null,
303
+ "mount_options": "defaults,nouuid,noatime",
304
+ "mount_point": "/",
305
+ "mountable": true,
306
+ "size": null,
307
+ "volume_id": null,
308
+ "resizable": false,
309
+ "snapshot_id": null,
310
+ "snapshot_name": null,
311
+ "tags": {
312
+
313
+ },
314
+ "_type": "ironfan.dsl.volume"
315
+ },
316
+ "name": "root",
317
+ "_type": "ironfan.broker.drive"
318
+ }
319
+ ],
320
+ "providers": [
321
+ "Ironfan::Provider::ChefServer",
322
+ "Ironfan::Provider::Ec2"
323
+ ],
324
+ "name": null,
325
+ "bogus": [
326
+
327
+ ],
328
+ "_type": "ironfan.broker.computer"
329
+ }
330
+ ]
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ require 'ironfan'
4
+
5
+ describe Ironfan::Dsl::Cluster do
6
+ let (:cluster) do
7
+ Ironfan.cluster "sparky" do
8
+
9
+ cloud(:ec2) do
10
+ security_group(:ssh).authorize_port_range 22..22
11
+ flavor 't1.micro'
12
+ end
13
+
14
+ facet :web do
15
+ instances 3
16
+ end
17
+
18
+ end
19
+ end
20
+
21
+ describe 'web facet server resolution' do
22
+ before { @facet = cluster.facets.values.first }
23
+ subject { @facet }
24
+ its(:name) { should eql "web" }
25
+
26
+ it 'should have the right number of servers' do
27
+ @facet.servers.length.should == 3
28
+ end
29
+
30
+ it 'should have one cloud provider, EC2' do
31
+ @facet.servers[0].clouds.keys.should == [ :ec2 ]
32
+ end
33
+ end
34
+
35
+ end
data/spec/spec_helper.rb CHANGED
@@ -2,6 +2,31 @@ $:.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'chef'
3
3
  require 'chef/knife'
4
4
  require 'fog'
5
-
6
5
  Fog.mock!
7
6
  Fog::Mock.delay = 0
7
+
8
+ require 'gorillib/pathname'
9
+
10
+ Pathname.register_paths(
11
+ code: File.expand_path('../..', __FILE__),
12
+ fixtures: [:code, 'spec', 'fixtures'],
13
+ )
14
+
15
+ RSpec.configure do |config|
16
+ def ironfan_go!
17
+ Chef::Knife.new.configure_chef
18
+ Chef::Config.instance_eval do
19
+ knife.merge!({
20
+ :aws_access_key_id => 'access_key',
21
+ :aws_secret_access_key => 'secret',
22
+ })
23
+ cluster_path Pathname.path_to(:fixtures).to_s
24
+ end
25
+
26
+ require 'ironfan'
27
+
28
+ Ironfan.ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
29
+ Ironfan.chef_config = { :verbosity => 0 }
30
+ Ironfan.cluster_path
31
+ end
32
+ end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: ironfan
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 4.2.1
5
+ version: 4.2.2
6
6
  platform: ruby
7
7
  authors:
8
8
  - Infochimps
@@ -203,14 +203,18 @@ files:
203
203
  - lib/ironfan/provider/ec2.rb
204
204
  - lib/ironfan/provider/ec2/ebs_volume.rb
205
205
  - lib/ironfan/provider/ec2/elastic_ip.rb
206
- - lib/ironfan/provider/ec2/key_pair.rb
206
+ - lib/ironfan/provider/ec2/keypair.rb
207
207
  - lib/ironfan/provider/ec2/machine.rb
208
208
  - lib/ironfan/provider/ec2/placement_group.rb
209
209
  - lib/ironfan/provider/ec2/security_group.rb
210
210
  - lib/ironfan/provider/virtualbox.rb
211
211
  - lib/ironfan/provider/virtualbox/machine.rb
212
212
  - lib/ironfan/requirements.rb
213
+ - spec/chef/cluster_bootstrap_spec.rb
214
+ - spec/fixtures/gunbai.rb
215
+ - spec/fixtures/gunbai_slice.json
213
216
  - spec/ironfan/cluster_spec.rb
217
+ - spec/ironfan/ec2/cloud_provider.rb
214
218
  - spec/ironfan/ec2/cloud_provider_spec.rb
215
219
  - spec/ironfan/ec2/security_group_spec.rb
216
220
  - spec/spec_helper.rb
@@ -230,7 +234,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
230
234
  requirements:
231
235
  - - ">="
232
236
  - !ruby/object:Gem::Version
233
- hash: -2791351356463943124
237
+ hash: 3459217144726680577
234
238
  segments:
235
239
  - 0
236
240
  version: "0"
@@ -252,5 +256,9 @@ test_files:
252
256
  - spec/ironfan/cluster_spec.rb
253
257
  - spec/ironfan/ec2/cloud_provider_spec.rb
254
258
  - spec/ironfan/ec2/security_group_spec.rb
259
+ - spec/ironfan/ec2/cloud_provider.rb
260
+ - spec/chef/cluster_bootstrap_spec.rb
261
+ - spec/fixtures/gunbai_slice.json
262
+ - spec/fixtures/gunbai.rb
255
263
  - spec/spec_helper.rb
256
264
  - spec/test_config.rb