diff --git a/lib/vagrant/environment.rb b/lib/vagrant/environment.rb index 126667dc6..cee3c9e5e 100644 --- a/lib/vagrant/environment.rb +++ b/lib/vagrant/environment.rb @@ -1,4 +1,5 @@ require 'fileutils' +require 'json' require 'pathname' require 'set' @@ -517,7 +518,61 @@ module Vagrant # # @param [Pathname] path The path to the dotfile def upgrade_v1_dotfile(path) - raise "V1 environment detected. An auto-upgrade process will be made soon." + @logger.info("Upgrading V1 dotfile to V2 directory structure...") + + # First, verify the file isn't empty. If it is an empty file, we + # just delete it and go on with life. + contents = path.read.strip + if contents.strip == "" + @logger.info("V1 dotfile was empty. Removing and moving on.") + path.delete + return + end + + # Otherwise, verify there is valid JSON in here since a Vagrant + # environment would always ensure valid JSON. This is a sanity check + # to make sure we don't nuke a dotfile that is not ours... + @logger.debug("Attempting to parse JSON of V1 file") + json_data = nil + begin + json_data = JSON.parse(contents) + @logger.debug("JSON parsed successfully. Things are okay.") + rescue JSON::ParserError + # The file could've been tampered with since Vagrant 1.0.x is + # supposed to ensure that the contents are valid JSON. Show an error. + raise Errors::DotfileUpgradeJSONError, + :state_file => path.to_s + end + + # Alright, let's upgrade this guy to the new structure. Start by + # backing up the old dotfile. + backup_file = path.dirname.join(".vagrant.v1.#{Time.now.to_i}") + @logger.info("Renaming old dotfile to: #{backup_file}") + path.rename(backup_file) + + # Now, we create the actual local data directory. This should succeed + # this time since we renamed the old conflicting V1. + setup_local_data_path + + if json_data["active"] + @logger.debug("Upgrading to V2 style for each active VM") + json_data["active"].each do |name, id| + @logger.info("Upgrading dotfile: #{name} (#{id})") + + # Create the machine configuration directory + directory = @local_data_path.join("machines/#{name}/virtualbox") + FileUtils.mkdir_p(directory) + + # Write the ID file + directory.join("id").open("w+") do |f| + f.write(id) + end + end + end + + # Upgrade complete! Let the user know + @ui.info(I18n.t("vagrant.general.upgraded_v1_dotfile", + :backup_path => backup_file.to_s)) end end end diff --git a/lib/vagrant/errors.rb b/lib/vagrant/errors.rb index f4961b45e..7d333a39d 100644 --- a/lib/vagrant/errors.rb +++ b/lib/vagrant/errors.rb @@ -166,6 +166,10 @@ module Vagrant error_key(:dotfile_is_directory) end + class DotfileUpgradeJSONError < VagrantError + error_key(:dotfile_upgrade_json_error) + end + class DownloaderFileDoesntExist < VagrantError status_code(37) error_key(:file_missing, "vagrant.downloaders.file") diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 6adf62cf4..de5be88be 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -9,6 +9,17 @@ en: Old: %{old} New: %{new} + upgraded_v1_dotfile: |- + A Vagrant 1.0.x state file was found for this environment. Vagrant has + gone ahead and auto-upgraded this to the latest format. Everything + should continue working as normal. Beware, however, that older versions + of Vagrant may no longer be used with this environment. + + However, in case anything went wrong, the old dotfile was backed up + to the location below. If everything is okay, it is safe to remove + this backup. + + Backup: %{backup_path} #------------------------------------------------------------------------------- # Translations for exception classes @@ -55,6 +66,24 @@ en: this command in another directory. If you aren't in a home directory, then please rename ".vagrant" to something else, or configure Vagrant to use another filename by modifying `config.vagrant.dotfile_name`. + dotfile_upgrade_json_error: |- + A Vagrant 1.0.x local state file was found. Vagrant is able to upgrade + this to the latest format automatically, however various checks are + put in place to verify data isn't incorrectly deleted. In this case, + the old state file was not valid JSON. Vagrant 1.0.x would store state + as valid JSON, meaning that this file was probably tampered with or + manually edited. Vagrant's auto-upgrade process cannot continue in this + case. + + In most cases, this can be resolve by simply removing the state file. + Note however though that if Vagrant was previously managing virtual + machines, they may be left in an "orphan" state. That is, if they are + running or exist, they'll have to manually be removed. + + If you're unsure what to do, ask the Vagrant mailing list or contact + support. + + State file path: %{state_file} environment_locked: |- An instance of Vagrant is already running. Only one instance of Vagrant may run at any given time to avoid problems with VirtualBox inconsistencies diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index e44d6b8da..78f3a5dbb 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -1,5 +1,7 @@ require File.expand_path("../../base", __FILE__) +require "json" require "pathname" +require "tempfile" require "vagrant/util/file_mode" @@ -91,6 +93,55 @@ describe Vagrant::Environment do instance = described_class.new(:local_data_path => dir) instance.local_data_path.to_s.should == dir end + + describe "upgrading V1 dotfiles" do + let(:v1_dotfile_tempfile) { Tempfile.new("vagrant") } + let(:v1_dotfile) { Pathname.new(v1_dotfile_tempfile.path) } + let(:local_data_path) { v1_dotfile_tempfile.path } + let(:instance) { described_class.new(:local_data_path => local_data_path) } + + it "should be fine if dotfile is empty" do + v1_dotfile.open("w+") do |f| + f.write("") + end + + expect { instance }.to_not raise_error + Pathname.new(local_data_path).should be_directory + end + + it "should upgrade all active VMs" do + active_vms = { + "foo" => "foo_id", + "bar" => "bar_id" + } + + v1_dotfile.open("w+") do |f| + f.write(JSON.dump({ + "active" => active_vms + })) + end + + expect { instance }.to_not raise_error + + local_data_pathname = Pathname.new(local_data_path) + foo_id_file = local_data_pathname.join("machines/foo/virtualbox/id") + foo_id_file.should be_file + foo_id_file.read.should == "foo_id" + + bar_id_file = local_data_pathname.join("machines/bar/virtualbox/id") + bar_id_file.should be_file + bar_id_file.read.should == "bar_id" + end + + it "should raise an error if invalid JSON" do + v1_dotfile.open("w+") do |f| + f.write("bad") + end + + expect { instance }. + to raise_error(Vagrant::Errors::DotfileUpgradeJSONError) + end + end end describe "default provider" do @@ -239,7 +290,7 @@ VF it "should return a machine object with the machine configuration" do # Create a provider - foo_config = Class.new do + foo_config = Class.new(Vagrant.plugin("2", :config)) do attr_accessor :value end