Add Windows support to Hyper-V synced folder plugin

This commit is contained in:
Zhongcheng Lao 2019-06-19 19:16:14 +08:00
parent dbe1507e47
commit a93e1215b3
5 changed files with 558 additions and 0 deletions

View File

@ -1,3 +1,5 @@
require 'json'
module VagrantPlugins
module GuestWindows
module Cap
@ -59,6 +61,47 @@ module VagrantPlugins
end
true
end
# Create directories at given locations on guest
#
# @param [Vagrant::Machine] machine Vagrant guest machine
# @param [array] paths to create on guest
def self.create_directories(machine, dirs)
return [] if dirs.empty?
remote_fn = create_tmp_path(machine, {})
tmp = Tempfile.new('hv_dirs')
begin
tmp.write dirs.join("\n") + "\n"
tmp.close
machine.communicate.upload(tmp.path, remote_fn)
ensure
tmp.close
tmp.unlink
end
created_paths = []
cmd = <<-EOH.gsub(/^ {6}/, "")
$files = Get-Content #{remote_fn}
foreach ($file in $files) {
if (-Not (Test-Path($file))) {
ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName)
} else {
if (-Not ((Get-Item $file) -is [System.IO.DirectoryInfo])) {
# Remove the file
Remove-Item -Path $file -Force
ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName)
}
}
}
EOH
machine.communicate.execute(cmd, shell: :powershell) do |type, data|
if type == :stdout
obj = JSON.parse(data)
created_paths << obj["FullName"].strip unless obj["FullName"].nil?
end
end
created_paths
end
end
end
end

View File

@ -0,0 +1,104 @@
require "vagrant/util/hyperv_daemons"
module VagrantPlugins
module GuestWindows
module Cap
class HypervDaemons
HYPERV_DAEMON_SERVICES = {kvp: "vmickvpexchange", vss: "vmicvss", fcopy: "vmicguestinterface" }
# https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicecontrollerstatus?view=netframework-4.8
STOPPED = 1
START_PENDING = 2
STOP_PENDING = 3
RUNNING = 4
CONTINUE_PENDING = 5
PAUSE_PENDING = 6
PAUSED = 7
MANUAL_MODE = 3
DISABLED_MODE = 4
def self.hyperv_daemons_activate(machine)
result = HYPERV_DAEMON_SERVICES.keys.map do |service|
hyperv_daemon_activate machine, service
end
result.all?
end
def self.hyperv_daemon_activate(machine, service)
comm = machine.communicate
service_name = hyperv_service_name(machine, service)
daemon_service = service_info(comm, service_name)
return false if daemon_service.nil?
if daemon_service["StartType"] == DISABLED_MODE
return false unless enable_service(comm, service_name)
end
return false unless restart_service(comm, service_name)
hyperv_daemon_running machine, service
end
def self.hyperv_daemons_running(machine)
result = HYPERV_DAEMON_SERVICES.keys.map do |service|
hyperv_daemon_running machine, service.to_sym
end
result.all?
end
def self.hyperv_daemon_running(machine, service)
comm = machine.communicate
service_name = hyperv_service_name(machine, service)
daemon_service = service_info(comm, service_name)
return daemon_service["Status"] == RUNNING unless daemon_service.nil?
false
end
def self.hyperv_daemons_installed(machine)
result = HYPERV_DAEMON_SERVICES.keys.map do |service|
hyperv_daemon_installed machine, service.to_sym
end
result.all?
end
def self.hyperv_daemon_installed(machine, service)
# Windows guest should have Hyper-V service installed
true
end
protected
def self.service_info(comm, service)
cmd = "ConvertTo-Json (Get-Service -Name #{service})"
result = []
comm.execute(cmd, shell: :powershell) do |type, data|
if type == :stdout
result << JSON.parse(data)
end
end
result[0] || {}
end
def self.restart_service(comm, service)
cmd = "Restart-Service -Name #{service} -Force"
comm.execute(cmd, shell: :powershell)
true
end
def self.enable_service(comm, service)
cmd = "Set-Service -Name #{service} -StartupType #{MANUAL_MODE}"
comm.execute(cmd, shell: :powershell)
true
end
def self.hyperv_service_name(machine, service)
hyperv_daemon_name(service)
end
def self.hyperv_daemon_name(service)
HYPERV_DAEMON_SERVICES[service]
end
end
end
end
end

View File

@ -44,6 +44,11 @@ module VagrantPlugins
Cap::FileSystem
end
guest_capability(:windows, :create_directories) do
require_relative "cap/file_system"
Cap::FileSystem
end
guest_capability(:windows, :mount_virtualbox_shared_folder) do
require_relative "cap/mount_shared_folder"
Cap::MountSharedFolder
@ -99,6 +104,21 @@ module VagrantPlugins
Cap::PublicKey
end
guest_capability(:windows, :hyperv_daemons_running) do
require_relative "cap/hyperv_daemons"
Cap::HypervDaemons
end
guest_capability(:windows, :hyperv_daemons_activate) do
require_relative "cap/hyperv_daemons"
Cap::HypervDaemons
end
guest_capability(:windows, :hyperv_daemons_installed) do
require_relative "cap/hyperv_daemons"
Cap::HypervDaemons
end
protected
def self.init!

View File

@ -1,3 +1,4 @@
require 'json'
require_relative "../../../../base"
describe "VagrantPlugins::GuestWindows::Cap::FileSystem" do
@ -82,4 +83,88 @@ describe "VagrantPlugins::GuestWindows::Cap::FileSystem" do
end
end
end
describe ".create_directories" do
let(:cap) { caps.get(:create_directories) }
let(:dirs) { %w(dir1 dir2) }
before { allow(cap).to receive(:create_tmp_path).and_return("TMP_DIR") }
after { expect(cap.create_directories(machine, dirs)).to eql(dirs) }
context "passes directories to be create" do
let(:temp_file) do
double("temp_file").tap do |temp_file|
allow(temp_file).to receive(:close)
allow(temp_file).to receive(:path).and_return("temp_path")
allow(temp_file).to receive(:unlink)
end
end
let(:sudo_block) do
Proc.new do |arg, &proc|
lines = arg.split("\n")
expect(lines[0]).to match(/TMP_DIR/)
dirs.each do |dir|
proc.call :stdout, { FullName: dir }.to_json
end
end
end
let(:cmd) do
<<-EOH.gsub(/^ {6}/, "")
$files = Get-Content TMP_DIR
foreach ($file in $files) {
if (-Not (Test-Path($file))) {
ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName)
} else {
if (-Not ((Get-Item $file) -is [System.IO.DirectoryInfo])) {
# Remove the file
Remove-Item -Path $file -Force
ConvertTo-Json (New-Item $file -type directory -Force | Select-Object FullName)
}
}
}
EOH
end
before do
allow(Tempfile).to receive(:new).and_return(temp_file)
allow(temp_file).to receive(:write)
allow(temp_file).to receive(:close)
allow(comm).to receive(:upload)
allow(comm).to receive(:execute, &sudo_block)
end
it "creates temporary file on guest" do
expect(cap).to receive(:create_tmp_path)
end
it "creates a temporary file to write dir list" do
expect(Tempfile).to receive(:new).and_return(temp_file)
end
it "writes dir list to a local temporary file" do
expect(temp_file).to receive(:write).with(dirs.join("\n") + "\n")
end
it "uploads the local temporary file with dir list to guest" do
expect(comm).to receive(:upload).with("temp_path", "TMP_DIR")
end
it "executes bash script to create directories on guest" do
expect(comm).to receive(:execute, &sudo_block).with(cmd, shell: :powershell)
end
end
context "passes empty dir list" do
let(:dirs) { [] }
after { expect(cap.create_directories(machine, dirs)).to eql([]) }
it "does nothing" do
expect(cap).to receive(:create_tmp_path).never
expect(Tempfile).to receive(:new).never
expect(comm).to receive(:upload).never
expect(comm).to receive(:execute).never
end
end
end
end

