├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── example └── test.rb ├── lib ├── rubygems_plugin.rb ├── xhyve.rb └── xhyve │ ├── dhcp.rb │ ├── guest.rb │ ├── vendor │ └── xhyve │ └── version.rb ├── spec ├── fixtures │ ├── dhcpd_leases.txt │ └── guest │ │ ├── README.md │ │ ├── initrd │ │ ├── loop.img │ │ └── vmlinuz ├── lib │ ├── dhcp_spec.rb │ └── guest_spec.rb └── spec_helper.rb └── xhyve-ruby.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | mkmf.log 3 | Makefile 4 | coverage 5 | lib/xhyve/vmnet/vmnet.bundle 6 | *.gem 7 | *.swo 8 | *.swp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: osx 2 | osx_image: xcode7.1 3 | rvm: 4 | - 2.1.5 5 | install: 6 | - sudo bundle install 7 | - sudo bundle exec rake install 8 | script: sudo bundle exec rake 9 | env: 10 | - TRAVIS=true 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | xhyve-ruby (0.0.6) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | diff-lcs (1.2.5) 10 | docile (1.1.5) 11 | json (1.8.3) 12 | net-ping (1.7.8) 13 | net-ssh (3.0.1) 14 | rake (10.4.2) 15 | rake-compiler (0.9.5) 16 | rake 17 | rspec (3.2.0) 18 | rspec-core (~> 3.2.0) 19 | rspec-expectations (~> 3.2.0) 20 | rspec-mocks (~> 3.2.0) 21 | rspec-core (3.2.3) 22 | rspec-support (~> 3.2.0) 23 | rspec-expectations (3.2.1) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.2.0) 26 | rspec-mocks (3.2.1) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.2.0) 29 | rspec-support (3.2.2) 30 | simplecov (0.10.0) 31 | docile (~> 1.1.0) 32 | json (~> 1.8) 33 | simplecov-html (~> 0.10.0) 34 | simplecov-html (0.10.0) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | net-ping (= 1.7.8) 41 | net-ssh (= 3.0.1) 42 | rake (= 10.4.2) 43 | rake-compiler (= 0.9.5) 44 | rspec (= 3.2.0) 45 | simplecov (= 0.10.0) 46 | xhyve-ruby! 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://travis-ci.org/dalehamel/xhyve-ruby.svg) 2 | 3 | # Ruby Xhyve 4 | 5 | This is a simple ruby-wrapper around [xhyve](https://github.com/mist64/xhyve), allowing you to start hypervisor Guests on OS-X 6 | 7 | # Usage 8 | 9 | You can run a guest fairly easily: 10 | 11 | ``` 12 | require 'xhyve' 13 | 14 | guest = Xhyve::Guest.new( 15 | kernel: 'guest/vmlinuz', # path to vmlinuz 16 | initrd: 'guest/initrd', # path to initrd 17 | cmdline: 'console=tty0', # boot flags to linux 18 | blockdevs: 'loop.img', # path to img files to use as block devs 19 | uuid: 'a-valid-uuid', # a valid UUID 20 | serial: 'com2', # com1 / com2 (maps to ttyS0, ttyS1, etc) 21 | memory: '200M', # amount of memory in M/G 22 | processors: 1, # number of processors 23 | networking: true, # Enable networking? (requires sudo) 24 | acpi: true, # set up acpi? (required for clean shutdown) 25 | ) 26 | 27 | pid = guest.start # starting the guest spawns an xhyve subprocess, returning the pid 28 | guest.running? # is the guest running? 29 | guest.ip # get the IP of the guest 30 | guest.mac # get MAC address of the guest 31 | guest.stop # stop the guest 32 | guest.destroy # forcefully stop the guest 33 | ``` 34 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/extensiontask' 2 | require 'rspec/core/rake_task' 3 | require 'fileutils' 4 | 5 | XHYVE_TMP = 'tmp/xhyve' 6 | 7 | # Compile native extensions task 8 | Rake::ExtensionTask.new 'vmnet' do |ext| 9 | ext.lib_dir = 'lib/xhyve/vmnet' 10 | end 11 | 12 | # Spec test 13 | RSpec::Core::RakeTask.new(:spec) 14 | 15 | desc 'Build xhyve binary' 16 | task :vendor do 17 | Dir.chdir('tmp') do 18 | unless Dir.exist?('xhyve/.git') 19 | system('git clone https://github.com/mist64/xhyve.git') || fail('Could not clone xhyve') 20 | end 21 | Dir.chdir('xhyve') do 22 | system('git fetch') || fail('Could not fetch') 23 | system('git reset --hard origin/master') || fail('Could not reset head') 24 | system('make') || fail('Make failed') 25 | end 26 | end 27 | FileUtils.mkdir_p('lib/xhyve/vendor') 28 | FileUtils.cp('tmp/xhyve/build/xhyve', 'lib/xhyve/vendor') 29 | end 30 | 31 | desc 'Build the ruby gem' 32 | task :build do 33 | system('gem build xhyve-ruby.gemspec') || fail('Failed to build gem') 34 | end 35 | 36 | desc 'Install gem' 37 | task install: :build do 38 | system('gem install xhyve-ruby*.gem') || fail('Couldn not install gem') 39 | end 40 | 41 | # Deps and defaults 42 | task default: :spec 43 | -------------------------------------------------------------------------------- /example/test.rb: -------------------------------------------------------------------------------- 1 | require 'xhyve' 2 | 3 | guest = Xhyve::Guest.new( 4 | kernel: 'spec/fixtures/guest/vmlinuz', # path to vmlinuz 5 | initrd: 'spec/fixtures/guest/initrd', # path to initrd 6 | cmdline: 'earlyprintk=true console=ttyS0', # boot flags to linux 7 | serial: 'com1', # com1 / com2 (maps to ttyS0, ttyS1, etc) 8 | memory: '200M', # amount of memory in M/G 9 | processors: 1, # number of processors 10 | networking: true, # use sudo? (required for network unless signed) 11 | acpi: true, # set up acpi? (required for clean shutdown) 12 | ) 13 | 14 | pid = guest.start # starting the guest spawns an xhyve subprocess, returning the pid 15 | puts pid 16 | puts guest.mac # get MAC address of the guest 17 | puts guest.ip # get the IP of the guest 18 | -------------------------------------------------------------------------------- /lib/rubygems_plugin.rb: -------------------------------------------------------------------------------- 1 | Gem.post_install do 2 | if Gem::Platform.local.os =~ /darwin/ 3 | # Required until https://github.com/mist64/xhyve/issues/60 is resolved 4 | bin = File.expand_path('../xhyve/vendor/xhyve', __FILE__) 5 | `/usr/bin/osascript -e 'do shell script "chown root #{bin} && chmod +s #{bin}" with administrator privileges'` 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/xhyve.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.expand_path('../lib', __FILE__)) 2 | 3 | require 'xhyve/version' 4 | require 'xhyve/guest' 5 | -------------------------------------------------------------------------------- /lib/xhyve/dhcp.rb: -------------------------------------------------------------------------------- 1 | module Xhyve 2 | # Parse DHCP leases file for a MAC address, and get its ip. 3 | module DHCP 4 | extend self 5 | LEASES_FILE = '/var/db/dhcpd_leases' 6 | WAIT_TIME = 1 7 | MAX_ATTEMPTS = 60 8 | 9 | def get_ip_for_mac(mac) 10 | max = ENV.key?('MAX_IP_WAIT') ? ENV['MAX_IP_WAIT'].to_i : nil 11 | ip = wait_for(max: max) do 12 | ip = parse_lease_file_for_mac(mac) 13 | end 14 | end 15 | 16 | def parse_lease_file_for_mac(mac) 17 | lease_file = (ENV['LEASES_FILE'] || LEASES_FILE) 18 | contents = wait_for do 19 | File.read(lease_file) if File.exists?(lease_file) 20 | end 21 | pattern = contents.match(/ip_address=(\S+)\n\thw_address=\d+,#{mac}/) 22 | if pattern 23 | addrs = pattern.captures 24 | addrs.first if addrs 25 | end 26 | end 27 | 28 | private 29 | 30 | def wait_for(max: nil) 31 | attempts = 0 32 | max ||= MAX_ATTEMPTS 33 | while attempts < max 34 | attempts += 1 35 | result = yield 36 | return result if result 37 | sleep(WAIT_TIME) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/xhyve/guest.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | require 'io/console' 3 | 4 | require 'xhyve/dhcp' 5 | 6 | module Xhyve 7 | BINARY_PATH = File.expand_path('../../../lib/xhyve/vendor/xhyve', __FILE__).freeze 8 | 9 | # An object to represent a guest that we can start and stop 10 | # Effectively, it's a command wrapper around xhyve to provide an 11 | # object oriented interface to a hypervisor guest 12 | class Guest 13 | PCI_BASE = 3 14 | NULLDEV = '/dev/null' 15 | 16 | attr_reader :pid, :uuid, :mac 17 | 18 | def initialize(**opts) 19 | @kernel = opts.fetch(:kernel) 20 | @initrd = opts.fetch(:initrd) 21 | @cmdline = opts.fetch(:cmdline) 22 | @blockdevs = [opts[:blockdevs] || []].flatten 23 | @memory = opts[:memory] || '500M' 24 | @processors = opts[:processors] || '1' 25 | @uuid = opts[:uuid] || SecureRandom.uuid 26 | @serial = opts[:serial] || 'com1' 27 | @acpi = opts.fetch(:acpi, true) 28 | @networking = opts.fetch(:networking, true) 29 | @foreground = opts[:foreground] || false 30 | @binary = opts[:binary] || BINARY_PATH 31 | @command = build_command 32 | @mac = find_mac 33 | end 34 | 35 | def start 36 | outfile, infile = redirection 37 | @pid = spawn(@command, [:out, :err] => outfile, in: infile) 38 | if @foreground 39 | Process.wait(@pid) 40 | outfile.cooked! 41 | infile.cooked! 42 | end 43 | @pid 44 | end 45 | 46 | def stop 47 | Process.kill('KILL', @pid) 48 | end 49 | 50 | def running? 51 | (true if Process.kill(0, @pid) rescue false) 52 | end 53 | 54 | def ip 55 | @ip ||= Xhyve::DHCP.get_ip_for_mac(@mac) 56 | end 57 | 58 | private 59 | 60 | def redirection 61 | if @foreground 62 | [$stdout.raw!, $stdin.raw! ] 63 | else 64 | [NULLDEV, NULLDEV] 65 | end 66 | end 67 | 68 | def find_mac 69 | `#{@command} -M`.strip.gsub(/MAC:\s+/,'') 70 | end 71 | 72 | def build_command 73 | [ 74 | "#{@binary}", 75 | "#{'-A' if @acpi}", 76 | '-U', @uuid, 77 | '-m', @memory, 78 | '-c', @processors, 79 | '-s', '0:0,hostbridge', 80 | "#{"-s #{PCI_BASE - 1}:0,virtio-net" if @networking }" , 81 | "#{"#{@blockdevs.each_with_index.map { |p, i| "-s #{PCI_BASE + i},virtio-blk,#{p}" }.join(' ')}" unless @blockdevs.empty? }", 82 | '-s', '31,lpc', 83 | '-l', "#{@serial},stdio", 84 | '-f' "kexec,#{@kernel},#{@initrd},'#{@cmdline}'" 85 | ].join(' ') 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /lib/xhyve/vendor/xhyve: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalehamel/xhyve-ruby/eebaee1cc91e8812cf74b2a46e76f1dbaf3208d9/lib/xhyve/vendor/xhyve -------------------------------------------------------------------------------- /lib/xhyve/version.rb: -------------------------------------------------------------------------------- 1 | # Common Xhyve functionality nampesace 2 | module Xhyve 3 | VERSION = '0.0.6' 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/dhcpd_leases.txt: -------------------------------------------------------------------------------- 1 | { 2 | name=box 3 | ip_address=192.168.64.34 4 | hw_address=1,9a:65:1b:12:cf:32 5 | identifier=1,9a:65:1b:12:cf:32 6 | lease=0x56551009 7 | } 8 | { 9 | name=localhost 10 | ip_address=192.168.64.5 11 | hw_address=1,a6:84:b2:34:cf:32 12 | identifier=1,a6:84:b2:34:cf:32 13 | lease=0x5653f52c 14 | } 15 | { 16 | name=localhost 17 | ip_address=192.168.64.4 18 | hw_address=1,ea:28:a:33:cf:32 19 | identifier=1,ea:28:a:33:cf:32 20 | lease=0x5653f4eb 21 | } 22 | { 23 | name=localhost 24 | ip_address=192.168.64.3 25 | hw_address=1,e2:ff:e:70:cf:32 26 | identifier=1,e2:ff:e:70:cf:32 27 | lease=0x5653f496 28 | } 29 | { 30 | name=localhost 31 | ip_address=192.168.64.2 32 | hw_address=1,5a:90:52:13:cf:32 33 | identifier=1,5a:90:52:13:cf:32 34 | lease=0x5653f3c5 35 | } 36 | -------------------------------------------------------------------------------- /spec/fixtures/guest/README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | We use tinycore linux, with a small persistence volume that just has openssh. 4 | 5 | To regenerate this persistence volume (on osx): 6 | 7 | ``` 8 | # Create a sparse filesystem 9 | 10 | dd if=/dev/zero of=loop.img bs=1 count=0 seek=10m 11 | 12 | # Mount the image in xhyve, (without opt=vda1) then create a partition table and mkfs.ext4 it. 13 | # It will likely be /dev/vda 14 | # Then reboot with opt=vda 15 | # confirm it's mounted over /opt, then make your changes as per 16 | # http://myblog-kenton.blogspot.ca/2012/03/install-openssh-server-on-tiny-core.html 17 | 18 | tce-load -iw openssh.tcz 19 | sudo cp /usr/local/etc/ssh/sshd_config_example /usr/local/etc/ssh/sshd_config 20 | cat >> /opt/.filetool.lst <> /opt/bootlocal.sh 27 | 28 | sudo /usr/local/etc/init.d/openssh start 29 | sudo filetool.sh -b 30 | 31 | ``` 32 | 33 | When booting set user=console boot flag, and it will create the console user with password defaulting to 'tcuser' 34 | -------------------------------------------------------------------------------- /spec/fixtures/guest/initrd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalehamel/xhyve-ruby/eebaee1cc91e8812cf74b2a46e76f1dbaf3208d9/spec/fixtures/guest/initrd -------------------------------------------------------------------------------- /spec/fixtures/guest/loop.img: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalehamel/xhyve-ruby/eebaee1cc91e8812cf74b2a46e76f1dbaf3208d9/spec/fixtures/guest/loop.img -------------------------------------------------------------------------------- /spec/fixtures/guest/vmlinuz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalehamel/xhyve-ruby/eebaee1cc91e8812cf74b2a46e76f1dbaf3208d9/spec/fixtures/guest/vmlinuz -------------------------------------------------------------------------------- /spec/lib/dhcp_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper.rb', __FILE__) 2 | 3 | RSpec.describe Xhyve::DHCP do 4 | let(:leases) do 5 | { 6 | '9a:65:1b:12:cf:32' => '192.168.64.34', 7 | 'a6:84:b2:34:cf:32' => '192.168.64.5', 8 | 'ea:28:a:33:cf:32' => '192.168.64.4', 9 | 'e2:ff:e:70:cf:32' => '192.168.64.3', 10 | '5a:90:52:13:cf:32' => '192.168.64.2' 11 | } 12 | end 13 | 14 | before :all do 15 | ENV['LEASES_FILE'] = File.join(FIXTURE_PATH, 'dhcpd_leases.txt') 16 | ENV['MAX_IP_WAIT'] = '1' 17 | end 18 | 19 | after :all do 20 | ENV.delete('LEASES_FILE') 21 | ENV.delete('MAX_IP_WAIT') 22 | end 23 | 24 | it 'parses the leases file to get an IP from a MAC' do 25 | leases.each do |mac, ip| 26 | expect(Xhyve::DHCP.get_ip_for_mac(mac)).to_not be_nil 27 | expect(Xhyve::DHCP.get_ip_for_mac(mac)).to eq(ip) 28 | end 29 | end 30 | 31 | it 'returns nil if no lease is found' do 32 | expect(Xhyve::DHCP.get_ip_for_mac('fakemac')).to be_nil 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/lib/guest_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper.rb', __FILE__) 2 | 3 | RSpec.describe Xhyve::Guest do 4 | before :all do 5 | kernel = File.join(FIXTURE_PATH, 'guest', 'vmlinuz') 6 | initrd = File.join(FIXTURE_PATH, 'guest', 'initrd') 7 | blockdev = File.join(FIXTURE_PATH, 'guest', 'loop.img') 8 | cmdline = 'earlyprintk=true console=ttyS0 user=console opt=vda tce=vda' 9 | uuid = SecureRandom.uuid # '32e54269-d1e2-4bdf-b4ff-bbe0eb42572d' # 10 | 11 | @guest = Xhyve::Guest.new(kernel: kernel, initrd: initrd, cmdline: cmdline, blockdevs: blockdev, uuid: uuid, serial: 'com1') 12 | @guest.start 13 | end 14 | 15 | after :all do 16 | @guest.stop 17 | end 18 | 19 | it 'Can start a guest' do 20 | expect(@guest.pid).to_not be_nil 21 | expect(@guest.pid).to be > 0 22 | expect(@guest.running?).to eq(true) 23 | end 24 | 25 | it 'Can get the MAC of a guest' do 26 | expect(@guest.mac).to_not be_nil 27 | expect(@guest.mac).to_not be_empty 28 | expect(@guest.mac).to match(/\w\w:\w\w:\w\w:\w\w:\w\w:\w\w/) 29 | end 30 | 31 | it 'Can get the IP of a guest' do 32 | expect(@guest.ip).to_not be_nil 33 | expect(@guest.ip).to match(/\d+\.+\d+\.\d+\.\d+/) 34 | end 35 | 36 | it 'Can ping the guest' do 37 | expect(ping(@guest.ip)).to eq(true) 38 | end 39 | 40 | it 'Can ssh to the guest' do 41 | expect(on_guest(@guest.ip, 'hostname')).to eq('box') 42 | end 43 | 44 | it 'Correctly sets processors' do 45 | expect(on_guest(@guest.ip, "cat /proc/cpuinfo | grep 'cpu cores' | awk '{print $4}'")).to eq('1') 46 | end 47 | 48 | it 'Correctly sets memory' do 49 | expect(on_guest(@guest.ip, "free -mm | grep 'Mem:' | awk '{print $2}'").to_i).to be_within(50).of(500) 50 | end 51 | end unless ENV['TRAVIS'] 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'securerandom' 5 | require 'net/ssh' 6 | require 'net/ping' 7 | require File.expand_path('../../lib/xhyve.rb', __FILE__) 8 | 9 | FIXTURE_PATH = File.expand_path('../../spec/fixtures', __FILE__) 10 | 11 | # def self.append_features(mod) 12 | # mod.class_eval %[ 13 | # around(:each) do |example| 14 | # example.run 15 | # end 16 | # ] 17 | # end 18 | # end 19 | 20 | def ping(ip) 21 | attempts = 0 22 | max_attempts = 60 23 | sleep_time = 1 24 | 25 | while attempts < max_attempts 26 | attempts += 1 27 | sleep(sleep_time) 28 | begin 29 | return true if Net::Ping::ICMP.new(ip).ping 30 | rescue 31 | end 32 | end 33 | end 34 | 35 | def on_guest(ip, command) 36 | output = '' 37 | Net::SSH.start(ip, 'console', password: 'tcuser') do |ssh| 38 | output = ssh.exec!(command) 39 | end 40 | output.strip 41 | end 42 | 43 | RSpec.configure do |config| 44 | config.order = :defined 45 | config.expect_with :rspec do |expectations| 46 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 47 | end 48 | 49 | config.mock_with :rspec do |mocks| 50 | mocks.verify_partial_doubles = true 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /xhyve-ruby.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'xhyve/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'xhyve-ruby' 7 | s.version = Xhyve::VERSION 8 | s.date = '2015-11-23' 9 | s.summary = 'Ruby wrapper for xhyve' 10 | s.description = 'Provides a means of interacting with xhyve from ruby' 11 | s.authors = ['Dale Hamel'] 12 | s.email = 'dale.hamel@srvthe.net' 13 | s.files = Dir['lib/**/*'] 14 | s.homepage = 15 | 'https://github.com/dalehamel/xhyve-ruby' 16 | s.license = 'MIT' 17 | s.add_development_dependency 'simplecov', ['=0.10.0'] 18 | s.add_development_dependency 'rspec', ['=3.2.0'] 19 | s.add_development_dependency 'net-ssh', ['=3.0.1'] 20 | s.add_development_dependency 'net-ping', ['=1.7.8'] 21 | s.add_development_dependency 'rake', ['=10.4.2'] 22 | s.add_development_dependency 'rake-compiler', ['=0.9.5'] 23 | end 24 | --------------------------------------------------------------------------------