vagrant-bhyve 0.1.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.
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