Chris Roberts 051c7a7823 Fix sudo usage in exports write within linux host cap
Only move new exports file to destination without sudo when the
file has write access and the directory has write access. Always
use sudo when changing file ownership.
2018-08-02 15:13:27 -07:00

341 lines
13 KiB
Ruby

require_relative "../../../../base"
require_relative "../../../../../../plugins/hosts/linux/cap/nfs"
require_relative "../../../../../../lib/vagrant/util"
describe VagrantPlugins::HostLinux::Cap::NFS do
include_context "unit"
let(:caps) do
VagrantPlugins::HostLinux::Plugin
.components
.host_capabilities[:linux]
end
let(:tmp_exports_path) do
@tmp_exports ||= temporary_file
end
let(:exports_path){ VagrantPlugins::HostLinux::Cap::NFS::NFS_EXPORTS_PATH }
let(:env){ double(:env) }
let(:ui){ double(:ui) }
let(:host){ double(:host) }
before do
@original_exports_path = VagrantPlugins::HostLinux::Cap::NFS::NFS_EXPORTS_PATH
VagrantPlugins::HostLinux::Cap::NFS.send(:remove_const, :NFS_EXPORTS_PATH)
VagrantPlugins::HostLinux::Cap::NFS.const_set(:NFS_EXPORTS_PATH, tmp_exports_path.to_s)
allow(Vagrant::Util::Subprocess).to receive(:execute).with("systemctl", "list-units", any_args).
and_return(Vagrant::Util::Subprocess::Result.new(1, "", ""))
allow(Vagrant::Util::Platform).to receive(:systemd?).and_return(false)
end
after do
VagrantPlugins::HostLinux::Cap::NFS.send(:remove_const, :NFS_EXPORTS_PATH)
VagrantPlugins::HostLinux::Cap::NFS.const_set(:NFS_EXPORTS_PATH, @original_exports_path)
VagrantPlugins::HostLinux::Cap::NFS.reset!
File.unlink(tmp_exports_path.to_s) if File.exist?(tmp_exports_path.to_s)
@tmp_exports = nil
end
describe ".nfs_service_name_systemd" do
let(:cap){ VagrantPlugins::HostLinux::Cap::NFS }
context "without service match" do
it "should use default service name" do
expect(cap.nfs_service_name_systemd).to eq(cap.const_get(:NFS_DEFAULT_NAME_SYSTEMD))
end
end
context "with service match" do
let(:custom_nfs_service_name){ "custom-nfs-server-service-name" }
before{ expect(Vagrant::Util::Subprocess).to receive(:execute).with("systemctl", "list-units", any_args).
and_return(Vagrant::Util::Subprocess::Result.new(0, custom_nfs_service_name, "")) }
it "should use the matched service name" do
expect(cap.nfs_service_name_systemd).to eq(custom_nfs_service_name)
end
end
end
describe ".nfs_service_name_sysv" do
let(:cap){ VagrantPlugins::HostLinux::Cap::NFS }
context "without service match" do
it "should use default service name" do
expect(cap.nfs_service_name_sysv).to eq(cap.const_get(:NFS_DEFAULT_NAME_SYSV))
end
end
context "with service match" do
let(:custom_nfs_service_name){ "/etc/init.d/custom-nfs-server-service-name" }
before{ expect(Dir).to receive(:glob).with(/.+init\.d.+/).and_return([custom_nfs_service_name]) }
it "should use the matched service name" do
expect(cap.nfs_service_name_sysv).to eq(File.basename(custom_nfs_service_name))
end
end
end
describe ".nfs_check_command" do
let(:cap){ caps.get(:nfs_check_command) }
context "without systemd" do
before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) }
it "should use init.d script" do
expect(cap.nfs_check_command(env)).to include("init.d")
end
end
context "with systemd" do
before do
expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true)
end
it "should use systemctl" do
expect(cap.nfs_check_command(env)).to include("systemctl")
end
end
end
describe ".nfs_start_command" do
let(:cap){ caps.get(:nfs_start_command) }
context "without systemd" do
before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(false) }
it "should use init.d script" do
expect(cap.nfs_start_command(env)).to include("init.d")
end
end
context "with systemd" do
before{ expect(Vagrant::Util::Platform).to receive(:systemd?).and_return(true) }
it "should use systemctl" do
expect(cap.nfs_start_command(env)).to include("systemctl")
end
end
end
describe ".nfs_export" do
let(:cap){ caps.get(:nfs_export) }
before do
allow(env).to receive(:host).and_return(host)
allow(host).to receive(:capability).with(:nfs_apply_command).and_return("/bin/true")
allow(host).to receive(:capability).with(:nfs_check_command).and_return("/bin/true")
allow(host).to receive(:capability).with(:nfs_start_command).and_return("/bin/true")
allow(ui).to receive(:info)
allow(Vagrant::Util::Subprocess).to receive(:execute).and_call_original
allow(Vagrant::Util::Subprocess).to receive(:execute).with("sudo", "/bin/true").and_return(double(:result, exit_code: 0))
allow(Vagrant::Util::Subprocess).to receive(:execute).with("/bin/true").and_return(double(:result, exit_code: 0))
end
it "should export new entries" do
cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], "tmp" => {:hostpath => "/tmp"})
exports_content = File.read(exports_path)
expect(exports_content).to match(/\/tmp.*127\.0\.0\.1/)
end
it "should not remove existing entries" do
File.write(exports_path, "/custom/directory hostname1(rw,sync,no_subtree_check)")
cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], "tmp" => {:hostpath => "/tmp"})
exports_content = File.read(exports_path)
expect(exports_content).to match(/\/tmp.*127\.0\.0\.1/)
expect(exports_content).to match(/\/custom\/directory.*hostname1/)
end
it "should remove entries no longer valid" do
valid_id = SecureRandom.uuid
other_id = SecureRandom.uuid
content =<<-EOH
# VAGRANT-BEGIN: #{Process.uid} #{other_id}
"/tmp" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=)
# VAGRANT-END: #{Process.uid} #{other_id}
# VAGRANT-BEGIN: #{Process.uid} #{valid_id}
"/var" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=)
# VAGRANT-END: #{Process.uid} #{valid_id}
EOH
File.write(exports_path, content)
cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], "home" => {:hostpath => "/home"})
exports_content = File.read(exports_path)
expect(exports_content).to include("/home")
expect(exports_content).to include("/tmp")
expect(exports_content).not_to include("/var")
end
it "throws an exception with at least 2 different nfs options" do
folders = {"/vagrant"=>
{:hostpath=>"/home/vagrant",
:linux__nfs_options=>["rw","all_squash"]},
"/var/www/project"=>
{:hostpath=>"/home/vagrant",
:linux__nfs_options=>["rw","sync"]}}
expect { cap.nfs_export(env, ui, SecureRandom.uuid, ["127.0.0.1"], folders) }.
to raise_error Vagrant::Errors::NFSDupePerms
end
it "writes only 1 hostpath for multiple exports" do
folders = {"/vagrant"=>
{:hostpath=>"/home/vagrant",
:linux__nfs_options=>["rw","all_squash"]},
"/var/www/otherproject"=>
{:hostpath=>"/newhome/otherproject",
:linux__nfs_options=>["rw","all_squash"]},
"/var/www/project"=>
{:hostpath=>"/home/vagrant",
:linux__nfs_options=>["rw","all_squash"]}}
valid_id = SecureRandom.uuid
content =<<-EOH
\n# VAGRANT-BEGIN: #{Process.uid} #{valid_id}
"/home/vagrant" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=)
"/newhome/otherproject" 127.0.0.1(rw,all_squash,anonuid=,anongid=,fsid=)
# VAGRANT-END: #{Process.uid} #{valid_id}
EOH
cap.nfs_export(env, ui, valid_id, ["127.0.0.1"], folders)
exports_content = File.read(exports_path)
expect(exports_content).to eq(content)
end
end
describe ".nfs_prune" do
let(:cap){ caps.get(:nfs_prune) }
before do
allow(ui).to receive(:info)
allow(Vagrant::Util::Subprocess).to receive(:execute).with("mv", any_args).
and_call_original
end
it "should remove entries no longer valid" do
invalid_id = SecureRandom.uuid
valid_id = SecureRandom.uuid
content =<<-EOH
# VAGRANT-BEGIN: #{Process.uid} #{invalid_id}
"/tmp" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=)
# VAGRANT-END: #{Process.uid} #{invalid_id}
# VAGRANT-BEGIN: #{Process.uid} #{valid_id}
"/var" 127.0.0.1(rw,no_subtree_check,all_squash,anonuid=,anongid=,fsid=)
# VAGRANT-END: #{Process.uid} #{valid_id}
EOH
File.write(exports_path, content)
cap.nfs_prune(env, ui, [valid_id])
exports_content = File.read(exports_path)
expect(exports_content).to include(valid_id)
expect(exports_content).not_to include(invalid_id)
expect(exports_content).to include("/var")
expect(exports_content).not_to include("/tmp")
end
end
describe ".nfs_write_exports" do
before do
File.write(tmp_exports_path, "original content")
allow(Vagrant::Util::Subprocess).to receive(:execute).with("mv", any_args).
and_call_original
end
it "should write updated contents to file" do
described_class.nfs_write_exports("new content")
exports_content = File.read(exports_path)
expect(exports_content).to include("new content")
expect(exports_content).not_to include("original content")
end
it "should only update contents if different" do
original_stat = File.stat(exports_path)
described_class.nfs_write_exports("original content")
updated_stat = File.stat(exports_path)
expect(original_stat).to eq(updated_stat)
end
it "should retain existing file permissions" do
File.chmod(0600, exports_path)
original_stat = File.stat(exports_path)
described_class.nfs_write_exports("original content")
updated_stat = File.stat(exports_path)
expect(original_stat.mode).to eq(updated_stat.mode)
end
it "should raise exception when failing to move new exports file" do
expect(Vagrant::Util::Subprocess).to receive(:execute).and_return(
Vagrant::Util::Subprocess::Result.new(1, "Failed to move file", "")
)
expect{ described_class.nfs_write_exports("new content") }.to raise_error(Vagrant::Errors::NFSExportsFailed)
end
context "exports file modification" do
let(:tmp_stat) { double("tmp_stat", uid: 100, gid: 100, mode: tmp_mode) }
let(:tmp_mode) { 0 }
let(:exports_stat) { double("stat", uid: exports_uid, gid: exports_gid, mode: exports_mode) }
let(:exports_uid) { -1 }
let(:exports_gid) { -1 }
let(:exports_mode) { 0 }
let(:new_exports_file) { double("new_exports_file", path: "/dev/null/exports") }
before do
allow(File).to receive(:stat).with(new_exports_file.path).and_return(tmp_stat)
allow(File).to receive(:stat).with(tmp_exports_path.to_s).and_return(exports_stat)
allow(new_exports_file).to receive(:puts)
allow(new_exports_file).to receive(:close)
allow(Vagrant::Util::Subprocess).to receive(:execute).and_return(Vagrant::Util::Subprocess::Result.new(0, "", ""))
allow(Tempfile).to receive(:create).with("vagrant").and_return(new_exports_file)
end
it "should retain existing file owner and group IDs" do
expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args|
expect(args).to include("sudo")
expect(args).to include("chown")
}.and_return(Vagrant::Util::Subprocess::Result.new(0, "", ""))
described_class.nfs_write_exports("new content")
end
it "should raise custom exception when chown fails" do
expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args|
expect(args).to include("sudo")
expect(args).to include("chown")
}.and_return(Vagrant::Util::Subprocess::Result.new(1, "", ""))
expect { described_class.nfs_write_exports("new content") }.to raise_error(Vagrant::Errors::NFSExportsFailed)
end
context "when user has write access to exports file" do
let(:file_writable?) { true }
let(:dir_writable?) { false }
let(:exports_pathname) { double("exports_pathname", writable?: file_writable?, dirname: exports_dir_pathname) }
let(:exports_dir_pathname) { double("exports_dir_pathname", writable?: dir_writable?) }
before do
allow(File).to receive(:stat).and_return(exports_stat)
allow(File).to receive(:exist?).and_return(false)
allow(Pathname).to receive(:new).with(tmp_exports_path.to_s).and_return(exports_pathname)
end
it "should use sudo when moving new file" do
expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args|
expect(args).to include("sudo")
expect(args).to include("mv")
}.and_return(Vagrant::Util::Subprocess::Result.new(0, "", ""))
described_class.nfs_write_exports("new content")
end
context "and write access to exports parent directory" do
let(:dir_writable?) { true }
it "should not use sudo when moving new file" do
expect(Vagrant::Util::Subprocess).to receive(:execute) { |*args|
expect(args).not_to include("sudo")
expect(args).to include("mv")
}.and_return(Vagrant::Util::Subprocess::Result.new(0, "", ""))
described_class.nfs_write_exports("new content")
end
end
end
end
end
end