Skip to content

Commit

Permalink
Merge pull request #459 from mattlqx/clonefile
Browse files Browse the repository at this point in the history
use clonefile copy for macvm boxes
  • Loading branch information
bineesh-n authored Oct 16, 2023
2 parents 5cc8045 + dbe3495 commit c10beb2
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 2 deletions.
55 changes: 54 additions & 1 deletion lib/vagrant-parallels/driver/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,26 @@ def clear_shared_folders
# @return [String] UUID of the new VM.
def clone_vm(src_name, options = {})
dst_name = "vagrant_temp_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
src_vm = json { execute_prlctl('list', '--json', '-i', src_name) }.first

if options[:linked] || !Util::Common.is_apfs?(src_vm.fetch('Home'))
# If linked clone is an option, or path to src is not on APFS, then do the normal clone.
prlctl_clone_vm(src_name, dst_name, options)
else
# We can use clonefile on APFS to do a fast CoW clone of the VM source and then register
copy_clone_vm(src_name, dst_name, options)
end
read_vms[dst_name]
end

# Uses prlctl to clone an existing registered VM
#
# @param [String] src_name Name or UUID of the source VM or template.
# @param [String] dst_name Name of the destination VM.
# @param [<String => String>] options Options to clone virtual machine.
def prlctl_clone_vm(src_name, dst_name, options = {})
list_args = ['list', '--json', '-i', src_name]
src_vm = json { execute_prlctl(*list_args) }.first
args = ['clone', src_name, '--name', dst_name]
args.concat(['--dst', options[:dst]]) if options[:dst]

Expand All @@ -97,7 +116,41 @@ def clone_vm(src_name, options = {})
yield $1.to_i if block_given?
end
end
read_vms[dst_name]
end

# Uses cp with clonefile flag to clone an existing registered VM
#
# @param [String] src_name Name or UUID of the source VM or template.
# @param [String] dst_name Name of the destination VM.
# @param [<String => String>] options Options to clone virtual machine.
def copy_clone_vm(src_name, dst_name, options = {})
list_args = ['list', '--json', '-i', src_name]
src_vm = json { execute_prlctl(*list_args) }.first
basepath = File.dirname(src_vm.fetch('Home')).delete_suffix('/')
extension = File.basename(src_vm.fetch('Home')).delete_suffix('/').split('.').last
clonepath = File.join(ENV['HOME'], "Parallels", "#{dst_name}.#{extension}")
execute('cp', '-c', '-R', '-p', src_vm.fetch('Home'), clonepath)

# Update config.pvs with dst_name as this is what Parallels uses when registering
update_vm_name(File.join(clonepath, 'config.pvs'), dst_name)

# Register the cloned path as a new VM
args = ['register', clonepath]
# Regenerate SourceVmUuid of the cloned VM
args << '--regenerate-src-uuid' if options[:regenerate_src_uuid]

# Regenerate SourceVmUuid of the cloned VM
execute_prlctl(*args)

# Don't need the box hanging around in Parallels
execute_prlctl('unregister', src_name)
end

def update_vm_name(config_pvs_path, name)
xml = Nokogiri::XML(File.read(config_pvs_path))
elem = xml.at_xpath('//ParallelsVirtualMachine/Identification/VmName')
elem.content = name
File.write(config_pvs_path, xml.to_xml)
end

# Compacts the specified virtual disk image
Expand Down
19 changes: 19 additions & 0 deletions lib/vagrant-parallels/util/common.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require 'shellwords'

module VagrantPlugins
module Parallels
module Util
Expand All @@ -9,6 +11,23 @@ def self.is_macvm(machine)
return !machine.box.nil? && !!Dir.glob(machine.box.directory.join('*.macvm')).first
end

# Determines if the box directory is on an APFS filesystem
def self.is_apfs?(path, &block)
output = {stdout: '', stderr: ''}
df_command = %w[df -T apfs]
df_command << Shellwords.escape(path)
execute(*df_command, &block).exit_code == 0
end

private

def self.execute(*command, &block)
command << { notify: [:stdout, :stderr] }

Vagrant::Util::Busy.busy(lambda {}) do
Vagrant::Util::Subprocess.execute(*command, &block)
end
end
end
end
end
Expand Down
43 changes: 42 additions & 1 deletion test/unit/support/shared/pd_driver_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,56 @@
end

describe 'clone_vm' do
it 'clones VM to the new one' do
before do
expect(subprocess).to receive(:execute).twice.
with('prlctl', 'list', '--json', '-i', an_instance_of(String),
an_instance_of(Hash)).
and_return(subprocess_result(stdout: '[{"Home": "/home/some/path"}]', exit_code: 0))
expect(subprocess).to receive(:execute).
with('prlctl', 'list', '--all', '--no-header', '--json', '-o', 'name,uuid', {:notify=>[:stdout, :stderr]}).
and_return(subprocess_result(stdout: '[]', exit_code: 0))
expect(subprocess).to receive(:execute).
with('prlctl', 'list', '--all', '--no-header', '--json', '-o', 'name,uuid', '--template', {:notify=>[:stdout, :stderr]}).
and_return(subprocess_result(stdout: '[]', exit_code: 0))
end

it 'clones VM to the new one when not on APFS' do
expect(subprocess).to receive(:execute).
with('df', '-T', 'apfs', '/home/some/path',
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 1))
expect(subprocess).to receive(:execute).
with('prlctl', 'clone', tpl_uuid, '--name', an_instance_of(String),
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 0))
subject.clone_vm(tpl_uuid)
end

it 'uses cp to clone VM to the new one when on APFS' do
expect(subprocess).to receive(:execute).
with('df', '-T', 'apfs', '/home/some/path',
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 0))
expect(subprocess).to receive(:execute).
with('cp', '-c', '-R', '-p', '/home/some/path', an_instance_of(String),
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 0))
expect(subprocess).to receive(:execute).
with('prlctl', 'register', an_instance_of(String),
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 0))
expect(subprocess).to receive(:execute).
with('prlctl', 'unregister', tpl_uuid, an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 0))
expect(driver).to receive(:update_vm_name).and_return(true)
subject.clone_vm(tpl_uuid)
end

it 'clones VM to the exported VM' do
expect(subprocess).to receive(:execute).
with('df', '-T', 'apfs', '/home/some/path',
an_instance_of(Hash)).
and_return(subprocess_result(exit_code: 1))
expect(subprocess).to receive(:execute).
with('prlctl', 'clone', uuid, '--name', an_instance_of(String),
'--dst', an_instance_of(String), an_instance_of(Hash)).
Expand Down

0 comments on commit c10beb2

Please sign in to comment.