View File

@ -0,0 +1,306 @@
require 'json'
require_relative "../../../../base"
require Vagrant.source_root.join("plugins/guests/windows/cap/hyperv_daemons")
describe VagrantPlugins::GuestWindows::Cap::HypervDaemons do
HYPERV_DAEMON_SERVICES = {kvp: "vmickvpexchange", vss: "vmicvss", fcopy: "vmicguestinterface" }
STOPPED = 1
RUNNING = 4
MANUAL_MODE = 3
DISABLED_MODE = 4
include_context "unit"
let(:machine) do
double("machine").tap do |machine|
allow(machine).to receive(:communicate).and_return(comm)
end
end
let(:comm) { double("comm") }
def name_for(service)
HYPERV_DAEMON_SERVICES[service]
end
def service_status(name, running: true, disabled: false)
{ "Name" => name,
"Status" => running ? RUNNING : STOPPED,
"StartType" => disabled ? DISABLED_MODE : MANUAL_MODE }
end
context "test declared methods" do
subject { described_class }
describe "#hyperv_daemon_running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
context "daemon #{service}" do
let(:service) { service }
let(:service_name) { name_for(service) }
it "checks daemon is running" do
expect(subject).to receive(:service_info).
with(comm, service_name).and_return(service_status(service_name, running: true))
expect(subject.hyperv_daemon_running(machine, service)).to be_truthy
end
it "checks daemon is not running" do
expect(subject).to receive(:service_info).
with(comm, service_name).and_return(service_status(service_name, running: false))
expect(subject.hyperv_daemon_running(machine, service)).to be_falsy
end
end
end
end
describe "#hyperv_daemons_running" do
it "checks hyperv daemons are running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(true)
end
expect(subject.hyperv_daemons_running(machine)).to be_truthy
end
it "checks hyperv daemons are not running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_running).with(machine, service).and_return(false)
end
expect(subject.hyperv_daemons_running(machine)).to be_falsy
end
end
describe "#hyperv_daemon_installed" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
context "daemon #{service}" do
let(:service) { service }
before { expect(described_class.hyperv_daemon_installed(subject, service)).to be_truthy }
it "does not call communicate#execute" do
expect(comm).to receive(:execute).never
end
end
end
end
describe "#hyperv_daemons_installed" do
it "checks hyperv daemons are running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(true)
end
expect(subject.hyperv_daemons_installed(machine)).to be_truthy
expect(comm).to receive(:execute).never
end
it "checks hyperv daemons are not running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_installed).with(machine, service).and_return(false)
end
expect(subject.hyperv_daemons_installed(machine)).to be_falsy
expect(comm).to receive(:execute).never
end
end
describe "#hyperv_daemon_activate" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
context "daemon #{service}" do
let(:service) { service }
let(:service_name) { name_for(service) }
let(:service_disabled_status) { service_status(service_name, disabled: true, running: false) }
let(:service_stopped_status) { service_status(service_name, running: false) }
let(:service_running_status) { service_status(service_name) }
context "activate succeeds" do
after { expect(subject.hyperv_daemon_activate(machine, service)).to be_truthy }
it "enables the service when service disabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
end
it "only restarts the service when service enabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
expect(subject).to receive(:enable_service).never
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
end
end
context "activate fails" do
after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy }
it "enables the service when service disabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_stopped_status)
end
it "does not restart service when failed to enable it" do
expect(subject).to receive(:service_info).
with(comm, service_name).and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(false)
expect(subject).to receive(:restart_service).never
end
end
end
end
end
describe "#hyperv_daemons_activate" do
it "activates hyperv daemons" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(true)
end
expect(subject.hyperv_daemons_activate(machine)).to be_truthy
end
it "fails to activate hyperv daemons" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(subject).to receive(:hyperv_daemon_activate).with(machine, service).and_return(false)
end
expect(subject.hyperv_daemons_activate(machine)).to be_falsy
end
end
describe "#hyperv_daemon_activate" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
context "daemon #{service}" do
let(:service) { service }
let(:service_name) { name_for(service) }
let(:service_disabled_status) { service_status(service_name, disabled: true, running: false) }
let(:service_stopped_status) { service_status(service_name, running: false) }
let(:service_running_status) { service_status(service_name) }
context "activate succeeds" do
after { expect(subject.hyperv_daemon_activate(machine, service)).to be_truthy }
it "enables the service when service disabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
end
it "only restarts the service when service enabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
expect(subject).to receive(:enable_service).never
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_running_status)
end
end
context "activate fails" do
after { expect(subject.hyperv_daemon_activate(machine, service)).to be_falsy }
it "enables the service when service disabled" do
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:restart_service).with(comm, service_name).and_return(true)
expect(subject).to receive(:service_info).
with(comm, service_name).ordered.and_return(service_stopped_status)
end
it "does not restart service when failed to enable it" do
expect(subject).to receive(:service_info).
with(comm, service_name).and_return(service_disabled_status)
expect(subject).to receive(:enable_service).with(comm, service_name).and_return(false)
expect(subject).to receive(:restart_service).never
end
end
end
end
end
describe "#service_info" do
let(:service_name) { name_for(:kvp) }
let(:status) { service_status(service_name) }
it "executes powershell script" do
cmd = "ConvertTo-Json (Get-Service -Name #{service_name})"
expect(comm).to receive(:execute).with(cmd, shell: :powershell) do |&proc|
proc.call :stdout, status.to_json
end
expect(subject.send(:service_info, comm, service_name)).to eq(status)
end
end
describe "#restart_service" do
let(:service_name) { name_for(:kvp) }
let(:status) { service_status(service_name) }
it "executes powershell script" do
cmd = "Restart-Service -Name #{service_name} -Force"
expect(comm).to receive(:execute).with(cmd, shell: :powershell)
expect(subject.send(:restart_service, comm, service_name)).to be_truthy
end
end
describe "#enable_service" do
let(:service_name) { name_for(:kvp) }
let(:status) { service_status(service_name) }
it "executes powershell script" do
cmd = "Set-Service -Name #{service_name} -StartupType #{MANUAL_MODE}"
expect(comm).to receive(:execute).with(cmd, shell: :powershell)
expect(subject.send(:enable_service, comm, service_name)).to be_truthy
end
end
end
context "calls through guest capabilities" do
let(:caps) do
VagrantPlugins::GuestWindows::Plugin.components.guest_capabilities[:windows]
end
describe "#hyperv_daemons_running" do
let(:cap) { caps.get(:hyperv_daemons_running) }
it "checks hyperv daemons are running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(cap).to receive(:hyperv_daemon_running).with(machine, service).and_return(true)
end
expect(cap.hyperv_daemons_running(machine)).to be_truthy
end
end
describe "#hyperv_daemons_installed" do
let(:cap) { caps.get(:hyperv_daemons_installed) }
it "checks hyperv daemons are running" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(cap).to receive(:hyperv_daemon_installed).with(machine, service).and_return(true)
end
expect(cap.hyperv_daemons_installed(machine)).to be_truthy
end
end
describe "#hyperv_daemons_activate" do
let(:cap) { caps.get(:hyperv_daemons_activate) }
it "activates hyperv daemons" do
HYPERV_DAEMON_SERVICES.keys.each do |service|
expect(cap).to receive(:hyperv_daemon_activate).with(machine, service).and_return(true)
end
expect(cap.hyperv_daemons_activate(machine)).to be_truthy
end
end
end
end