📜 ⬆️ ⬇️

Configuration Management

Topic spawned human laziness :)
Many times we had to deal with the situation when you need to change a parameter that appears in several configs at once, for example, the ip address of the interface. And, as it is logical to guess, remembering “immediately” what files it turned out to be difficult.
The idea is not new and is based on the use of templates; language is ruby.

I decided to choose XML as the type of the original configuration file, it is quite good for this; You can look towards YAML and others.
  <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> 

Parse faster using LibXML , and chose it. As an analogue, you can consider REXML or a ready-made alternative to XmlSimple , which creates a hash from an xml-file, but this will be slower, this point will be discussed below.
For work with templates I chose erubis .
According to the tests, he seems to be faster than ERB and eruby .

Actually the code itself:
  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 

I will not describe the code in detail, I will say only that the recursive method call xml2hash is used - if the beginning of the block, then we fall deeper, if the end of the block or further empty element / block, then return above.
The template file is config_name from xml + '.eruby', i.e. in my case rc.conf.eruby.
The output is done in config_name (rc.conf).

Template content:
<% 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 %>

Yes, it looks somewhat unreadable, but it is only at first glance :)
')
As promised, let's move on to the tests 'code above' vs XmlSimple.
xml_test.rb:
  require 'rubygems'
 require 'xml'
 require 'benchmark'

 def xml2hash (xml = nil)
   # method definition see above
 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 


Yes, this is exactly what the hash looks like on the output after parsing xml.
Although there is a time difference, but I think it is not so critical for this task, so the choice is yours.

As a result, the output I got:
  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.

Source: https://habr.com/ru/post/73258/


All Articles