Add support for snapshots (#1456)

Builds on the work started by @randomvariable in #840 addressing the comments
left there.

Fixes: #529

Signed-off-by: Rodolfo Olivieri rodolfo.olivieri3@gmail.com
This commit is contained in:
Rodolfo Olivieri
2022-06-03 05:34:35 -04:00
committed by GitHub
parent 37597e22f9
commit 7233c85504
14 changed files with 388 additions and 4 deletions

View File

@@ -92,7 +92,7 @@ can help a lot :-)
* Setup hostname and network interfaces. * Setup hostname and network interfaces.
* Provision domains with any built-in Vagrant provisioner. * Provision domains with any built-in Vagrant provisioner.
* Synced folder support via `rsync`, `nfs`, `9p` or `virtiofs`. * Synced folder support via `rsync`, `nfs`, `9p` or `virtiofs`.
* Snapshots via [sahara](https://github.com/jedi4ever/sahara). * Snapshots
* Package caching via * Package caching via
[vagrant-cachier](http://fgrehm.viewdocs.io/vagrant-cachier/). [vagrant-cachier](http://fgrehm.viewdocs.io/vagrant-cachier/).
* Use boxes from other Vagrant providers via * Use boxes from other Vagrant providers via

View File

@@ -40,6 +40,12 @@ module VagrantPlugins
autoload :SetNameOfDomain, action_root.join('set_name_of_domain') autoload :SetNameOfDomain, action_root.join('set_name_of_domain')
autoload :SetBootOrder, action_root.join('set_boot_order') autoload :SetBootOrder, action_root.join('set_boot_order')
autoload :SetupComplete, action_root.join('cleanup_on_failure') autoload :SetupComplete, action_root.join('cleanup_on_failure')
# Snapshot autoload
autoload :SnapshotDelete, action_root.join('snapshot_delete')
autoload :SnapshotSave, action_root.join('snapshot_save')
autoload :SnapshotRestore, action_root.join('snapshot_restore')
# I don't think we need it anymore # I don't think we need it anymore
autoload :ShareFolders, action_root.join('share_folders') autoload :ShareFolders, action_root.join('share_folders')
autoload :ShutdownDomain, action_root.join('shutdown_domain') autoload :ShutdownDomain, action_root.join('shutdown_domain')
@@ -392,6 +398,48 @@ module VagrantPlugins
end end
end end
# This is the action that is primarily responsible for deleting a snapshot
def self.action_snapshot_delete
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use Call, IsCreated do |env, b2|
unless env[:result]
raise Vagrant::Errors::VMNotCreatedError
end
b2.use SnapshotDelete
end
end
end
# This is the action that is primarily responsible for restoring a snapshot
def self.action_snapshot_restore
Vagrant::Action::Builder.new.tap do |b|
b.use ConfigValidate
b.use Call, IsCreated do |env, b2|
unless env[:result]
raise Vagrant::Errors::VMNotCreatedError
end
b2.use SnapshotRestore
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 ConfigValidate
b.use Call, IsCreated do |env, b2|
unless env[:result]
raise Vagrant::Errors::VMNotCreatedError
end
b2.use SnapshotSave
end
end
end
end end
end end
end end

View File

@@ -34,7 +34,7 @@ module VagrantPlugins
begin begin
libvirt_domain.lookup_snapshot_by_name(name).delete libvirt_domain.lookup_snapshot_by_name(name).delete
rescue => e rescue => e
raise Errors::DeleteSnapshotError, error_message: e.message raise Errors::SnapshotDeletionError, error_message: e.message
end end
end end
rescue rescue

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module VagrantPlugins
module ProviderLibvirt
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], env[:snapshot_name])
env[:ui].success(I18n.t(
"vagrant.actions.vm.snapshot.deleted",
name: env[:snapshot_name]))
@app.call(env)
end
end
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module VagrantPlugins
module ProviderLibvirt
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], env[:snapshot_name])
@app.call(env)
end
end
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module VagrantPlugins
module ProviderLibvirt
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], 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

View File

@@ -0,0 +1,12 @@
module VagrantPlugins
module ProviderLibvirt
module Cap
class Snapshots
def self.snapshot_list(machine)
return if machine.state.id == :not_created
machine.provider.driver.list_snapshots(machine)
end
end
end
end
end

View File

