mirror of
https://github.com/vagrant-libvirt/vagrant-libvirt.git
synced 2025-02-25 18:55:27 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
26
lib/vagrant-libvirt/action/snapshot_delete.rb
Normal file
26
lib/vagrant-libvirt/action/snapshot_delete.rb
Normal 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
|
||||
22
lib/vagrant-libvirt/action/snapshot_restore.rb
Normal file
22
lib/vagrant-libvirt/action/snapshot_restore.rb
Normal 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
|
||||
27
lib/vagrant-libvirt/action/snapshot_save.rb
Normal file
27
lib/vagrant-libvirt/action/snapshot_save.rb
Normal 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
|
||||
12
lib/vagrant-libvirt/cap/snapshots.rb
Normal file
12
lib/vagrant-libvirt/cap/snapshots.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
13
tests/snapshot/Vagrantfile
vendored
Normal 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
|
||||
Reference in New Issue
Block a user