vagrant-bhyve 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +111 -0
  8. data/Rakefile +6 -0
  9. data/Vagrantfiles/Vagrantfile_CentOS-6 +14 -0
  10. data/Vagrantfiles/Vagrantfile_CentOS-7 +11 -0
  11. data/Vagrantfiles/Vagrantfile_Debian_Ubuntu +11 -0
  12. data/example_box/README.md +46 -0
  13. data/example_box/Vagrantfile +9 -0
  14. data/example_box/metadata.json +3 -0
  15. data/lib/vagrant-bhyve.rb +21 -0
  16. data/lib/vagrant-bhyve/action.rb +215 -0
  17. data/lib/vagrant-bhyve/action/boot.rb +26 -0
  18. data/lib/vagrant-bhyve/action/cleanup.rb +23 -0
  19. data/lib/vagrant-bhyve/action/create_bridge.rb +33 -0
  20. data/lib/vagrant-bhyve/action/create_tap.rb +31 -0
  21. data/lib/vagrant-bhyve/action/destroy.rb +23 -0
  22. data/lib/vagrant-bhyve/action/forward_ports.rb +53 -0
  23. data/lib/vagrant-bhyve/action/import.rb +32 -0
  24. data/lib/vagrant-bhyve/action/load.rb +27 -0
  25. data/lib/vagrant-bhyve/action/prepare_nfs_settings.rb +50 -0
  26. data/lib/vagrant-bhyve/action/prepare_nfs_valid_ids.rb +23 -0
  27. data/lib/vagrant-bhyve/action/setup.rb +27 -0
  28. data/lib/vagrant-bhyve/action/shutdown.rb +26 -0
  29. data/lib/vagrant-bhyve/action/wait_until_up.rb +48 -0
  30. data/lib/vagrant-bhyve/config.rb +75 -0
  31. data/lib/vagrant-bhyve/driver.rb +674 -0
  32. data/lib/vagrant-bhyve/errors.rb +55 -0
  33. data/lib/vagrant-bhyve/executor.rb +38 -0
  34. data/lib/vagrant-bhyve/plugin.rb +73 -0
  35. data/lib/vagrant-bhyve/provider.rb +126 -0
  36. data/lib/vagrant-bhyve/version.rb +5 -0
  37. data/locales/en.yml +90 -0
  38. data/vagrant-bhyve.gemspec +24 -0
  39. metadata +137 -0
