From 4e649cc98792c192c1589a0bd86e8433e90f60cd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 26 Dec 2012 22:24:45 -0800 Subject: [PATCH] Upgrade V1-style dotfile to V2 See the code and comments for details on how this is done. As usual, we are very careful about this so as not to inadvertently destruct real user data. --- lib/vagrant/environment.rb | 57 ++++++++++++++++++++++++++- lib/vagrant/errors.rb | 4 ++ templates/locales/en.yml | 29 ++++++++++++++ test/unit/vagrant/environment_test.rb | 53 ++++++++++++++++++++++++- 4 files changed, 141 insertions(+), 2 deletions(-) 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