├── .rspec ├── example_box ├── metadata.json ├── Vagrantfile └── README.md ├── .travis.yml ├── spec ├── spec_helper.rb └── vagrant-bhyve │ └── bhyve_spec.rb ├── lib ├── vagrant-bhyve │ ├── version.rb │ ├── action │ │ ├── destroy.rb │ │ ├── cleanup.rb │ │ ├── prepare_nfs_valid_ids.rb │ │ ├── boot.rb │ │ ├── shutdown.rb │ │ ├── load.rb │ │ ├── setup.rb │ │ ├── create_tap.rb │ │ ├── import.rb │ │ ├── create_bridge.rb │ │ ├── prepare_nfs_settings.rb │ │ ├── wait_until_up.rb │ │ └── forward_ports.rb │ ├── executor.rb │ ├── errors.rb │ ├── config.rb │ ├── plugin.rb │ ├── provider.rb │ ├── action.rb │ └── driver.rb └── vagrant-bhyve.rb ├── Rakefile ├── .gitignore ├── Vagrantfiles ├── Vagrantfile_Debian_Ubuntu ├── Vagrantfile_CentOS-7 └── Vagrantfile_CentOS-6 ├── Gemfile ├── vagrant-bhyve.gemspec ├── LICENSE.txt ├── locales └── en.yml └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /example_box/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "provider" : "bhyve" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.3.1 4 | before_install: gem install bundler -v 1.10.6 5 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'vagrant/bhyve' 3 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/version.rb: -------------------------------------------------------------------------------- 1 | module VagrantPlugins 2 | module ProviderBhyve 3 | VERSION = "0.1.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /example_box/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.provider :bhyve do |vm| 6 | vm.memory = "512M" 7 | vm.cpus = "1" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /svn-commit.tmp 11 | /vendor/ 12 | /example_box/*img 13 | /example_box/*raw 14 | /bin/ 15 | /Vagrantfile 16 | /.vagrant 17 | -------------------------------------------------------------------------------- /Vagrantfiles/Vagrantfile_Debian_Ubuntu: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.provider :bhyve do |vm| 6 | vm.grub_run_partition = "msdos1" 7 | vm.memory = "512M" 8 | vm.cpus = "1" 9 | end 10 | end 11 | 12 | -------------------------------------------------------------------------------- /spec/vagrant-bhyve/bhyve_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Vagrant::Bhyve do 4 | it 'has a version number' do 5 | expect(Vagrant::Bhyve::VERSION).not_to be nil 6 | end 7 | 8 | it 'does something useful' do 9 | expect(false).to eq(true) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Vagrantfiles/Vagrantfile_CentOS-7: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.provider :bhyve do |vm| 6 | vm.grub_run_partition = "msdos2" 7 | vm.grub_run_dir = "/grub2" 8 | vm.memory = "512M" 9 | vm.cpus = "1" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'vagrant', :git => 'https://github.com/mitchellh/vagrant.git', tag: 'v1.8.4' 7 | end 8 | 9 | group :plugins do 10 | gem 'vagrant-bhyve', path: '.' 11 | #gem 'vagrant-sshfs', '1.1.0' 12 | #gem 'vagrant-mutate', :git => 'https://github.com/swills/vagrant-mutate' 13 | end 14 | -------------------------------------------------------------------------------- /Vagrantfiles/Vagrantfile_CentOS-6: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.provider :bhyve do |vm| 6 | vm.grub_config_file = %{ 7 | linux (hd0,msdos1)/vmlinuz-2.6.32-642.1.1.el6.x86_64 root=/dev/mapper/VolGroup00-LogVol00 8 | initrd (hd0,msdos1)/initramfs-2.6.32-642.1.1.el6.x86_64.img 9 | boot 10 | } 11 | vm.memory = "512M" 12 | vm.cpus = "1" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/destroy.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Destroy 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::destroy") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.destroying') 15 | env[:machine].provider.driver.destroy 16 | 17 | @app.call(env) 18 | end 19 | 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/cleanup.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Cleanup 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::cleanup") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.halt.cleaning_up') 15 | env[:machine].provider.driver.cleanup 16 | 17 | @app.call(env) 18 | end 19 | 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/prepare_nfs_valid_ids.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "securerandom" 3 | require "digest/md5" 4 | 5 | module VagrantPlugins 6 | module ProviderBhyve 7 | module Action 8 | class PrepareNFSValidIds 9 | 10 | def initialize(app, env) 11 | @logger = Log4r::Logger.new("vagrant_bhyve::action::prepare_nfs_valid_ids") 12 | @app = app 13 | end 14 | 15 | def call(env) 16 | env[:nfs_valid_ids] = [env[:machine].id] 17 | @app.call(env) 18 | end 19 | 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/boot.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Boot 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::boot") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @driver = @machine.provider.driver 16 | 17 | env[:ui].detail I18n.t('vagrant_bhyve.action.vm.boot.booting') 18 | @driver.boot(@machine, env[:ui]) 19 | 20 | @app.call(env) 21 | end 22 | 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/shutdown.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Shutdown 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::shutdown") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @ui = env[:ui] 16 | @driver = @machine.provider.driver 17 | 18 | @ui.info I18n.t('vagrant_bhyve.action.vm.halt.shutting_down') 19 | @driver.shutdown(@ui) 20 | @app.call(env) 21 | end 22 | 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/load.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Load 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::load") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @driver = @machine.provider.driver 16 | firmware = @driver.get_attr('firmware') 17 | 18 | env[:ui].detail I18n.t('vagrant_bhyve.action.vm.boot.load_kernel') 19 | @driver.load(@machine, env[:ui]) if firmware == 'bios' 20 | 21 | @app.call(env) 22 | end 23 | 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | lib_path = Pathname.new(File.expand_path("../vagrant-bhyve", __FILE__)) 6 | autoload :Action, lib_path.join('action') 7 | autoload :Executor, lib_path.join('executor') 8 | autoload :Driver, lib_path.join('driver') 9 | autoload :Errors, lib_path.join('errors') 10 | 11 | 12 | # This function returns the path to the source of this plugin 13 | # 14 | # @return [Pathname] 15 | def self.source_root 16 | @source_root ||= Pathname.new(File.expand_path("../../", __FILE__)) 17 | end 18 | end 19 | end 20 | 21 | require "vagrant-bhyve/plugin" 22 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/setup.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class Setup 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::setup") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @driver = env[:machine].provider.driver 15 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.setup_environment') 16 | @driver.check_bhyve_support 17 | module_list = %w(vmm nmdm if_bridge if_tap pf) 18 | for kernel_module in module_list 19 | @driver.load_module(kernel_module) 20 | end 21 | @app.call(env) 22 | end 23 | 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/create_tap.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class CreateTap 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::create_tap") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @driver = @machine.provider.driver 16 | 17 | env[:ui].detail I18n.t('vagrant_bhyve.action.vm.boot.create_tap') 18 | vm_name = @driver.get_attr('vm_name') 19 | tap_name = "vagrant_bhyve_#{vm_name}" 20 | tap_list = [tap_name] 21 | # The switch name is used as created bridge device's description 22 | tap_list.each do |tap| 23 | @driver.create_network_device(tap, "tap") 24 | end 25 | @app.call(env) 26 | end 27 | 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/import.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "securerandom" 3 | require "digest/md5" 4 | 5 | module VagrantPlugins 6 | module ProviderBhyve 7 | module Action 8 | class Import 9 | 10 | def initialize(app, env) 11 | @logger = Log4r::Logger.new("vagrant_bhyve::action::import") 12 | @app = app 13 | end 14 | 15 | def call(env) 16 | @machine = env[:machine] 17 | @driver = @machine.provider.driver 18 | 19 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.import_box') 20 | @machine.id = SecureRandom.uuid 21 | vm_name = @machine.id.gsub('-', '')[0..30] 22 | mac = @driver.get_mac_address(vm_name) 23 | @driver.store_attr('vm_name', vm_name) 24 | @driver.store_attr('mac', mac) 25 | @driver.import(@machine, env[:ui]) 26 | @app.call(env) 27 | end 28 | 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/create_bridge.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class CreateBridge 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::create_bridge") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @ui = env[:ui] 16 | @driver = @machine.provider.driver 17 | 18 | @ui.info I18n.t('vagrant.actions.vm.boot.booting') 19 | @ui.detail I18n.t('vagrant_bhyve.action.vm.boot.setup_nat') 20 | 21 | bridge_list = %w(vagrant_bhyve_default_bridge) 22 | # The bridge name is used as created bridge device's description 23 | bridge_list.each do |bridge| 24 | @driver.create_network_device(bridge, "bridge") 25 | @driver.enable_nat(bridge, @ui) 26 | end 27 | @app.call(env) 28 | end 29 | 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /vagrant-bhyve.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require File.expand_path('../lib/vagrant-bhyve/version', __FILE__) 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "vagrant-bhyve" 6 | spec.version = VagrantPlugins::ProviderBhyve::VERSION 7 | spec.authors = ["Tong Li"] 8 | spec.email = ["jesa7955@gmail.com"] 9 | 10 | spec.summary = %q{Vagrant provider plugin to support bhyve} 11 | spec.description = spec.summary 12 | spec.homepage = "https://github.com/jesa7955/vagrant-bhyve" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 16 | spec.bindir = "exe" 17 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_development_dependency "bundler", "~> 1.10" 21 | spec.add_development_dependency "rake", "~> 10.0" 22 | spec.add_development_dependency "rspec" 23 | spec.add_runtime_dependency "ruby_expect" 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tong Li 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/executor.rb: -------------------------------------------------------------------------------- 1 | require "vagrant/util/busy" 2 | require "vagrant/util/subprocess" 3 | 4 | module VagrantPlugins 5 | module ProviderBhyve 6 | module Executor 7 | # This class is used to execute commands as subprocess. 8 | class Exec 9 | # When we need the command's exit code we should set parameter 10 | # exit_code to true, otherwise this method will return executed 11 | # command's stdout 12 | def execute(exit_code, *cmd, **opts, &block) 13 | # Append in the options for subprocess 14 | cmd << { notify: [:stdout, :stderr] } 15 | cmd.unshift('sh', '-c') 16 | 17 | interrupted = false 18 | # Lambda to change interrupted to true 19 | int_callback = ->{ interrupted = true } 20 | result = ::Vagrant::Util::Busy.busy(int_callback) do 21 | ::Vagrant::Util::Subprocess.execute(*cmd, &block) 22 | end 23 | 24 | return result.exit_code if exit_code 25 | 26 | result.stderr.gsub!("\r\n", "\n") 27 | result.stdout.gsub!("\r\n", "\n") 28 | 29 | if result.exit_code != 0 || interrupted 30 | raise Errors::ExecuteError 31 | end 32 | 33 | result.stdout[0..-2] 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/prepare_nfs_settings.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "securerandom" 3 | require "digest/md5" 4 | 5 | module VagrantPlugins 6 | module ProviderBhyve 7 | module Action 8 | class PrepareNFSSettings 9 | include Vagrant::Action::Builtin::MixinSyncedFolders 10 | 11 | def initialize(app, env) 12 | @logger = Log4r::Logger.new("vagrant_bhyve::action::prepare_nfs_settings") 13 | @app = app 14 | end 15 | 16 | def call(env) 17 | @machine = env[:machine] 18 | @driver = @machine.provider.driver 19 | @app.call(env) 20 | 21 | if using_nfs? 22 | tap_device = @driver.get_attr('tap') 23 | #host_ip = @driver.get_ip_address(nil, :host) 24 | guest_ip = @driver.get_ip_address(tap_device, :guest) 25 | host_ip = read_host_ip(guest_ip) 26 | env[:nfs_machine_ip] = guest_ip 27 | env[:nfs_host_ip] = host_ip 28 | end 29 | end 30 | 31 | def using_nfs? 32 | !!synced_folders(@machine)[:nfs] 33 | end 34 | 35 | # Ruby way to get host ip 36 | def read_host_ip(ip) 37 | UDPSocket.open do |s| 38 | if(ip.kind_of?(Array)) 39 | s.connect(ip.last, 1) 40 | else 41 | s.connect(ip, 1) 42 | end 43 | s.addr.last 44 | end 45 | end 46 | 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/wait_until_up.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | require "vagrant/util/retryable" 3 | 4 | module VagrantPlugins 5 | module ProviderBhyve 6 | module Action 7 | class WaitUntilUP 8 | include Vagrant::Util::Retryable 9 | 10 | def initialize(app, env) 11 | @logger = Log4r::Logger.new("vagrant_bhyve::action::wait_until_up") 12 | @app = app 13 | end 14 | 15 | def call(env) 16 | @driver = env[:machine].provider.driver 17 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.boot.wait_until_up') 18 | 19 | vm_name = @driver.get_attr('vm_name') 20 | # Check whether ip is assigned 21 | env[:uncleaned] = false 22 | while !env[:uncleaned] 23 | sleep 1 24 | env[:uncleaned] = true if @driver.state(vm_name) == :uncleaned 25 | if @driver.ip_ready? 26 | sleep 2 27 | env[:uncleaned] = true if @driver.state(vm_name) == :uncleaned 28 | break 29 | end 30 | end 31 | 32 | # Check whether we have ssh access 33 | while !env[:uncleaned] 34 | sleep 1 35 | env[:uncleaned] = true if @driver.state(vm_name) == :uncleaned 36 | if @driver.ssh_ready?(env[:machine].provider.ssh_info) 37 | sleep 2 38 | env[:uncleaned] = true if @driver.state(vm_name) == :uncleaned 39 | break 40 | end 41 | end 42 | @app.call(env) 43 | end 44 | 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/errors.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Errors 6 | class VagrantBhyveError < Vagrant::Errors::VagrantError 7 | error_namespace('vagrant_bhyve.errors') 8 | end 9 | 10 | class SystemVersionIsTooLow < VagrantBhyveError 11 | error_key(:system_version_too_low) 12 | end 13 | 14 | class MissingPopcnt < VagrantBhyveError 15 | error_key(:missing_popcnt) 16 | end 17 | 18 | class MissingEpt < VagrantBhyveError 19 | error_key(:missing_ept) 20 | end 21 | 22 | class MissingIommu < VagrantBhyveError 23 | error_key(:missing_iommu) 24 | end 25 | 26 | class HasNoRootPrivilege < VagrantBhyveError 27 | error_key(:has_no_root_privilege) 28 | end 29 | 30 | class ExecuteError < VagrantBhyveError 31 | error_key(:execute_error) 32 | end 33 | 34 | class UnableToLoadModule < VagrantBhyveError 35 | error_key(:unable_to_load_module) 36 | end 37 | 38 | class UnableToCreateInterface < VagrantBhyveError 39 | error_key(:unable_to_create_interface) 40 | end 41 | 42 | class GrubBhyveNotinstalled < VagrantBhyveError 43 | error_key(:grub_bhyve_not_installed) 44 | end 45 | 46 | class RestartServiceFailed < VagrantBhyveError 47 | error_key(:restart_service_failed) 48 | end 49 | 50 | class NotFoundLeasesInfo < VagrantBhyveError 51 | error_key(:not_found_leases_info) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action/forward_ports.rb: -------------------------------------------------------------------------------- 1 | require "log4r" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | module Action 6 | class ForwardPorts 7 | 8 | def initialize(app, env) 9 | @logger = Log4r::Logger.new("vagrant_bhyve::action::forward_ports") 10 | @app = app 11 | end 12 | 13 | def call(env) 14 | @machine = env[:machine] 15 | @driver = @machine.provider.driver 16 | 17 | env[:ui].info I18n.t('vagrant_bhyve.action.vm.forward_ports') 18 | 19 | env[:forwarded_ports] = compile_forwarded_ports(@machine.config) 20 | tap_device = @driver.get_attr('tap') 21 | gateway = @driver.get_attr('gateway') 22 | env[:forwarded_ports].each do |item| 23 | forward_information = { 24 | adapter: item[:adapter] || gateway, 25 | guest_port: item[:guest], 26 | host_port: item[:host] 27 | } 28 | @driver.forward_port(forward_information, tap_device) 29 | end 30 | @app.call(env) 31 | end 32 | 33 | private 34 | 35 | def compile_forwarded_ports(config) 36 | mappings = {} 37 | config.vm.networks.each do |type, options| 38 | next if options[:disabled] 39 | 40 | if type == :forwarded_port && options[:id] != 'ssh' 41 | if options.fetch(:host_ip, '').to_s.strip.empty? 42 | options.delete(:host_ip) 43 | end 44 | mappings[options[:host]] = options 45 | end 46 | end 47 | mappings.values 48 | 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/config.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | 3 | module VagrantPlugins 4 | module ProviderBhyve 5 | class Config < Vagrant.plugin('2', :config) 6 | 7 | # Guest like CentOS-6 requires a customized grub config file 8 | #attr_accessor :grub_config_file 9 | 10 | # Some arguments required by grub-bhyve 11 | #attr_accessor :grub_run_partition 12 | #attr_accessor :grub_run_dir 13 | 14 | # Specify the number of virtual CPUs. 15 | attr_accessor :cpus 16 | # Specify the size of guest physical memory. 17 | attr_accessor :memory 18 | # Specify virtual devices will be attached to bhyve's emulated 19 | # PCI bus. Network interface and disk will both attched as this kind 20 | # of devices. 21 | attr_accessor :pcis 22 | # Specify console device which will be attached to the VM 23 | attr_accessor :lpc 24 | attr_accessor :hostbridge 25 | # Addition storage 26 | attr_accessor :disks 27 | attr_accessor :cdroms 28 | 29 | def initialize 30 | @cpus = UNSET_VALUE 31 | @memory = UNSET_VALUE 32 | @pcis = UNSET_VALUE 33 | @lpc = UNSET_VALUE 34 | @hostbridge = UNSET_VALUE 35 | @disks = [] 36 | @cdroms = [] 37 | #@grub_config_file = '' 38 | #@grub_run_partition = '' 39 | #@grub_run_dir = '' 40 | end 41 | 42 | def storage(options={}) 43 | if options[:device] == :cdrom 44 | _handle_cdrom_storage(options) 45 | elsif options[:device] == :disk 46 | _handle_disk_storage(options) 47 | end 48 | end 49 | 50 | def _handle_disk_storage(options={}) 51 | cdrom = { 52 | path: options[:path] 53 | } 54 | @cdroms << cdrom 55 | end 56 | 57 | def _handle_cdrom_storage(options={}) 58 | options = { 59 | path: nil, 60 | name: nil, 61 | size: "20G", 62 | format: "raw", 63 | }.merge(options) 64 | 65 | disk = { 66 | path: options[:path], 67 | size: options[:size], 68 | } 69 | 70 | @disks << disk 71 | end 72 | 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /example_box/README.md: -------------------------------------------------------------------------------- 1 | # Vagrant Bhyve Example Box 2 | 3 | Box format of bhyve provider is a plain directory consist of `Vagrantfile`, `metadata.json`, bhyve disk file, and an optional `uefi.fd` 4 | 5 | ``` 6 | | 7 | |- Vagrantfile This is where Bhyve is configured. 8 | |- disk.img The disk image including basic OS. 9 | |- metadata.json Box metadata. 10 | `- uefi.fd UEFI firmware (only for guests who use uefi as firmware). 11 | ``` 12 | 13 | Available configurations for the provider are: 14 | 15 | * `memory`: Amount of memory, e.g. `512M` 16 | * `cpus`: Number of CPUs, e.g. `1` 17 | * `disks`: An array of disks which users want to attach to the guest other than the box shipped one. Each disk is described by a hash which has three members. 18 | * **path/name**: path is used to specify a image file outside .vagrant direcotory while name is used to specify an image file name which will be created inside .vagrant directory for the box. Only one of these two arguments is needed to describe an additional disk file. 19 | * **size**: specify the image file's virutal size. 20 | * **format**: specify the format of disk image file. Bhyve only support raw images now but maybe we can extend vagrant-bhyve when bhyve supports more. 21 | * `cdroms`: Like `disks`, this is an array contains all ISO files which users want to attach to bhyve. Now, each cdrom is described by a hash contains only the path to a ISO file. 22 | * ~~`grub_config_file`:~~ 23 | * ~~`grub_run_partition`:~~ 24 | :* ~~`grub_run_dir`:~~ 25 | 26 | Put everything we need in a directory and run the command below to package them as a box file. 27 | ``` 28 | $ tar cvzf test.box ./metadata.json ./Vagrantfile ./disk.img 29 | ``` 30 | 31 | This box works by using Vagrant's built-in Vagrantfile merging to setup 32 | defaults for Bhyve. These defaults can easily be overwritten by higher-level 33 | Vagrantfiles (such as project root Vagrantfiles). 34 | 35 | ## Box Metadata 36 | 37 | Bhyve box should define at least two data fields in `metadata.json` file. 38 | 39 | * provider - Provider name is bhyve. 40 | 41 | ## Converting Boxes 42 | 43 | Instead of creating a box from scratch, you can use 44 | [vagrant-mutate](https://github.com/sciurus/vagrant-mutate) 45 | to take boxes created for other Vagrant providers and use them 46 | with vagrant-bhyve 47 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/plugin.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require "vagrant" 3 | rescue LoadError 4 | raise "The Vagrant Bhyve plugin must be run within Vagrant." 5 | end 6 | 7 | ############################################################# 8 | # TBD some version check # 9 | ############################################################# 10 | 11 | module VagrantPlugins 12 | module ProviderBhyve 13 | class Plugin < Vagrant.plugin('2') 14 | name "bhyve" 15 | description <<-DESC 16 | This plugin allows vagrant to manage VMs in bhyve, the hypervisor 17 | provided by FreeBSD's kernel 18 | DESC 19 | 20 | config(:bhyve, :provider) do 21 | require_relative "config" 22 | Config 23 | end 24 | 25 | provider(:bhyve) do 26 | require_relative "provider" 27 | Provider 28 | end 29 | 30 | # This initializes the internationalization strings. 31 | def self.setup_i18n 32 | I18n.load_path << File.expand_path('locales/en.yml', 33 | ProviderBhyve.source_root) 34 | I18n.reload! 35 | end 36 | 37 | # This sets up our log level to be whatever VAGRANT_LOG is. 38 | def self.setup_logging 39 | require 'log4r' 40 | 41 | level = nil 42 | begin 43 | level = Log4r.const_get(ENV['VAGRANT_LOG'].upcase) 44 | rescue NameError 45 | # This means that the logging constant wasn't found, 46 | # which is fine. We just keep `level` as `nil`. But 47 | # we tell the user. 48 | level = nil 49 | end 50 | 51 | # Some constants, such as "true" resolve to booleans, so the 52 | # above error checking doesn't catch it. This will check to make 53 | # sure that the log level is an integer, as Log4r requires. 54 | level = nil if !level.is_a?(Integer) 55 | 56 | # Set the logging level on all "vagrant" namespaced 57 | # logs as long as we have a valid level. 58 | if level 59 | logger = Log4r::Logger.new('vagrant_bhyve') 60 | logger.outputters = Log4r::Outputter.stderr 61 | logger.level = level 62 | logger = nil 63 | end 64 | end 65 | 66 | # Setup logging and i18n before any autoloading loads other classes 67 | # with logging configured as this prevents inheritance of the log level 68 | # from the parent logger. 69 | setup_logging 70 | setup_i18n 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | vagrant_bhyve: 3 | commands: 4 | common: 5 | vm_not_created: |- 6 | You are destroying a uncreated instance. 7 | vm_not_running: |- 8 | You are invokng this action on a box which is not running. 9 | vm_already_running: |- 10 | Your box has already be booted. 11 | 12 | action: 13 | vm: 14 | setup_environment: |- 15 | Checking if your system supports bhyve and loading kernel modules 16 | we need to create bhyve VMs. You will be asked for password for 17 | sudo if you are not running vagrant with root user. 18 | import_box: |- 19 | Copying box files. 20 | forward_ports: |- 21 | Setup port forwarding(if any). 22 | boot: 23 | setup_nat: |- 24 | Setting up a nat environment using pf and dnsmasq to provide 25 | network for VMs. You will need to have dnsmasq installed on 26 | your system. 27 | create_tap: |- 28 | Creating tap device for VM to use. 29 | load_kernel: |- 30 | Loading guest OS's kernel into memory. 31 | booting: |- 32 | Booting VM with bhyve. 33 | wait_until_up: |- 34 | Please wait for a while until we can ssh into box. 35 | halt: 36 | shutting_down: |- 37 | Trying to shutdown your VM through ACPI. 38 | force_halt: |- 39 | We have to force your VM to halt. 40 | cleaning_up: |- 41 | Cleaning up your environment. 42 | reload: 43 | reloading: |- 44 | Rebooting your box. 45 | destroying: |- 46 | Deleting copied box files. 47 | 48 | states: 49 | uncreated: |- 50 | Your instance of this box haven't been created. 51 | running: |- 52 | Bhyve is running right now. 53 | stopped: |- 54 | Bhyve is not running right now. 55 | uncleaned: |- 56 | It seems that the box has been shutdown but there reamains a vmm 57 | device. Maybe you shutdown the box from inside. Run `vagrant halt` 58 | to clean. 59 | short_not_created: |- 60 | environment_not_created 61 | long_not_created: |- 62 | The environment has not been created yet. Run `vagrant up` to create 63 | the environment 64 | 65 | errors: 66 | system_version_too_low: |- 67 | To use bhyve your FreeBSD's version should be newer then 10.0 68 | missing_popcnt: |- 69 | Popcnt is not supported by your CPU. 70 | missing_ept: |- 71 | EPT is not supported by your CPU. 72 | missing_iommu: |- 73 | IOMMU is not supported by your CPU. 74 | has_no_root_privilege: |- 75 | You need root privilege to manage bhyve VMs. 76 | execute_error: |- 77 | Failed when execute commands 78 | unable_to_load_module: |- 79 | Failed to load kernel modules we need to run bhyve VMs and provide 80 | them networking. 81 | unable_to_create_interface: |- 82 | Failed to create network device. 83 | grub_bhyve_not_installed: |- 84 | Grub2-bhyve is not found on you system. You can install with `pkg 85 | install grub2-bhyve` or install through port(sysutils/grub2-bhyve) 86 | restart_service_failed: |- 87 | Failed to restart a service on your system. 88 | not_found_leases_info: |- 89 | Unable to found IP from dnsmasq's leases file, please retry after a 90 | few seconds. 91 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/provider.rb: -------------------------------------------------------------------------------- 1 | require "vagrant" 2 | require "log4r" 3 | 4 | module VagrantPlugins 5 | module ProviderBhyve 6 | autoload :Driver, 'vagrant-bhyve/driver' 7 | 8 | class Provider < Vagrant.plugin('2', :provider) 9 | 10 | def initialize(machine) 11 | @logger = Log4r::Logger.new("vagrant::provider::bhyve") 12 | @machine = machine 13 | end 14 | 15 | def driver 16 | return @driver if @driver 17 | @driver = Driver.new(@machine) 18 | end 19 | 20 | # This should return a hash of information that explains how to SSH 21 | # into the machine. If the machine is not at a point where SSH is 22 | # even possiable, then 'nil' should be returned 23 | # 24 | # The general structure of this returned hash should be the 25 | # following: 26 | # 27 | # { 28 | # host: "1.2.3.4", 29 | # port: "22", 30 | # username: "vagrant", 31 | # private_key_path: "/path/to/my/key" 32 | # } 33 | def ssh_info 34 | # We just return nil if were not able to identify the VM's IP and 35 | # let Vagrant core deal with it like docker provider does 36 | return nil if state.id != :running 37 | tap_device = driver.get_attr('tap') 38 | ip = driver.get_ip_address(tap_device) unless tap_device.length == 0 39 | return nil if !ip 40 | ssh_info = { 41 | host: ip, 42 | port: 22, 43 | } 44 | end 45 | 46 | # This is called early, before a machine is instantiated, to check 47 | # if this provider is usable. This should return true or false. 48 | # 49 | # If raise_error is true, then instead of returning false, this 50 | # should raise an error with a helpful message about why this 51 | # provider cannot be used. 52 | # 53 | # @param [Boolean] raise_error If true, raise exception if not usable. 54 | # @return [Boolean] 55 | def self.usable?(raise_error=false) 56 | # Return true by default for backwards compat since this was 57 | # introduced long after providers were being written. 58 | true 59 | end 60 | 61 | # This is called early, before a machine is instantiated, to check 62 | # if this provider is installed. This should return true or false. 63 | # 64 | # If the provider is not installed and Vagrant determines it is 65 | # able to install this provider, then it will do so. Installation 66 | # is done by calling Environment.install_provider. 67 | # 68 | # If Environment.can_install_provider? returns false, then an error 69 | # will be shown to the user. 70 | def self.installed? 71 | # By default return true for backwards compat so all providers 72 | # continue to work. 73 | true 74 | end 75 | 76 | # This should return an action callable for the given name. 77 | # 78 | # @param [Symbol] name Name of the action. 79 | # @return [Object] A callable action sequence object, whether it 80 | # is a proc, object, etc. 81 | def action(name) 82 | # Attrmpt to get the action method from the Action class if it 83 | # exists, otherwise return nil to show that we don't support the 84 | # given action 85 | action_method = "action_#{name}" 86 | return Action.send(action_method) if Action.respond_to?(action_method) 87 | nil 88 | end 89 | 90 | # This method is called if the underying machine ID changes. Providers 91 | # can use this method to load in new data for the actual backing 92 | # machine or to realize that the machine is now gone (the ID can 93 | # become `nil`). No parameters are given, since the underlying machine 94 | # is simply the machine instance given to this object. And no 95 | # return value is necessary. 96 | def machine_id_changed 97 | end 98 | 99 | # This should return the state of the machine within this provider. 100 | # The state must be an instance of {MachineState}. Please read the 101 | # documentation of that class for more information. 102 | # 103 | # @return [MachineState] 104 | def state 105 | state_id = nil 106 | if @machine.id 107 | vm_name = driver.get_attr('vm_name') 108 | if vm_name.nil? 109 | state_id = :uncreated 110 | else 111 | state_id = driver.state(vm_name) 112 | end 113 | else 114 | state_id = :uncreated 115 | end 116 | short = state_id.to_s.gsub("_", " ") 117 | long = I18n.t("vagrant_bhyve.states.#{state_id}") 118 | # If we're not created, then specify the special ID flag 119 | state_id = Vagrant::MachineState::NOT_CREATED_ID if state_id == :uncreated 120 | # Return the MachineState object 121 | Vagrant::MachineState.new(state_id, short, long) 122 | end 123 | 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vagrant-bhyve 2 | 3 | This is a Vagrant plugin which enable FreeBSD's hypervisor bhyve as its backend. 4 | 5 | - [Status](#status) 6 | - [Functions](#functions) 7 | - [Boxes](#boxes) 8 | - [Test](#test) 9 | - [Setup environment](#setup-environment) 10 | - [Create a box](#create-a-box) 11 | - [Add the box](#add-the-box) 12 | - [Run the box](#run-the-box) 13 | - [SSH into the box](#ssh-into-the-box) 14 | - [Shutdown the box and cleanup](#shutdown-the-box-and-cleanup) 15 | - [Known Issues](#known-issues) 16 | - [Installation](#installation) 17 | 18 | ## Status 19 | 20 | ### Functions 21 | 22 | | Function | Status 23 | | :---------- | :----- 24 | | Box format | Defined 25 | | Check bhyve support | Working 26 | | Cloning | Working(needs gcp package to copy image) 27 | | Booting(BIOS) | Working 28 | | Network | Working(needs pf and dnsmasq to provider NAT and DHCP) 29 | | SSH/SSH run | Working(SSH run may needs bash) 30 | | Graceful shutdown | Working 31 | | ACPI shutdown | Working 32 | | Destroying | Working 33 | | Provision | Working 34 | | File sharing | Working(NFS and vagrant-sshfs, maybe switch to VirtFS in the future) 35 | | Booting(UEFI) | Not working 36 | | Port forwarding | Not working 37 | | Suspend | Not supported by bhyve yet 38 | | Resume | Not supported by bhyve yet 39 | 40 | ### Boxes 41 | 42 | Collecting status of boxes from [Atlas](https://atlas.hashicorp.com/boxes/search) other than those provided by [FreeBSD](https://atlas.hashicorp.com/freebsd) 43 | 44 | | Function | Status 45 | | :--------------------------------------------------------------------------------- | :------ 46 | | [ubuntu/trusty64](https://atlas.hashicorp.com/ubuntu/boxes/trusty64) | Working 47 | | [laravel/homestead](https://atlas.hashicorp.com/laravel/boxes/homestead) | Untested 48 | | [hashicorp/precise64](https://atlas.hashicorp.com/hashicorp/boxes/precise64) | Untested 49 | | [hashicorp/precise32](https://atlas.hashicorp.com/hashicorp/boxes/precise32) | Untested 50 | | [centos/7](https://atlas.hashicorp.com/centos/boxes/7) | Working 51 | | [puphpet/ubuntu1404-x64](https://atlas.hashicorp.com/puphpet/boxes/ubuntu1404-x64) | Untested 52 | | [ubuntu/trusty32](https://atlas.hashicorp.com/ubuntu/boxes/trusty32) | Untested 53 | | [puphpet/debian75-x64](https://atlas.hashicorp.com/puphpet/boxes/debian75-x64) | Untested 54 | | [debian/jessie64](https://atlas.hashicorp.com/debian/boxes/jessie64) | Working 55 | | [scotch/box](https://atlas.hashicorp.com/scotch/boxes/box) | Untested 56 | | [centos/6](https://atlas.hashicorp.com/centos/boxes/6) | Working 57 | 58 | ## Test 59 | 60 | ### Setup environment 61 | 62 | $ git clone https://github.com/jesa7955/vagrant-bhyve.git 63 | $ cd vagrant-bhyve 64 | $ bundle install --path vendor/bundle --binstubs 65 | 66 | Note we will need package coreutils and dnsmasq(and of course we will need grub-bhyve to boot Linux box), vagrant-bhyve will try to install them with pkg. 67 | 68 | ### Create a box 69 | 70 | Thanks to [Steve Wills](https://github.com/swills)'s work, now you can convert a VirtualBox box to a bhyve one with [vagrant-mutate](https://github.com/sciurus/vagrant-mutate). 71 | 72 | ### Run the box 73 | 74 | After a box is created, you should create another Vagrantfile. 75 | 76 | ```ruby 77 | Vagrant.configure("2") do |config| 78 | config.vm.box = "boxname" 79 | end 80 | ``` 81 | 82 | then execute this command to start the box with bhyve 83 | 84 | $ /path/to/vagrant-bhyve/bin/vagrant up --provider=bhyve 85 | 86 | ### SSH into the box 87 | 88 | After the box is booted(uped), you can ssh into by executing this command. Note that you will have to use password to authorize for now. 89 | 90 | $ /path/to/vagrant-bhyve/bin/vagrant ssh 91 | 92 | ### Shutdown the box and cleanup 93 | 94 | This command will shutdown the booted VM and clean up environment 95 | 96 | $ /path/to/vagrant-bhyve/bin/vagrant halt 97 | 98 | ### Destroy the box 99 | 100 | $ /path/to/vagrant-bhyve/vagrant destroy 101 | 102 | ## Known Issues 103 | 104 | ### FreeBSD can't be shutdown gracefully 105 | 106 | This issue seems like a bug of Vagrant core. It even appears when I test 107 | with virtualbox provider. The are two know solutions: 108 | * Add `config.vm.guest = :freebsd` to Vagrantfile 109 | * Add `config.ssh.shell = "sh"` to Vagrantfile 110 | 111 | ### Synced folder is not working correctlly 112 | 113 | I met this issue when I try to use vagrant-bhyve to boot `centos/7` box. 114 | Vagrant uses NFS as default synced folder type. When it fails on your 115 | machine and box, you can: 116 | * Add `config.vm.synced_folder ".", "/vagrant", type: "rsync"` to your 117 | Vagrantfile to ensure that rsync type is used. Vagrant core will raise an 118 | error to inform you when there is not rsync find in PATH 119 | * Run `vagrant plugin install vagrant-sshfs` to enable vagrant-sshfs 120 | 121 | 122 | 123 | ## Installation 124 | 125 | Now this gem has been published on [rubygems.org](https://rubygems.org/gems/vagrant-bhyve). You can install it through `vagrant plugin install vagrant-bhyve` 126 | to install it in a normal Vagrant environment 127 | 128 | ## Contributing 129 | 130 | Bug reports and pull requests are welcome on GitHub at https://github.com/jesa7955/vagrant-bhyve. 131 | 132 | 133 | ## License 134 | 135 | MIT 136 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/action.rb: -------------------------------------------------------------------------------- 1 | require "pathname" 2 | require "vagrant/action/builder" 3 | 4 | module VagrantPlugins 5 | module ProviderBhyve 6 | module Action 7 | include Vagrant::Action::Builtin 8 | 9 | action_root = Pathname.new(File.expand_path('../action', __FILE__)) 10 | autoload :Boot, action_root.join('boot') 11 | autoload :CreateBridge, action_root.join('create_bridge') 12 | autoload :CreateTap, action_root.join('create_tap') 13 | autoload :Cleanup, action_root.join('cleanup') 14 | autoload :Destroy, action_root.join('destroy') 15 | autoload :ForwardPorts, action_root.join('forward_ports') 16 | autoload :Import, action_root.join('import') 17 | autoload :Load, action_root.join('load') 18 | autoload :PrepareNFSSettings, action_root.join('prepare_nfs_settings') 19 | autoload :PrepareNFSValidIds, action_root.join('prepare_nfs_valid_ids') 20 | autoload :Setup, action_root.join('setup') 21 | autoload :Shutdown, action_root.join('shutdown') 22 | autoload :WaitUntilUP, action_root.join('wait_until_up') 23 | 24 | def self.action_boot 25 | Vagrant::Action::Builder.new.tap do |b| 26 | b.use CreateBridge 27 | b.use CreateTap 28 | b.use Load 29 | b.use Boot 30 | b.use Call, WaitUntilUP do |env, b1| 31 | if env[:uncleaned] 32 | b1.use action_reload 33 | else 34 | b1.use ForwardPorts 35 | end 36 | end 37 | b.use PrepareNFSValidIds 38 | b.use SyncedFolderCleanup 39 | b.use SyncedFolders 40 | b.use PrepareNFSSettings 41 | end 42 | end 43 | 44 | def self.action_halt 45 | Vagrant::Action::Builder.new.tap do |b| 46 | b.use ConfigValidate 47 | b.use Call, IsState, :running do |env, b1| 48 | if env[:result] 49 | b1.use Call, GracefulHalt, :uncleaned, :running do |env1, b2| 50 | if !env1[:result] 51 | b3.use Shutdown 52 | end 53 | end 54 | end 55 | end 56 | b.use Call, IsState, :uncleaned do |env, b1| 57 | if env[:result] 58 | b1.use Cleanup 59 | end 60 | end 61 | end 62 | end 63 | 64 | def self.action_reload 65 | Vagrant::Action::Builder.new.tap do |b| 66 | b.use Message, I18n.t('vagrant_bhyve.action.vm.reload.reloading') 67 | b.use Call, IsState, Vagrant::MachineState::NOT_CREATED_ID do |env, b1| 68 | if env[:result] 69 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_created') 70 | next 71 | end 72 | b1.use Call, IsState, :stopped do |env1, b2| 73 | if !env1[:result] 74 | b2.use action_halt 75 | end 76 | end 77 | b1.use ConfigValidate 78 | b1.use action_start 79 | end 80 | end 81 | end 82 | 83 | def self.action_ssh 84 | Vagrant::Action::Builder.new.tap do |b| 85 | b.use ConfigValidate 86 | b.use Call, IsState, :running do |env, b1| 87 | if !env[:result] 88 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_running') 89 | next 90 | end 91 | b1.use SSHExec 92 | end 93 | end 94 | end 95 | 96 | def self.action_ssh_run 97 | Vagrant::Action::Builder.new.tap do |b| 98 | b.use ConfigValidate 99 | b.use Call, IsState, :running do |env, b1| 100 | if !env[:result] 101 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_running') 102 | next 103 | end 104 | b1.use SSHRun 105 | end 106 | end 107 | end 108 | 109 | def self.action_start 110 | Vagrant::Action::Builder.new.tap do |b| 111 | b.use ConfigValidate 112 | b.use Call, IsState, :running do |env, b1| 113 | if env[:result] 114 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_already_running') 115 | next 116 | end 117 | b1.use Call, IsState, :uncleaned do |env1, b2| 118 | if env1[:result] 119 | b2.use Cleanup 120 | end 121 | end 122 | b1.use Setup 123 | b1.use action_boot 124 | end 125 | end 126 | end 127 | 128 | def self.action_up 129 | Vagrant::Action::Builder.new.tap do |b| 130 | b.use Call, IsState, Vagrant::MachineState::NOT_CREATED_ID do |env, b1| 131 | if env[:result] 132 | b1.use HandleBox 133 | end 134 | end 135 | 136 | b.use ConfigValidate 137 | b.use Call, IsState, Vagrant::MachineState::NOT_CREATED_ID do |env,b1| 138 | if env[:result] 139 | b1.use Import 140 | b1.use Provision 141 | end 142 | end 143 | b.use action_start 144 | end 145 | end 146 | 147 | def self.action_destroy 148 | Vagrant::Action::Builder.new.tap do |b| 149 | b.use ConfigValidate 150 | b.use Call, IsState, Vagrant::MachineState::NOT_CREATED_ID do |env, b1| 151 | if env[:result] 152 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_created') 153 | next 154 | end 155 | 156 | b1.use Call, DestroyConfirm do |env1, b2| 157 | if !env1[:result] 158 | b2.use Message, I18n.t( 159 | 'vagrant.commands.destroy.will_not_destroy', 160 | name: env1[:machine].name) 161 | next 162 | end 163 | b2.use Call, IsState, :running do |env2, b3| 164 | if env2[:result] 165 | b3.use action_halt 166 | end 167 | end 168 | b2.use Call, IsState, :uncleaned do |env2, b3| 169 | if env2[:result] 170 | b3.use Cleanup 171 | end 172 | end 173 | b2.use Destroy 174 | b2.use ProvisionerCleanup 175 | b2.use PrepareNFSValidIds 176 | b2.use SyncedFolderCleanup 177 | end 178 | end 179 | end 180 | end 181 | 182 | def self.action_provision 183 | Vagrant::Action::Builder.new.tap do |b| 184 | b.use ConfigValidate 185 | b.use Call, IsState, Vagrant::MachineState::NOT_CREATED_ID do |env,b1| 186 | if env[:result] 187 | b1.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_created') 188 | next 189 | end 190 | b1.use Call, IsState, :running do |env1, b2| 191 | if !env1[:result] 192 | b2.use Message, I18n.t('vagrant_bhyve.commands.common.vm_not_running') 193 | next 194 | end 195 | b2.use Provision 196 | end 197 | end 198 | end 199 | end 200 | 201 | def self.action_suspend 202 | Vagrant::Action::Builder.new.tap do |b| 203 | b.use Warn, I18n.t('vagrant_bhyve.actions.vm.suspend.not_supported') 204 | end 205 | end 206 | 207 | def self.action_resume 208 | Vagrant::Action::Builder.new.tap do |b| 209 | b.use Warn, I18n.t('vagrant_bhyve.actions.vm.resume.not_supported') 210 | end 211 | end 212 | 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/vagrant-bhyve/driver.rb: -------------------------------------------------------------------------------- 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 675 | --------------------------------------------------------------------------------