@@ -0,0 +1,674 @@
1
+ require "log4r"
2
+ require "fileutils"
3
+ require "digest/md5"
4
+ require "io/console"
5
+ require "ruby_expect"
6
+
7
+
8
+ module VagrantPlugins
9
+ module ProviderBhyve
10
+ class Driver
11
+
12
+ # This executor is responsible for actually executing commands, including
13
+ # bhyve, dnsmasq and other shell utils used to get VM's state
14
+ attr_accessor :executor
15
+
16
+ def initialize(machine)
17
+ @logger = Log4r::Logger.new("vagrant_bhyve::driver")
18
+ @machine = machine
19
+ @data_dir = @machine.data_dir
20
+ @executor = Executor::Exec.new
21
+
22
+ # if vagrant is excecuted by root (or with sudo) then the variable
23
+ # will be empty string, otherwise it will be 'sudo' to make sure we
24
+ # can run bhyve, bhyveload and pf with sudo privilege
25
+ if Process.uid == 0
26
+ @sudo = ''
27
+ else
28
+ @sudo = 'sudo'
29
+ end
30
+ end
31
+
32
+ def import(machine, ui)
33
+ box_dir = machine.box.directory
34
+ instance_dir = @data_dir
35
+ store_attr('id', machine.id)
36
+ password = ''
37
+ check_and_install('gcp', 'coreutils', ui)
38
+ check_and_install('fdisk-linux', 'linuxfdisk', ui)
39
+ execute(false, "gcp --sparse=always #{box_dir.join('disk.img').to_s} #{instance_dir.to_s}")
40
+ if box_dir.join('uefi.fd').exist?
41
+ FileUtils.copy(box_dir.join('uefi.fd'), instance_dir)
42
+ store_attr('firmware', 'uefi')
43
+ else
44
+ store_attr('firmware', 'bios')
45
+ boot_partition = execute(false, "cd #{instance_dir.to_s} && fdisk-linux -lu disk.img | grep 'disk.img' | grep -E '\\*' | awk '{print $1}'")
46
+ if boot_partition == ''
47
+ store_attr('bootloader', 'bhyveload')
48
+ else
49
+ if execute(true, "sudo -n grub-bhyve --help") != 0
50
+ ui.warn "We need to use your password to commmunicate with grub-bhyve, please make sure the password you input is correct."
51
+ password = ui.ask("Password:", echo: false)
52
+ end
53
+ store_attr('bootloader', 'grub-bhyve')
54
+ # We need vmm module to be loaded to use grub-bhyve
55
+ load_module('vmm')
56
+ # Check whether grub-bhyve is installed
57
+ check_and_install('grub-bhyve', 'grub2-bhyve', ui)
58
+ instance_dir.join('device.map').open('w') do |f|
59
+ f.puts "(hd0) #{instance_dir.join('disk.img').to_s}"
60
+ end
61
+ partition_index = boot_partition =~ /\d/
62
+ partition_id = boot_partition[partition_index..-1]
63
+ grub_run_partition = "msdos#{partition_id}"
64
+ files = grub_bhyve_execute("ls (hd0,#{grub_run_partition})/", password, :match)
65
+ if files =~ /grub2\//
66
+ grub_run_dir = "/grub2"
67
+ store_attr('grub_run_partition', grub_run_partition)
68
+ store_attr('grub_run_dir', grub_run_dir)
69
+ elsif files =~ /grub\//
70
+ files = grub_bhyve_execute("ls (hd0,#{grub_run_partition})/grub/", password, :match)
71
+ if files =~ /grub\.conf/
72
+ grub_conf = grub_bhyve_execute("cat (hd0,#{grub_run_partition})/grub/grub.conf", password, :before)
73
+ info_index = grub_conf =~ /title/
74
+ boot_info = grub_conf[info_index..-1]
75
+ kernel_info_index = boot_info =~ /kernel/
76
+ initrd_info_index = boot_info =~ /initrd/
77
+ kernel_info = boot_info[kernel_info_index..initrd_info_index - 1].gsub("\r\e[1B", "").gsub("kernel ", "linux (hd0,#{grub_run_partition})")
78
+ initrd_info = boot_info[initrd_info_index..-1].gsub("\r\e[1B", "").gsub("initrd ", "initrd (hd0,#{grub_run_partition})")
79
+ instance_dir.join('grub.cfg').open('w') do |f|
80
+ f.puts kernel_info
81
+ f.puts initrd_info
82
+ f.puts "boot"
83
+ end
84
+ elsif files =~ /grub\.cfg/
85
+ store_attr('grub_run_partition', grub_run_partition)
86
+ end
87
+ else
88
+ if files =~ /boot\//
89
+ files = grub_bhyve_execute("ls (hd0,#{grub_run_partition})/boot/", password, :match)
90
+ if files =~ /grub2/
91
+ grub_run_dir = "/boot/grub2"
92
+ store_attr('grub_run_partition', grub_run_partition)
93
+ store_attr('grub_run_dir', grub_run_dir)
94
+ elsif files =~ /grub/
95
+ files = grub_bhyve_execute("ls (hd0,#{grub_run_partition})/boot/grub/", password, :match)
96
+ if files =~ /grub\.conf/
97
+ grub_conf = grub_bhyve_execute("cat (hd0,#{grub_run_partition})/boot/grub/grub.conf", password, :before)
98
+ info_index = grub_conf =~ /title/
99
+ boot_info = grub_conf[info_index..-1]
100
+ kernel_info_index = boot_info =~ /kernel/
101
+ initrd_info_index = boot_info =~ /initrd/
102
+ kernel_info = boot_info[kernel_info_index..initrd_info_index - 1].gsub("\r\e[1B", "").gsub("kernel ","linux (hd0,#{grub_run_partition})/boot")
103
+ initrd_info = boot_info[initrd_info_index..-1].gsub("\r\e[1B", "").gsub("initrd ", "initrd (hd0,#{grub_run_partition})/boot")
104
+ instance_dir.join('grub.cfg').open('w') do |f|
105
+ f.puts kernel_info
106
+ f.puts initrd_info
107
+ f.puts "boot"
108
+ end
109
+ elsif files =~ /grub\.cfg/
110
+ store_attr('grub_run_partition', grub_run_partition)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+
119
+ def destroy
120
+ FileUtils.rm_rf(Dir.glob(@data_dir.join('*').to_s))
121
+ end
122
+
123
+ def check_bhyve_support
124
+ # Check whether FreeBSD version is lower than 10
125
+ result = execute(true, "test $(uname -K) -lt 1000000")
126
+ raise Errors::SystemVersionIsTooLow if result == 0
127
+
128
+ # Check whether POPCNT is supported
129
+ result = execute(false, "#{@sudo} grep -E '^[ ] +Features2' /var/run/dmesg.boot | tail -n 1")
130
+ raise Errors::MissingPopcnt unless result =~ /POPCNT/
131
+
132
+ # Check whether EPT is supported for Intel
133
+ result = execute(false, "#{@sudo} grep -E '^[ ]+VT-x' /var/run/dmesg.boot | tail -n 1")
134
+ raise Errors::MissingEpt unless result =~ /EPT/
135
+
136
+ # Check VT-d
137
+ #result = execute(false, "#{@sudo} acpidump -t | grep DMAR")
138
+ #raise Errors::MissingIommu if result.length == 0
139
+ end
140
+
141
+ def load_module(module_name)
142
+ result = execute(true, "#{@sudo} kldstat -qm #{module_name} >/dev/null 2>&1")
143
+ if result != 0
144
+ result = execute(true, "#{@sudo} kldload #{module_name} >/dev/null 2>&1")
145
+ raise Errors::UnableToLoadModule if result != 0
146
+ end
147
+ end
148
+
149
+ def create_network_device(device_name, device_type)
150
+ return if device_name.length == 0
151
+
152
+ # Check whether the bridge has been created
153
+ interface_name = get_interface_name(device_name)
154
+ interface_name = execute(false, "#{@sudo} ifconfig #{device_type} create") if interface_name.length == 0
155
+ raise Errors::UnableToCreateInterface if interface_name.length == 0
156
+ # Add new created device's description
157
+ execute(false, "#{@sudo} ifconfig #{interface_name} description #{device_name} up")
158
+
159
+ # Store the new created network device's name
160
+ store_attr(device_type, interface_name)
161
+
162
+ # Configure tap device
163
+ if device_type == 'tap'
164
+ # Add the tap device as bridge's member
165
+ bridge = get_attr('bridge')
166
+ # Make sure the tap deivce has the same mtu value
167
+ # with the bridge
168
+ mtu = execute(false, "ifconfig #{bridge} | head -n1 | awk '{print $NF}'")
169
+ execute(false, "#{@sudo} ifconfig #{interface_name} mtu #{mtu}") if mtu.length != 0 and mtu != '1500'
170
+ execute(false, "#{@sudo} ifconfig #{bridge} addm #{interface_name}")
171
+ # Setup VM-specific pf rules
172
+ id = get_attr('id')
173
+ pf_conf = @data_dir.join('pf.conf')
174
+ pf_conf.open('w') do |f|
175
+ f.puts "set skip on #{interface_name}"
176
+ end
177
+ comment_mark = "# vagrant-bhyve #{interface_name}"
178
+ if execute(true, "test -s /etc/pf.conf") == 0
179
+ if execute(true, "grep \"#{comment_mark}\" /etc/pf.conf") != 0
180
+ comment_mark_bridge = "# vagrant-bhyve #{bridge}"
181
+ if execute(true, "grep \"#{comment_mark_bridge}\" /etc/pf.conf") != 0
182
+ execute(false, "#{@sudo} sed -i '' '1i\\\n#{comment_mark}\n' /etc/pf.conf")
183
+ execute(false, "#{@sudo} sed -i '' '2i\\\ninclude \"#{pf_conf.to_s}\"\n' /etc/pf.conf")
184
+ else
185
+ bridge_line = execute(false, "grep -A 1 \"#{comment_mark_bridge}\" /etc/pf.conf | tail -1")
186
+ bridge_line = bridge_line.gsub("\"", "\\\"")
187
+ bridge_line = bridge_line.gsub("/", "\\/")
188
+ execute(false, "#{@sudo} sed -i '' '/#{bridge_line}/a\\\n#{comment_mark}\n' /etc/pf.conf")
189
+ execute(false, "#{@sudo} sed -i '' '/#{comment_mark}/a\\\ninclude \"#{pf_conf.to_s}\"\n' /etc/pf.conf")
190
+ end
191
+ end
192
+ else
193
+ execute(false, "echo \"#{comment_mark}\" | #{@sudo} tee -a /etc/pf.conf")
194
+ execute(false, "echo \"include \\\"#{pf_conf.to_s}\\\"\" | #{@sudo} tee -a /etc/pf.conf")
195
+ end
196
+ restart_service('pf')
197
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{id}' -f #{pf_conf.to_s}")
198
+ #if !pf_enabled?
199
+ # execute(false, "#{@sudo} pfctl -e")
200
+ #end
201
+ end
202
+ end
203
+
204
+ # For now, only IPv4 is supported
205
+ def enable_nat(bridge, ui)
206
+ bridge_name = get_interface_name(bridge)
207
+ return if execute(true, "ifconfig #{bridge_name} | grep inet") == 0
208
+
209
+ directory = @data_dir
210
+ # Choose a subnet for this bridge
211
+ index = bridge_name =~ /\d/
212
+ bridge_num = bridge_name[index..-1]
213
+ sub_net = "172.16." + bridge_num
214
+
215
+ # Config IP for the bridge
216
+ execute(false, "#{@sudo} ifconfig #{bridge_name} #{sub_net}.1/24")
217
+
218
+ # Get default gateway
219
+ gateway = execute(false, "netstat -4rn | grep default | awk '{print $4}'")
220
+ store_attr('gateway', gateway)
221
+ # Add gateway as a bridge member
222
+ #execute(false, "#{@sudo} ifconfig #{bridge_name} addm #{gateway}")
223
+
224
+ # Enable forwarding
225
+ execute(false, "#{@sudo} sysctl net.inet.ip.forwarding=1 >/dev/null 2>&1")
226
+ execute(false, "#{@sudo} sysctl net.inet6.ip6.forwarding=1 >/dev/null 2>&1")
227
+
228
+ # Change pf's configuration
229
+ pf_conf = directory.join("pf.conf")
230
+ pf_conf.open("w") do |pf_file|
231
+ pf_file.puts "set skip on #{bridge_name}"
232
+ pf_file.puts "nat on #{gateway} from {#{sub_net}.0/24} to any -> (#{gateway})"
233
+ end
234
+ pf_bridge_conf = "/usr/local/etc/pf.#{bridge_name}.conf"
235
+ comment_mark = "# vagrant-bhyve #{bridge_name}"
236
+ execute(false, "#{@sudo} mv #{pf_conf.to_s} #{pf_bridge_conf}")
237
+ if execute(true, "test -s /etc/pf.conf") == 0
238
+ if execute(true, "grep \"#{comment_mark}\" /etc/pf.conf") != 0
239
+ execute(false, "#{@sudo} sed -i '' '1i\\\n#{comment_mark}\n' /etc/pf.conf")
240
+ execute(false, "#{@sudo} sed -i '' '2i\\\ninclude \"#{pf_bridge_conf}\"\n' /etc/pf.conf")
241
+ end
242
+ else
243
+ execute(false, "echo \"#{comment_mark}\" | #{@sudo} tee -a /etc/pf.conf")
244
+ execute(false, "echo \"include \\\"#{pf_bridge_conf}\\\"\" | #{@sudo} tee -a /etc/pf.conf")
245
+ end
246
+ restart_service('pf')
247
+ # Use pfctl to enable pf rules
248
+ #execute(false, "#{@sudo} cp #{pf_conf.to_s} /usr/local/etc/pf.#{bridge_name}.conf")
249
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{bridge_name}' -f /usr/local/etc/pf.#{bridge_name}.conf")
250
+ # execute(false, "#{@sudo} pfctl -a '/vagrant_#{bridge_name}' -sr")
251
+
252
+ # Create a basic dnsmasq setting
253
+ # Basic settings
254
+ check_and_install('dnsmasq', 'dnsmasq', ui)
255
+ dnsmasq_conf = directory.join("dnsmasq.conf")
256
+ dnsmasq_conf.open("w") do |dnsmasq_file|
257
+ dnsmasq_file.puts <<-EOF
258
+ domain-needed
259
+ except-interface=lo0
260
+ bind-interfaces
261
+ local-service
262
+ dhcp-authoritative
263
+ EOF
264
+ # DHCP part
265
+ dnsmasq_file.puts "interface=#{bridge_name}"
266
+ dnsmasq_file.puts "dhcp-range=#{sub_net + ".10," + sub_net + ".254"}"
267
+ dnsmasq_file.puts "dhcp-option=option:dns-server,#{sub_net + ".1"}"
268
+ end
269
+ execute(false, "#{@sudo} cp #{dnsmasq_conf.to_s} /usr/local/etc/dnsmasq.#{bridge_name}.conf")
270
+ dnsmasq_cmd = "dnsmasq -C /usr/local/etc/dnsmasq.#{bridge_name}.conf -l /var/run/dnsmasq.#{bridge_name}.leases -x /var/run/dnsmasq.#{bridge_name}.pid"
271
+ execute(false, "#{@sudo} #{dnsmasq_cmd}")
272
+
273
+ end
274
+
275
+ def get_ip_address(interface_name, type=:guest)
276
+ bridge_name = get_attr('bridge')
277
+ if type == :guest
278
+ return nil if execute(true, "test -e /var/run/dnsmasq.#{bridge_name}.pid") != 0
279
+ mac = get_attr('mac')
280
+ leases_file = Pathname.new("/var/run/dnsmasq.#{bridge_name}.leases")
281
+ leases_info = leases_file.open('r'){|f| f.readlines}.select{|line| line.match(mac)}
282
+ raise Errors::NotFoundLeasesInfo if leases_info == []
283
+ # IP address for a device is on third coloum
284
+ ip = leases_info[0].split[2]
285
+ elsif type == :host
286
+ return nil if execute(true, "ifconfig #{bridge_name}")
287
+ ip = execute(false, "ifconfig #{bridge_name} | grep -i inet").split[1]
288
+ end
289
+ end
290
+
291
+ def ip_ready?
292
+ bridge_name = get_attr('bridge')
293
+ mac = get_attr('mac')
294
+ leases_file = Pathname.new("/var/run/dnsmasq.#{bridge_name}.leases")
295
+ return (leases_file.open('r'){|f| f.readlines}.select{|line| line.match(mac)} != [])
296
+ end
297
+
298
+ def ssh_ready?(ssh_info)
299
+ if ssh_info
300
+ return execute(true, "nc -z #{ssh_info[:host]} #{ssh_info[:port]}") == 0
301
+ end
302
+ return false
303
+ end
304
+
305
+ def load(machine, ui)
306
+ loader_cmd = @sudo
307
+ directory = @data_dir
308
+ config = machine.provider_config
309
+ loader = get_attr('bootloader')
310
+ case loader
311
+ when 'bhyveload'
312
+ loader_cmd += ' bhyveload'
313
+ # Set autoboot, and memory and disk
314
+ loader_cmd += " -m #{config.memory}"
315
+ loader_cmd += " -d #{directory.join('disk.img').to_s}"
316
+ loader_cmd += " -e autoboot_delay=0"
317
+ when 'grub-bhyve'
318
+ loader_cmd += " grub-bhyve"
319
+ loader_cmd += " -m #{directory.join('device.map').to_s}"
320
+ loader_cmd += " -M #{config.memory}"
321
+ # Maybe there should be some grub config in Vagrantfile, for now
322
+ # we just use this hd0,1 as default root and don't use -d -g
323
+ # argument
324
+ grub_cfg = directory.join('grub.cfg')
325
+ grub_run_partition = get_attr('grub_run_partition')
326
+ grub_run_dir = get_attr('grub_run_dir')
327
+ if grub_cfg.exist?
328
+ loader_cmd += " -r host -d #{directory.to_s}"
329
+ else
330
+ if grub_run_partition
331
+ loader_cmd += " -r hd0,#{grub_run_partition}"
332
+ else
333
+ loader_cmd += " -r hd0,1"
334
+ end
335
+
336
+ if grub_run_dir
337
+ loader_cmd += " -d #{grub_run_dir}"
338
+ end
339
+ # Find an available nmdm device and add it as loader's -m argument
340
+ nmdm_num = find_available_nmdm
341
+ loader_cmd += " -c /dev/nmdm#{nmdm_num}A"
342
+ end
343
+ end
344
+
345
+ vm_name = get_attr('vm_name')
346
+ loader_cmd += " #{vm_name}"
347
+ execute(false, loader_cmd)
348
+ end
349
+
350
+ def boot(machine, ui)
351
+ firmware = get_attr('firmware')
352
+ loader = get_attr('bootloader')
353
+ directory = @data_dir
354
+ config = machine.provider_config
355
+
356
+ # Run in bhyve in background
357
+ bhyve_cmd = "sudo -b"
358
+ # Prevent virtual CPU use 100% of host CPU
359
+ bhyve_cmd += " bhyve -HP"
360
+
361
+ # Configure for hostbridge & lpc device, Windows need slot 0 and 31
362
+ # while others don't care, so we use slot 0 and 31
363
+ case config.hostbridge
364
+ when 'amd'
365
+ bhyve_cmd += " -s 0,amd_hostbridge"
366
+ when 'no'
367
+ else
368
+ bhyve_cmd += " -s 0,hostbridge"
369
+ end
370
+ bhyve_cmd += " -s 31,lpc"
371
+
372
+ # Generate ACPI tables for FreeBSD guest
373
+ bhyve_cmd += " -A" if loader == 'bhyveload'
374
+
375
+ # For UEFI, we need to point a UEFI firmware which should be
376
+ # included in the box.
377
+ bhyve_cmd += " -l bootrom,#{directory.join('uefi.fd').to_s}" if firmware == "uefi"
378
+
379
+ # TODO Enable graphics if the box is configed so
380
+
381
+ uuid = get_attr('id')
382
+ bhyve_cmd += " -U #{uuid}"
383
+
384
+ # Allocate resources
385
+ bhyve_cmd += " -c #{config.cpus}"
386
+ bhyve_cmd += " -m #{config.memory}"
387
+
388
+ # Disk(if any)
389
+ bhyve_cmd += " -s 1:0,ahci-hd,#{directory.join("disk.img").to_s}"
390
+ disk_id = 1
391
+ config.disks.each do |disk|
392
+ if disk[:format] == "raw"
393
+ if disk[:path]
394
+ path = disk[:path]
395
+ else
396
+ path = directory.join(disk[:name].to_s).to_s + ".img"
397
+ end
398
+ execute(false, "truncate -s #{disk[:size]} #{path}")
399
+ bhyve_cmd += " -s 1:#{disk_id.to_s},ahci-hd,#{path.to_s}"
400
+ end
401
+ disk_id += 1
402
+ end
403
+
404
+ # CDROM(if any)
405
+ cdrom_id = 0
406
+ config.cdroms.each do |cdrom|
407
+ path = File.realpath(cdrom[:path])
408
+ bhyve_cmd += " -s 2:#{cdrom_id.to_s},ahci-cd,#{path.to_s}"
409
+ cdrom_id += 1
410
+ end
411
+
412
+
413
+ # Tap device
414
+ tap_device = get_attr('tap')
415
+ mac_address = get_attr('mac')
416
+ bhyve_cmd += " -s 3:0,virtio-net,#{tap_device},mac=#{mac_address}"
417
+
418
+ # Console
419
+ nmdm_num = find_available_nmdm
420
+ @data_dir.join('nmdm_num').open('w') { |nmdm_file| nmdm_file.write nmdm_num }
421
+ bhyve_cmd += " -l com1,/dev/nmdm#{nmdm_num}A"
422
+
423
+ vm_name = get_attr('vm_name')
424
+ bhyve_cmd += " #{vm_name} >/dev/null 2>&1"
425
+
426
+ execute(false, bhyve_cmd)
427
+ while state(vm_name) != :running
428
+ sleep 0.5
429
+ end
430
+ end
431
+
432
+ def shutdown(ui)
433
+ vm_name = get_attr('vm_name')
434
+ if state(vm_name) == :not_running
435
+ ui.warn "You are trying to shutdown a VM which is not running"
436
+ else
437
+ bhyve_pid = execute(false, "pgrep -fx 'bhyve: #{vm_name}'")
438
+ loader_pid = execute(false, "pgrep -fl 'grub-bhyve|bhyveload' | grep #{vm_name} | cut -d' ' -f1")
439
+ if bhyve_pid.length != 0
440
+ # We need to kill bhyve process twice and wait some time to make
441
+ # sure VM is shuted down.
442
+ while bhyve_pid.length != 0
443
+ begin
444
+ execute(false, "#{@sudo} kill -s TERM #{bhyve_pid}")
445
+ sleep 1
446
+ bhyve_pid = execute(false, "pgrep -fx 'bhyve: #{vm_name}'")
447
+ rescue Errors::ExecuteError
448
+ break
449
+ end
450
+ end
451
+ elsif loader_pid.length != 0
452
+ ui.warn "Guest is going to be exit in bootloader stage"
453
+ execute(false, "#{@sudo} kill #{loader_pid}")
454
+ else
455
+ ui.warn "Unable to locate process id for #{vm_name}"
456
+ end
457
+ end
458
+ end
459
+
460
+ def forward_port(forward_information, tap_device)
461
+ id = get_attr('id')
462
+ ip_address = get_ip_address(tap_device)
463
+ pf_conf = @data_dir.join('pf.conf')
464
+ rule = "rdr on #{forward_information[:adapter]} proto {udp, tcp} from any to any port #{forward_information[:host_port]} -> #{ip_address} port #{forward_information[:guest_port]}"
465
+
466
+ pf_conf.open('a') do |pf_file|
467
+ pf_file.puts rule
468
+ end
469
+ # Update pf rules
470
+ comment_mark = "# vagrant-bhyve #{tap_device}"
471
+ if execute(true, "test -s /etc/pf.conf") == 0
472
+ if execute(true, "grep \"#{comment_mark}\" /etc/pf.conf") != 0
473
+ execute(false, "#{@sudo} sed -i '' '1i\\\n#{comment_mark}\n' /etc/pf.conf")
474
+ execute(false, "#{@sudo} sed -i '' '2i\\\ninclude \"#{pf_conf.to_s}\"\n' /etc/pf.conf")
475
+ end
476
+ else
477
+ execute(false, "echo \"#{comment_mark}\" | #{@sudo} tee -a /etc/pf.conf")
478
+ execute(false, "echo \"include \\\"#{pf_conf.to_s}\\\"\" | #{@sudo} tee -a /etc/pf.conf")
479
+ end
480
+ restart_service('pf')
481
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{id}' -f #{pf_conf.to_s}")
482
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{id}' -sr")
483
+ #execute(false, "#{@sudo} pfctl -a vagrant_#{id} -F all")
484
+
485
+ end
486
+
487
+ def cleanup
488
+ bridge = get_attr('bridge')
489
+ tap = get_attr('tap')
490
+ vm_name = get_attr('vm_name')
491
+ id = get_attr('id')
492
+ mac = get_attr('mac')
493
+ directory = @data_dir
494
+
495
+ return unless bridge && tap
496
+ # Destroy vmm device
497
+ execute(false, "#{@sudo} bhyvectl --destroy --vm=#{vm_name} >/dev/null 2>&1") if state(vm_name) == :uncleaned
498
+
499
+ # Clean instance-specific pf rules
500
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{id}' -F all")
501
+ comment_mark_tap = "# vagrant-bhyve #{tap}"
502
+ if execute(true, "grep \"#{comment_mark_tap}\" /etc/pf.conf") == 0
503
+ execute(false, "#{@sudo} sed -i '' '/#{comment_mark_tap}/,+1d' /etc/pf.conf")
504
+ end
505
+ # Destory tap interfaces
506
+ execute(false, "#{@sudo} ifconfig #{tap} destroy") if execute(true, "ifconfig #{tap}") == 0
507
+ execute(false, "#{@sudo} sed -i '' '/#{mac}/d' /var/run/dnsmasq.#{bridge}.leases") if execute(true, "grep \"#{mac}\" /var/run/dnsmasq.#{bridge}.leases") == 0
508
+
509
+ # Delete configure files
510
+ #FileUtils.rm directory.join('dnsmasq.conf').to_s if directory.join('dnsmasq.conf').exist?
511
+ #FileUtils.rm directory.join('pf.conf').to_s if directory.join('pf.conf').exist?
512
+
513
+ # Clean nat configurations if there is no VMS is using the bridge
514
+ member_num = 3
515
+ bridge_exist = execute(true, "ifconfig #{bridge}")
516
+ member_num = execute(false, "ifconfig #{bridge} | grep -c 'member' || true") if bridge_exist == 0
517
+
518
+ if bridge_exist != 0 || member_num.to_i < 2
519
+ #execute(false, "#{@sudo} pfctl -a '/vagrant_#{bridge}' -F all")
520
+ comment_mark_bridge = "# vagrant-bhyve #{bridge}"
521
+ if execute(true, "grep \"#{comment_mark_bridge}\" /etc/pf.conf") == 0
522
+ execute(false, "#{@sudo} sed -i '' '/#{comment_mark_bridge}/,+1d' /etc/pf.conf")
523
+ end
524
+ restart_service('pf')
525
+ #if directory.join('pf_disabled').exist?
526
+ # FileUtils.rm directory.join('pf_disabled')
527
+ # execute(false, "#{@sudo} pfctl -d")
528
+ #end
529
+ execute(false, "#{@sudo} ifconfig #{bridge} destroy") if bridge_exist == 0
530
+ pf_conf = "/usr/local/etc/pf.#{bridge}.conf"
531
+ execute(false, "#{@sudo} rm #{pf_conf}") if execute(true, "test -e #{pf_conf}") == 0
532
+ if execute(true, "test -e /var/run/dnsmasq.#{bridge}.pid") == 0
533
+ dnsmasq_cmd = "dnsmasq -C /usr/local/etc/dnsmasq.#{bridge}.conf -l /var/run/dnsmasq.#{bridge}.leases -x /var/run/dnsmasq.#{bridge}.pid"
534
+ dnsmasq_conf = "/var/run/dnsmasq.#{bridge}.leases"
535
+ dnsmasq_leases = "/var/run/dnsmasq.#{bridge}.pid"
536
+ dnsmasq_pid = "/usr/local/etc/dnsmasq.#{bridge}.conf"
537
+ execute(false, "#{@sudo} kill -9 $(pgrep -fx \"#{dnsmasq_cmd}\")")
538
+ execute(false, "#{@sudo} rm #{dnsmasq_leases}") if execute(true, "test -e #{dnsmasq_leases}") == 0
539
+ execute(false, "#{@sudo} rm #{dnsmasq_pid}") if execute(true, "test -e #{dnsmasq_pid}") == 0
540
+ execute(false, "#{@sudo} rm #{dnsmasq_conf}") if execute(true, "test -e #{dnsmasq_conf}") == 0
541
+ end
542
+ end
543
+ end
544
+
545
+ def state(vm_name)
546
+ vmm_exist = execute(true, "test -e /dev/vmm/#{vm_name}") == 0
547
+ if vmm_exist
548
+ if execute(true, "pgrep -fx \"bhyve: #{vm_name}\"") == 0
549
+ :running
550
+ else
551
+ :uncleaned
552
+ end
553
+ else
554
+ :stopped
555
+ end
556
+ end
557
+
558
+ def execute(*cmd, **opts, &block)
559
+ @executor.execute(*cmd, **opts, &block)
560
+ end
561
+
562
+ def get_mac_address(vm_name)
563
+ # Generate a mac address for this tap device from its vm_name
564
+ # IEEE Standards OUI for bhyve
565
+ mac = "58:9c:fc:0"
566
+ mac += Digest::MD5.hexdigest(vm_name).scan(/../).select.with_index{ |_, i| i.even? }[0..2].join(':')[1..-1]
567
+ end
568
+
569
+ # Get the interface name for a bridge(like 'bridge0')
570
+ def get_interface_name(device_name)
571
+ desc = device_name + '\$'
572
+ cmd = "ifconfig -a | grep -B 1 #{desc} | head -n1 | awk -F: '{print $1}'"
573
+ result = execute(false, cmd)
574
+ end
575
+
576
+ def restart_service(service_name)
577
+ status = execute(true, "#{@sudo} pfctl -s all | grep -i disabled")
578
+ if status == 0
579
+ cmd = "onerestart"
580
+ else
581
+ cmd = "onestart"
582
+ end
583
+ status = execute(true, "#{@sudo} service #{service_name} #{cmd} >/dev/null 2>&1")
584
+ raise Errors::RestartServiceFailed if status != 0
585
+ end
586
+
587
+ def pf_enabled?
588
+ status = execute(true, "#{@sudo} pfctl -s all | grep -i disabled")
589
+ if status == 0
590
+ store_attr('pf_disabled', 'yes')
591
+ false
592
+ else
593
+ true
594
+ end
595
+ end
596
+
597
+ def find_available_nmdm
598
+ nmdm_num = 0
599
+ while true
600
+ result = execute(true, "ls -l /dev/ | grep 'nmdm#{nmdm_num}A'")
601
+ break if result != 0
602
+ nmdm_num += 1
603
+ end
604
+ nmdm_num
605
+ end
606
+
607
+ def get_attr(attr)
608
+ name_file = @data_dir.join(attr)
609
+ if File.exist?(name_file)
610
+ name_file.open('r') { |f| f.readline }
611
+ else
612
+ nil
613
+ end
614
+ end
615
+
616
+ def pkg_install(package)
617
+ execute(false, "#{@sudo} ASSUME_ALWAYS_YES=yes pkg install #{package}")
618
+ end
619
+
620
+ def store_attr(name, value)
621
+ @data_dir.join(name).open('w') { |f| f.write value }
622
+ end
623
+
624
+ def check_and_install(command, package, ui)
625
+ command_exist = execute(true, "which #{command}")
626
+ if command_exist != 0
627
+ ui.warn "We need #{command} in #{package} package, installing with pkg..."
628
+ pkg_install(package)
629
+ end
630
+ end
631
+
632
+ def grub_bhyve_execute(command, password, member)
633
+ vm_name = get_attr('vm_name')
634
+ exp = RubyExpect::Expect.spawn("sudo grub-bhyve -m #{@data_dir.join('device.map').to_s} -M 128M #{vm_name}")
635
+ if password == ''
636
+ exp.procedure do
637
+ each do
638
+ expect /grub> / do
639
+ send command
640
+ end
641
+ expect /.*(grub> )$/ do
642
+ send 'exit'
643
+ end
644
+ end
645
+ end
646
+ else
647
+ exp.procedure do
648
+ each do
649
+ expect /Password:/ do
650
+ send password
651
+ end
652
+ expect /grub> / do
653
+ send command
654
+ end
655
+ expect /.*(grub> )$/ do
656
+ send 'exit'
657
+ end
658
+ end
659
+ end
660
+ end
661
+ execute(false, "#{@sudo} bhyvectl --destroy --vm=#{vm_name}")
662
+ case member
663
+ when :match
664
+ return exp.match.to_s
665
+ when :before
666
+ return exp.before.to_s
667
+ when :last_match
668
+ return exp.last_match.to_s
669
+ end
670
+ end
671
+
672
+ end
673
+ end
674
+ end