From 95938c652db44297754750249c5169a6ee6a7009 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 25 Apr 2010 01:46:51 -0700 Subject: [PATCH] New abstraction: Systems. Updated config and environment to properly load configured system. This allows the OS-specific tasks to be pulled out into separate classes, so that other systems could potentially be supported. For now, a "Linux" system has been created. --- config/default.rb | 1 + lib/vagrant/config.rb | 1 + lib/vagrant/environment.rb | 7 +-- lib/vagrant/systems/base.rb | 44 ++++++++++++++ lib/vagrant/systems/linux.rb | 46 ++++++++++++++ lib/vagrant/vm.rb | 36 +++++++++-- templates/errors.yml | 10 +++ test/test_helper.rb | 1 + test/vagrant/environment_test.rb | 5 +- test/vagrant/systems/linux_test.rb | 98 ++++++++++++++++++++++++++++++ test/vagrant/vm_test.rb | 59 +++++++++++++++++- 11 files changed, 292 insertions(+), 16 deletions(-) create mode 100644 lib/vagrant/systems/base.rb create mode 100644 lib/vagrant/systems/linux.rb create mode 100644 test/vagrant/systems/linux_test.rb diff --git a/config/default.rb b/config/default.rb index 9c9009943..cd5b74ccf 100644 --- a/config/default.rb +++ b/config/default.rb @@ -21,6 +21,7 @@ Vagrant::Config.run do |config| config.vm.shared_folder_uid = nil config.vm.shared_folder_gid = nil config.vm.boot_mode = "vrdp" + config.vm.system = :linux config.package.name = 'vagrant' config.package.extension = '.box' diff --git a/lib/vagrant/config.rb b/lib/vagrant/config.rb index 78e8bccfe..cdb3b91af 100644 --- a/lib/vagrant/config.rb +++ b/lib/vagrant/config.rb @@ -86,6 +86,7 @@ module Vagrant attr_accessor :provisioner attr_accessor :shared_folder_uid attr_accessor :shared_folder_gid + attr_accessor :system def initialize @forwarded_ports = {} diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index eff6c4c3a..8ccd281a2 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -174,8 +174,7 @@ module Vagrant return if !root_path || !File.file?(dotfile_path) File.open(dotfile_path) do |f| - @vm = Vagrant::VM.find(f.read) - @vm.env = self if @vm + @vm = Vagrant::VM.find(f.read, self) end rescue Errno::ENOENT @vm = nil @@ -206,9 +205,7 @@ module Vagrant # in {Command.up}. This will very likely be refactored at a later # time. def create_vm - @vm = VM.new - @vm.env = self - @vm + @vm = VM.new(self) end # Persists this environment's VM to the dotfile so it can be diff --git a/lib/vagrant/systems/base.rb b/lib/vagrant/systems/base.rb new file mode 100644 index 000000000..66d45df81 --- /dev/null +++ b/lib/vagrant/systems/base.rb @@ -0,0 +1,44 @@ +module Vagrant + module Systems + # The base class for a "system." A system represents an installed + # operating system on a given box. There are some portions of + # Vagrant which are fairly OS-specific (such as mounting shared + # folders) and while the number is few, this abstraction allows + # more obscure operating systems to be installed without having + # to directly modify Vagrant internals. + # + # Subclasses of the system base class are expected to implement + # all the methods. These methods are described in the comments + # above their definition. + # + # **This is by no means a complete specification. The methods + # required by systems can and will change at any time. Any + # changes will be noted on release notes.** + class Base + include Vagrant::Util + + # The VM which this system is tied to. + attr_reader :vm + + # Initializes the system. Any subclasses MUST make sure this + # method is called on the parent. Therefore, if a subclass overrides + # `initialize`, then you must call `super`. + def initialize(vm) + @vm = vm + end + + # Mounts a shared folder. This method is called by the shared + # folder action with an open SSH session (passed in as `ssh`). + # This method should create, mount, and properly set permissions + # on the shared folder. This method should also properly + # adhere to any configuration values such as `shared_folder_uid` + # on `config.vm`. + # + # @param [Object] ssh The Net::SSH session. + # @param [String] name The name of the shared folder. + # @param [String] guestpath The path on the machine which the user + # wants the folder mounted. + def mount_shared_folder(ssh, name, guestpath); end + end + end +end \ No newline at end of file diff --git a/lib/vagrant/systems/linux.rb b/lib/vagrant/systems/linux.rb new file mode 100644 index 000000000..778275591 --- /dev/null +++ b/lib/vagrant/systems/linux.rb @@ -0,0 +1,46 @@ +module Vagrant + module Systems + # A general Vagrant system implementation for "linux." In general, + # any linux-based OS will work fine with this system, although its + # not tested exhaustively. BSD or other based systems may work as + # well, but that hasn't been tested at all. + # + # At any rate, this system implementation should server as an + # example of how to implement any custom systems necessary. + class Linux < Base + #------------------------------------------------------------------- + # Overridden methods + #------------------------------------------------------------------- + def mount_shared_folder(ssh, name, guestpath) + ssh.exec!("sudo mkdir -p #{guestpath}") + mount_folder(ssh, name, guestpath) + ssh.exec!("sudo chown #{vm.env.config.ssh.username} #{guestpath}") + end + + #------------------------------------------------------------------- + # "Private" methods which assist above methods + #------------------------------------------------------------------- + def mount_folder(ssh, name, guestpath, sleeptime=5) + # Determine the permission string to attach to the mount command + perms = [] + perms << "uid=#{vm.env.config.vm.shared_folder_uid}" + perms << "gid=#{vm.env.config.vm.shared_folder_gid}" + perms = " -o #{perms.join(",")}" if !perms.empty? + + attempts = 0 + while true + result = ssh.exec!("sudo mount -t vboxsf#{perms} #{name} #{guestpath}") do |ch, type, data| + # net/ssh returns the value in ch[:result] (based on looking at source) + ch[:result] = !!(type == :stderr && data =~ /No such device/i) + end + + break unless result + + attempts += 1 + raise Actions::ActionException.new(:vm_mount_fail) if attempts >= 10 + sleep sleeptime + end + end + end + end +end \ No newline at end of file diff --git a/lib/vagrant/vm.rb b/lib/vagrant/vm.rb index 000698ee3..71b7a84cf 100644 --- a/lib/vagrant/vm.rb +++ b/lib/vagrant/vm.rb @@ -2,22 +2,48 @@ module Vagrant class VM < Actions::Runner include Vagrant::Util - attr_accessor :env + attr_reader :env + attr_reader :system attr_accessor :vm - attr_accessor :from class << self # Finds a virtual machine by a given UUID and either returns # a Vagrant::VM object or returns nil. - def find(uuid) + def find(uuid, env=nil) vm = VirtualBox::VM.find(uuid) return nil if vm.nil? - new(vm) + new(env, vm) end end - def initialize(vm=nil) + def initialize(env, vm=nil) + @env = env @vm = vm + + load_system! + end + + def load_system! + system = env.config.vm.system + + if system.is_a?(Class) + @system = system.new(self) + error_and_exit(:system_invalid_class, :system => system.to_s) unless @system.is_a?(Systems::Base) + elsif system.is_a?(Symbol) + # Hard-coded internal systems + mapping = { + :linux => Systems::Linux + } + + if !mapping.has_key?(system) + error_and_exit(:system_unknown_type, :system => system.to_s) + return # for tests + end + + @system = mapping[system].new(self) + else + error_and_exit(:system_unspecified) + end end def uuid diff --git a/templates/errors.yml b/templates/errors.yml index 3798de95f..71ff40111 100644 --- a/templates/errors.yml +++ b/templates/errors.yml @@ -103,7 +103,17 @@ For a more detailed guide please consult: http://vagrantup.com/docs/getting-started/windows.html +:system_invalid_class: |- + The specified system does not inherit from `Vagrant::Systems::Base`. The + specified system class must inherit from this class. + The specified system class was: <%= system %> +:system_unknown_type: |- + The specified system type is unknown: <%= system %>. Please change this + to a proper value. +:system_unspecified: |- + A VM system type must be specified! This is done via the `config.vm.system` + configuration value. Please read the documentation online for more information. :virtualbox_import_failure: |- The VM import failed! Try running `VBoxManage import` on the box file manually for more verbose error output. diff --git a/test/test_helper.rb b/test/test_helper.rb index a6e45ddb6..65de972e6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -41,6 +41,7 @@ class Test::Unit::TestCase config.vm.forward_port("ssh", 22, 2222) config.vm.shared_folder_uid = nil config.vm.shared_folder_gid = nil + config.vm.system = :linux config.package.name = 'vagrant' config.package.extension = '.box' diff --git a/test/vagrant/environment_test.rb b/test/vagrant/environment_test.rb index 5a1c18ec5..aa3c90216 100644 --- a/test/vagrant/environment_test.rb +++ b/test/vagrant/environment_test.rb @@ -356,11 +356,10 @@ class EnvironmentTest < Test::Unit::TestCase should "loading of the uuid from the dotfile" do vm = mock("vm") - vm.expects(:env=).with(@env) filemock = mock("filemock") filemock.expects(:read).returns("foo") - Vagrant::VM.expects(:find).with("foo").returns(vm) + Vagrant::VM.expects(:find).with("foo", @env).returns(vm) File.expects(:open).with(@env.dotfile_path).once.yields(filemock) File.expects(:file?).with(@env.dotfile_path).once.returns(true) @env.load_vm! @@ -371,7 +370,7 @@ class EnvironmentTest < Test::Unit::TestCase should "not set the environment if the VM is nil" do filemock = mock("filemock") filemock.expects(:read).returns("foo") - Vagrant::VM.expects(:find).with("foo").returns(nil) + Vagrant::VM.expects(:find).with("foo", @env).returns(nil) File.expects(:open).with(@env.dotfile_path).once.yields(filemock) File.expects(:file?).with(@env.dotfile_path).once.returns(true) diff --git a/test/vagrant/systems/linux_test.rb b/test/vagrant/systems/linux_test.rb new file mode 100644 index 000000000..017b74826 --- /dev/null +++ b/test/vagrant/systems/linux_test.rb @@ -0,0 +1,98 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'test_helper') + +class LinuxSystemTest < Test::Unit::TestCase + setup do + @klass = Vagrant::Systems::Linux + + @vm = mock("vm") + @vm.stubs(:env).returns(mock_environment) + @instance = @klass.new(@vm) + end + + context "mounting shared folders" do + setup do + @ssh = mock("ssh") + @name = "foo" + @guestpath = "/bar" + end + + should "create the dir, mount the folder, then set permissions" do + mount_seq = sequence("mount_seq") + @ssh.expects(:exec!).with("sudo mkdir -p #{@guestpath}").in_sequence(mount_seq) + @instance.expects(:mount_folder).with(@ssh, @name, @guestpath).in_sequence(mount_seq) + @ssh.expects(:exec!).with("sudo chown #{@vm.env.config.ssh.username} #{@guestpath}").in_sequence(mount_seq) + + @instance.mount_shared_folder(@ssh, @name, @guestpath) + end + end + + #------------------------------------------------------------------- + # "Private" methods tests + #------------------------------------------------------------------- + context "mounting the main folder" do + setup do + @ssh = mock("ssh") + @name = "foo" + @guestpath = "bar" + @sleeptime = 0 + @limit = 10 + + @success_return = false + end + + def mount_folder + @instance.mount_folder(@ssh, @name, @guestpath, @sleeptime) + end + + should "execute the proper mount command" do + @ssh.expects(:exec!).with("sudo mount -t vboxsf -o uid=#{@vm.env.config.ssh.username},gid=#{@vm.env.config.ssh.username} #{@name} #{@guestpath}").returns(@success_return) + mount_folder + end + + should "test type of text and text string to detect error" do + data = mock("data") + data.expects(:[]=).with(:result, !@success_return) + + @ssh.expects(:exec!).yields(data, :stderr, "No such device").returns(@success_return) + mount_folder + end + + should "test type of text and test string to detect success" do + data = mock("data") + data.expects(:[]=).with(:result, @success_return) + + @ssh.expects(:exec!).yields(data, :stdout, "Nothing such device").returns(@success_return) + mount_folder + end + + should "raise an ActionException if the command fails constantly" do + @ssh.expects(:exec!).times(@limit).returns(!@success_return) + + assert_raises(Vagrant::Actions::ActionException) { + mount_folder + } + end + + should "not raise any exception if the command succeeded" do + @ssh.expects(:exec!).once.returns(@success_return) + + assert_nothing_raised { + mount_folder + } + end + + should "add uid AND gid to mount" do + uid = "foo" + gid = "bar" + env = mock_environment do |config| + config.vm.shared_folder_uid = uid + config.vm.shared_folder_gid = gid + end + + @vm.stubs(:env).returns(env) + + @ssh.expects(:exec!).with("sudo mount -t vboxsf -o uid=#{uid},gid=#{gid} #{@name} #{@guestpath}").returns(@success_return) + mount_folder + end + end +end diff --git a/test/vagrant/vm_test.rb b/test/vagrant/vm_test.rb index e49066b00..4791318e0 100644 --- a/test/vagrant/vm_test.rb +++ b/test/vagrant/vm_test.rb @@ -14,7 +14,7 @@ class VMTest < Test::Unit::TestCase context "being an action runner" do should "be an action runner" do - vm = Vagrant::VM.new + vm = Vagrant::VM.new(@env) assert vm.is_a?(Vagrant::Actions::Runner) end end @@ -27,7 +27,7 @@ class VMTest < Test::Unit::TestCase should "return a Vagrant::VM object for that VM otherwise" do VirtualBox::VM.expects(:find).with("foo").returns("bar") - result = Vagrant::VM.find("foo") + result = Vagrant::VM.find("foo", mock_environment) assert result.is_a?(Vagrant::VM) assert_equal "bar", result.vm end @@ -35,10 +35,63 @@ class VMTest < Test::Unit::TestCase context "vagrant VM instance" do setup do - @vm = Vagrant::VM.new(@mock_vm) + @vm = Vagrant::VM.new(@env, @mock_vm) @mock_vm.stubs(:uuid).returns("foo") end + context "loading associated system" do + should "error and exit if system is not specified" do + @vm.env.config.vm.system = nil + + @vm.expects(:error_and_exit).with(:system_unspecified).once + @vm.load_system! + end + + context "with a class" do + class FakeSystemClass + def initialize(vm); end + end + + should "initialize class if given" do + @vm.env.config.vm.system = Vagrant::Systems::Linux + + @vm.expects(:error_and_exit).never + @vm.load_system! + + assert @vm.system.is_a?(Vagrant::Systems::Linux) + end + + should "error and exit if class has invalid parent" do + @vm.env.config.vm.system = FakeSystemClass + @vm.expects(:error_and_exit).with(:system_invalid_class, :system => @vm.env.config.vm.system.to_s).once + @vm.load_system! + end + end + + context "with a symbol" do + should "initialize proper symbols" do + valid = { + :linux => Vagrant::Systems::Linux + } + + valid.each do |symbol, klass| + @vm.env.config.vm.system = symbol + @vm.expects(:error_and_exit).never + @vm.load_system! + + assert @vm.system.is_a?(klass) + assert_equal @vm, @vm.system.vm + end + end + + should "error and exit with invalid symbol" do + @vm.env.config.vm.system = :shall_never_exist + @vm.expects(:error_and_exit).with(:system_unknown_type, :system => @vm.env.config.vm.system.to_s).once + @vm.load_system! + end + end + end + context "uuid" do should "call UUID on VM object" do uuid = mock("uuid")