Skip to content

Instantly share code, notes, and snippets.

@csabahenk
Last active August 29, 2015 14:04
Show Gist options
  • Save csabahenk/6128bda18ccdb742b33d to your computer and use it in GitHub Desktop.
Save csabahenk/6128bda18ccdb742b33d to your computer and use it in GitHub Desktop.
Testing NFS-Ganesha export changes
#!/usr/bin/env ruby
require 'json'
require 'shellwords'
require 'tempfile'
MCB = "```"
class String
def indent n
n == 0 ? dup : split("\n").map {|l| l =~ /\S/ ? l.sub(/^/, " "*n) : l }.join("\n")
end
def mdcode
[MCB, self, MCB].join("\n")
end
end
class Hash
def deep_merge! oh
oh.each { |k,v|
if Hash === v and Hash === self[k]
self[k].deep_merge! v
else
self[k] = v
end
}
self
end
def deep_merge oh
Marshal.load(Marshal.dump self).deep_merge! oh
end
end
class GaneshaTest
IWIDTH = 2
def self.conf2json s
# first do a basic tokenization
# to separate quoted strings from
# rest and filter comments
a = [""]
in_quote = false
in_comment = false
escape = false
set_escape = proc { escape = true }
start_new = proc { a << "" }
cbk = []
s.each_char { |c|
if in_quote
if !escape
case c
when '"'
in_quote = false
cbk << start_new
when '\\'
cbk << set_escape
end
end
else
if c == "#"
in_comment = true
end
if in_comment
if c == "\n"
in_comment = false
end
else
if c == '"'
a << ""
in_quote = true
end
end
end
escape = false
a.last << c unless in_comment
while x = cbk.shift
x[]
end
}
raise "unterminated quoted string" if in_quote
# jsonify tokens
ta = ["{",
a.map { |w|
# leave quoted strings intact
w[0] == '"' and next w
# add omitted "=" signs to block openings
w.gsub(/([^=\s])\s*{/m, '\1={').
# delete trailing semicolons in blocks
gsub(/;\s*}/m, "}").
# add omitted semicolons after blocks
gsub(/}\s*([^}\s])/, '};\1').
# separate syntactically significant characters
gsub(/[;{}=]/, ' \0 ').split.
# map tokens to JSON equivalents
map { |wx|
case wx
when "="
":"
when ";"
","
when "{", "}", /^-?[1-9]\d*(\.\d+)?$/
wx
else
wx.inspect
end
}
},
"}"].flatten
# group quoted strings
aa = []
ta.each { |w|
if w[0] == '"'
aa << [] unless Array === aa.last
aa.last << w
else
aa << w
end
}
# process quoted string groups by joining them
aa.map { |x|
case x
when Array
['"', x.map { |w| w[1...-1] }, '"']
else
x
end
}.flatten.join
end
def self.to_conf d, out=STDOUT, indent=0
case d
when Hash
d.each { |k,v|
out << " " * (indent * IWIDTH) << k << " "
case v
when Hash
out << "{\n"
to_conf v, out, indent+1
out << " " * (indent * IWIDTH) << "}"
else
out << "= "
to_conf v, out, indent
out << ";"
end
out << "\n"
}
else
di = d.inspect
out << (d == di[1...-1] ? d : di)
end
out
end
def self.normalize_param mod
mod ||= {}
String === mod and mod = mod.split
if Array === mod
m = {}
mod.each { |e| m[e] = true }
mod = m
end
mod
end
def run *aa, scope: :client, frail:true, **kw
cmdc = aa.flatten
cmds = (@sudo ? ["sudo"] : []) + cmdc
cmd, pinf = if scope == :client and @client
[["ssh", @client, cmds.map(&:shellescape).join(" ")],
@client]
else
[cmds, ""]
end
prompt = "#{Time.now.strftime "%y/%m/%d %H:%M:%S"} #{pinf}>"
puts "#{prompt} #{cmdc.join " "}"
pid, res = Process.wait2 spawn(*cmd, **kw)
if frail and !res.success?
rs = res.termsig ? "SIG #{res.termsig}" : res.exitstatus
raise "#{cmdc[0]} failed with #{rs}"
end
res.success?
end
def dbus_run dmeth, *aa, **kw
kw[:scope]||=:local
service = kw.delete(:service) || "exportmgr"
run %w[dbus-send --print-reply --system --dest=org.ganesha.nfsd /org/ganesha/nfsd/ExportMgr], "org.ganesha.nfsd.#{service}.#{dmeth}", *aa, **kw
end
def initialize conf:nil,host:'localhost',client:nil,mount:nil,sudo:false,sleep:nil,relax_fs_errors:false
raise "conf not given" unless conf
raise "mount not given" unless mount
if client and host == 'localhost'
raise "host can't be local if client is given"
end
@conf = conf
@client = client
@mount = mount
@host = host
@sudo = sudo
@sleep = sleep
@relax_fs_errors = relax_fs_errors
features = []
sudo and features << "sudo"
relax_fs_errors and features << "relax_fs_errors"
sleep and features << "sleep: #{sleep}"
puts "# Initialize", "",
"- base conf:", self.class.to_conf(conf, "").mdcode.indent(4),
"- host: `#{host}`",
"- client: `#{client}:#{mount}`"
puts "- features: #{features.join ", "}" unless features.empty?
puts "", MCB
run %w[showmount -e], scope: :local
run "mountpoint", mount, frail:false or run "mount", "#{host}:/", mount
puts MCB, ""
end
def patch overlay, remount:false
puts "- overlay:", self.class.to_conf(overlay, "").mdcode.indent(4),
"", MCB
cnf = @conf.deep_merge overlay
exid = cnf["EXPORT"]["Export_Id"]
tf = Tempfile.new %w[ganeshaexport- .conf]
added = false
begin
self.class.to_conf cnf, tf
tf.close
run "cat", tf.path, scope: :local
dbus_run "AddExport", "string:#{tf.path}", "string:EXPORT(Export_Id=#{exid})"
added = true
if remount
run %w[umount -l], @mount
run "mount", "#{@host}:/", @mount
elsif @sleep
run "sleep", @sleep.to_s
end
run "find", @mount, frail:!@relax_fs_errors
ensure
tf.close
tf.unlink
dbus_run "RemoveExport", "uint16:#{exid}" if added
puts MCB, ""
end
end
def patchmod volpath, mod, i
puts "# Turn № #{i}: `#{mod.to_json}`", ""
vol = @conf["EXPORT"]["FSAL"]["Volume"]
ca = ["/", vol]
mod["extendpath"] and ca << volpath
path = File.join *ca
ovl = {"EXPORT" => {"Path"=>path, "Pseudo"=>path}}
mod["volpath"] and ovl["EXPORT"]["FSAL"] = {"Volpath"=>volpath}
patch ovl, remount:mod["remount"]
end
def randloop volpath, turns:nil, preset:nil
preset = self.class.normalize_param preset
i = 1
loop {
mod = {}
%w[extendpath volpath remount].each {|k| mod[k] = (rand(2) == 1) }
mod.merge! preset
patchmod volpath, mod, i
i == turns and break
i += 1
}
end
def scriptloop volpath, scr
scr.each_with_index { |mod,i|
mod = self.class.normalize_param mod
%w[extendpath volpath remount].each {|k| mod[k] ||= false }
patchmod volpath, mod, i+1
}
end
end
if __FILE__ == $0
require 'optparse'
dataload = proc { |s|
if ['-', nil].include? s
STDIN.read
elsif File.file? s
IO.read s
else
s
end
}
opts = {}
turns = nil
volpath = nil
playscript = nil
preset = nil
fetching = proc { |q| proc { |v| opts[q] = v } }
op = OptionParser.new
op.banner << " [ganesha-export.conf]"
op.on("-c", "--client=<user>@<remhost>", &fetching[:client])
op.on("-h", "--host=<host>", &fetching[:host])
op.on("-m", "--mount=<path>", &fetching[:mount])
op.on("-S", "--sleep=<sec>", Float, &fetching[:sleep])
op.on("-s", "--sudo") { opts[:sudo] = true }
op.on("--relax-fs-errors") { opts[:relax_fs_errors] = true }
op.on("-t", "--turns=N", Integer) { |n| turns = n }
op.on("-v", "--volpath=<path>") { |v| volpath = v }
op.on("-p", "--playscript=<json>") { |v| playscript = v }
op.on("-P", "--preset=<json>") { |v| preset = v }
args = $*.dup
op.parse!
raise "volpath not given" unless volpath
puts "Invoked as `#{[$0,args].flatten.join " "}`", ""
opts[:conf] = JSON.load GaneshaTest.conf2json(dataload[$*[0]])
playscript and scr = JSON.load(dataload[playscript])
pscr = preset ? JSON.load(dataload[preset]) : nil
gt = GaneshaTest.new(**opts)
if playscript
gt.scriptloop volpath, scr
else
gt.randloop volpath, turns:turns, preset:pscr
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment