Add support for cygwin guest

This commit is contained in:
Zdenek Zambersky 2019-09-11 18:40:50 +02:00
parent 84bf9aefe9
commit 3eaa62364d
11 changed files with 610 additions and 0 deletions

View File

@ -0,0 +1,79 @@
module VagrantPlugins
module GuestCygwin
module Cap
class FileSystem
# Create a temporary file or directory on the guest
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [Hash] opts Path options
# @return [String] path to temporary file or directory
def self.create_tmp_path(machine, opts)
template = "vagrant-XXXXXX"
if opts[:extension]
template << opts[:extension].to_s
end
cmd = ["mktemp", "--tmpdir"]
if opts[:type] == :directory
cmd << "-d"
end
cmd << template
tmp_path = ""
machine.communicate.execute(cmd.join(" ")) do |type, data|
if type == :stdout
tmp_path << data
end
end
tmp_path.strip
end
# Decompress tgz file on guest to given location
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [String] compressed_file Path to compressed file on guest
# @param [String] destination Path for decompressed files on guest
def self.decompress_tgz(machine, compressed_file, destination, opts={})
comm = machine.communicate
extract_dir = create_tmp_path(machine, type: :directory)
cmds = []
if opts[:type] == :directory
cmds << "mkdir -p '#{destination}'"
else
cmds << "mkdir -p '#{File.dirname(destination)}'"
end
cmds += [
"tar -C '#{extract_dir}' -xzf '#{compressed_file}'",
"mv '#{extract_dir}'/* '#{destination}'",
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
true
end
# Decompress zip file on guest to given location
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [String] compressed_file Path to compressed file on guest
# @param [String] destination Path for decompressed files on guest
def self.decompress_zip(machine, compressed_file, destination, opts={})
comm = machine.communicate
extract_dir = create_tmp_path(machine, type: :directory)
cmds = []
if opts[:type] == :directory
cmds << "mkdir -p '#{destination}'"
else
cmds << "mkdir -p '#{File.dirname(destination)}'"
end
cmds += [
"unzip '#{compressed_file}' -d '#{extract_dir}'",
"mv '#{extract_dir}'/* '#{destination}'",
"rm -f '#{compressed_file}'",
"rm -rf '#{extract_dir}'"
]
cmds.each{ |cmd| comm.execute(cmd) }
true
end
end
end
end
end

View File

@ -0,0 +1,62 @@
require "tempfile"
require "vagrant/util/shell_quote"
module VagrantPlugins
module GuestCygwin
module Cap
class PublicKey
def self.insert_public_key(machine, contents)
comm = machine.communicate
contents = contents.strip << "\n"
remote_path = "/tmp/vagrant-insert-pubkey-#{Time.now.to_i}"
Tempfile.open("vagrant-linux-insert-public-key") do |f|
f.binmode
f.write(contents)
f.fsync
f.close
comm.upload(f.path, remote_path)
end
# Use execute (not sudo) because we want to execute this as the SSH
# user (which is "vagrant" by default).
comm.execute <<-EOH.gsub(/^ */, "")
mkdir -p ~/.ssh
chmod 0700 ~/.ssh
cat '#{remote_path}' >> ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys
result=$?
rm -f '#{remote_path}'
exit $result
EOH
end
def self.remove_public_key(machine, contents)
comm = machine.communicate
contents = contents.strip << "\n"
remote_path = "/tmp/vagrant-remove-pubkey-#{Time.now.to_i}"
Tempfile.open("vagrant-linux-remove-public-key") do |f|
f.binmode
f.write(contents)
f.fsync
f.close
comm.upload(f.path, remote_path)
end
# Use execute (not sudo) because we want to execute this as the SSH
# user (which is "vagrant" by default).
comm.execute <<-EOH.sub(/^ */, "")
if test -f ~/.ssh/authorized_keys; then
grep -v -x -f '#{remote_path}' ~/.ssh/authorized_keys > ~/.ssh/authorized_keys.tmp
mv ~/.ssh/authorized_keys.tmp ~/.ssh/authorized_keys && chmod 0600 ~/.ssh/authorized_keys
result=$?
fi
rm -f '#{remote_path}'
exit $result
EOH
end
end
end
end
end

View File

@ -0,0 +1,27 @@
require_relative "../../../synced_folders/rsync/default_unix_cap"
module VagrantPlugins
module GuestCygwin
module Cap
class RSync
extend VagrantPlugins::SyncedFolderRSync::DefaultUnixCap
def self.build_rsync_chown(opts)
guest_path = Shellwords.escape(opts[:guestpath])
if(opts[:exclude] && !Array(opts[:exclude]).empty?)
exclude_base = Pathname.new(opts[:guestpath])
exclusions = Array(opts[:exclude]).map do |ex_path|
ex_path = ex_path.slice(1, ex_path.size) if ex_path.start_with?(File::SEPARATOR)
"-path #{Shellwords.escape(exclude_base.join(ex_path))} -prune"
end.join(" -o ") + " -o "
end
# in cygwin group does not automatically exists for user (so ignore group)
"find #{guest_path} #{exclusions}" \
"'!' -type l -a " \
"'(' ! -user #{opts[:owner]} ')' -exec " \
"chown #{opts[:owner]} '{}' +"
end
end
end
end
end

View File

@ -0,0 +1,32 @@
module VagrantPlugins
module GuestCygwin
module Cap
class ShellExpandGuestPath
def self.shell_expand_guest_path(machine, path)
real_path = nil
path = path.gsub(/ /, '\ ')
machine.communicate.execute("echo; printf #{path}") do |type, data|
if type == :stdout
real_path ||= ""
real_path += data
end
end
if real_path
# The last line is the path we care about
real_path = real_path.split("\n").last.chomp
end
if !real_path
# If no real guest path was detected, this is really strange
# and we raise an exception because this is a bug.
raise Vagrant::Errors::ShellExpandFailed
end
# Chomp the string so that any trailing newlines are killed
return real_path.chomp
end
end
end
end
end

View File

@ -0,0 +1,12 @@
module VagrantPlugins
module GuestCygwin
class Guest < Vagrant.plugin("2", :guest)
# Name used for guest detection
GUEST_DETECTION_NAME = "cygwin".freeze
def detect?(machine)
machine.communicate.test("uname | grep -i cygwin")
end
end
end
end

View File

@ -0,0 +1,66 @@
require "vagrant"
module VagrantPlugins
module GuestCygwin
class Plugin < Vagrant.plugin("2")
name "Cygwin guest."
description "Cygwin guest support."
guest(:cygwin) do
require_relative "guest"
Guest
end
guest_capability(:cygwin, :create_tmp_path) do
require_relative "cap/file_system"
Cap::FileSystem
end
guest_capability(:cygwin, :decompress_tgz) do
require_relative "cap/file_system"
Cap::FileSystem
end
guest_capability(:cygwin, :decompress_zip) do
require_relative "cap/file_system"
Cap::FileSystem
end
guest_capability(:cygwin, :insert_public_key) do
require_relative "cap/public_key"
Cap::PublicKey
end
guest_capability(:cygwin, :remove_public_key) do
require_relative "cap/public_key"
Cap::PublicKey
end
guest_capability(:cygwin, :shell_expand_guest_path) do
require_relative "cap/shell_expand_guest_path"
Cap::ShellExpandGuestPath
end
guest_capability(:cygwin, :rsync_installed) do
require_relative "cap/rsync"
Cap::RSync
end
guest_capability(:cygwin, :rsync_command) do
require_relative "cap/rsync"
Cap::RSync
end
guest_capability(:cygwin, :rsync_post) do
require_relative "cap/rsync"
Cap::RSync
end
guest_capability(:cygwin, :rsync_pre) do
require_relative "cap/rsync"
Cap::RSync
end
end
end
end

View File

@ -0,0 +1,127 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestCygwin::Cap::FileSystem" do
let(:caps) do
VagrantPlugins::GuestCygwin::Plugin
.components
.guest_capabilities[:cygwin]
end
let(:machine) { double("machine", communicate: comm) }
let(:comm) { double("comm") }
before { allow(comm).to receive(:execute) }
describe ".create_tmp_path" do
let(:cap) { caps.get(:create_tmp_path) }
let(:opts) { {} }
it "should generate path on guest" do
expect(comm).to receive(:execute).with(/mktemp/)
cap.create_tmp_path(machine, opts)
end
it "should capture path generated on guest" do
expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH")
expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH")
end
it "should strip newlines on path" do
expect(comm).to receive(:execute).with(/mktemp/).and_yield(:stdout, "TMP_PATH\n")
expect(cap.create_tmp_path(machine, opts)).to eq("TMP_PATH")
end
context "when type is a directory" do
before { opts[:type] = :directory }
it "should create guest path as a directory" do
expect(comm).to receive(:execute).with(/-d/)
cap.create_tmp_path(machine, opts)
end
end
end
describe ".decompress_tgz" do
let(:cap) { caps.get(:decompress_tgz) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_tgz(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file with tar" do
expect(comm).to receive(:execute).with(/tar/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
end
end
end
describe ".decompress_zip" do
let(:cap) { caps.get(:decompress_zip) }
let(:comp) { "compressed_file" }
let(:dest) { "path/to/destination" }
let(:opts) { {} }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after{ cap.decompress_zip(machine, comp, dest, opts) }
it "should create temporary directory for extraction" do
expect(cap).to receive(:create_tmp_path)
end
it "should extract file with zip" do
expect(comm).to receive(:execute).with(/zip/)
end
it "should extract file to temporary directory" do
expect(comm).to receive(:execute).with(/TMP_DIR/)
end
it "should remove compressed file from guest" do
expect(comm).to receive(:execute).with(/rm .*#{comp}/)
end
it "should remove extraction directory from guest" do
expect(comm).to receive(:execute).with(/rm .*TMP_DIR/)
end
it "should create parent directories for destination" do
expect(comm).to receive(:execute).with(/mkdir -p .*to'/)
end
context "when type is directory" do
before { opts[:type] = :directory }
it "should create destination directory" do
expect(comm).to receive(:execute).with(/mkdir -p .*destination'/)
end
end
end
end

View File

@ -0,0 +1,32 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestCygwin::Cap::InsertPublicKey" do
let(:caps) do
VagrantPlugins::GuestCygwin::Plugin
.components
.guest_capabilities[:cygwin]
end
let(:machine) { double("machine") }
let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) }
before do
allow(machine).to receive(:communicate).and_return(comm)
end
after do
comm.verify_expectations!
end
describe ".insert_public_key" do
let(:cap) { caps.get(:insert_public_key) }
it "inserts the public key" do
cap.insert_public_key(machine, "ssh-rsa ...")
expect(comm.received_commands[0]).to match(/mkdir -p ~\/.ssh/)
expect(comm.received_commands[0]).to match(/chmod 0700 ~\/.ssh/)
expect(comm.received_commands[0]).to match(/cat '\/tmp\/vagrant-(.+)' >> ~\/.ssh\/authorized_keys/)
expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/)
end
end
end

View File

@ -0,0 +1,32 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestCygwin::Cap::RemovePublicKey" do
let(:caps) do
VagrantPlugins::GuestCygwin::Plugin
.components
.guest_capabilities[:cygwin]
end
let(:machine) { double("machine") }
let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) }
before do
allow(machine).to receive(:communicate).and_return(comm)
end
after do
comm.verify_expectations!
end
describe ".remove_public_key" do
let(:cap) { caps.get(:remove_public_key) }
it "removes the public key" do
cap.remove_public_key(machine, "ssh-rsa ...")
expect(comm.received_commands[0]).to match(/grep -v -x -f '\/tmp\/vagrant-(.+)' ~\/\.ssh\/authorized_keys > ~\/.ssh\/authorized_keys\.tmp/)
expect(comm.received_commands[0]).to match(/mv ~\/.ssh\/authorized_keys\.tmp ~\/.ssh\/authorized_keys/)
expect(comm.received_commands[0]).to match(/chmod 0600 ~\/.ssh\/authorized_keys/)
expect(comm.received_commands[0]).to match(/rm -f '\/tmp\/vagrant-(.+)'/)
end
end
end

View File

@ -0,0 +1,97 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestCygwin::Cap::Rsync" do
let(:caps) do
VagrantPlugins::GuestCygwin::Plugin
.components
.guest_capabilities[:cygwin]
end
let(:machine) { double("machine") }
let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) }
let(:guest_directory){ "/guest/directory/path" }
before do
allow(machine).to receive(:communicate).and_return(comm)
end
after do
comm.verify_expectations!
end
describe ".rsync_installed" do
let(:cap) { caps.get(:rsync_installed) }
it "checks if the command is installed" do
comm.expect_command("which rsync")
cap.rsync_installed(machine)
end
end
describe ".rsync_command" do
let(:cap) { caps.get(:rsync_command) }
it "provides the rsync command to use" do
expect(cap.rsync_command(machine)).to eq("sudo rsync")
end
end
describe ".rsync_pre" do
let(:cap) { caps.get(:rsync_pre) }
it "creates target directory on guest" do
comm.expect_command("mkdir -p #{guest_directory}")
cap.rsync_pre(machine, :guestpath => guest_directory)
end
end
describe ".rsync_post" do
let(:cap) { caps.get(:rsync_post) }
let(:host_directory){ '.' }
let(:owner) { "vagrant-user" }
let(:group) { "vagrant-group" }
let(:excludes) { false }
let(:options) do
{
hostpath: host_directory,
guestpath: guest_directory,
owner: owner,
group: group,
exclude: excludes
}
end
it "chowns files within the guest directory" do
comm.expect_command(
"find #{guest_directory} '!' -type l -a '(' ! -user #{owner} " \
"')' -exec chown #{owner} '{}' +"
)
cap.rsync_post(machine, options)
end
context "with excludes provided" do
let(:excludes){ ["tmp", "state/*", "path/with a/space"] }
it "ignores files that are excluded" do
# comm.expect_command(
# "find #{guest_directory} -path #{Shellwords.escape(File.join(guest_directory, excludes.first))} -prune -o " \
# "-path #{Shellwords.escape(File.join(guest_directory, excludes.last))} -prune -o '!' " \
# "-path -type l -a '(' ! -user " \
# "#{owner} -or ! -group #{group} ')' -exec chown #{owner}:#{group} '{}' +"
# )
cap.rsync_post(machine, options)
excludes.each do |ex_path|
expect(comm.received_commands.first).to include("-path #{Shellwords.escape(File.join(guest_directory, ex_path))} -prune")
end
end
it "properly escapes excluded directories" do
cap.rsync_post(machine, options)
exclude_with_space = excludes.detect{|ex| ex.include?(' ')}
escaped_exclude_with_space = Shellwords.escape(exclude_with_space)
expect(comm.received_commands.first).not_to include(exclude_with_space)
expect(comm.received_commands.first).to include(escaped_exclude_with_space)
end
end
end
end

