ironfan 4.8.7 → 4.9.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,6 @@
1
+ # v4.9.0
2
+ * initial vSphere support (thanks @brandonbell) - see lib/ironfan/dsl/vsphere and lib/ironfan/provider/vsphere for more info
3
+
1
4
  # v4.8.7
2
5
  * ec2/machine: fixing stop and start to wait for all valid end-states
3
6
 
data/Gemfile.lock CHANGED
@@ -27,16 +27,16 @@ GEM
27
27
  configliere (0.4.18)
28
28
  highline (>= 1.5.2)
29
29
  multi_json (>= 1.1)
30
- diff-lcs (1.2.1)
30
+ diff-lcs (1.2.2)
31
31
  erubis (2.7.0)
32
- excon (0.19.3)
33
- fog (1.9.0)
32
+ excon (0.20.1)
33
+ fog (1.10.1)
34
34
  builder
35
- excon (~> 0.14)
35
+ excon (~> 0.20)
36
36
  formatador (~> 0.2.0)
37
37
  mime-types
38
38
  multi_json (~> 1.0)
39
- net-scp (~> 1.0.4)
39
+ net-scp (~> 1.1)
40
40
  net-ssh (>= 2.1.3)
41
41
  nokogiri (~> 1.5.0)
42
42
  ruby-hmac
@@ -46,19 +46,19 @@ GEM
46
46
  configliere (>= 0.4.13)
47
47
  json
48
48
  multi_json (>= 1.1)
49
- guard (1.6.2)
49
+ guard (1.7.0)
50
+ formatador (>= 0.2.4)
50
51
  listen (>= 0.6.0)
51
52
  lumberjack (>= 1.0.2)
52
53
  pry (>= 0.9.10)
53
- terminal-table (>= 1.4.3)
54
54
  thor (>= 0.14.6)
55
- guard-rspec (2.4.1)
55
+ guard-rspec (2.5.2)
56
56
  guard (>= 1.1)
57
57
  rspec (~> 2.11)
58
- guard-yard (2.0.1)
58
+ guard-yard (2.1.0)
59
59
  guard (>= 1.1.0)
60
60
  yard (>= 0.7.0)
61
- highline (1.6.15)
61
+ highline (1.6.16)
62
62
  ipaddress (0.8.0)
63
63
  jeweler (1.8.4)
64
64
  bundler (~> 1.0)
@@ -69,26 +69,26 @@ GEM
69
69
  linecache19 (0.5.12)
70
70
  ruby_core_source (>= 0.1.4)
71
71
  listen (0.7.3)
72
- lumberjack (1.0.2)
72
+ lumberjack (1.0.3)
73
73
  method_source (0.8.1)
74
- mime-types (1.21)
74
+ mime-types (1.22)
75
75
  mixlib-authentication (1.3.0)
76
76
  mixlib-log
77
77
  mixlib-cli (1.3.0)
78
78
  mixlib-config (1.1.2)
79
- mixlib-log (1.4.1)
79
+ mixlib-log (1.6.0)
80
80
  mixlib-shellout (1.1.0)
81
81
  moneta (0.6.0)
82
- multi_json (1.6.1)
83
- net-scp (1.0.4)
84
- net-ssh (>= 1.99.1)
85
- net-ssh (2.6.5)
82
+ multi_json (1.7.2)
83
+ net-scp (1.1.0)
84
+ net-ssh (>= 2.6.5)
85
+ net-ssh (2.6.6)
86
86
  net-ssh-gateway (1.2.0)
87
87
  net-ssh (>= 2.6.5)
88
88
  net-ssh-multi (1.1)
89
89
  net-ssh (>= 2.1.4)
90
90
  net-ssh-gateway (>= 0.99.0)
91
- nokogiri (1.5.6)
91
+ nokogiri (1.5.9)
92
92
  ohai (6.16.0)
93
93
  ipaddress
94
94
  mixlib-cli
@@ -97,14 +97,14 @@ GEM
97
97
  mixlib-shellout
98
98
  systemu
99
99
  yajl-ruby
100
- oj (2.0.7)
100
+ oj (2.0.10)
101
101
  polyglot (0.3.3)
102
102
  pry (0.9.12)
103
103
  coderay (~> 1.0.5)
104
104
  method_source (~> 0.8)
105
105
  slop (~> 3.4)
106
- rake (10.0.3)
107
- rdoc (4.0.0)
106
+ rake (10.0.4)
107
+ rdoc (4.0.1)
108
108
  json (~> 1.4)
109
109
  redcarpet (2.2.2)
110
110
  rest-client (1.6.7)
@@ -113,10 +113,10 @@ GEM
113
113
  rspec-core (~> 2.13.0)
114
114
  rspec-expectations (~> 2.13.0)
115
115
  rspec-mocks (~> 2.13.0)
116
- rspec-core (2.13.0)
116
+ rspec-core (2.13.1)
117
117
  rspec-expectations (2.13.0)
118
118
  diff-lcs (>= 1.1.3, < 2.0)
119
- rspec-mocks (2.13.0)
119
+ rspec-mocks (2.13.1)
120
120
  ruby-debug-base19 (0.11.25)
121
121
  columnize (>= 0.3.1)
122
122
  linecache19 (>= 0.5.11)
@@ -133,10 +133,9 @@ GEM
133
133
  multi_json (~> 1.0)
134
134
  simplecov-html (~> 0.7.1)
135
135
  simplecov-html (0.7.1)
136
- slop (3.4.3)
136
+ slop (3.4.4)
137
137
  systemu (2.5.2)
138
- terminal-table (1.4.5)
139
- thor (0.17.0)
138
+ thor (0.18.1)
140
139
  treetop (1.4.12)
141
140
  polyglot
142
141
  polyglot (>= 0.3.1)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.8.7
1
+ 4.9.0
@@ -148,4 +148,4 @@ rm -r /tmp/knife-bootstrap
148
148
  updatedb
149
149
 
150
150
  echo -e "`date` \n\n**** \n**** Cluster Chef client bootstrap complete\n****\n"
151
- EOF
151
+ EOF
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.8.7"
8
+ s.version = "4.9.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 = "2013-03-05"
12
+ s.date = "2013-04-09"
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 = [
@@ -73,6 +73,7 @@ Gem::Specification.new do |s|
73
73
  "lib/ironfan/dsl/server.rb",
74
74
  "lib/ironfan/dsl/virtualbox.rb",
75
75
  "lib/ironfan/dsl/volume.rb",
76
+ "lib/ironfan/dsl/vsphere.rb",
76
77
  "lib/ironfan/headers.rb",
77
78
  "lib/ironfan/provider.rb",
78
79
  "lib/ironfan/provider/chef.rb",
@@ -90,6 +91,9 @@ Gem::Specification.new do |s|
90
91
  "lib/ironfan/provider/ec2/security_group.rb",
91
92
  "lib/ironfan/provider/virtualbox.rb",
92
93
  "lib/ironfan/provider/virtualbox/machine.rb",
94
+ "lib/ironfan/provider/vsphere.rb",
95
+ "lib/ironfan/provider/vsphere/keypair.rb",
96
+ "lib/ironfan/provider/vsphere/machine.rb",
93
97
  "lib/ironfan/requirements.rb",
94
98
  "notes/Future-development-proposals.md",
95
99
  "notes/Home.md",
@@ -149,7 +153,7 @@ Gem::Specification.new do |s|
149
153
  s.homepage = "http://infochimps.com/labs"
150
154
  s.licenses = ["apachev2"]
151
155
  s.require_paths = ["lib"]
152
- s.rubygems_version = "1.8.24"
156
+ s.rubygems_version = "1.8.23"
153
157
  s.summary = "Infochimps' lightweight cloud orchestration toolkit, built on top of Chef."
154
158
  s.test_files = ["spec/spec_helper/dummy_chef.rb", "spec/integration/spec_helper/launch_cluster.rb", "spec/integration/minimal-chef-repo/roles/systemwide.rb", "spec/integration/minimal-chef-repo/environments/_default.json", "spec/integration/minimal-chef-repo/chefignore", "spec/integration/minimal-chef-repo/knife/knife.rb", "spec/integration/minimal-chef-repo/knife/credentials/knife-org.rb", "spec/integration/spec/simple_cluster_spec.rb", "spec/integration/spec/elb_build_spec.rb", "spec/integration/spec_helper.rb", "spec/ironfan/cluster_spec.rb", "spec/ironfan/ec2/cloud_provider_spec.rb", "spec/ironfan/ec2/elb_spec.rb", "spec/ironfan/ec2/security_group_spec.rb", "spec/chef/cluster_launch_spec.rb", "spec/chef/cluster_bootstrap_spec.rb", "spec/fixtures/gunbai_slice.json", "spec/fixtures/ec2/elb/snakeoil.key", "spec/fixtures/ec2/elb/snakeoil.crt", "spec/fixtures/gunbai.rb", "spec/spec_helper.rb", "spec/test_config.rb"]
155
159
 
@@ -148,4 +148,4 @@ rm -r /tmp/knife-bootstrap
148
148
  updatedb
149
149
 
150
150
  echo -e "`date` \n\n**** \n**** Cluster Chef client bootstrap complete\n****\n"
151
- EOF
151
+ EOF
@@ -9,6 +9,7 @@ module Ironfan
9
9
  require 'chef/node'
10
10
  require 'chef/api_client'
11
11
  require 'fog'
12
+ require 'rbvmomi'
12
13
  end
13
14
 
14
15
  def load_ironfan
@@ -12,6 +12,7 @@ module Ironfan
12
12
  case obj[:name]
13
13
  when :ec2 then Ec2
14
14
  when :virtualbox then VirtualBox
15
+ when :vsphere then Vsphere
15
16
  else raise "Unsupported cloud #{obj[:name]}"
16
17
  end
17
18
  end
@@ -0,0 +1,89 @@
1
+ module Ironfan
2
+ class Dsl
3
+
4
+ class Compute < Ironfan::Dsl
5
+ def vsphere(*attrs,&block)
6
+ cloud(:vsphere, *attrs,&block)
7
+ end
8
+ end
9
+
10
+ class Vsphere < Cloud
11
+ magic :backing, String, :default => ''
12
+ magic :bits, Integer, :default => "64"
13
+ magic :bootstrap_distro, String, :default => 'ubuntu12.04-gems'
14
+ magic :chef_client_script, String
15
+ magic :cluster, String
16
+ magic :cpus, String, :default => "1"
17
+ magic :datacenter, String, :default => ->{ default_datacenter }
18
+ magic :datastore, String
19
+ magic :dns_servers, Array
20
+ magic :domain, String
21
+ magic :default_datacenter, String, :default => ->{ vsphere_datacenters.first }
22
+ magic :gateway, Array
23
+ magic :image_name, String
24
+ magic :ip, String
25
+ magic :memory, String, :default => "4" # Gigabytes
26
+ magic :provider, Whatever, :default => Ironfan::Provider::Vsphere
27
+ magic :ssh_identity_dir, String, :default => ->{ Chef::Config.vsphere_key_dir }
28
+ magic :ssh_user, String, :default => "root"
29
+ magic :subnet, String
30
+ magic :template, String
31
+ magic :validation_key, String, :default => ->{ IO.read(Chef::Config.validation_key) rescue '' }
32
+ magic :virtual_disks, Array, :default => []
33
+ magic :vsphere_datacenters, Array, :default => ['New Datacenter']
34
+ magic :network, String, :default => "VM Network"
35
+
36
+ def image_info
37
+ bit_str = "#{self.bits.to_i}-bit" # correct for legacy image info.
38
+ keys = [datacenter, bit_str, image_name]
39
+ # info = Chef::Config[:vsphere_image_info][ keys ]
40
+ info = nil
41
+ ui.warn("Can't find image for #{[datacenter, bit_str, image_name].inspect}") if info.blank?
42
+ return info || {}
43
+ end
44
+
45
+ def implied_volumes
46
+ result = []
47
+ # FIXME : This is really making assumptions
48
+ result << Ironfan::Dsl::Volume.new(:name => 'root') do
49
+ device '/dev/sda1'
50
+ fstype 'ext4'
51
+ keep false
52
+ mount_point '/'
53
+ end
54
+ return result unless virtual_disks.length > 0
55
+
56
+ virtual_disks.each_with_index do |vd, idx|
57
+ dev, mnt = ["/dev/sd%s" %[(66 + idx).chr.downcase], idx == 0 ? "/mnt" : "/mnt#{idx}"] # WHAAAaaa 0 o ???
58
+ virtualdisk = Ironfan::Dsl::Volume.new(:name => "virtualdisk#{idx}") do
59
+ attachable "VirtualDisk"
60
+ fstype vd[:fs]
61
+ device dev
62
+ mount_point vd[:mount_point] || mnt
63
+ formattable true
64
+ create_at_launch true
65
+ mount_options 'defaults,noatime'
66
+ tags vd[:tags]
67
+ end
68
+ result << virtualdisk
69
+ end
70
+ result
71
+ end
72
+
73
+ def ssh_identity_file(computer)
74
+ cluster = computer.server.cluster_name
75
+ "%s/%s.pem" %[ssh_identity_dir, cluster]
76
+ end
77
+
78
+ def to_display(style,values={})
79
+ return values if style == :minimal
80
+
81
+ values["Datacenter"] = datacenter
82
+ return values if style == :default
83
+
84
+ values
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -30,6 +30,7 @@ module Ironfan
30
30
  class IamServerCertificate < Ironfan::Dsl; end
31
31
  end
32
32
  class VirtualBox < Cloud; end
33
+ class Vsphere < Cloud; end
33
34
  end
34
35
 
35
36
  class Provider < Builder
@@ -57,6 +58,11 @@ module Ironfan
57
58
  class VirtualBox < Ironfan::IaasProvider
58
59
  class Machine < Ironfan::IaasProvider::Machine; end
59
60
  end
61
+ class Vsphere < Ironfan::IaasProvider
62
+ class Machine < Ironfan::IaasProvider::Machine; end
63
+ class Keypair < Ironfan::Provider::Resource; end
64
+ end
65
+
60
66
  end
61
67
 
62
68
  end
@@ -0,0 +1,74 @@
1
+ module Ironfan
2
+ class Provider
3
+ class Vsphere
4
+
5
+ class Keypair < Ironfan::Provider::Resource
6
+ delegate :connection, :connection=, :name, :private_key,
7
+ :to => :adaptee
8
+ # delegate :_dump, :collection, :collection=, :connection,
9
+ # :connection=, :destroy, :fingerprint, :fingerprint=, :identity,
10
+ # :identity=, :name, :name=, :new_record?, :public_key,
11
+ # :public_key=, :reload, :requires, :requires_one, :save,
12
+ # :symbolize_keys, :wait_for, :writable?, :write,
13
+ # :to => :adaptee
14
+
15
+ field :key_filename, String, :default => ->{ "#{Keypair.key_dir}/#{name}.pem" }
16
+
17
+ def self.shared? ; true ; end
18
+ def self.multiple? ; false ; end
19
+ def self.resource_type ; :keypair ; end
20
+
21
+ def self.expected_ids(computer)
22
+ [computer.server.cluster_name]
23
+ end
24
+
25
+ def private_key
26
+ puts "pkey"
27
+ File.open(key_filename, "rb").read
28
+ end
29
+
30
+ def self.public_key(computer)
31
+ key_filename = "%s/%s.pem" %[key_dir, computer.server.cluster_name]
32
+ key = OpenSSL::PKey::RSA.new(File.open(key_filename, "rb").read)
33
+ data = [ key.to_blob ].pack('m0')
34
+ "#{key.ssh_type} #{data}"
35
+ end
36
+
37
+ def self.create_private_key(key, name)
38
+ key_filename = "%s/%s.pem" %[key_dir, name]
39
+ File.open(key_filename, "w", 0600){|f| f.print( key.to_s ) }
40
+ end
41
+
42
+ def to_s
43
+ "<%-15s %-12s>" % [self.class.handle, name]
44
+ end
45
+
46
+ #
47
+ # Manipulation
48
+ #
49
+
50
+ def self.prepare!(computers)
51
+ return if computers.empty?
52
+ name = computers.values[0].server.cluster_name
53
+ return if recall? name
54
+ return if File.exists?("%s/%s.pem" %[key_dir, name])
55
+ Ironfan.step(name, "creating key pair for #{name}", :blue)
56
+ Dir.mkdir(key_dir) if !FileTest::directory?(key_dir)
57
+ create_private_key(OpenSSL::PKey::RSA.new(2048), name)
58
+ end
59
+
60
+ #
61
+ # Utility
62
+ #
63
+
64
+ def self.key_dir
65
+ return Chef::Config.vsphere_key_dir if Chef::Config.vsphere_key_dir
66
+ dir = "#{ENV['HOME']}/.chef/credentials/vshere_keys"
67
+ warn "Please set 'vsphere_key_dir' in your knife.rb. Will use #{dir} as a default"
68
+ dir
69
+ end
70
+
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,343 @@
1
+ module Ironfan
2
+ class Provider
3
+ class Vsphere
4
+
5
+ class Machine < Ironfan::IaasProvider::Machine
6
+ delegate :config, :connection, :connection=, :disks, :Destroy_Task, :guest, :PowerOffVM_Task,
7
+ :PowerOnVM_Task, :powerState, :ReconfigVM_Task, :runtime,
8
+ :to => :adaptee
9
+
10
+ def self.shared?() false; end
11
+ def self.multiple?() false; end
12
+ def self.resource_type() :machine; end
13
+ def self.expected_ids(computer) [computer.server.full_name]; end
14
+
15
+ def name
16
+ return adaptee.config.name
17
+ end
18
+
19
+ def keypair
20
+ end
21
+
22
+ def vpc_id
23
+ return true
24
+ end
25
+
26
+ def dns_name
27
+ host = adaptee.guest.hostName
28
+ domain = adaptee.guest.domainName
29
+ return host unless domain
30
+ return "%s.%s" %[host, domain]
31
+ end
32
+
33
+ def public_ip_address
34
+ # FIXME
35
+ return adaptee.guest.ipAddress
36
+ end
37
+
38
+ def private_ip_address
39
+ return adaptee.guest.ipAddress
40
+ end
41
+
42
+ def public_hostname
43
+ # FIXME
44
+ return nil
45
+ end
46
+
47
+ def destroy
48
+ adaptee.PowerOffVM_Task.wait_for_completion unless adaptee.runtime.powerState == "poweredOff"
49
+ adaptee.Destroy_Task.wait_for_completion
50
+ state = "destroyed"
51
+ end
52
+
53
+ def created?
54
+ ["poweredOn", "poweredOff"].include? adaptee.runtime.powerState
55
+ end
56
+
57
+ def wait_for
58
+ return true
59
+ end
60
+
61
+ def running?
62
+ adaptee.runtime.powerState == "poweredOn"
63
+ state = "poweredOn"
64
+ end
65
+
66
+ def stopped?
67
+ adaptee.runtime.powerState == "poweredOff"
68
+ state = "poweredOff"
69
+ end
70
+
71
+ def start
72
+ adaptee.PowerOnVM_Task.wait_for_completion
73
+ end
74
+
75
+ def stop
76
+ # TODO: Gracefully shutdown guest using adatee.ShutdownGuest ?
77
+ # There is no "wait_for_completion" with this however...
78
+ adaptee.PowerOffVM_Task.wait_for_completion
79
+ end
80
+
81
+ def self.ip_settings(settings, hostname)
82
+ # FIXME: A lot of this is required if using a static IP
83
+ # Heavily referenced Serengeti's FOG here
84
+
85
+ ip_settings = RbVmomi::VIM::CustomizationIPSettings.new(:ip =>
86
+ RbVmomi::VIM::CustomizationFixedIp(:ipAddress => settings[:ip]), :gateway => settings[:gateway], :subnetMask => settings[:subnet]) if settings[:ip]
87
+ ip_settings ||= RbVmomi::VIM::CustomizationIPSettings.new("ip" => RbVmomi::VIM::CustomizationDhcpIpGenerator.new())
88
+ ip_settings.dnsDomain = settings[:domain]
89
+ global_ip_settings = RbVmomi::VIM.CustomizationGlobalIPSettings
90
+ global_ip_settings.dnsServerList = settings[:dnsServers]
91
+ global_ip_settings.dnsSuffixList = [settings[:domain]]
92
+ hostname = RbVmomi::VIM::CustomizationFixedName.new(:name => hostname)
93
+ linux_prep = RbVmomi::VIM::CustomizationLinuxPrep.new( :domain => settings[:domain], :hostName => hostname )
94
+ adapter_mapping = [RbVmomi::VIM::CustomizationAdapterMapping.new("adapter" => ip_settings)]
95
+ spec = RbVmomi::VIM::CustomizationSpec.new( :identity => linux_prep,
96
+ :globalIPSettings => global_ip_settings,
97
+ :nicSettingMap => adapter_mapping )
98
+
99
+ return spec
100
+
101
+ end
102
+
103
+ def self.generate_clone_spec(src_config, dc, cpus, memory, datastore, virtual_disks, network, cluster)
104
+ # TODO : A lot of this should be moved to utilities in providers/vsphere.rb
105
+ rspec = Vsphere.get_rspec(dc, cluster)
106
+ rspec.datastore = datastore
107
+
108
+ clone_spec = RbVmomi::VIM.VirtualMachineCloneSpec(:location => rspec, :template => false, :powerOn => true)
109
+ clone_spec.config = RbVmomi::VIM.VirtualMachineConfigSpec(:deviceChange => Array.new, :extraConfig => nil)
110
+
111
+ network = Vsphere.find_network(network, dc)
112
+ card = src_config.hardware.device.find { |d| d.deviceInfo.label == "Network adapter 1" }
113
+ begin
114
+ switch_port = RbVmomi::VIM.DistributedVirtualSwitchPortConnection(
115
+ :switchUuid => network.config.distributedVirtualSwitch.uuid,
116
+ :portgroupKey => network.key
117
+ )
118
+ card.backing.port = switch_port
119
+ rescue
120
+ card.backing.deviceName = network.name
121
+ end
122
+
123
+ network_spec = RbVmomi::VIM.VirtualDeviceConfigSpec(:device => card, :operation => "edit")
124
+ clone_spec.config.deviceChange.push network_spec
125
+
126
+ virtual_disks.each_with_index do |vd, idx|
127
+ size = vd[:size].to_i
128
+ label = vd[:label].to_i
129
+ key = 2001 + idx # key 2000 -> SCSI0, 2001 -> SCSI1...
130
+ filename = vd[:datastore] || datastore.name
131
+
132
+ disk = RbVmomi::VIM.VirtualDisk(
133
+ :key => key,
134
+ :capacityInKB => size * 1024 * 1024,
135
+ :controllerKey => 1000, # SCSI controller
136
+ :unitNumber => idx + 1,
137
+ :backing => RbVmomi::VIM.VirtualDiskFlatVer2BackingInfo(
138
+ :fileName => "[#{filename}]",
139
+ :diskMode => :persistent,
140
+ :thinProvisioned => true,
141
+ :datastore => Vsphere.find_ds(dc, vd[:datastore]) || datastore
142
+ ),
143
+ :deviceInfo => RbVmomi::VIM.Description(
144
+ :label => label,
145
+ :summary => "%sGB" %[size]
146
+ ),
147
+ )
148
+
149
+ disk_spec = {:operation => :add, :fileOperation => :create, :device => disk }
150
+ clone_spec.config.deviceChange.push disk_spec
151
+ end
152
+
153
+ clone_spec.config.numCPUs = Integer(cpus)
154
+ clone_spec.config.memoryMB = Integer(memory) * 1024
155
+
156
+ clone_spec
157
+ end
158
+
159
+ def self.create!(computer)
160
+ return if computer.machine? and computer.machine.created?
161
+ Ironfan.step(computer.name,"creating cloud machine", :green)
162
+
163
+ errors = lint(computer)
164
+ if errors.present? then raise ArgumentError, "Failed validation: #{errors.inspect}" ; end
165
+
166
+ # TODO: Pass launch_desc to a single function and let it do the rest... like fog does
167
+ launch_desc = launch_description(computer)
168
+ cpus = launch_desc[:cpus]
169
+ datacenter = launch_desc[:datacenter]
170
+ memory = launch_desc[:memory]
171
+ template = launch_desc[:template]
172
+ user_data = launch_desc[:user_data]
173
+ virtual_disks = launch_desc[:virtual_disks]
174
+ network = launch_desc[:network]
175
+ ip_settings = launch_desc[:ip_settings]
176
+
177
+ datacenter = Vsphere.find_dc(launch_desc[:datacenter])
178
+ raise "Couldn't find #{launch_desc[:datacenter]} datacenter" unless datacenter
179
+ cluster = Vsphere.find_in_folder(datacenter.hostFolder, RbVmomi::VIM::ClusterComputeResource, launch_desc[:cluster])
180
+ raise "Couldn't find #{launch_desc[:cluster]} cluster in #{datacenter}" unless cluster
181
+ datastore = Vsphere.find_ds(datacenter, launch_desc[:datastore]) # FIXME: add in round robin choosing?
182
+ raise "Couldn't find #{launch_desc[:datastore]} datastore in #{datacenter}" unless datastore
183
+ src_folder = datacenter.vmFolder
184
+ src_vm = Vsphere.find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, template)
185
+ raise "Couldn't find template #{template} in #{datacenter}:#{cluster}" unless src_vm
186
+
187
+ Ironfan.safely do
188
+ clone_spec = generate_clone_spec(src_vm.config, datacenter, cpus, memory, datastore, virtual_disks, network, cluster)
189
+ clone_spec.customization = ip_settings(ip_settings, computer.name) if ip_settings[:ip]
190
+
191
+ vsphere_server = src_vm.CloneVM_Task(:folder => src_vm.parent, :name => computer.name, :spec => clone_spec)
192
+
193
+ state = vsphere_server.info.state # RbVmomi wait_for_completion stops world...
194
+ while (state != 'error') and (state != 'success')
195
+ sleep 2
196
+ state = vsphere_server.info.state
197
+ end
198
+
199
+ # Will break if the VM isn't finished building.
200
+ new_vm = Vsphere.find_in_folder(src_folder, RbVmomi::VIM::VirtualMachine, computer.name)
201
+ machine = Machine.new(:adaptee => new_vm)
202
+ computer.machine = machine
203
+ remember machine, :id => machine.name
204
+
205
+ Ironfan.step(computer.name,"pushing keypair", :green)
206
+ public_key = Vsphere::Keypair.public_key(computer)
207
+ # TODO - This probably belongs somewhere else. This is how we'll enject the pubkey as well as any user information we may want
208
+ # This data is not persistent across reboots...
209
+ extraConfig = [{:key => 'guestinfo.pubkey', :value => public_key}, {:key => "guestinfo.user_data", :value => user_data}]
210
+ machine.ReconfigVM_Task(:spec => RbVmomi::VIM::VirtualMachineConfigSpec(:extraConfig => extraConfig)).wait_for_completion
211
+
212
+ # FIXME: This is extremelty fragile right now
213
+ if ip_settings[:ip]
214
+ Ironfan.step(computer.name,"waiting for ip customizations to complete", :green)
215
+ while machine.guest.ipAddress != ip_settings[:ip]
216
+ sleep 2
217
+ end
218
+ end
219
+
220
+ end
221
+ end
222
+
223
+ #
224
+ # Discovery
225
+ #
226
+ def self.load!(cluster=nil)
227
+ # FIXME: Fix this one day when we have multiple "datacenters".
228
+ cloud = cluster.servers.values[0].cloud(:vsphere)
229
+ vm_folder = Vsphere.find_dc(cloud.datacenter).vmFolder
230
+ Vsphere.find_all_in_folder(vm_folder, RbVmomi::VIM::VirtualMachine).each do |fs|
231
+ machine = new(:adaptee => fs)
232
+ if recall? machine.name
233
+ machine.bogus << :duplicate_machines
234
+ recall(machine.name).bogus << :duplicate_machines
235
+ remember machine, :append_id => "duplicate:#{machine.config.uuid}"
236
+ else # never seen it
237
+ remember machine
238
+ end
239
+ Chef::Log.debug("Loaded #{machine}")
240
+ end
241
+ end
242
+
243
+ def self.destroy!(computer)
244
+ return unless computer.machine?
245
+ forget computer.machine.name
246
+ computer.machine.destroy
247
+ end
248
+
249
+ def self.launch_description(computer)
250
+ cloud = computer.server.cloud(:vsphere)
251
+ user_data_hsh = {
252
+ :chef_server => Chef::Config[:chef_server_url],
253
+ :client_key => computer.private_key,
254
+ :cluster_name => computer.server.cluster_name,
255
+ :facet_index => computer.server.index,
256
+ :facet_name => computer.server.facet_name,
257
+ :node_name => computer.name,
258
+ :organization => Chef::Config[:organization],
259
+ }
260
+
261
+ ip_settings = {
262
+ :dnsServers => cloud.dns_servers,
263
+ :domain => cloud.domain,
264
+ :gateway => cloud.gateway,
265
+ :ip => cloud.ip,
266
+ :subnet => cloud.subnet,
267
+ }
268
+
269
+ description = {
270
+ :cpus => cloud.cpus,
271
+ :cluster => cloud.cluster,
272
+ :datacenter => cloud.datacenter,
273
+ :datastore => cloud.datastore,
274
+ :memory => cloud.memory,
275
+ :network => cloud.network,
276
+ :template => cloud.template,
277
+ :user_data => JSON.pretty_generate(user_data_hsh),
278
+ :virtual_disks => cloud.virtual_disks,
279
+ :ip_settings => ip_settings
280
+ }
281
+ description
282
+ end
283
+
284
+ # @returns [Hash{String, Array}] of 'what you did wrong' => [relevant, info]
285
+ def self.lint(computer)
286
+ cloud = computer.server.cloud(:vsphere)
287
+ info = [computer.name, cloud.inspect]
288
+ errors = {}
289
+ server_errors = computer.server.lint
290
+ errors["Unhappy Server"] = server_errors if server_errors.present?
291
+ errors["Datacenter"] = info if cloud.datacenter.blank?
292
+ errors["Template"] = info if cloud.template.blank?
293
+ errors["Datastore"] = info if cloud.datastore.blank?
294
+ errors['Missing client'] = info unless computer.client?
295
+ errors['Missing private_key'] = computer.client unless computer.private_key
296
+ #
297
+ errors
298
+ end
299
+
300
+ def to_display(style,values={})
301
+ # style == :minimal
302
+ values["State"] = runtime.powerState rescue "Terminated"
303
+ values["MachineID"] = config.uuid rescue ""
304
+ # values["Public IP"] = public_ip_address
305
+ values["Private IP"] = guest.ipAddress rescue ""
306
+ # values["Created On"] = created_at.to_date
307
+ return values if style == :minimal
308
+
309
+ # style == :default
310
+ # values["Flavor"] = flavor_id
311
+ # values["AZ"] = availability_zone
312
+ return values if style == :default
313
+
314
+ # style == :expanded
315
+ # values["Image"] = image_id
316
+ values["Virtual Disks"] = disks.map { |d| d.backing.fileName }.join(', ')
317
+ # values["SSH Key"] = key_name
318
+ values
319
+ end
320
+
321
+ def ssh_key
322
+ keypair = cloud.keypair || computer.server.cluster_name
323
+ end
324
+
325
+ def self.validate_resources!(computers)
326
+ recall.each_value do |machine|
327
+ next unless machine.users.empty? and machine.name
328
+ if machine.name.match("^#{computers.cluster.name}-")
329
+ machine.bogus << :unexpected_machine
330
+ end
331
+ next unless machine.bogus?
332
+ fake = Ironfan::Broker::Computer.new
333
+ fake[:machine] = machine
334
+ fake.name = machine.name
335
+ machine.users << fake
336
+ computers << fake
337
+ end
338
+ end
339
+
340
+ end
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,79 @@
1
+ module Ironfan
2
+ class Provider
3
+
4
+ class Vsphere < Ironfan::IaasProvider
5
+ self.handle = :vsphere
6
+
7
+ def self.resources()
8
+ [ Machine, Keypair]
9
+ end
10
+
11
+ #
12
+ # Utility functions
13
+ #
14
+
15
+ # Some methods cribbed and modified from https://github.com/ezrapagel/knife-vsphere, Ezra Pagel, Jesse Campbell
16
+ def self.connection
17
+ @@connection ||= RbVmomi::VIM.connect(self.vsphere_credentials)
18
+ end
19
+
20
+ def self.find_ds(vsphere_dc, name = nil)
21
+ ds = vsphere_dc.datastore
22
+ return ds.grep(RbVmomi::VIM::Datastore).find { |d| d.name == name } if !name.nil?
23
+ end
24
+
25
+ def self.find_dc(vsphere_dc)
26
+ connection.serviceInstance.find_datacenter(vsphere_dc)
27
+ end
28
+
29
+ def self.find_in_folder(folder, type, name)
30
+ folder.childEntity.grep(type).find { |o| o.name == name }
31
+ end
32
+
33
+ def self.get_rspec(dc, cluster)
34
+ if cluster
35
+ resource_pool = cluster.resourcePool
36
+ else
37
+ hosts = find_all_in_folder(dc.hostFolder, RbVmomi::VIM::ComputeResource).find { |o| o.class == RbVmomi::VIM::ComputeResource }
38
+ resource_pool = hosts.resourcePool || hosts.first.resourcePool
39
+ end
40
+ RbVmomi::VIM.VirtualMachineRelocateSpec(:pool => resource_pool)
41
+ end
42
+
43
+ def self.find_all_in_folder(folder, type)
44
+ if folder.instance_of?(RbVmomi::VIM::ClusterComputeResource)
45
+ folder = folder.resourcePool
46
+ end
47
+
48
+ if folder.instance_of?(RbVmomi::VIM::ResourcePool)
49
+ folder.resourcePool.grep(type)
50
+ elsif folder.instance_of?(RbVmomi::VIM::Folder)
51
+ folder.childEntity.grep(type)
52
+ else
53
+ puts "Unknown type #{folder.class}, not enumerating"
54
+ nil
55
+ end
56
+ end
57
+
58
+ def self.find_network(network, dc)
59
+ baseEntity = dc.network
60
+ baseEntity.find { |f| f.name == network }
61
+ end
62
+
63
+ def self.get_pub_key(cluster)
64
+ puts Keypair.private_key
65
+ end
66
+
67
+ private
68
+ def self.vsphere_credentials
69
+ return {
70
+ :user => Chef::Config[:knife][:vsphere_username],
71
+ :port => Chef::Config[:knife][:vsphere_port] || 443,
72
+ :password => Chef::Config[:knife][:vsphere_password],
73
+ :host => Chef::Config[:knife][:vsphere_server],
74
+ :insecure => Chef::Config[:knife][:vsphere_insecure] || true
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
@@ -13,6 +13,7 @@ module Ironfan
13
13
  case obj[:name]
14
14
  when :chef then Chef
15
15
  when :ec2 then Ec2
16
+ when :vsphere then Vsphere
16
17
  when :virtualbox then VirtualBox
17
18
  else raise "Unsupported provider #{obj[:name]}"
18
19
  end
@@ -19,6 +19,7 @@ require 'ironfan/dsl/volume'
19
19
 
20
20
  require 'ironfan/dsl/cloud'
21
21
  require 'ironfan/dsl/ec2'
22
+ require 'ironfan/dsl/vsphere'
22
23
 
23
24
 
24
25
  # Providers for specific resources
@@ -42,6 +43,10 @@ require 'ironfan/provider/ec2/iam_server_certificate'
42
43
  require 'ironfan/provider/virtualbox'
43
44
  require 'ironfan/provider/virtualbox/machine'
44
45
 
46
+ require 'ironfan/provider/vsphere'
47
+ require 'ironfan/provider/vsphere/machine'
48
+ require 'ironfan/provider/vsphere/keypair'
49
+
45
50
 
46
51
  # Broker classes to coordinate DSL expectations and provider resources
47
52
  require 'ironfan/broker'
metadata CHANGED
@@ -1,148 +1,201 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: ironfan
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 4.9.0
4
5
  prerelease:
5
- version: 4.8.7
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Infochimps
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2013-03-05 00:00:00 Z
14
- dependencies:
15
- - !ruby/object:Gem::Dependency
12
+ date: 2013-04-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
16
15
  name: chef
17
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
18
17
  none: false
19
- requirements:
18
+ requirements:
20
19
  - - ~>
21
- - !ruby/object:Gem::Version
22
- version: "10.16"
20
+ - !ruby/object:Gem::Version
21
+ version: '10.16'
23
22
  type: :runtime
24
23
  prerelease: false
25
- version_requirements: *id001
26
- - !ruby/object:Gem::Dependency
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '10.16'
30
+ - !ruby/object:Gem::Dependency
27
31
  name: fog
28
- requirement: &id002 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
29
33
  none: false
30
- requirements:
34
+ requirements:
31
35
  - - ~>
32
- - !ruby/object:Gem::Version
33
- version: "1.2"
36
+ - !ruby/object:Gem::Version
37
+ version: '1.2'
34
38
  type: :runtime
35
39
  prerelease: false
36
- version_requirements: *id002
37
- - !ruby/object:Gem::Dependency
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.2'
46
+ - !ruby/object:Gem::Dependency
38
47
  name: formatador
39
- requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
40
49
  none: false
41
- requirements:
50
+ requirements:
42
51
  - - ~>
43
- - !ruby/object:Gem::Version
44
- version: "0.2"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
45
54
  type: :runtime
46
55
  prerelease: false
47
- version_requirements: *id003
48
- - !ruby/object:Gem::Dependency
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.2'
62
+ - !ruby/object:Gem::Dependency
49
63
  name: gorillib
50
- requirement: &id004 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
51
65
  none: false
52
- requirements:
66
+ requirements:
53
67
  - - ~>
54
- - !ruby/object:Gem::Version
68
+ - !ruby/object:Gem::Version
55
69
  version: 0.4.2
56
70
  type: :runtime
57
71
  prerelease: false
58
- version_requirements: *id004
59
- - !ruby/object:Gem::Dependency
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.4.2
78
+ - !ruby/object:Gem::Dependency
60
79
  name: json
61
- requirement: &id005 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
62
81
  none: false
63
- requirements:
64
- - - "="
65
- - !ruby/object:Gem::Version
82
+ requirements:
83
+ - - '='
84
+ - !ruby/object:Gem::Version
66
85
  version: 1.5.4
67
86
  type: :runtime
68
87
  prerelease: false
69
- version_requirements: *id005
70
- - !ruby/object:Gem::Dependency
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - '='
92
+ - !ruby/object:Gem::Version
93
+ version: 1.5.4
94
+ - !ruby/object:Gem::Dependency
71
95
  name: bundler
72
- requirement: &id006 !ruby/object:Gem::Requirement
96
+ requirement: !ruby/object:Gem::Requirement
73
97
  none: false
74
- requirements:
98
+ requirements:
75
99
  - - ~>
76
- - !ruby/object:Gem::Version
77
- version: "1.0"
100
+ - !ruby/object:Gem::Version
101
+ version: '1.0'
78
102
  type: :development
79
103
  prerelease: false
80
- version_requirements: *id006
81
- - !ruby/object:Gem::Dependency
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '1.0'
110
+ - !ruby/object:Gem::Dependency
82
111
  name: rake
83
- requirement: &id007 !ruby/object:Gem::Requirement
112
+ requirement: !ruby/object:Gem::Requirement
84
113
  none: false
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: "0"
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
89
118
  type: :development
90
119
  prerelease: false
91
- version_requirements: *id007
92
- - !ruby/object:Gem::Dependency
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
93
127
  name: rspec
94
- requirement: &id008 !ruby/object:Gem::Requirement
128
+ requirement: !ruby/object:Gem::Requirement
95
129
  none: false
96
- requirements:
130
+ requirements:
97
131
  - - ~>
98
- - !ruby/object:Gem::Version
99
- version: "2.8"
132
+ - !ruby/object:Gem::Version
133
+ version: '2.8'
100
134
  type: :development
101
135
  prerelease: false
102
- version_requirements: *id008
103
- - !ruby/object:Gem::Dependency
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '2.8'
142
+ - !ruby/object:Gem::Dependency
104
143
  name: yard
105
- requirement: &id009 !ruby/object:Gem::Requirement
144
+ requirement: !ruby/object:Gem::Requirement
106
145
  none: false
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: "0.7"
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0.7'
111
150
  type: :development
112
151
  prerelease: false
113
- version_requirements: *id009
114
- - !ruby/object:Gem::Dependency
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0.7'
158
+ - !ruby/object:Gem::Dependency
115
159
  name: redcarpet
116
- requirement: &id010 !ruby/object:Gem::Requirement
160
+ requirement: !ruby/object:Gem::Requirement
117
161
  none: false
118
- requirements:
119
- - - ">="
120
- - !ruby/object:Gem::Version
121
- version: "2.1"
162
+ requirements:
163
+ - - ! '>='
164
+ - !ruby/object:Gem::Version
165
+ version: '2.1'
122
166
  type: :development
123
167
  prerelease: false
124
- version_requirements: *id010
125
- - !ruby/object:Gem::Dependency
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '2.1'
174
+ - !ruby/object:Gem::Dependency
126
175
  name: oj
127
- requirement: &id011 !ruby/object:Gem::Requirement
176
+ requirement: !ruby/object:Gem::Requirement
128
177
  none: false
129
- requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: "1.2"
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '1.2'
133
182
  type: :development
134
183
  prerelease: false
135
- version_requirements: *id011
136
- 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.
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '1.2'
190
+ description: Ironfan allows you to orchestrate not just systems but clusters of machines.
191
+ It includes a powerful layer on top of knife and a collection of cloud cookbooks.
137
192
  email: coders@infochimps.com
138
193
  executables: []
139
-
140
194
  extensions: []
141
-
142
- extra_rdoc_files:
195
+ extra_rdoc_files:
143
196
  - LICENSE.md
144
197
  - README.md
145
- files:
198
+ files:
146
199
  - .gitignore
147
200
  - .rspec
148
201
  - .yardopts
@@ -199,6 +252,7 @@ files:
199
252
  - lib/ironfan/dsl/server.rb
200
253
  - lib/ironfan/dsl/virtualbox.rb
201
254
  - lib/ironfan/dsl/volume.rb
255
+ - lib/ironfan/dsl/vsphere.rb
202
256
  - lib/ironfan/headers.rb
203
257
  - lib/ironfan/provider.rb
204
258
  - lib/ironfan/provider/chef.rb
@@ -216,6 +270,9 @@ files:
216
270
  - lib/ironfan/provider/ec2/security_group.rb
217
271
  - lib/ironfan/provider/virtualbox.rb
218
272
  - lib/ironfan/provider/virtualbox/machine.rb
273
+ - lib/ironfan/provider/vsphere.rb
274
+ - lib/ironfan/provider/vsphere/keypair.rb
275
+ - lib/ironfan/provider/vsphere/machine.rb
219
276
  - lib/ironfan/requirements.rb
220
277
  - notes/Future-development-proposals.md
221
278
  - notes/Home.md
@@ -272,36 +329,34 @@ files:
272
329
  - spec/test_config.rb
273
330
  - tasks/chef_config.rake
274
331
  homepage: http://infochimps.com/labs
275
- licenses:
332
+ licenses:
276
333
  - apachev2
277
334
  post_install_message:
278
335
  rdoc_options: []
279
-
280
- require_paths:
336
+ require_paths:
281
337
  - lib
282
- required_ruby_version: !ruby/object:Gem::Requirement
338
+ required_ruby_version: !ruby/object:Gem::Requirement
283
339
  none: false
284
- requirements:
285
- - - ">="
286
- - !ruby/object:Gem::Version
287
- hash: -3865228418823474445
288
- segments:
340
+ requirements:
341
+ - - ! '>='
342
+ - !ruby/object:Gem::Version
343
+ version: '0'
344
+ segments:
289
345
  - 0
290
- version: "0"
291
- required_rubygems_version: !ruby/object:Gem::Requirement
346
+ hash: 3090386787638060354
347
+ required_rubygems_version: !ruby/object:Gem::Requirement
292
348
  none: false
293
- requirements:
294
- - - ">="
295
- - !ruby/object:Gem::Version
296
- version: "0"
349
+ requirements:
350
+ - - ! '>='
351
+ - !ruby/object:Gem::Version
352
+ version: '0'
297
353
  requirements: []
298
-
299
354
  rubyforge_project:
300
- rubygems_version: 1.8.24
355
+ rubygems_version: 1.8.23
301
356
  signing_key:
302
357
  specification_version: 3
303
358
  summary: Infochimps' lightweight cloud orchestration toolkit, built on top of Chef.
304
- test_files:
359
+ test_files:
305
360
  - spec/spec_helper/dummy_chef.rb
306
361
  - spec/integration/spec_helper/launch_cluster.rb
307
362
  - spec/integration/minimal-chef-repo/roles/systemwide.rb