From b96acce79f2f6fc04d50cc92d5bfcb3de1b28e33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 Jul 2010 20:47:53 -0700 Subject: [PATCH] Packaging middleware and modifying VM#package to use it --- lib/vagrant/action/builtin.rb | 8 + lib/vagrant/action/vm/package.rb | 92 ++++++++++ lib/vagrant/actions/vm/package.rb | 2 +- lib/vagrant/vm.rb | 4 +- test/vagrant/action/vm/package_test.rb | 227 +++++++++++++++++++++++++ test/vagrant/vm_test.rb | 9 +- 6 files changed, 332 insertions(+), 10 deletions(-) create mode 100644 lib/vagrant/action/vm/package.rb create mode 100644 test/vagrant/action/vm/package_test.rb diff --git a/lib/vagrant/action/builtin.rb b/lib/vagrant/action/builtin.rb index 48204e3fa..282a56e16 100644 --- a/lib/vagrant/action/builtin.rb +++ b/lib/vagrant/action/builtin.rb @@ -74,6 +74,14 @@ module Vagrant end register :destroy, destroy + + # package - Export and package the VM + package = Builder.new do + use VM::Export + use VM::Package + end + + register :package, package end end end diff --git a/lib/vagrant/action/vm/package.rb b/lib/vagrant/action/vm/package.rb new file mode 100644 index 000000000..3647890f0 --- /dev/null +++ b/lib/vagrant/action/vm/package.rb @@ -0,0 +1,92 @@ +module Vagrant + class Action + module VM + class Package + include Util + + def initialize(app, env) + @app = app + @env = env + @env["package.output"] ||= "package" + @env["package.include"] ||= [] + + env.error!(:box_file_exists, :output_file => tar_path) if File.exist?(tar_path) + end + + def call(env) + @env = env + + return env.error!(:package_requires_export) if !@env["export.temp_dir"] + return if !verify_included_files + compress + + @app.call(env) + end + + def verify_included_files + @env["package.include"].each do |file| + if !File.exist?(file) + @env.error!(:package_include_file_doesnt_exist, :filename => file) + return false + end + end + + true + end + + # This method copies the include files (passed in via command line) + # to the temporary directory so they are included in a sub-folder within + # the actual box + def copy_include_files + if @env["package.include"].length > 0 + include_dir = File.join(@env["export.temp_dir"], "include") + FileUtils.mkdir_p(include_dir) + + @env["package.include"].each do |f| + @env.logger.info "Packaging additional file: #{f}" + FileUtils.cp(f, include_dir) + end + end + end + + # This method creates the auto-generated Vagrantfile at the root of the + # box. This Vagrantfile contains the MAC address so that the user doesn't + # have to worry about it. + def create_vagrantfile + File.open(File.join(@env["export.temp_dir"], "Vagrantfile"), "w") do |f| + f.write(TemplateRenderer.render("package_Vagrantfile", { + :base_mac => @env["vm"].vm.network_adapters.first.mac_address + })) + end + end + + # Compress the exported file into a package + def compress + @env.logger.info "Packaging VM into #{tar_path}..." + File.open(tar_path, Platform.tar_file_options) do |tar| + Archive::Tar::Minitar::Output.open(tar) do |output| + begin + current_dir = FileUtils.pwd + + copy_include_files + create_vagrantfile + + FileUtils.cd(@env["export.temp_dir"]) + Dir.glob(File.join(".", "**", "*")).each do |entry| + Archive::Tar::Minitar.pack_file(entry, output) + end + ensure + FileUtils.cd(current_dir) + end + end + end + end + + # Path to the final box output file + def tar_path + File.join(FileUtils.pwd, "#{@env["package.output"]}#{@env.env.config.package.extension}") + end + end + end + end +end diff --git a/lib/vagrant/actions/vm/package.rb b/lib/vagrant/actions/vm/package.rb index f4f47de2d..3146466d7 100644 --- a/lib/vagrant/actions/vm/package.rb +++ b/lib/vagrant/actions/vm/package.rb @@ -19,7 +19,7 @@ module Vagrant # Get the export action and store a reference to it @export_action = @runner.find_action(Export) - raise ActionException.new(:packaged_requires_export) unless @export_action + raise ActionException.new(:package_requires_export) unless @export_action end def execute! diff --git a/lib/vagrant/vm.rb b/lib/vagrant/vm.rb index 7f14213d0..d24f60659 100644 --- a/lib/vagrant/vm.rb +++ b/lib/vagrant/vm.rb @@ -94,9 +94,7 @@ module Vagrant end def package(options=nil) - add_action(Actions::VM::Export, options) - add_action(Actions::VM::Package, options) - execute! + env.actions.run(:package, options) end def up(options=nil) diff --git a/test/vagrant/action/vm/package_test.rb b/test/vagrant/action/vm/package_test.rb new file mode 100644 index 000000000..3e7fc7666 --- /dev/null +++ b/test/vagrant/action/vm/package_test.rb @@ -0,0 +1,227 @@ +require File.join(File.dirname(__FILE__), '..', '..', '..', 'test_helper') + +class PackageVMActionTest < Test::Unit::TestCase + setup do + @klass = Vagrant::Action::VM::Package + @app, @env = mock_action_data + + @vm = mock("vm") + @env["vm"] = @vm + + @internal_vm = mock("internal") + @vm.stubs(:vm).returns(@internal_vm) + end + + context "initializing" do + setup do + @tar_path = "foo" + File.stubs(:exist?).returns(false) + @klass.any_instance.stubs(:tar_path).returns(@tar_path) + end + + should "initialize fine" do + @klass.new(@app, @env) + assert !@env.error? + end + + should "error the environment if the output file exists" do + File.stubs(:exist?).with(@tar_path).returns(true) + @klass.new(@app, @env) + assert @env.error? + assert_equal :box_file_exists, @env.error.first + end + + should "set the output path to 'package' by default" do + @klass.new(@app, @env) + assert_equal "package", @env["package.output"] + end + + should "not set the output path if it is already set" do + @env["package.output"] = "foo" + @klass.new(@app, @env) + assert_equal "foo", @env["package.output"] + end + + should "set the included files to empty by default" do + @klass.new(@app, @env) + assert_equal [], @env["package.include"] + end + + should "not set the output path if it is already set" do + @env["package.include"] = "foo" + @klass.new(@app, @env) + assert_equal "foo", @env["package.include"] + end + end + + context "with an instance" do + setup do + File.stubs(:exist?).returns(false) + @instance = @klass.new(@app, @env) + + @env["export.temp_dir"] = "foo" + end + + context "calling" do + should "call the proper methods then continue chain" do + seq = sequence("seq") + @instance.expects(:verify_included_files).in_sequence(seq).returns(true) + @instance.expects(:compress).in_sequence(seq) + @app.expects(:call).with(@env).in_sequence(seq) + + @instance.call(@env) + end + + should "halt the chain if verify failed" do + @instance.expects(:verify_included_files).returns(false) + @instance.expects(:compress).never + @app.expects(:call).never + + @instance.call(@env) + end + + should "halt the chain if export didn't run" do + @env["export.temp_dir"] = nil + @app.expects(:call).never + @instance.call(@env) + + assert @env.error? + assert_equal :package_requires_export, @env.error.first + end + end + + context "verifying included files" do + setup do + @env["package.include"] = ["foo"] + File.stubs(:exist?).returns(true) + end + + should "error if included file is not found" do + File.expects(:exist?).with("foo").returns(false) + assert !@instance.verify_included_files + assert @env.error? + assert_equal :package_include_file_doesnt_exist, @env.error.first + end + + should "return true if all exist" do + assert @instance.verify_included_files + assert !@env.error? + end + end + + context "copying include files" do + setup do + @env["package.include"] = [] + end + + should "do nothing if no include files are specified" do + assert @env["package.include"].empty? + FileUtils.expects(:mkdir_p).never + FileUtils.expects(:cp).never + @instance.copy_include_files + end + + should "create the include directory and copy files to it" do + include_dir = File.join(@env["export.temp_dir"], "include") + copy_seq = sequence("copy_seq") + FileUtils.expects(:mkdir_p).with(include_dir).once.in_sequence(copy_seq) + + 5.times do |i| + file = mock("f#{i}") + @env["package.include"] << file + FileUtils.expects(:cp).with(file, include_dir).in_sequence(copy_seq) + end + + @instance.copy_include_files + end + end + + context "creating vagrantfile" do + setup do + @network_adapter = mock("nic") + @network_adapter.stubs(:mac_address).returns("mac_address") + @internal_vm.stubs(:network_adapters).returns([@network_adapter]) + end + + should "write the rendered vagrantfile to temp_path Vagrantfile" do + f = mock("file") + rendered = mock("rendered") + File.expects(:open).with(File.join(@env["export.temp_dir"], "Vagrantfile"), "w").yields(f) + Vagrant::Util::TemplateRenderer.expects(:render).returns(rendered).with("package_Vagrantfile", { + :base_mac => @internal_vm.network_adapters.first.mac_address + }) + f.expects(:write).with(rendered) + + @instance.create_vagrantfile + end + end + + context "compression" do + setup do + @env["package.include"] = [] + + @tar_path = "foo" + @instance.stubs(:tar_path).returns(@tar_path) + + @pwd = "bar" + FileUtils.stubs(:pwd).returns(@pwd) + FileUtils.stubs(:cd) + + @file = mock("file") + File.stubs(:open).yields(@file) + + @output = mock("output") + @tar = Archive::Tar::Minitar + Archive::Tar::Minitar::Output.stubs(:open).yields(@output) + @tar.stubs(:pack_file) + + @instance.stubs(:copy_include_files) + @instance.stubs(:create_vagrantfile) + end + + should "open the tar file with the tar path properly" do + File.expects(:open).with(@tar_path, Vagrant::Util::Platform.tar_file_options).once + @instance.compress + end + + should "open tar file" do + Archive::Tar::Minitar::Output.expects(:open).with(@file).once + @instance.compress + end + + #---------------------------------------------------------------- + # Methods below this comment test the block yielded by Minitar open + #---------------------------------------------------------------- + should "cd to the directory and append the directory" do + @files = [] + compress_seq = sequence("compress_seq") + + FileUtils.expects(:pwd).once.returns(@pwd).in_sequence(compress_seq) + @instance.expects(:copy_include_files).once.in_sequence(compress_seq) + @instance.expects(:create_vagrantfile).once.in_sequence(compress_seq) + FileUtils.expects(:cd).with(@env["export.temp_dir"]).in_sequence(compress_seq) + Dir.expects(:glob).returns(@files).in_sequence(compress_seq) + + 5.times do |i| + file = mock("file#{i}") + @tar.expects(:pack_file).with(file, @output).once.in_sequence(compress_seq) + @files << file + end + + FileUtils.expects(:cd).with(@pwd).in_sequence(compress_seq) + @instance.compress + end + + should "pop back to the current directory even if an exception is raised" do + cd_seq = sequence("cd_seq") + FileUtils.expects(:cd).with(@env["export.temp_dir"]).raises(Exception).in_sequence(cd_seq) + FileUtils.expects(:cd).with(@pwd).in_sequence(cd_seq) + + assert_raises(Exception) { + @instance.compress + } + end + end + + end +end diff --git a/test/vagrant/vm_test.rb b/test/vagrant/vm_test.rb index 54f5e82b4..8776a7daf 100644 --- a/test/vagrant/vm_test.rb +++ b/test/vagrant/vm_test.rb @@ -152,12 +152,9 @@ class VMTest < Test::Unit::TestCase end context "packaging" do - should "queue up the actions and execute" do - action_seq = sequence("action_seq") - @vm.expects(:add_action).with(Vagrant::Actions::VM::Export, nil).once.in_sequence(action_seq) - @vm.expects(:add_action).with(Vagrant::Actions::VM::Package, nil).once.in_sequence(action_seq) - @vm.expects(:execute!).in_sequence(action_seq) - @vm.package + should "execute the package action" do + @vm.env.actions.expects(:run).with(:package, :foo => :bar).once + @vm.package(:foo => :bar) end end