Топик породила человеческая лень :)
Неоднократно приходилось сталкиваться с ситуацией, когда нужно изменить какой-либо параметр, фигурирующий сразу в нескольких конфигах, например, ip адрес интерфейса. И, как логично догадаться, вспомнить «сходу» какие это файлы оказывалось непросто.
Идея не нова и построена на использовании шаблонов; язык — ruby.
В качестве типа исходного конфигурационного файла я решил выбрать XML, он для этого достаточно неплохо подходит; вы же можете посмотреть в сторону YAML и прочих.
Парсить быстрее с помощью LibXML, его и выбрал. В качестве аналога можно рассмотреть REXML или уже готовую альтернативу XmlSimple, создающую хэш из xml-файла, но это будет медленнее, этот момент будет рассмотрен ниже.
Для работы с шаблонами выбрал erubis.
По тестам он, вроде как, шустрее ERB и eruby.
Собственно сам код:
Код детально описывать не буду, скажу лишь, что применяется рекурсивный вызов метода xml2hash — если начало блока, то проваливаемся глубже, если конец блока или далее пустой элемент/блок, то возвращаемся выше.
Файл шаблона — config_name из xml + '.eruby', т.е. в моём случае rc.conf.eruby.
Вывод осуществляется в config_name (rc.conf).
Содержимое шаблона:
Да, выглядит несколько нечитабельно, но это только на первый взгляд :)
Как и обещал, перейдём к тестам 'код выше' vs XmlSimple.
xml_test.rb:
xml_simple.rb:
Да-да, именно так выглядит на выходе хэш после распарсивания xml.
Хотя и разница во времени есть, но, я думаю, она не настолько критична для данной задачи, поэтому выбор остаётся за вами.
В результате на выходе у меня получилось:
The end.
Неоднократно приходилось сталкиваться с ситуацией, когда нужно изменить какой-либо параметр, фигурирующий сразу в нескольких конфигах, например, ip адрес интерфейса. И, как логично догадаться, вспомнить «сходу» какие это файлы оказывалось непросто.
Идея не нова и построена на использовании шаблонов; язык — ruby.
В качестве типа исходного конфигурационного файла я решил выбрать XML, он для этого достаточно неплохо подходит; вы же можете посмотреть в сторону YAML и прочих.
<xml>
<config name='rc.conf'>
<interfaces>
<iface name='re0' tag='internal'>
<ip>172.16.0.1</ip>
<netmask>255.255.255.240</netmask>
<media>1000baseTX</media>
<mediaopt>full-duplex</mediaopt>
<opts>polling</opts>
</iface>
<iface name='re1' tag='external'>
<ip>172.16.0.17</ip>
<netmask>255.255.255.228</netmask>
</iface>
<iface name='fxp0'>
<dhcp>true</dhcp>
<opts>polling</opts>
</iface>
<alias iface='re0' num='0'>
<ip>10.0.0.1</ip>
<netmask>255.255.255.0</netmask>
</alias>
<cloned>
vlan101
vlan110
vlan111
</cloned>
</interfaces>
</config>
</xml>Парсить быстрее с помощью LibXML, его и выбрал. В качестве аналога можно рассмотреть REXML или уже готовую альтернативу XmlSimple, создающую хэш из xml-файла, но это будет медленнее, этот момент будет рассмотрен ниже.
Для работы с шаблонами выбрал erubis.
По тестам он, вроде как, шустрее ERB и eruby.
Собственно сам код:
require 'rubygems'
require 'erubis'
require 'xml'
def xml2hash(xml = nil)
raise "missed 'xml' param" if xml.nil?
v = []; h = {}
loop do
xml.read
break if (xml.node_type == XML::Reader::TYPE_END_ELEMENT || xml.empty_element?)
if (xml.node_type == XML::Reader::TYPE_ELEMENT)
name = xml.name
h[name] ||= []
attrib = {}
if (xml.attribute_count > 0) then
count=xml.attribute_count
while (count > 0) do
count -= 1
xml.move_to_attribute(count)
attrib.merge!({xml.name => xml.value})
end
end
r = xml2hash(xml)
case r
when Hash
r.merge!(attrib) unless attrib.empty?
h[name].push( r )
when Array
h[name].push(attrib) unless attrib.empty?
h[name].push(*r)
else
h[name].push(attrib) unless attrib.empty?
h[name].push( r )
end unless r.nil?
elsif (xml.node_type == XML::Reader::TYPE_TEXT && !xml.value.nil?)
xml.value.split("\n").each{|i| i.gsub!(/^\s*?(\S+)\s*$/,'\1'); v.push(i) unless (i.empty? || i =~ /^\s*$/); }
end
end
r = []
r.push(h) unless (h.nil? || h.empty?)
r.push(*v) unless (v.nil? || v.empty?)
return *r
rescue => e
puts "#{e.class}: #{e} (method: xml2hash)"
end
begin
xml = XML::Reader.file('params.xml')
r = xml2hash(xml)
xml.close
r['xml'].first['config'].each do |i|
template = File.read(i['name'] + ".eruby")
eruby = Erubis::Eruby.new(template)
File.open(i['name'], "w") { |file| file.puts eruby.evaluate({:list => i}) }
end
rescue => e
puts "#{e.class}: #{e} (method: main)"
endКод детально описывать не буду, скажу лишь, что применяется рекурсивный вызов метода xml2hash — если начало блока, то проваливаемся глубже, если конец блока или далее пустой элемент/блок, то возвращаемся выше.
Файл шаблона — config_name из xml + '.eruby', т.е. в моём случае rc.conf.eruby.
Вывод осуществляется в config_name (rc.conf).
Содержимое шаблона:
<% for item in @list['interfaces'].first['iface'] %>
ifconfig_<%= item['name'] %>="<% unless item['dhcp'] %>inet <%= item['ip'].first %> netmask <%= item['netmask'].first %><% else %>DHCP<% end %><% unless (item['media'].nil? || item['media'].empty?) %> media <%= item['media'].first %><% end %><% unless (item['mediaopt'].nil? || item['mediaopt'].empty?) %> mediaopt <%= item['mediaopt'].first %><% end %><% unless (item['opts'].nil? || item['opts'].empty?) %> <%= item['opts'].first %><% end %>"
<% end %>
<% for item in @list['interfaces'].first['alias'] %>
ifconfig_<%= item['iface'] %>_alias<%= item['num'] %>="inet <%= item['ip'].first %> netmask <%= item['netmask'].first %>"
<% end %>
<% if @list['interfaces'].first.key?('cloned') && !@list['interfaces'].first['cloned'].empty? %>cloned_interfaces="<%= @list['interfaces'].first['cloned'].join(' ') %>"<% end %>Да, выглядит несколько нечитабельно, но это только на первый взгляд :)
Как и обещал, перейдём к тестам 'код выше' vs XmlSimple.
xml_test.rb:
require 'rubygems'
require 'xml'
require 'benchmark'
def xml2hash(xml = nil)
# определение метода см. выше
end
begin
xml = ''
Benchmark.bm do |x|
x.report { 100.times do
xml_read = XML::Reader.file('params.xml')
xml = xml2hash(xml_read)
xml_read.close
end }
end
p xml
rescue => e
puts "#{e.class}: #{e} (method: main)"
endezhik@pollux:~$ time ruby xml_test.rb
user system total real
0.210000 0.010000 0.220000 ( 0.253391)
{"xml"=>[{"config"=>[{"name"=>"rc.conf", "interfaces"=>[{"alias"=>[{"netmask"=>["255.255.255.0"], "num"=>"0", "ip"=>["10.0.0.1"], "iface"=>"re0"}], "iface"=>[{"name"=>"re0", "netmask"=>["255.255.255.240"], "opts"=>["polling"], "tag"=>"internal", "mediaopt"=>["full-duplex"], "media"=>["1000baseTX"], "ip"=>["172.16.0.1"]}, {"name"=>"re1", "netmask"=>["255.255.255.228"], "tag"=>"external", "ip"=>["172.16.0.17"]}, {"name"=>"fxp0", "opts"=>["polling"], "dhcp"=>["true"]}], "cloned"=>["vlan101", "vlan110", "vlan111"]}]}]}]}
real 0m0.447s
user 0m0.344s
sys 0m0.036sxml_simple.rb:
require 'rubygems'
require 'xmlsimple'
require 'benchmark'
xml = ''
Benchmark.bm do |x|
x.report { 100.times do
xml = XmlSimple.xml_in('params.xml')
end }
end
p xmlezhik@pollux:~$ time ruby xml_simple.rb
user system total real
1.770000 0.100000 1.870000 ( 1.956282)
{"config"=>[{"name"=>"rc.conf", "interfaces"=>[{"alias"=>[{"netmask"=>["255.255.255.0"], "num"=>"0", "ip"=>["10.0.0.1"], "iface"=>"re0"}], "iface"=>[{"netmask"=>["255.255.255.240"], "name"=>"re0", "opts"=>["polling"], "tag"=>"internal", "mediaopt"=>["full-duplex"], "media"=>["1000baseTX"], "ip"=>["172.16.0.1"]}, {"netmask"=>["255.255.255.228"], "name"=>"re1", "tag"=>"external", "ip"=>["172.16.0.17"]}, {"name"=>"fxp0", "opts"=>["polling"], "dhcp"=>["true"]}], "cloned"=>["\n vlan101\n vlan110\n vlan111\n "]}]}]}
real 0m2.279s
user 0m2.012s
sys 0m0.124sДа-да, именно так выглядит на выходе хэш после распарсивания xml.
Хотя и разница во времени есть, но, я думаю, она не настолько критична для данной задачи, поэтому выбор остаётся за вами.
В результате на выходе у меня получилось:
ifconfig_re0="inet 172.16.0.1 netmask 255.255.255.240 media 1000baseTX mediaopt full-duplex polling" ifconfig_re1="inet 172.16.0.17 netmask 255.255.255.228" ifconfig_fxp0="DHCP polling" ifconfig_re0_alias0="inet 10.0.0.1 netmask 255.255.255.0" cloned_interfaces="vlan101 vlan110 vlan111"
The end.
