pauper 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,12 @@
1
+ To make a new base VM:
2
+
3
+ 1) Install your OS (only tested with Ubuntu 10.04 64-bit)
4
+ 2) Setup a user with a password that you don't mind hardcoding in your Pauperfile
5
+ 3) Make sure that user has no-password sudo access
6
+ 4) sudo apt-get install ruby1.8 ruby1.8-dev libopenssl-ruby rubygems ssh
7
+ 5) sudo gem install chef
8
+ 6) Delete /etc/udev/rules.d/70-persistent-net.whatever
9
+ 7) Shut down VM
10
+ 8) Remove all *.lck files from inside the VM's vmwarevm folder
11
+
12
+ DONE!
data/bin/pauper ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'thor'
5
+ require 'pauper'
6
+
7
+ class CLI < Thor
8
+ desc "destroy [NODENAME]", "Completely destroy a VM"
9
+ def destroy(node_name=nil)
10
+ pauper = Pauper.new
11
+ if node_name
12
+ pauper.destroy(node_name)
13
+ else
14
+ pauper.destroy_all
15
+ end
16
+ end
17
+
18
+ desc "start [NODENAME]", "Start a VM"
19
+ def start(node_name=nil)
20
+ pauper = Pauper.new
21
+ if node_name
22
+ pauper.start(node_name)
23
+ else
24
+ pauper.start_all
25
+ end
26
+ end
27
+
28
+ desc "stop [NODENAME]", "Stop a VM"
29
+ def stop(node_name=nil)
30
+ pauper = Pauper.new
31
+ if node_name
32
+ pauper.stop(node_name)
33
+ else
34
+ pauper.stop_all
35
+ end
36
+ end
37
+
38
+ desc "setup [NODENAME]", "Refresh configs and run Chef on a VM"
39
+ def setup(node_name=nil)
40
+ pauper = Pauper.new
41
+ if node_name
42
+ pauper.setup(node_name)
43
+ else
44
+ pauper.setup_all
45
+ end
46
+ end
47
+
48
+ desc 'write_hosts', 'Write out a new /etc/hosts file'
49
+ def write_hosts
50
+ pauper = Pauper.new
51
+ pauper.write_hosts
52
+ end
53
+ end
54
+
55
+ CLI.start
data/lib/client.rb.erb ADDED
@@ -0,0 +1,7 @@
1
+ # Managed by Pauper
2
+
3
+ node_name "<%= node_config.name %>"
4
+ chef_server_url "<%= config[:chef_server_url] %>"
5
+ validation_client_name "<%= config[:validation_client_name] %>"
6
+ environment "<%= config[:chef_environment] %>"
7
+ json_attribs "/etc/chef/client-config.json"
data/lib/dhcpd.rb ADDED
@@ -0,0 +1,84 @@
1
+ class DHCPD
2
+ attr_reader :config
3
+
4
+ def initialize(conf_path)
5
+ @conf_path = conf_path
6
+ @preamble = ''
7
+ @postamble = ''
8
+ @config = {}
9
+
10
+ parse
11
+ end
12
+
13
+ def save
14
+ tmp_path = '.tmp.dhcpd.conf'
15
+ File.open(tmp_path, 'w') do |f|
16
+ f.write @preamble
17
+ f.puts BEGIN_BUM
18
+
19
+ @config.each do |host, host_config|
20
+ f.puts "host #{host} {"
21
+ host_config.each do |key, value|
22
+ f.puts "\t#{key} #{value};"
23
+ end
24
+ f.puts "}"
25
+ end
26
+
27
+ f.puts END_BUM
28
+ f.write @postamble
29
+ end
30
+
31
+ system 'sudo', 'mv', tmp_path, @conf_path
32
+ end
33
+
34
+ def restart
35
+ system 'sudo "/Library/Application Support/VMware Fusion/boot.sh" --restart >>vmware.log 2>&1'
36
+ end
37
+
38
+
39
+ private
40
+
41
+ BEGIN_BUM = "#### BEGIN BUM ####"
42
+ END_BUM = "#### END BUM ####"
43
+
44
+ def parse
45
+ state = :preamble
46
+ host = nil
47
+
48
+ File.open(@conf_path).each_line do |rawline|
49
+ line = rawline.strip
50
+
51
+ case state
52
+ when :preamble
53
+ if line == BEGIN_BUM
54
+ state = :outside_host
55
+ else
56
+ @preamble << rawline
57
+ end
58
+
59
+ when :outside_host
60
+ next unless line =~ /^host .* \{$/
61
+ words = line.split(' ').map { |x| x.strip }
62
+ host = words[1]
63
+ @config[host] = {}
64
+ state = :inside_host
65
+
66
+ when :inside_host
67
+ next if line.size == 0 || line.strip =~ /^#/
68
+ if line == '}'
69
+ state = :outside_host
70
+ host = nil
71
+ elsif line == END_BUM
72
+ state = :postamble
73
+ else
74
+ parts = line.chomp(';').split(' ')
75
+ @config[host][parts[0..-2].join(' ')] = parts[-1]
76
+ end
77
+
78
+ when :postamble
79
+ @postamble << rawline
80
+ end
81
+ end
82
+
83
+ end
84
+ end
data/lib/hosts.rb ADDED
@@ -0,0 +1,61 @@
1
+ # Class for parsing and writing /etc/hosts file
2
+
3
+ class Hosts
4
+ def initialize
5
+ @preamble = ""
6
+ @postamble = ""
7
+ @config = {}
8
+
9
+ parse
10
+ end
11
+
12
+ attr_reader :config
13
+
14
+ def save
15
+ File.open('.tmp.hosts','w') do |f|
16
+ f.write @preamble
17
+ f.puts BEGIN_LINE
18
+
19
+ @config.each do |ip, host|
20
+ f.puts "#{ip}\t#{host}"
21
+ end
22
+
23
+ f.puts END_LINE
24
+ f.write @postamble
25
+ end
26
+ system "sudo mv .tmp.hosts /etc/hosts"
27
+ end
28
+
29
+ private
30
+
31
+ BEGIN_LINE = "#### BEGIN PAUPER ####"
32
+ END_LINE = "#### END PAUPER ####"
33
+
34
+ def parse
35
+ state = :preamble
36
+
37
+ File.open('/etc/hosts').each_line do |line|
38
+ case state
39
+ when :preamble
40
+ if line.include? BEGIN_LINE
41
+ state = :pauper
42
+ else
43
+ @preamble << line
44
+ end
45
+ when :pauper
46
+ if line.include? END_LINE
47
+ state = :postamble
48
+ else
49
+ if line.strip =~ /^#/
50
+ # skip
51
+ else
52
+ ip, host = line.split(/\s+/)
53
+ @config[ip] = host
54
+ end
55
+ end
56
+ when :postamble
57
+ @postamble << line
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/pauper.rb ADDED
@@ -0,0 +1,385 @@
1
+ require 'rubygems'
2
+ require 'net/ssh'
3
+ require 'net/scp'
4
+ require 'json'
5
+
6
+ require 'fileutils'
7
+ require 'erb'
8
+ require 'ostruct'
9
+
10
+ require 'vmx'
11
+ require 'dhcpd'
12
+ require 'hosts'
13
+
14
+ class Pauper
15
+ DEFAULT_PAUPERFILE = './Pauperfile'
16
+ DEFAULT_VM_PATH = File.expand_path('~/Documents/Virtual Machines.localized/')
17
+
18
+ VMWARE_PATH = '/Library/Application Support/VMware Fusion'
19
+ DHCPD_CONF_PATH = "#{VMWARE_PATH}/vmnet8/dhcpd.conf"
20
+
21
+ def initialize(pauperfile=DEFAULT_PAUPERFILE)
22
+ @pauper_config = Pauper::Config.new(DEFAULT_PAUPERFILE)
23
+ end
24
+
25
+ def bootstrap
26
+ vmx = template_vmx
27
+ vmx.data['sharedFolder0.hostPath'] = File.expand_path('~')
28
+ vmx.data['sharedFolder0.guestName'] = 'host_home'
29
+ vmx.save
30
+ end
31
+
32
+ def create(node_name)
33
+ node_config = get_node_config(node_name)
34
+
35
+ raise "VM already exists!" if vm_exists?(node_name)
36
+
37
+ puts "Cloning template..."
38
+ clone_template node_name
39
+
40
+ puts "Updating machine configuration..."
41
+ mac = generate_mac
42
+ uuid = generate_uuid
43
+
44
+ vmx = node_vmx(node_name)
45
+ vmx.data['displayName'] = node_name
46
+ vmx.data['memsize'] = node_config.config[:ram] || @pauper_config.config[:default_ram] || 512
47
+ vmx.data['numvcpus'] = node_config.config[:cpus] || @pauper_config.config[:default_cpus] || 1
48
+ vmx.data['ethernet0.address'] = mac
49
+ vmx.data['uuid.bios'] = uuid
50
+ vmx.data['uuid.location'] = uuid
51
+ vmx.save
52
+
53
+ dhcpd = DHCPD.new(DHCPD_CONF_PATH)
54
+ dhcpd.config[node_name] = {
55
+ 'hardware ethernet' => mac,
56
+ 'fixed-address' => node_config.config[:ip]
57
+ }
58
+ dhcpd.save
59
+
60
+ puts "Restarting dhcpd..."
61
+ dhcpd.restart
62
+
63
+ puts "Starting..."
64
+ start_node(node_name)
65
+
66
+ setup(node_name)
67
+ end
68
+
69
+ def setup(node_name)
70
+ node_config = get_node_config(node_name)
71
+
72
+ puts "Setting up #{node_name}..."
73
+
74
+ client_erb = ERB.new(File.read(File.join(File.dirname(__FILE__), 'client.rb.erb')))
75
+ client_rb_data = client_erb.result(OpenStruct.new(:config => @pauper_config.config, :node_config => node_config).send(:binding))
76
+ tmp_client_rb_path = ".tmp.#{node_name}.client.rb"
77
+ File.open(tmp_client_rb_path,'w') do |f|
78
+ f.puts client_rb_data
79
+ end
80
+
81
+ chef_attribs = {
82
+ :run_list => @pauper_config.config[:default_run_list] + node_config.config[:run_list],
83
+ :ip => {
84
+ :private => node_config.config[:ip],
85
+ :private_netmask => '255.255.255.0'
86
+ }
87
+ }.merge(@pauper_config.config[:chef_options]).merge(node_config.config[:chef_options])
88
+
89
+ puts "Uploading Chef files..."
90
+ Net::SCP.start node_config.config[:ip], @pauper_config.config[:ssh_user], :password => @pauper_config.config[:ssh_password] do |scp|
91
+ scp.upload! tmp_client_rb_path, "client.rb"
92
+ scp.upload! @pauper_config.config[:validation_key_path], "validation.pem"
93
+ scp.upload! StringIO.new(chef_attribs.to_json), "client-config.json"
94
+ end
95
+
96
+ FileUtils.rm(tmp_client_rb_path)
97
+
98
+ enable_shared_folders node_name
99
+ @pauper_config.config[:shares].each do |share_name, guest_path, host_path, options|
100
+ cmd "mkdir -p #{host_path}"
101
+ share_folder node_name, share_name, host_path
102
+ end
103
+
104
+ puts "Connecting over SSH..."
105
+ Net::SSH.start node_config.config[:ip], @pauper_config.config[:ssh_user], :password => @pauper_config.config[:ssh_password] do |ssh|
106
+ ssh_exec ssh, "sudo hostname #{node_name}"
107
+ ssh_exec ssh, "sudo mkdir /etc/chef"
108
+
109
+ @pauper_config.config[:shares].each do |share_name, guest_path, host_path, options|
110
+ ssh_exec ssh, "sudo mkdir -p #{File.dirname(guest_path)} && sudo ln -sf /mnt/hgfs/#{share_name} #{guest_path}"
111
+ end
112
+
113
+ ssh_exec ssh, "sudo mv client.rb /etc/chef/"
114
+ ssh_exec ssh, "sudo mv validation.pem /etc/chef/"
115
+ ssh_exec ssh, "sudo mv client-config.json /etc/chef/"
116
+
117
+ ssh.exec! "sudo /var/lib/gems/1.8/bin/chef-client" do |channel, stream, data|
118
+ print data
119
+ end
120
+ end
121
+ end
122
+
123
+ def destroy(node_name)
124
+ node_config = get_node_config(node_name)
125
+
126
+ if vm_exists?(node_name)
127
+ stop_node(node_name)
128
+
129
+ puts "Removing #{node_name} from Chef..."
130
+ `knife node delete #{node_name} -y`
131
+ `knife client delete #{node_name} -y`
132
+
133
+ puts "Destroying #{node_name}..."
134
+ FileUtils.rm_rf(node_name_to_vm_path(node_name))
135
+ else
136
+ puts "#{node_name} hasn't even been created yet, thusly you can't destroy it!"
137
+ end
138
+ end
139
+
140
+ def stop(node_name)
141
+ node_config = get_node_config(node_name)
142
+
143
+ if vm_running?(node_name)
144
+ puts "Stopping #{node_name}..."
145
+ stop_node(node_name)
146
+ else
147
+ puts "wtf, #{node_name} is not running."
148
+ end
149
+ end
150
+
151
+ def start(node_name)
152
+ node_config = get_node_config(node_name)
153
+
154
+ if vm_running?(node_name)
155
+ puts "Dude, #{node_name} is already running!"
156
+ elsif vm_exists?(node_name)
157
+ puts "Starting #{node_name}..."
158
+ start_node node_name
159
+ else
160
+ puts "Generating, then starting #{node_name}..."
161
+ create node_name
162
+ end
163
+ end
164
+
165
+ def write_hosts
166
+ puts "Writing /etc/hosts file..."
167
+ hosts = Hosts.new
168
+ @pauper_config.config[:nodes].each do |node|
169
+ hosts.config[node.config[:ip]] = node.name
170
+ end
171
+ hosts.save
172
+ end
173
+
174
+
175
+ def start_all
176
+ puts "Starting all nodes..."
177
+ @pauper_config.config[:nodes].each { |n| start(n.name) }
178
+ end
179
+
180
+ def stop_all
181
+ puts "Stopping all nodes..."
182
+ @pauper_config.config[:nodes].each { |n| stop(n.name) }
183
+ end
184
+
185
+ def destroy_all
186
+ puts "Destroying all nodes..."
187
+ @pauper_config.config[:nodes].each { |n| destroy(n.name) }
188
+ end
189
+
190
+ def setup_all
191
+ puts "Setting up all nodes..."
192
+ @pauper_config.config[:nodes].each { |n| setup(n.name) }
193
+ end
194
+
195
+ private
196
+
197
+ def cmd(*args)
198
+ puts ["local>", *args].join(" ")
199
+ system *args
200
+ end
201
+
202
+ def ssh_exec(ssh, cmd)
203
+ puts "ssh> #{cmd}"
204
+ print ssh.exec!(cmd)
205
+ end
206
+
207
+ def enable_shared_folders(node_name)
208
+ cmd "'#{VMWARE_PATH}/vmrun' enableSharedFolders '#{node_name_to_vmx(node_name)}'"
209
+ end
210
+
211
+ def share_folder(node_name, share_name, path)
212
+ cmd "'#{VMWARE_PATH}/vmrun' addSharedFolder '#{node_name_to_vmx(node_name)}' '#{share_name}' '#{path}'"
213
+ end
214
+
215
+ def get_node_config(node_name)
216
+ node_config = @pauper_config.get_node(node_name)
217
+ raise "Alas, I don't know about a node named #{node_name.inspect}..." unless node_config
218
+ node_config
219
+ end
220
+
221
+ def vm_exists?(node_name)
222
+ File.exists? node_name_to_vm_path(node_name)
223
+ end
224
+
225
+ def vm_running?(node_name)
226
+ `'#{VMWARE_PATH}/vmrun' list`.include?("#{node_name}.vmwarevm")
227
+ end
228
+
229
+ def clone_template(node_name)
230
+ FileUtils.cp_r(template_vm_path, node_name_to_vm_path(node_name))
231
+ end
232
+
233
+ def template_vmx
234
+ @template_vmx ||= VMX.new(@pauper_config.config[:vmx])
235
+ end
236
+
237
+ def node_vmx(node_name)
238
+ VMX.new(node_name_to_vmx(node_name))
239
+ end
240
+
241
+ def start_node(node_name)
242
+ cmd "'#{VMWARE_PATH}/vmrun' start '#{node_name_to_vmx(node_name)}' nogui >>vmware.log 2>&1"
243
+ end
244
+
245
+ def stop_node(node_name)
246
+ cmd "'#{VMWARE_PATH}/vmrun' stop '#{node_name_to_vmx(node_name)}' >>vmware.log 2>&1"
247
+ end
248
+
249
+ def generate_mac
250
+ "00:50:56:" + 3.times.map { ("%2s" % rand(256).to_s(16)).gsub(' ','0') }.join(':').upcase
251
+ end
252
+
253
+ # not actually universal - doesn't fucking matter
254
+ def generate_uuid
255
+ 2.times.map {
256
+ 8.times.map {
257
+ ("%2s" % rand(256).to_s(16)).gsub(' ','0')
258
+ }.join(' ')
259
+ }.join('-')
260
+ end
261
+
262
+ def node_name_to_vmx(node_name)
263
+ Dir[File.join(node_name_to_vm_path(node_name), '*.vmx')][0]
264
+ end
265
+
266
+ def node_name_to_vm_path(node_name)
267
+ File.join(DEFAULT_VM_PATH, "#{node_name}.vmwarevm")
268
+ end
269
+
270
+ def template_vm_path
271
+ File.dirname(@pauper_config.config[:vmx])
272
+ end
273
+
274
+ class Config
275
+ attr_reader :config
276
+
277
+ def initialize(pauperfile)
278
+ @config = {
279
+ :nodes => [],
280
+ :ssh_user => 'dev',
281
+ :ssh_password => 'password',
282
+ :run_list => [],
283
+ :shares => [],
284
+ :suffix => '',
285
+ :chef_options => {}
286
+ }
287
+ instance_eval File.read(pauperfile)
288
+ end
289
+
290
+ def get_node(node_name)
291
+ @config[:nodes].detect { |n| n.name == node_name }
292
+ end
293
+
294
+
295
+ def ssh_user(user)
296
+ @config[:ssh_user] = user
297
+ end
298
+
299
+ def ssh_password(pass)
300
+ @config[:ssh_password] = pass
301
+ end
302
+
303
+ def vmx(vmx_file)
304
+ @config[:vmx] = File.expand_path(vmx_file)
305
+ end
306
+
307
+ def node(name, &block)
308
+ @config[:nodes] << Node.new(name + @config[:suffix], name, &block)
309
+ end
310
+
311
+ def ram(megabytes)
312
+ @config[:default_ram] = megabytes
313
+ end
314
+
315
+ def cpus(count)
316
+ @config[:default_cpus] = count
317
+ end
318
+
319
+ def chef_server_url(url)
320
+ @config[:chef_server_url] = url
321
+ end
322
+
323
+ def validation_key_path(path)
324
+ @config[:validation_key_path] = File.expand_path(path)
325
+ end
326
+
327
+ def validation_client_name(name)
328
+ @config[:validation_client_name] = name
329
+ end
330
+
331
+ def chef_environment(env)
332
+ @config[:chef_environment] = env
333
+ end
334
+
335
+ def chef_options(opts)
336
+ @config[:chef_options] = opts
337
+ end
338
+
339
+ def default_run_list(list)
340
+ @config[:default_run_list] = list
341
+ end
342
+
343
+ def share(share_name, guest_path, host_path, options={})
344
+ @config[:shares] << [share_name, guest_path, File.expand_path(host_path), options]
345
+ end
346
+
347
+ def node_suffix(suffix)
348
+ @config[:suffix] = suffix
349
+ end
350
+
351
+ class Node
352
+ attr_reader :name, :pretty_name, :config
353
+
354
+ def initialize(name, pretty_name, &block)
355
+ @name = name
356
+ @pretty_name = pretty_name
357
+ @config = {
358
+ :run_list => [],
359
+ :chef_options => {}
360
+ }
361
+ instance_eval &block
362
+ end
363
+
364
+ def run_list(*recipes)
365
+ @config[:run_list] = recipes
366
+ end
367
+
368
+ def ip(addr)
369
+ @config[:ip] = addr
370
+ end
371
+
372
+ def chef_options(options)
373
+ @config[:chef_options] = options
374
+ end
375
+
376
+ def ram(megabytes)
377
+ @config[:ram] = megabytes
378
+ end
379
+
380
+ def cpus(count)
381
+ @config[:cpus] = count
382
+ end
383
+ end
384
+ end
385
+ end
data/lib/vmx.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'fileutils'
2
+
3
+ class VMX
4
+ attr_reader :data
5
+
6
+ def initialize(filename)
7
+ @filename = filename
8
+ @data = {}
9
+
10
+ File.open(filename).each_line do |line|
11
+ key, value = line.split('=', 2)
12
+ @data[key.strip] = eval(value)
13
+ end
14
+ end
15
+
16
+ def save(filename=@filename)
17
+ FileUtils.cp(filename, filename + '.bak')
18
+ File.open(filename, 'w') do |f|
19
+ @data.sort_by { |(k,v)| k }.each do |(k,v)|
20
+ f.puts "#{k} = #{v.to_s.inspect}"
21
+ end
22
+ end
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pauper
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 4
9
+ version: 0.0.4
10
+ platform: ruby
11
+ authors:
12
+ - Tyler McMullen
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-08-17 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: thor
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: net-ssh
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ segments:
40
+ - 0
41
+ version: "0"
42
+ type: :runtime
43
+ version_requirements: *id002
44
+ - !ruby/object:Gem::Dependency
45
+ name: net-scp
46
+ prerelease: false
47
+ requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ segments:
52
+ - 0
53
+ version: "0"
54
+ type: :runtime
55
+ version_requirements: *id003
56
+ - !ruby/object:Gem::Dependency
57
+ name: json
58
+ prerelease: false
59
+ requirement: &id004 !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ type: :runtime
67
+ version_requirements: *id004
68
+ description: Inspired by Vagrant but much much simpler. Also uses VMware, instead of Virtualbox.
69
+ email:
70
+ - tyler@fastly.com
71
+ executables:
72
+ - pauper
73
+ extensions: []
74
+
75
+ extra_rdoc_files: []
76
+
77
+ files:
78
+ - bin/pauper
79
+ - lib/client.rb.erb
80
+ - lib/dhcpd.rb
81
+ - lib/hosts.rb
82
+ - lib/pauper.rb
83
+ - lib/vmx.rb
84
+ - README
85
+ has_rdoc: true
86
+ homepage: http://github.com/fastly/Pauper
87
+ licenses: []
88
+
89
+ post_install_message:
90
+ rdoc_options: []
91
+
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ requirements: []
109
+
110
+ rubyforge_project:
111
+ rubygems_version: 1.3.6
112
+ signing_key:
113
+ specification_version: 3
114
+ summary: A semi-sane way to manage a multi-vm dev environment
115
+ test_files: []
116
+