diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index 1d3a66799..910f64098 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -432,6 +432,10 @@ module Vagrant error_key(:host_explicit_not_detected) end + class ISOBuildFailed < VagrantError + error_key(:iso_build_failed) + end + class LinuxMountFailed < VagrantError error_key(:linux_mount_failed) end diff --git a/lib/vagrant/util/directory.rb b/lib/vagrant/util/directory.rb new file mode 100644 index 000000000..1e5a8cb7e --- /dev/null +++ b/lib/vagrant/util/directory.rb @@ -0,0 +1,19 @@ +require 'pathname' + +module Vagrant + module Util + class Directory + # Check if directory has any new updates + # + # @param [Pathname, String] Path to directory + # @param [Time] time to compare to eg. has any file in dir_path + # changed since this time + # @return [Boolean] + def self.directory_changed?(dir_path, threshold_time) + Dir.glob(Pathname.new(dir_path).join("**", "*")).any? do |path| + Pathname.new(path).mtime > threshold_time + end + end + end + end +end diff --git a/plugins/hosts/darwin/cap/fs_iso.rb b/plugins/hosts/darwin/cap/fs_iso.rb new file mode 100644 index 000000000..e5e5bd4fd --- /dev/null +++ b/plugins/hosts/darwin/cap/fs_iso.rb @@ -0,0 +1,78 @@ +require "tempfile" +require 'fileutils' +require 'pathname' +require "vagrant/util/subprocess" +require "vagrant/util/map_command_options" +require "vagrant/util/directory" + +module VagrantPlugins + module HostDarwin + module Cap + class FsISO + @@logger = Log4r::Logger.new("vagrant::host::darwin::fs_iso") + + BUILD_ISO_CMD = "hdiutil".freeze + + # Check that the host has the ability to generate ISOs + # + # @param [Vagrant::Environment] env + # @return [Boolean] + def self.isofs_available(env) + !!Vagrant::Util::Which.which(BUILD_ISO_CMD) + end + + # Generate an ISO file of the given source directory + # + # @param [Vagrant::Environment] env + # @param [String] source_directory Contents of ISO + # @param [Map] extra arguments to pass to the iso building command + # :file_destination (string) location to store ISO + # :volume_id (String) to set the volume name + # @return [Pathname] ISO location + # @note If file_destination exists, source_directory will be checked + # for recent modifications and a new ISO will be generated if requried. + def self.create_iso(env, source_directory, **extra_opts) + file_destination = extra_opts[:file_destination] + source_directory = Pathname.new(source_directory) + if file_destination.nil? + @@logger.info("No file destination specified, creating temp location") + tmpfile = Tempfile.new(["vagrant", ".iso"]) + file_destination = Pathname.new(tmpfile.path) + tmpfile.delete + else + file_destination = Pathname.new(file_destination.to_s) + # If the file destination path is a folder, target the output to a randomly named + # file in that dir + if file_destination.extname != ".iso" + file_destination = file_destination.join("#{SecureRandom.hex(3)}_vagrant.iso") + end + end + # Ensure destination directory is available + FileUtils.mkdir_p(file_destination.dirname) + + # If the destrination does not exist or there have been changes in the source directory since the last build, then build + if !file_destination.exist? || Vagrant::Util::Directory.directory_changed?(source_directory, file_destination.mtime) + @@logger.info("Building ISO from source #{source_directory}") + iso_command = [BUILD_ISO_CMD, "makehybrid"] + iso_command << "-hfs" + iso_command << "-iso" + iso_command << "-joliet" + iso_command << "-ov" + iso_command.concat(["-default-volume-name", extra_opts[:volume_id]]) if extra_opts[:volume_id] + iso_command << "-o" + iso_command << file_destination.to_s + iso_command << source_directory.to_s + result = Vagrant::Util::Subprocess.execute(*iso_command) + + if result.exit_code != 0 + raise Vagrant::Errors::ISOBuildFailed, cmd: iso_command.join(" "), stdout: result.stdout, stderr: result.stderr + end + end + + @@logger.info("ISO available at #{file_destination}") + file_destination + end + end + end + end +end diff --git a/plugins/hosts/darwin/plugin.rb b/plugins/hosts/darwin/plugin.rb index d27e2eda4..f3a7e6a55 100644 --- a/plugins/hosts/darwin/plugin.rb +++ b/plugins/hosts/darwin/plugin.rb @@ -11,6 +11,16 @@ module VagrantPlugins Host end + host_capability("darwin", "isofs_available") do + require_relative "cap/fs_iso" + Cap::FsISO + end + + host_capability("darwin", "create_iso") do + require_relative "cap/fs_iso" + Cap::FsISO + end + host_capability("darwin", "provider_install_virtualbox") do require_relative "cap/provider_install_virtualbox" Cap::ProviderInstallVirtualBox diff --git a/templates/locales/en.yml b/templates/locales/en.yml index a73840b90..7ce0f57e3 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -942,6 +942,18 @@ en: ("%{value}") could not be found. Please verify that the plugin is installed which implements this host and that the value you used for `config.vagrant.host` is correct. + iso_build_failed: |- + Failed to build iso image. The following command returned an error: + + %{cmd} + + Stdout from the command: + + %{stdout} + + Stderr from the command: + + %{stderr} hyperv_virtualbox_error: |- Hyper-V and VirtualBox cannot be used together and will result in a system crash. Vagrant will now exit. Please disable Hyper-V if you wish diff --git a/test/unit/plugins/hosts/darwin/cap/fs_iso_test.rb b/test/unit/plugins/hosts/darwin/cap/fs_iso_test.rb new file mode 100644 index 000000000..d44a7747c --- /dev/null +++ b/test/unit/plugins/hosts/darwin/cap/fs_iso_test.rb @@ -0,0 +1,82 @@ +require "pathname" +require_relative "../../../../base" +require_relative "../../../../../../plugins/hosts/darwin/cap/fs_iso" + +describe VagrantPlugins::HostDarwin::Cap::FsISO do + include_context "unit" + + let(:subject){ VagrantPlugins::HostDarwin::Cap::FsISO } + let(:env) { double("env") } + + describe ".isofs_available" do + it "finds iso building utility when available" do + expect(Vagrant::Util::Which).to receive(:which).and_return(true) + expect(subject.isofs_available(env)).to eq(true) + end + + it "does not find iso building utility when not available" do + expect(Vagrant::Util::Which).to receive(:which).and_return(false) + expect(subject.isofs_available(env)).to eq(false) + end + end + + describe ".create_iso" do + let(:file_destination) { "/woo/out.iso" } + + before do + allow(file_destination).to receive(:nil?).and_return(false) + end + + it "builds an iso" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with( + "hdiutil", "makehybrid", "-hfs", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ + ).and_return(double(exit_code: 0)) + expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) + + output = subject.create_iso(env, "/foo/src", file_destination: file_destination) + expect(output.to_s).to eq("/woo/out.iso") + end + + it "builds an iso with volume_id" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with( + "hdiutil", "makehybrid", "-hfs", "-iso", "-joliet", "-ov", "-default-volume-name", "cidata", "-o", /.iso/, /\/foo\/src/ + ).and_return(double(exit_code: 0)) + expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) + + output = subject.create_iso(env, "/foo/src", file_destination: file_destination, volume_id: "cidata") + expect(output.to_s).to eq("/woo/out.iso") + end + + it "builds an iso given a file destination without an extension" do + expect(Vagrant::Util::Subprocess).to receive(:execute).with( + "hdiutil", "makehybrid", "-hfs", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ + ).and_return(double(exit_code: 0)) + expect(FileUtils).to receive(:mkdir_p).with(Pathname.new("/woo/out_dir")) + + + output = subject.create_iso(env, "/foo/src", file_destination: "/woo/out_dir") + expect(output.to_s).to match(/\/woo\/out_dir\/[\w]{6}_vagrant.iso/) + end + + it "builds an iso when no file destination is given" do + allow(Tempfile).to receive(:new).and_return(file_destination) + allow(file_destination).to receive(:path).and_return(file_destination) + allow(file_destination).to receive(:delete) + expect(Vagrant::Util::Subprocess).to receive(:execute).with( + "hdiutil", "makehybrid", "-hfs", "-iso", "-joliet", "-ov", "-o", /.iso/, /\/foo\/src/ + ).and_return(double(exit_code: 0)) + # Should create a directory wherever Tempfile creates files by default + expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) + + output = subject.create_iso(env, "/foo/src") + expect(output.to_s).to eq(file_destination) + end + + it "raises an error if iso build failed" do + allow(Vagrant::Util::Subprocess).to receive(:execute).with(any_args).and_return(double(stdout: "nope", stderr: "nope", exit_code: 1)) + expect(FileUtils).to receive(:mkdir_p).with(Pathname.new(file_destination).dirname) + + expect{ subject.create_iso(env, "/foo/src", file_destination: file_destination) }.to raise_error(Vagrant::Errors::ISOBuildFailed) + end + end +end diff --git a/test/unit/vagrant/util/directory_test.rb b/test/unit/vagrant/util/directory_test.rb new file mode 100644 index 000000000..71bc4675c --- /dev/null +++ b/test/unit/vagrant/util/directory_test.rb @@ -0,0 +1,22 @@ +require File.expand_path("../../../base", __FILE__) +require "vagrant/util/directory" +require "time" + +describe Vagrant::Util::Directory do + include_context "unit" + + let(:subject){ Vagrant::Util::Directory } + + describe ".directory_changed?" do + + it "should return false if the threshold time is larger the all mtimes" do + t = Time.new("3008", "09", "09") + expect(subject.directory_changed?(Dir.getwd, t)).to eq(false) + end + + it "should return true if the threshold time is less than any mtimes" do + t = Time.new("1990", "06", "06") + expect(subject.directory_changed?(Dir.getwd, t)).to eq(true) + end + end +end