Как стать автором
Обновить

Управление конфигурациями

Время на прочтение5 мин
Количество просмотров608
Топик породила человеческая лень :)
Неоднократно приходилось сталкиваться с ситуацией, когда нужно изменить какой-либо параметр, фигурирующий сразу в нескольких конфигах, например, 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.
Теги:
Хабы:
Всего голосов 4: ↑2 и ↓20
Комментарии9

Публикации

Истории

Ближайшие события