View File

@ -0,0 +1,44 @@
require_relative "../../../../base"
describe "VagrantPlugins::GuestCygwin::Cap::ShellExpandGuestPath" do
let(:caps) do
VagrantPlugins::GuestCygwin::Plugin
.components
.guest_capabilities[:cygwin]
end
let(:machine) { double("machine") }
let(:comm) { VagrantTests::DummyCommunicator::Communicator.new(machine) }
before do
allow(machine).to receive(:communicate).and_return(comm)
end
describe "#shell_expand_guest_path" do
let(:cap) { caps.get(:shell_expand_guest_path) }
it "expands the path" do
path = "/home/vagrant/folder"
allow(machine.communicate).to receive(:execute).
with(any_args).and_yield(:stdout, "/home/vagrant/folder")
cap.shell_expand_guest_path(machine, path)
end
it "raises an exception if no path was detected" do
path = "/home/vagrant/folder"
expect { cap.shell_expand_guest_path(machine, path) }.
to raise_error(Vagrant::Errors::ShellExpandFailed)
end
it "returns a path with a space in it" do
path = "/home/vagrant folder/folder"
path_with_spaces = "/home/vagrant\\ folder/folder"
allow(machine.communicate).to receive(:execute).
with(any_args).and_yield(:stdout, path_with_spaces)
expect(machine.communicate).to receive(:execute).with("echo; printf #{path_with_spaces}")
cap.shell_expand_guest_path(machine, path)
end
end
end