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.
* Provision domains with any built-in Vagrant provisioner.
* Synced folder support via `rsync`, `nfs`, `9p` or `virtiofs`.
* Snapshots via [sahara](https://github.com/jedi4ever/sahara).
* Snapshots
* Package caching via
[vagrant-cachier](http://fgrehm.viewdocs.io/vagrant-cachier/).
* 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 :SetBootOrder, action_root.join('set_boot_order')
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
autoload :ShareFolders, action_root.join('share_folders')
autoload :ShutdownDomain, action_root.join('shutdown_domain')
@@ -392,6 +398,48 @@ module VagrantPlugins
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

View File

@@ -34,7 +34,7 @@ module VagrantPlugins
begin
libvirt_domain.lookup_snapshot_by_name(name).delete
rescue => e
raise Errors::DeleteSnapshotError, error_message: e.message
raise Errors::SnapshotDeletionError, error_message: e.message
end
end
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
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)
# may be other error states with initial retreival we can't handle
begin
@@ -216,6 +268,21 @@ module VagrantPlugins
ip_address
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

View File

@@ -186,8 +186,24 @@ module VagrantPlugins
error_key(:no_ip_address_error)
end
class DeleteSnapshotError < VagrantLibvirtError
error_key(:delete_snapshot_error)
class SnapshotMissing < VagrantLibvirtError
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
class SerialCannotCreatePathError < VagrantLibvirtError

View File

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

View File

@@ -183,6 +183,16 @@ en:
management_network_required: |-
Management network can't be disabled when VM use box.
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: |-
Error creating path for serial port output log: %{path}

View File

@@ -170,4 +170,92 @@ describe VagrantPlugins::ProviderLibvirt::Action do
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

View File

@@ -223,3 +223,53 @@ 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