Skip to content

Instantly share code, notes, and snippets.

@csabahenk
Last active August 29, 2015 14:04
Show Gist options
  • Save csabahenk/8719f0b91e739d4e289a to your computer and use it in GitHub Desktop.
Save csabahenk/8719f0b91e739d4e289a to your computer and use it in GitHub Desktop.
Tool to manipulate NFS Ganesha configuration
#!/usr/bin/env ruby
require 'json'
require 'tempfile'
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_dup
Marshal.load(Marshal.dump self)
end
def deep_merge oh
deep_dup.deep_merge! oh
end
end
class GaneshaMod
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 run *aa, frail:true, **kw
cmdc = aa.flatten
cmd = (@sudo ? ["sudo"] : []) + cmdc
prompt = "#{Time.now.strftime "%y/%m/%d %H:%M:%S"}>"
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
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,sudo:false
@conf = conf
@sudo = sudo
end
def patch *overlays
cnf = @conf.deep_dup
overlays.each { |ovl| cnf.deep_merge! ovl }
cnf
end
def apply cnf, force:false
exid = cnf["EXPORT"]["Export_Id"]
tf = Tempfile.new %w[ganeshaexport- .conf]
if force
dbus_run "RemoveExport", "uint16:#{exid}", frail:false
end
begin
self.class.to_conf cnf, tf
tf.close
run "cat", tf.path
dbus_run "AddExport", "string:#{tf.path}", "string:EXPORT(Export_Id=#{exid})"
ensure
tf.close
tf.unlink
end
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 = {}
dump = false
json = false
force = false
op = OptionParser.new
op.banner << " [ganesha-export.conf ...]"
op.on("-s", "--sudo") { opts[:sudo] = true }
op.on("-d", "--dump") { dump = true }
op.on("-j", "--json") { json = true }
op.on("-f", "--force") { force = true }
args = $*.dup
op.parse!
confs = ($*.empty? ? [nil] : $*).map { |a|
d = dataload[a]
begin
JSON.load d
rescue JSON::ParserError
JSON.load GaneshaMod.conf2json(d)
end
}
opts[:conf] = confs.shift
gm = GaneshaMod.new(**opts)
cnf = gm.patch *confs
if dump
json ? puts(JSON.pretty_generate(cnf)) : GaneshaMod.to_conf(cnf)
else
gm.apply(cnf, force:force)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment