-
Notifications
You must be signed in to change notification settings - Fork 4.4k
/
nfs.rb
296 lines (257 loc) · 10.8 KB
/
nfs.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: BUSL-1.1
require "shellwords"
require "vagrant/util"
require "vagrant/util/shell_quote"
require "vagrant/util/retryable"
module VagrantPlugins
module HostLinux
module Cap
class NFS
NFS_EXPORTS_PATH = "/etc/exports".freeze
NFS_DEFAULT_NAME_SYSTEMD = "nfs-server.service".freeze
NFS_DEFAULT_NAME_SYSV = "nfs-kernel-server".freeze
extend Vagrant::Util::Retryable
def self.nfs_service_name_systemd
if !defined?(@_nfs_systemd)
result = Vagrant::Util::Subprocess.execute("systemctl", "list-units",
"*nfs*server*", "--no-pager", "--no-legend")
if result.exit_code == 0
@_nfs_systemd = result.stdout.to_s.split(/\s+/).first
end
if @_nfs_systemd.to_s.empty?
@_nfs_systemd = NFS_DEFAULT_NAME_SYSTEMD
end
end
@_nfs_systemd
end
def self.nfs_service_name_sysv
if !defined?(@_nfs_sysv)
@_nfs_sysv = Dir.glob("/etc/init.d/*nfs*server*").first.to_s
if @_nfs_sysv.empty?
@_nfs_sysv = NFS_DEFAULT_NAME_SYSV
else
@_nfs_sysv = File.basename(@_nfs_sysv)
end
end
@_nfs_sysv
end
def self.nfs_apply_command(env)
"exportfs -ar"
end
def self.nfs_check_command(env)
if Vagrant::Util::Platform.systemd?
"systemctl status --no-pager #{nfs_service_name_systemd}"
else
"/etc/init.d/#{nfs_service_name_sysv} status"
end
end
def self.nfs_start_command(env)
if Vagrant::Util::Platform.systemd?
"systemctl start #{nfs_service_name_systemd}"
else
"/etc/init.d/#{nfs_service_name_sysv} start"
end
end
def self.nfs_export(env, ui, id, ips, folders)
# Get some values we need before we do anything
nfs_apply_command = env.host.capability(:nfs_apply_command)
nfs_check_command = env.host.capability(:nfs_check_command)
nfs_start_command = env.host.capability(:nfs_start_command)
nfs_opts_setup(folders)
folders = folder_dupe_check(folders)
ips = ips.uniq
output = Vagrant::Util::TemplateRenderer.render('nfs/exports_linux',
uuid: id,
ips: ips,
folders: folders,
user: Process.uid)
ui.info I18n.t("vagrant.hosts.linux.nfs_export")
sleep 0.5
nfs_cleanup("#{Process.uid} #{id}")
output = nfs_exports_content + output
nfs_write_exports(output)
if nfs_running?(nfs_check_command)
Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_apply_command)).exit_code == 0
else
Vagrant::Util::Subprocess.execute("sudo", *Shellwords.split(nfs_start_command)).exit_code == 0
end
end
def self.nfs_installed(environment)
if Vagrant::Util::Platform.systemd?
Vagrant::Util::Subprocess.execute("/bin/sh", "-c",
"systemctl --no-pager --no-legend --plain list-unit-files --all --type=service " \
"| grep #{nfs_service_name_systemd}").exit_code == 0
else
Vagrant::Util::Subprocess.execute(modinfo_path, "nfsd").exit_code == 0 ||
Vagrant::Util::Subprocess.execute("grep", "nfsd", "/proc/filesystems").exit_code == 0
end
end
def self.nfs_prune(environment, ui, valid_ids)
return if !File.exist?(NFS_EXPORTS_PATH)
logger = Log4r::Logger.new("vagrant::hosts::linux")
logger.info("Pruning invalid NFS entries...")
user = Process.uid
# Create editor instance for removing invalid IDs
editor = Vagrant::Util::StringBlockEditor.new(nfs_exports_content)
# Build composite IDs with UID information and discover invalid entries
composite_ids = valid_ids.map do |v_id|
"#{user} #{v_id}"
end
remove_ids = editor.keys - composite_ids
logger.debug("Known valid NFS export IDs: #{valid_ids}")
logger.debug("Composite valid NFS export IDs with user: #{composite_ids}")
logger.debug("NFS export IDs to be removed: #{remove_ids}")
if !remove_ids.empty?
ui.info I18n.t("vagrant.hosts.linux.nfs_prune")
nfs_cleanup(remove_ids)
end
end
protected
# Takes a hash of folders and removes any duplicate exports that
# share the same hostpath to avoid duplicate entries in /etc/exports
# ref: GH-4666
def self.folder_dupe_check(folders)
return_folders = {}
# Group by hostpath to see if there are multiple exports coming
# from the same folder
export_groups = folders.values.group_by { |h| h[:hostpath] }
# We need to check that each group key only has 1 value,
# and if not, check each nfs option. If all nfs options are the same
# we're good, otherwise throw an exception
export_groups.each do |path,group|
if group.size > 1
# if the linux nfs options aren't all the same throw an exception
group1_opts = group.first[:linux__nfs_options]
if !group.all? {|g| g[:linux__nfs_options] == group1_opts}
raise Vagrant::Errors::NFSDupePerms, hostpath: group.first[:hostpath]
else
# if they're the same just pick the first one
return_folders[path] = group.first
end
else
# just return folder, there are no duplicates
return_folders[path] = group.first
end
end
return_folders
end
def self.nfs_cleanup(remove_ids)
return if !File.exist?(NFS_EXPORTS_PATH)
editor = Vagrant::Util::StringBlockEditor.new(nfs_exports_content)
remove_ids = Array(remove_ids)
# Remove all invalid ID entries
remove_ids.each do |r_id|
editor.delete(r_id)
end
nfs_write_exports(editor.value)
end
def self.nfs_write_exports(new_exports_content)
if(nfs_exports_content != new_exports_content.strip)
begin
exports_path = Pathname.new(NFS_EXPORTS_PATH)
# Write contents out to temporary file
new_exports_path = File.join(Dir.tmpdir, "vagrant-exports")
FileUtils.rm_f(new_exports_path)
new_exports_file = File.open(new_exports_path, "w+")
new_exports_file.puts(new_exports_content)
new_exports_file.close
# Ensure new file mode and uid/gid match existing file to replace
existing_stat = File.stat(NFS_EXPORTS_PATH)
new_stat = File.stat(new_exports_path)
if existing_stat.mode != new_stat.mode
File.chmod(existing_stat.mode, new_exports_path)
end
if existing_stat.uid != new_stat.uid || existing_stat.gid != new_stat.gid
chown_cmd = "sudo chown #{existing_stat.uid}:#{existing_stat.gid} #{new_exports_path}"
result = Vagrant::Util::Subprocess.execute(*Shellwords.split(chown_cmd))
if result.exit_code != 0
raise Vagrant::Errors::NFSExportsFailed,
command: chown_cmd,
stderr: result.stderr,
stdout: result.stdout
end
end
# Always force move the file to prevent overwrite prompting
sudo_command = "sudo " if !exports_path.writable? || !exports_path.dirname.writable?
mv_cmd = "#{sudo_command}mv -f #{new_exports_path} #{NFS_EXPORTS_PATH}"
result = Vagrant::Util::Subprocess.execute(*Shellwords.split(mv_cmd))
if result.exit_code != 0
raise Vagrant::Errors::NFSExportsFailed,
command: mv_cmd,
stderr: result.stderr,
stdout: result.stdout
end
ensure
if !new_exports_path.nil? && File.exist?(new_exports_path)
File.unlink(new_exports_path)
end
end
end
end
def self.nfs_exports_content
if(File.exist?(NFS_EXPORTS_PATH))
if(File.readable?(NFS_EXPORTS_PATH))
File.read(NFS_EXPORTS_PATH)
else
cmd = "sudo cat #{NFS_EXPORTS_PATH}"
result = Vagrant::Util::Subprocess.execute(*Shellwords.split(cmd))
if result.exit_code != 0
raise Vagrant::Errors::NFSExportsFailed,
command: cmd,
stderr: result.stderr,
stdout: result.stdout
else
result.stdout
end
end
else
""
end
end
def self.nfs_opts_setup(folders)
folders.each do |k, opts|
if !opts[:linux__nfs_options]
opts[:linux__nfs_options] ||= ["rw", "no_subtree_check", "all_squash"]
end
# Only automatically set anonuid/anongid if they weren't
# explicitly set by the user.
hasgid = false
hasuid = false
opts[:linux__nfs_options].each do |opt|
hasgid = !!(opt =~ /^anongid=/) if !hasgid
hasuid = !!(opt =~ /^anonuid=/) if !hasuid
end
opts[:linux__nfs_options] << "anonuid=#{opts[:map_uid]}" if !hasuid
opts[:linux__nfs_options] << "anongid=#{opts[:map_gid]}" if !hasgid
opts[:linux__nfs_options] << "fsid=#{opts[:uuid]}"
end
end
def self.nfs_running?(check_command)
Vagrant::Util::Subprocess.execute(*Shellwords.split(check_command)).exit_code == 0
end
def self.modinfo_path
if !defined?(@_modinfo_path)
@_modinfo_path = Vagrant::Util::Which.which("modinfo")
if @_modinfo_path.to_s.empty?
path = "/sbin/modinfo"
if File.file?(path)
@_modinfo_path = path
end
end
if @_modinfo_path.to_s.empty?
@_modinfo_path = "modinfo"
end
end
@_modinfo_path
end
# @private
# Reset the cached values for capability. This is not considered a public
# API and should only be used for testing.
def self.reset!
instance_variables.each(&method(:remove_instance_variable))
end
end
end
end
end