Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use clonefile copy for macvm boxes #459

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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