Топик породила человеческая лень :)
Неоднократно приходилось сталкиваться с ситуацией, когда нужно изменить какой-либо параметр, фигурирующий сразу в нескольких конфигах, например, 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)" end
ezhik@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.036s
xml_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 xml
ezhik@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.