@@ -125,6 +125,58 @@ module VagrantPlugins
ip_address ip_address
end end
def restore_snapshot(machine, snapshot_name)
domain = get_libvirt_domain(machine)
snapshot = get_snapshot_if_exists(machine, snapshot_name)
begin
# 4 is VIR_DOMAIN_SNAPSHOT_REVERT_FORCE
# needed due to https://bugzilla.redhat.com/show_bug.cgi?id=1006886
domain.revert_to_snapshot(snapshot, 4)
rescue Fog::Errors::Error => e
raise Errors::SnapshotReversionError, error_message: e.message
end
end
def list_snapshots(machine)
get_libvirt_domain(machine).list_snapshots
rescue Fog::Errors::Error => e
raise Errors::SnapshotListError, error_message: e.message
end
def delete_snapshot(machine, snapshot_name)
get_snapshot_if_exists(machine, snapshot_name).delete
rescue Errors::SnapshotMissing => e
raise Errors::SnapshotDeletionError, error_message: e.message
end
def create_new_snapshot(machine, snapshot_name)
snapshot_desc = <<-EOF
<domainsnapshot>
<name>#{snapshot_name}</name>
<description>Snapshot for vagrant sandbox</description>
</domainsnapshot>
EOF
get_libvirt_domain(machine).snapshot_create_xml(snapshot_desc)
rescue Fog::Errors::Error => e
raise Errors::SnapshotCreationError, error_message: e.message
end
def create_snapshot(machine, snapshot_name)
begin
delete_snapshot(machine, snapshot_name)
rescue Errors::SnapshotDeletionError
end
create_new_snapshot(machine, snapshot_name)
end
# if we can get snapshot description without exception it exists
def get_snapshot_if_exists(machine, snapshot_name)
snapshot = get_libvirt_domain(machine).lookup_snapshot_by_name(snapshot_name)
return snapshot if snapshot.xml_desc
rescue Libvirt::RetrieveError => e
raise Errors::SnapshotMissing, error_message: e.message
end
def state(machine) def state(machine)
# may be other error states with initial retreival we can't handle # may be other error states with initial retreival we can't handle
begin begin
@@ -216,6 +268,21 @@ module VagrantPlugins
ip_address ip_address
end end
def get_libvirt_domain(machine)
begin
libvirt_domain = connection.client.lookup_domain_by_uuid(machine.id)
rescue Libvirt::RetrieveError => e
if e.libvirt_code == ProviderLibvirt::Util::ErrorCodes::VIR_ERR_NO_DOMAIN
@logger.debug("machine #{machine.name} not found #{e}.")
return nil
else
raise e
end
end
libvirt_domain
end
end end
end end
end end

View File

@@ -186,8 +186,24 @@ module VagrantPlugins
error_key(:no_ip_address_error) error_key(:no_ip_address_error)
end end
class DeleteSnapshotError < VagrantLibvirtError class SnapshotMissing < VagrantLibvirtError
error_key(:delete_snapshot_error) error_key(:snapshot_missing)
end
class SnapshotDeletionError < VagrantLibvirtError
error_key(:snapshot_deletion_error)
end
class SnapshotListError < VagrantLibvirtError
error_key(:snapshot_list_error)
end
class SnapshotCreationError < VagrantLibvirtError
error_key(:snapshot_creation_error)
end
class SnapshotReversionError < VagrantLibvirtError
error_key(:snapshot_reversion_error)
end end
class SerialCannotCreatePathError < VagrantLibvirtError class SerialCannotCreatePathError < VagrantLibvirtError

View File

@@ -51,6 +51,11 @@ module VagrantPlugins
Cap::PublicAddress Cap::PublicAddress
end end
provider_capability(:libvirt, :snapshot_list) do
require_relative 'cap/snapshots'
Cap::Snapshots
end
# lower priority than nfs or rsync # lower priority than nfs or rsync
# https://github.com/vagrant-libvirt/vagrant-libvirt/pull/170 # https://github.com/vagrant-libvirt/vagrant-libvirt/pull/170
synced_folder('9p', 4) do synced_folder('9p', 4) do

View File

