diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index fcf8d916d..db4875dbb 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -20,6 +20,7 @@ module Vagrant autoload :HandleBox, "vagrant/action/builtin/handle_box" autoload :HandleBoxUrl, "vagrant/action/builtin/handle_box_url" autoload :HandleForwardedPortCollisions, "vagrant/action/builtin/handle_forwarded_port_collisions" + autoload :IsEnvSet, "vagrant/action/builtin/is_env_set" autoload :IsState, "vagrant/action/builtin/is_state" autoload :Lock, "vagrant/action/builtin/lock" autoload :Message, "vagrant/action/builtin/message" diff --git a/lib/vagrant/action/builtin/is_env_set.rb b/lib/vagrant/action/builtin/is_env_set.rb new file mode 100644 index 000000000..269a6479c --- /dev/null +++ b/lib/vagrant/action/builtin/is_env_set.rb @@ -0,0 +1,22 @@ +module Vagrant + module Action + module Builtin + # This middleware is meant to be used with Call and can check if + # a variable in env is set. + class IsEnvSet + def initialize(app, env, key, **opts) + @app = app + @logger = Log4r::Logger.new("vagrant::action::builtin::is_env_set") + @key = key + end + + def call(env) + @logger.debug("Checking if env is set: '#{@key}'") + env[:result] = !!env[@key] + @logger.debug(" - Result: #{env[:result].inspect}") + @app.call(env) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/delete.rb b/plugins/commands/snapshot/command/delete.rb new file mode 100644 index 000000000..0d5118847 --- /dev/null +++ b/plugins/commands/snapshot/command/delete.rb @@ -0,0 +1,35 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Delete < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot delete [options] [vm-name] " + o.separator "" + o.separator "Delete a snapshot taken previously with snapshot save." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_delete, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/list.rb b/plugins/commands/snapshot/command/list.rb new file mode 100644 index 000000000..d0c433b94 --- /dev/null +++ b/plugins/commands/snapshot/command/list.rb @@ -0,0 +1,47 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class List < Vagrant.plugin("2", :command) + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot list [options] [vm-name]" + o.separator "" + o.separator "List all snapshots taken for a machine." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + with_target_vms(argv) do |vm| + if !vm.id + vm.ui.info(I18n.t("vagrant.commands.common.vm_not_created")) + next + end + + if !vm.provider.capability?(:snapshot_list) + vm.ui.info(I18n.t("vagrant.commands.snapshot.not_supported")) + next + end + + snapshots = vm.provider.capability(:snapshot_list) + if snapshots.empty? + vm.ui.output(I18n.t("vagrant.actions.vm.snapshot.list_none")) + vm.ui.detail(I18n.t("vagrant.actions.vm.snapshot.list_none_detail")) + next + end + + snapshots.each do |snapshot| + vm.ui.output(snapshot, prefix: false) + end + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/pop.rb b/plugins/commands/snapshot/command/pop.rb new file mode 100644 index 000000000..2c3499f43 --- /dev/null +++ b/plugins/commands/snapshot/command/pop.rb @@ -0,0 +1,28 @@ +require 'json' +require 'optparse' + +require_relative "push_shared" + +module VagrantPlugins + module CommandSnapshot + module Command + class Pop < Vagrant.plugin("2", :command) + include PushShared + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot pop [options] [vm-name]" + o.separator "" + o.separator "Restore state that was pushed with `vagrant snapshot push`." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + return shared_exec(argv, method(:pop)) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/push.rb b/plugins/commands/snapshot/command/push.rb new file mode 100644 index 000000000..c1168bcf2 --- /dev/null +++ b/plugins/commands/snapshot/command/push.rb @@ -0,0 +1,33 @@ +require 'json' +require 'optparse' + +require_relative "push_shared" + +module VagrantPlugins + module CommandSnapshot + module Command + class Push < Vagrant.plugin("2", :command) + include PushShared + + def execute + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot push [options] [vm-name]" + o.separator "" + o.separator "Take a snapshot of the current state of the machine and 'push'" + o.separator "it onto the stack of states. You can use `vagrant snapshot pop`" + o.separator "to restore back to this state at any time." + o.separator "" + o.separator "If you use `vagrant snapshot save` or restore at any point after" + o.separator "a push, pop will still bring you back to this pushed state." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + + return shared_exec(argv, method(:push)) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/push_shared.rb b/plugins/commands/snapshot/command/push_shared.rb new file mode 100644 index 000000000..a82779ee5 --- /dev/null +++ b/plugins/commands/snapshot/command/push_shared.rb @@ -0,0 +1,57 @@ +require 'json' + +module VagrantPlugins + module CommandSnapshot + module Command + module PushShared + def shared_exec(argv, m) + with_target_vms(argv) do |vm| + if !vm.id + vm.ui.info("Not created. Cannot push snapshot state.") + next + end + + vm.env.lock("machine-snapshot-stack") do + m.call(vm) + end + end + + # Success, exit with 0 + 0 + end + + def push(machine) + snapshot_name = "push_#{Time.now.to_i}_#{rand(10000)}" + + # Save the snapshot. This will raise an exception if it fails. + machine.action(:snapshot_save, snapshot_name: snapshot_name) + end + + def pop(machine) + # By reverse sorting, we should be able to find the first + # pushed snapshot. + name = nil + snapshots = machine.provider.capability(:snapshot_list) + snapshots.sort.reverse.each do |snapshot| + if snapshot =~ /^push_\d+_\d+$/ + name = snapshot + break + end + end + + # If no snapshot was found, we never pushed + if !name + machine.ui.info(I18n.t("vagrant.commands.snapshot.no_push_snapshot")) + return + end + + # Restore the snapshot and tell the provider to delete it as well. + machine.action( + :snapshot_restore, + snapshot_name: name, + snapshot_delete: true) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/restore.rb b/plugins/commands/snapshot/command/restore.rb new file mode 100644 index 000000000..930a811f4 --- /dev/null +++ b/plugins/commands/snapshot/command/restore.rb @@ -0,0 +1,35 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Restore < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot restore [options] [vm-name] " + o.separator "" + o.separator "Restore a snapshot taken previously with snapshot save." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_restore, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/root.rb b/plugins/commands/snapshot/command/root.rb new file mode 100644 index 000000000..4ced72277 --- /dev/null +++ b/plugins/commands/snapshot/command/root.rb @@ -0,0 +1,89 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Root < Vagrant.plugin("2", :command) + def self.synopsis + "manages snapshots: saving, restoring, etc." + end + + def initialize(argv, env) + super + + @main_args, @sub_command, @sub_args = split_main_and_subcommand(argv) + + @subcommands = Vagrant::Registry.new + @subcommands.register(:save) do + require_relative "save" + Save + end + + @subcommands.register(:restore) do + require_relative "restore" + Restore + end + + @subcommands.register(:delete) do + require_relative "delete" + Delete + end + + @subcommands.register(:list) do + require_relative "list" + List + end + + @subcommands.register(:push) do + require_relative "push" + Push + end + + @subcommands.register(:pop) do + require_relative "pop" + Pop + end + end + + def execute + if @main_args.include?("-h") || @main_args.include?("--help") + # Print the help for all the commands. + return help + end + + # If we reached this far then we must have a subcommand. If not, + # then we also just print the help and exit. + command_class = @subcommands.get(@sub_command.to_sym) if @sub_command + return help if !command_class || !@sub_command + @logger.debug("Invoking command class: #{command_class} #{@sub_args.inspect}") + + # Initialize and execute the command class + command_class.new(@sub_args, @env).execute + end + + # Prints the help out for this command + def help + opts = OptionParser.new do |opts| + opts.banner = "Usage: vagrant snapshot []" + opts.separator "" + opts.separator "Available subcommands:" + + # Add the available subcommands as separators in order to print them + # out as well. + keys = [] + @subcommands.each { |key, value| keys << key.to_s } + + keys.sort.each do |key| + opts.separator " #{key}" + end + + opts.separator "" + opts.separator "For help on any individual subcommand run `vagrant snapshot -h`" + end + + @env.ui.info(opts.help, prefix: false) + end + end + end + end +end diff --git a/plugins/commands/snapshot/command/save.rb b/plugins/commands/snapshot/command/save.rb new file mode 100644 index 000000000..496891eac --- /dev/null +++ b/plugins/commands/snapshot/command/save.rb @@ -0,0 +1,40 @@ +require 'optparse' + +module VagrantPlugins + module CommandSnapshot + module Command + class Save < Vagrant.plugin("2", :command) + def execute + options = {} + + opts = OptionParser.new do |o| + o.banner = "Usage: vagrant snapshot save [options] [vm-name] " + o.separator "" + o.separator "Take a snapshot of the current state of the machine. The snapshot" + o.separator "can be restored via `vagrant snapshot restore` at any point in the" + o.separator "future to get back to this exact machine state." + o.separator "" + o.separator "Snapshots are useful for experimenting in a machine and being able" + o.separator "to rollback quickly." + end + + # Parse the options + argv = parse_options(opts) + return if !argv + if argv.empty? || argv.length > 2 + raise Vagrant::Errors::CLIInvalidUsage, + help: opts.help.chomp + end + + name = argv.pop + with_target_vms(argv) do |vm| + vm.action(:snapshot_save, snapshot_name: name) + end + + # Success, exit status 0 + 0 + end + end + end + end +end diff --git a/plugins/commands/snapshot/plugin.rb b/plugins/commands/snapshot/plugin.rb new file mode 100644 index 000000000..5b2af81df --- /dev/null +++ b/plugins/commands/snapshot/plugin.rb @@ -0,0 +1,15 @@ +require "vagrant" + +module VagrantPlugins + module CommandSnapshot + class Plugin < Vagrant.plugin("2") + name "snapshot command" + description "The `snapshot` command gives you a way to manage snapshots." + + command("snapshot") do + require_relative "command/root" + Command::Root + end + end + end +end diff --git a/plugins/providers/virtualbox/action.rb b/plugins/providers/virtualbox/action.rb index 088221ed1..9c4d68c72 100644 --- a/plugins/providers/virtualbox/action.rb +++ b/plugins/providers/virtualbox/action.rb @@ -42,6 +42,9 @@ module VagrantPlugins autoload :SaneDefaults, File.expand_path("../action/sane_defaults", __FILE__) autoload :SetName, File.expand_path("../action/set_name", __FILE__) autoload :SetupPackageFiles, File.expand_path("../action/setup_package_files", __FILE__) + autoload :SnapshotDelete, File.expand_path("../action/snapshot_delete", __FILE__) + autoload :SnapshotRestore, File.expand_path("../action/snapshot_restore", __FILE__) + autoload :SnapshotSave, File.expand_path("../action/snapshot_save", __FILE__) autoload :Suspend, File.expand_path("../action/suspend", __FILE__) # Include the built-in modules so that we can use them as top-level @@ -222,6 +225,59 @@ module VagrantPlugins end end + def self.action_snapshot_delete + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use SnapshotDelete + else + b2.use MessageNotCreated + end + end + end + end + + # This is the action that is primarily responsible for saving a snapshot + def self.action_snapshot_restore + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if !env[:result] + b2.use MessageNotCreated + next + end + + b2.use CheckAccessible + b2.use EnvSet, force_halt: true + b2.use action_halt + b2.use SnapshotRestore + + b2.use Call, IsEnvSet, :snapshot_delete do |env2, b3| + if env2[:result] + b3.use action_snapshot_delete + end + end + + b2.use action_start + end + end + end + + # This is the action that is primarily responsible for saving a snapshot + def self.action_snapshot_save + Vagrant::Action::Builder.new.tap do |b| + b.use CheckVirtualbox + b.use Call, Created do |env, b2| + if env[:result] + b2.use SnapshotSave + else + b2.use MessageNotCreated + end + end + end + end + # This is the action that will exec into an SSH shell. def self.action_ssh Vagrant::Action::Builder.new.tap do |b| diff --git a/plugins/providers/virtualbox/action/snapshot_delete.rb b/plugins/providers/virtualbox/action/snapshot_delete.rb new file mode 100644 index 000000000..1d8cecc73 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_delete.rb @@ -0,0 +1,32 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotDelete + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.deleting", + name: env[:snapshot_name])) + env[:machine].provider.driver.delete_snapshot( + env[:machine].id, env[:snapshot_name]) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + env[:ui].clear_line + + env[:ui].success(I18n.t( + "vagrant.actions.vm.snapshot.deleted", + name: env[:snapshot_name])) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/snapshot_restore.rb b/plugins/providers/virtualbox/action/snapshot_restore.rb new file mode 100644 index 000000000..f655471c1 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_restore.rb @@ -0,0 +1,28 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotRestore + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.restoring", + name: env[:snapshot_name])) + env[:machine].provider.driver.restore_snapshot( + env[:machine].id, env[:snapshot_name]) do |progress| + env[:ui].clear_line + env[:ui].report_progress(progress, 100, false) + end + + # Clear the line one last time since the progress meter doesn't disappear + # immediately. + env[:ui].clear_line + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/action/snapshot_save.rb b/plugins/providers/virtualbox/action/snapshot_save.rb new file mode 100644 index 000000000..98b720763 --- /dev/null +++ b/plugins/providers/virtualbox/action/snapshot_save.rb @@ -0,0 +1,25 @@ +module VagrantPlugins + module ProviderVirtualBox + module Action + class SnapshotSave + def initialize(app, env) + @app = app + end + + def call(env) + env[:ui].info(I18n.t( + "vagrant.actions.vm.snapshot.saving", + name: env[:snapshot_name])) + env[:machine].provider.driver.create_snapshot( + env[:machine].id, env[:snapshot_name]) + + env[:ui].success(I18n.t( + "vagrant.actions.vm.snapshot.saved", + name: env[:snapshot_name])) + + @app.call(env) + end + end + end + end +end diff --git a/plugins/providers/virtualbox/cap.rb b/plugins/providers/virtualbox/cap.rb index e459c97bf..77f8ee1ad 100644 --- a/plugins/providers/virtualbox/cap.rb +++ b/plugins/providers/virtualbox/cap.rb @@ -22,6 +22,13 @@ module VagrantPlugins def self.nic_mac_addresses(machine) machine.provider.driver.read_mac_addresses end + + # Returns a list of the snapshots that are taken on this machine. + # + # @return [Array] Snapshot Name + def self.snapshot_list(machine) + machine.provider.driver.list_snapshots(machine.id) + end end end end diff --git a/plugins/providers/virtualbox/driver/base.rb b/plugins/providers/virtualbox/driver/base.rb index 768bd9cef..8dc0ba994 100644 --- a/plugins/providers/virtualbox/driver/base.rb +++ b/plugins/providers/virtualbox/driver/base.rb @@ -386,7 +386,8 @@ module VagrantPlugins if errored raise Vagrant::Errors::VBoxManageError, command: command.inspect, - stderr: r.stderr + stderr: r.stderr, + stdout: r.stdout end end diff --git a/plugins/providers/virtualbox/driver/meta.rb b/plugins/providers/virtualbox/driver/meta.rb index df52f0849..136ab196a 100644 --- a/plugins/providers/virtualbox/driver/meta.rb +++ b/plugins/providers/virtualbox/driver/meta.rb @@ -89,6 +89,7 @@ module VagrantPlugins :create_host_only_network, :create_snapshot, :delete, + :delete_snapshot, :delete_unused_host_only_networks, :discard_saved_state, :enable_adapters, @@ -97,6 +98,7 @@ module VagrantPlugins :forward_ports, :halt, :import, + :list_snapshots, :read_forwarded_ports, :read_bridged_interfaces, :read_dhcp_servers, @@ -113,6 +115,7 @@ module VagrantPlugins :read_vms, :reconfig_host_only, :remove_dhcp_server, + :restore_snapshot, :resume, :set_mac_address, :set_name, diff --git a/plugins/providers/virtualbox/driver/version_4_2.rb b/plugins/providers/virtualbox/driver/version_4_2.rb index 0f1f79698..b3f33f110 100644 --- a/plugins/providers/virtualbox/driver/version_4_2.rb +++ b/plugins/providers/virtualbox/driver/version_4_2.rb @@ -608,6 +608,85 @@ module VagrantPlugins execute("showvminfo", uuid) return true end + + def create_snapshot(machine_id, snapshot_name) + execute("snapshot", machine_id, "take", snapshot_name) + end + + def delete_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + + result = [] + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise + end + + def restore_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end end end end diff --git a/plugins/providers/virtualbox/driver/version_4_3.rb b/plugins/providers/virtualbox/driver/version_4_3.rb index bc2c87478..b51eec37a 100644 --- a/plugins/providers/virtualbox/driver/version_4_3.rb +++ b/plugins/providers/virtualbox/driver/version_4_3.rb @@ -90,6 +90,81 @@ module VagrantPlugins execute("snapshot", machine_id, "take", snapshot_name) end + def delete_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + + result = [] + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise + end + + def restore_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + def delete execute("unregistervm", @uuid, "--delete") end diff --git a/plugins/providers/virtualbox/driver/version_5_0.rb b/plugins/providers/virtualbox/driver/version_5_0.rb index df833bb3f..9762eda42 100644 --- a/plugins/providers/virtualbox/driver/version_5_0.rb +++ b/plugins/providers/virtualbox/driver/version_5_0.rb @@ -86,6 +86,81 @@ module VagrantPlugins execute("snapshot", machine_id, "take", snapshot_name) end + def delete_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + # Snapshot and report the % progress + execute("snapshot", machine_id, "delete", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + + def list_snapshots(machine_id) + output = execute( + "snapshot", machine_id, "list", "--machinereadable", + retryable: true) + + result = [] + output.split("\n").each do |line| + if line =~ /^SnapshotName.*?="(.+?)"$/i + result << $1.to_s + end + end + + result.sort + rescue Vagrant::Errors::VBoxManageError => e + return [] if e.extra_data[:stdout].include?("does not have") + raise + end + + def restore_snapshot(machine_id, snapshot_name) + # Start with 0% + last = 0 + total = "" + yield 0 if block_given? + + execute("snapshot", machine_id, "restore", snapshot_name) do |type, data| + if type == :stderr + # Append the data so we can see the full view + total << data.gsub("\r", "") + + # Break up the lines. We can't get the progress until we see an "OK" + lines = total.split("\n") + + # The progress of the import will be in the last line. Do a greedy + # regular expression to find what we're looking for. + match = /.+(\d{2})%/.match(lines.last) + if match + current = match[1].to_i + if current > last + last = current + yield current if block_given? + end + end + end + end + end + def delete execute("unregistervm", @uuid, "--delete") end diff --git a/plugins/providers/virtualbox/plugin.rb b/plugins/providers/virtualbox/plugin.rb index 18e33d4bb..84f86ba51 100644 --- a/plugins/providers/virtualbox/plugin.rb +++ b/plugins/providers/virtualbox/plugin.rb @@ -33,6 +33,11 @@ module VagrantPlugins require_relative "cap" Cap end + + provider_capability(:virtualbox, :snapshot_list) do + require_relative "cap" + Cap + end end autoload :Action, File.expand_path("../action", __FILE__) diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 171cd60ba..929962453 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -1501,6 +1501,17 @@ en: Post install message from the '%{name}' plugin: %{message} + snapshot: + not_supported: |- + This provider doesn't support snapshots. + + This may be intentional or this may be a bug. If this provider + should support snapshots, then please report this as a bug to the + maintainer of the provider. + no_push_snapshot: |- + No pushed snapshot found! + + Use `vagrant snapshot push` to push a snapshot to restore to. status: aborted: |- The VM is in an aborted state. This means that it was abruptly @@ -1765,6 +1776,26 @@ en: set_name: setting_name: |- Setting the name of the VM: %{name} + snapshot: + deleting: |- + Deleting the snapshot '%{name}'... + deleted: |- + Snapshot deleted! + list_none: |- + No snapshots have been taken yet! + list_none_detail: |- + You can take a snapshot using `vagrant snapshot save`. Note that + not all providers support this yet. Once a snapshot is taken, you + can list them using this command, and use commands such as + `vagrant snapshot restore` to go back to a certain snapshot. + restoring: |- + Restoring the snapshot '%{name}'... + saving: |- + Snapshotting the machine as '%{name}'... + saved: |- + Snapshot saved! You can restore the snapshot at any time by + using `vagrant snapshot restore`. You can delete it using + `vagrant snapshot delete`. suspend: suspending: Saving VM state and suspending execution... diff --git a/test/unit/plugins/commands/snapshot/command/pop_test.rb b/test/unit/plugins/commands/snapshot/command/pop_test.rb new file mode 100644 index 000000000..66687868f --- /dev/null +++ b/test/unit/plugins/commands/snapshot/command/pop_test.rb @@ -0,0 +1,52 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/snapshot/command/pop") + +describe VagrantPlugins::CommandSnapshot::Command::Pop do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + it "calls snapshot_restore with the last pushed snapshot" do + machine.id = "foo" + + allow(machine.provider).to receive(:capability). + with(:snapshot_list).and_return(["push_2_0", "push_1_0"]) + + expect(machine).to receive(:action) do |name, opts| + expect(name).to eq(:snapshot_restore) + expect(opts[:snapshot_name]).to eq("push_2_0") + end + + expect(subject.execute).to eq(0) + end + + it "isn't an error if no matching snapshot" do + machine.id = "foo" + + allow(machine.provider).to receive(:capability). + with(:snapshot_list).and_return(["foo"]) + + expect(machine).to_not receive(:action) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/plugins/commands/snapshot/command/push_test.rb b/test/unit/plugins/commands/snapshot/command/push_test.rb new file mode 100644 index 000000000..3da907cfa --- /dev/null +++ b/test/unit/plugins/commands/snapshot/command/push_test.rb @@ -0,0 +1,46 @@ +require File.expand_path("../../../../../base", __FILE__) + +require Vagrant.source_root.join("plugins/commands/snapshot/command/push") + +describe VagrantPlugins::CommandSnapshot::Command::Push do + include_context "unit" + + let(:iso_env) do + # We have to create a Vagrantfile so there is a root path + env = isolated_environment + env.vagrantfile("") + env.create_vagrant_env + end + + let(:guest) { double("guest") } + let(:host) { double("host") } + let(:machine) { iso_env.machine(iso_env.machine_names[0], :dummy) } + + let(:argv) { [] } + + subject { described_class.new(argv, iso_env) } + + before do + allow(subject).to receive(:with_target_vms) { |&block| block.call machine } + end + + describe "execute" do + it "calls snapshot_save with a random snapshot name" do + machine.id = "foo" + + expect(machine).to receive(:action) do |name, opts| + expect(name).to eq(:snapshot_save) + expect(opts[:snapshot_name]).to match(/^push_/) + end + + expect(subject.execute).to eq(0) + end + + it "doesn't snapshot a non-existent machine" do + machine.id = nil + + expect(machine).to_not receive(:action) + expect(subject.execute).to eq(0) + end + end +end diff --git a/test/unit/vagrant/action/builtin/is_env_set_test.rb b/test/unit/vagrant/action/builtin/is_env_set_test.rb new file mode 100644 index 000000000..56eafa956 --- /dev/null +++ b/test/unit/vagrant/action/builtin/is_env_set_test.rb @@ -0,0 +1,31 @@ +require "pathname" +require "tmpdir" + +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::IsEnvSet do + let(:app) { lambda { |env| } } + let(:env) { { } } + + describe "#call" do + it "sets result to true if it is set" do + env[:bar] = true + + subject = described_class.new(app, env, :bar) + + expect(app).to receive(:call).with(env) + + subject.call(env) + expect(env[:result]).to be_true + end + + it "sets result to false if it isn't set" do + subject = described_class.new(app, env, :bar) + + expect(app).to receive(:call).with(env) + + subject.call(env) + expect(env[:result]).to be_false + end + end +end diff --git a/website/docs/source/layouts/layout.erb b/website/docs/source/layouts/layout.erb index 4fedc3627..259577d79 100644 --- a/website/docs/source/layouts/layout.erb +++ b/website/docs/source/layouts/layout.erb @@ -108,6 +108,7 @@ >reload >resume >share + >snapshot >ssh >ssh-config >status diff --git a/website/docs/source/v2/cli/snapshot.html.md b/website/docs/source/v2/cli/snapshot.html.md new file mode 100644 index 000000000..42a2dfc68 --- /dev/null +++ b/website/docs/source/v2/cli/snapshot.html.md @@ -0,0 +1,79 @@ +--- +page_title: "vagrant snapshot - Command-Line Interface" +sidebar_current: "cli-snapshot" +--- + +# Snapshot + +**Command: `vagrant snapshot`** + +This is the command used to manage snapshots with the guest machine. +Snapshots record a point-in-time state of a guest machine. You can then +quickly restore to this environment. This lets you experiment and try things +and quickly restore back to a previous state. + +Snapshotting is not supported by every provider. If it isn't supported, +Vagrant will give you an error message. + +The main functionality of this command is exposed via even more subcommands: + +* `push` +* `pop` +* `save` +* `restore` +* `list` +* `delete` + +# Snapshot Push + +**Command: `vagrant snapshot push`** + +This takes a snapshot and pushes it onto the snapshot stack. + +This is a shorthand for `vagrant snapshot save` where you don't need +to specify a name. When you call the inverse `vagrant snapshot pop`, it will +restore the pushed state. + +~> **Warning:** If you are using `push` and `pop`, avoid using `save` + and `restore` which are unsafe to mix. + +# Snapshot Pop + +**Command: `vagrant snapshot pop`** + +This command is the inverse of `vagrant snapshot push`: it will restore +the pushed state. + +# Snapshot Save + +**Command: `vagrant snapshot save NAME`** + +This command saves a new named snapshot. If this command is used, the +`push` and `pop` subcommands cannot be safely used. + +# Snapshot Restore + +**Command: `vagrant snapshot restore NAME`** + +This command restores the named snapshot. + +# Snapshot List + +**Command: `vagrant snapshot list`** + +This command will list all the snapshots taken. + +# Snapshot Delete + +**Command: `vagrant snapshot delete NAME`** + +This command will delete the named snapshot. + +Some providers require all "child" snapshots to be deleted first. Vagrant +itself doesn't track what these children are. If this is the case (such +as with VirtualBox), then you must be sure to delete the snapshots in the +reverse order they were taken. + +This command is typically _much faster_ if the machine is halted prior to +snapshotting. If this isn't an option, or isn't ideal, then the deletion +can also be done online with most providers.