From 3eaa62364d0586f8f3761132d03408c2ecc640ab Mon Sep 17 00:00:00 2001 From: Zdenek Zambersky Date: Wed, 11 Sep 2019 18:40:50 +0200 Subject: [PATCH] Add support for cygwin guest --- plugins/guests/cygwin/cap/file_system.rb | 79 +++++++++++ plugins/guests/cygwin/cap/public_key.rb | 62 +++++++++ plugins/guests/cygwin/cap/rsync.rb | 27 ++++ .../cygwin/cap/shell_expand_guest_path.rb | 32 +++++ plugins/guests/cygwin/guest.rb | 12 ++ plugins/guests/cygwin/plugin.rb | 66 +++++++++ .../guests/cygwin/cap/file_system_test.rb | 127 ++++++++++++++++++ .../cygwin/cap/insert_public_key_test.rb | 32 +++++ .../cygwin/cap/remove_public_key_test.rb | 32 +++++ .../plugins/guests/cygwin/cap/rsync_test.rb | 97 +++++++++++++ .../cap/shell_expand_guest_path_test.rb | 44 ++++++ 11 files changed, 610 insertions(+) create mode 100644 plugins/guests/cygwin/cap/file_system.rb create mode 100644 plugins/guests/cygwin/cap/public_key.rb create mode 100644 plugins/guests/cygwin/cap/rsync.rb create mode 100644 plugins/guests/cygwin/cap/shell_expand_guest_path.rb create mode 100644 plugins/guests/cygwin/guest.rb create mode 100644 plugins/guests/cygwin/plugin.rb create mode 100644 test/unit/plugins/guests/cygwin/cap/file_system_test.rb create mode 100644 test/unit/plugins/guests/cygwin/cap/insert_public_key_test.rb create mode 100644 test/unit/plugins/guests/cygwin/cap/remove_public_key_test.rb create mode 100644 test/unit/plugins/guests/cygwin/cap/rsync_test.rb create mode 100644 test/unit/plugins/guests/cygwin/cap/shell_expand_guest_path_test.rb diff --git a/plugins/guests/cygwin/cap/file_system.rb b/plugins/guests/cygwin/cap/file_system.rb new file mode 100644 index 000000000..c8df4786c --- /dev/null +++ b/plugins/guests/cygwin/cap/file_system.rb @@ -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 diff --git a/plugins/guests/cygwin/cap/public_key.rb b/plugins/guests/cygwin/cap/public_key.rb new file mode 100644 index 000000000..a42f42862 --- /dev/null +++ b/plugins/guests/cygwin/cap/public_key.rb @@ -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 diff --git a/plugins/guests/cygwin/cap/rsync.rb b/plugins/guests/cygwin/cap/rsync.rb new file mode 100644 index 000000000..5b9084670 --- /dev/null +++ b/plugins/guests/cygwin/cap/rsync.rb @@ -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 diff --git a/plugins/guests/cygwin/cap/shell_expand_guest_path.rb b/plugins/guests/cygwin/cap/shell_expand_guest_path.rb new file mode 100644 index 000000000..9e4794043 --- /dev/null +++ b/plugins/guests/cygwin/cap/shell_expand_guest_path.rb @@ -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 diff --git a/plugins/guests/cygwin/guest.rb b/plugins/guests/cygwin/guest.rb new file mode 100644 index 000000000..4ffca50d3 --- /dev/null +++ b/plugins/guests/cygwin/guest.rb @@ -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 diff --git a/plugins/guests/cygwin/plugin.rb b/plugins/guests/cygwin/plugin.rb new file mode 100644 index 000000000..2a694ada0 --- /dev/null +++ b/plugins/guests/cygwin/plugin.rb @@ -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 diff --git a/test/unit/plugins/guests/cygwin/cap/file_system_test.rb b/test/unit/plugins/guests/cygwin/cap/file_system_test.rb new file mode 100644 index 000000000..cef35347a --- /dev/null +++ b/test/unit/plugins/guests/cygwin/cap/file_system_test.rb @@ -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 diff --git a/test/unit/plugins/guests/cygwin/cap/insert_public_key_test.rb b/test/unit/plugins/guests/cygwin/cap/insert_public_key_test.rb new file mode 100644 index 000000000..edc730577 --- /dev/null +++ b/test/unit/plugins/guests/cygwin/cap/insert_public_key_test.rb @@ -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 diff --git a/test/unit/plugins/guests/cygwin/cap/remove_public_key_test.rb b/test/unit/plugins/guests/cygwin/cap/remove_public_key_test.rb new file mode 100644 index 000000000..60e94bc62 --- /dev/null +++ b/test/unit/plugins/guests/cygwin/cap/remove_public_key_test.rb @@ -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 diff --git a/test/unit/plugins/guests/cygwin/cap/rsync_test.rb b/test/unit/plugins/guests/cygwin/cap/rsync_test.rb new file mode 100644 index 000000000..0cfc2464e --- /dev/null +++ b/test/unit/plugins/guests/cygwin/cap/rsync_test.rb @@ -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 diff --git a/test/unit/plugins/guests/cygwin/cap/shell_expand_guest_path_test.rb b/test/unit/plugins/guests/cygwin/cap/shell_expand_guest_path_test.rb new file mode 100644 index 000000000..a138d8033 --- /dev/null +++ b/test/unit/plugins/guests/cygwin/cap/shell_expand_guest_path_test.rb @@ -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