@@ -183,6 +183,16 @@ en:
management_network_required: |- management_network_required: |-
Management network can't be disabled when VM use box. Management network can't be disabled when VM use box.
Please fix your configuration and run vagrant again. Please fix your configuration and run vagrant again.
snapshot_deletion_error: |-
Error while deleting snapshot: %{error_message}.
snapshot_missing: |-
Snapshot not found: %{error_message}.
snapshot_list_error: |-
Cannot list snapshots: %{error_message}.
snapshot_creation_error: |-
Cannot create snapshot(s): %{error_message}.
snapshot_reversion_error: |-
Cannot revert snapshot(s): %{error_message}.
serial_cannot_create_path_error: |- serial_cannot_create_path_error: |-
Error creating path for serial port output log: %{path} Error creating path for serial port output log: %{path}

View File

@@ -170,4 +170,92 @@ describe VagrantPlugins::ProviderLibvirt::Action do
end end
end end
end end
describe '#action_snapshot_delete' do
context 'when not created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, false)
end
it 'should cause an error' do
expect{ machine.action(:snapshot_delete, snapshot_opts: {})}.to raise_error(Vagrant::Errors::VMNotCreatedError)
end
end
context 'when created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, true)
end
context 'when running' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsRunning, true)
end
it 'should call SnapshotDelete' do
expect_any_instance_of(VagrantPlugins::ProviderLibvirt::Action::SnapshotDelete).to receive(:call).and_return(0)
expect(machine.action(:snapshot_delete, snapshot_opts: {})).to match(hash_including({:action_name => :machine_action_snapshot_delete}))
end
end
end
end
describe '#action_snapshot_restore' do
context 'when not created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, false)
end
it 'should cause an error' do
expect{ machine.action(:snapshot_restore, snapshot_opts: {})}.to raise_error(Vagrant::Errors::VMNotCreatedError)
end
end
context 'when created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, true)
end
context 'when running' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsRunning, true)
end
it 'should call SnapshotRestore' do
expect_any_instance_of(VagrantPlugins::ProviderLibvirt::Action::SnapshotRestore).to receive(:call).and_return(0)
expect(machine.action(:snapshot_restore, snapshot_opts: {})).to match(hash_including({:action_name => :machine_action_snapshot_restore}))
end
end
end
end
describe '#action_snapshot_save' do
context 'when not created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, false)
end
it 'should cause an error' do
expect{ machine.action(:snapshot_save, snapshot_opts: {})}.to raise_error(Vagrant::Errors::VMNotCreatedError)
end
end
context 'when created' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsCreated, true)
end
context 'when running' do
before do
allow_action_env_result(VagrantPlugins::ProviderLibvirt::Action::IsRunning, true)
end
it 'should call SnapshotSave' do
expect_any_instance_of(VagrantPlugins::ProviderLibvirt::Action::SnapshotSave).to receive(:call).and_return(0)
expect(machine.action(:snapshot_save, snapshot_opts: {})).to match(hash_including({:action_name => :machine_action_snapshot_save}))
end
end
end
end
end end

View File

@@ -223,3 +223,53 @@ cleanup() {
cleanup cleanup
} }
@test "bring up and save a snapshot and restore it" {
export VAGRANT_CWD=tests/snapshot
cleanup
run ${VAGRANT_CMD} up ${VAGRANT_OPT}
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} ssh -- -t 'touch a.txt'
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} snapshot save default test
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} ssh -- -t 'rm a.txt'
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} ssh -- -t 'ls a.txt'
echo "${output}"
echo "status = ${status}"
# This means that the file does not exist on the box.
[ "$status" -eq 1 ]
run ${VAGRANT_CMD} ssh -- -t 'touch b.txt'
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} snapshot restore default test
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} ssh -- -t 'ls b.txt'
echo "${output}"
echo "status = ${status}"
# This means that the file does not exist on the box.
[ "$status" -eq 1 ]
run ${VAGRANT_CMD} ssh -- -t 'ls a.txt'
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
run ${VAGRANT_CMD} snapshot delete default test
echo "${output}"
echo "status = ${status}"
[ "$status" -eq 0 ]
cleanup
}

13
tests/snapshot/Vagrantfile vendored Normal file
View File

@@ -0,0 +1,13 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
#
# frozen_string_literal: true
Vagrant.configure("2") do |config|
config.vm.box = "infernix/tinycore"
config.ssh.shell = "/bin/sh"
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.provider :libvirt do |libvirt|
libvirt.cpus = 2
end
end