diff options
| author | Saku Ytti <saku@ytti.fi> | 2013-04-20 00:41:23 +0300 | 
|---|---|---|
| committer | Saku Ytti <saku@ytti.fi> | 2013-04-20 00:41:23 +0300 | 
| commit | f1287a7925901bf3518eb69079304bfb97f7434d (patch) | |
| tree | 3dd2779e4d087b73b99d7136f37db88c76091d8d | |
| parent | 5a393d6102655f575549311e6b250534b4dcbb59 (diff) | |
Example of Syslog triggered fetch
'syslog.rb' listed to UDP port (or reads file). When IOS or JunOS style
config change/commit message is seen, it triggers immediate update ot
config
It transports commit message (junos) remote host from which change was
mde (ios) and who made the change (junos+ios). This is carried over to
the 'output' methods, that is, 'git blame' will show IOS/JunOS user-name
who made the change.
| -rw-r--r-- | TODO.md | 7 | ||||
| -rw-r--r-- | extra/rest_client.rb | 25 | ||||
| -rwxr-xr-x | extra/syslog.rb | 111 | ||||
| -rw-r--r-- | lib/oxidized/api/rest.rb | 45 | ||||
| -rw-r--r-- | lib/oxidized/config/bootstrap.rb | 4 | ||||
| -rw-r--r-- | lib/oxidized/config/defaults.rb | 1 | ||||
| -rw-r--r-- | lib/oxidized/node.rb | 6 | ||||
| -rw-r--r-- | lib/oxidized/nodes.rb | 10 | ||||
| -rw-r--r-- | lib/oxidized/worker.rb | 6 | ||||
| -rwxr-xr-x | lib/tst | 19 | 
10 files changed, 214 insertions, 20 deletions
| @@ -16,7 +16,10 @@ I don't really need it myself, since I don't have platforms where it would be ne  # config -   * save keys as strings, load as symbols? +  * save keys as strings, load as symbols? + +# REST API +  * figure out if is somehow possible to run rack/puma/sinatra stack with oxidized select()  # other   should it offer cli mass config-pusher? (I think not, I have ideas for such @@ -31,5 +34,5 @@ use sidekiq? Any benefits?    * rspec tests -# multiple input methods +# input method fallback    * ssh, with telnet fallback diff --git a/extra/rest_client.rb b/extra/rest_client.rb new file mode 100644 index 0000000..2c58d04 --- /dev/null +++ b/extra/rest_client.rb @@ -0,0 +1,25 @@ +module Oxidized +  class RestClient +    require 'net/http' +    require 'json' +    HOST = 'localhost' +    PORT = 8888 + +    class << self +      def next node, opt={}, host=HOST, port=PORT +        web = new host, port +        web.next node, opt +      end +    end + +    def initialize host=HOST, port=PORT +      @web = Net::HTTP.new host, port +    end + +    def next node, opt={} +      data = JSON.dump :node => node, :user => opt[:user], :msg => opt[:msg],  :from => opt[:from] +      @web.put '/nodes/next/' + node.to_s, data +    end + +  end +end diff --git a/extra/syslog.rb b/extra/syslog.rb new file mode 100755 index 0000000..10757bc --- /dev/null +++ b/extra/syslog.rb @@ -0,0 +1,111 @@ +#!/usr/bin/env ruby + +# IOS: +# logging discriminator CFG mnemonics includes CONFIG_I  +# logging host SERVER discriminator CFG + +# JunOS: +# set system syslog host SERVER interactive-commands notice +# set system syslog host SERVER match "^mgd\[[0-9]+\]: UI_COMMIT: .*" + +# sudo setcap 'cap_net_bind_service=+ep' /usr/bin/ruby + +# exit if fork   ## TODO: proper daemonize + +require 'socket' +require 'resolv' +require './rest_client' + +module Oxidized +  class SyslogMonitor +    NAME_MAP = { +      /(.*)\.ip\.tdc\.net/ => '\1', +      /(.*)\.ip\.fi/       => '\1', +    } +    PORT = 514 +    FILE = 'messages' +    MSG = { +      :ios   => '%SYS-5-CONFIG_I:', +      :junos => 'UI_COMMIT:', +    } + +    class << self +      def udp port=PORT, listen=0 +        io = UDPSocket.new +        io.bind listen, port +        new io, :udp +      end +      def file syslog_file=FILE +        io = open syslog_file, 'r' +        io.seek 0, IO::SEEK_END +        new io, :file +      end +    end + +    private  + +    def initialize io, mode=:udp +      @mode = mode +      run io +    end + +    def rest opt +      Oxidized::RestClient.next opt[:name], :user => opt[:user], :msg => opt[:msg], +                                            :from => opt[:from] +    end + +    def ios ip, log, i +      # TODO: we need to fetch 'ip/name' in mode == :file here +      user = log[i+5] +      from = log[-1][1..-2] +      rest( :user => user, :from => from, :model => 'ios', :ip => ip, +            :name => getname(ip) ) +    end + +    def jnpr ip, log, i +      # TODO: we need to fetch 'ip/name' in mode == :file here +      user = log[i+2][1..-2] +      msg  = log[(i+6)..-1].join(' ')[10..-2] +      msg  = nil if msg == 'none' +      rest( :user => user, :msg => msg, :model => 'jnpr', :ip => ip, +            :name => getname(ip) ) +    end + +    def handle_log log, ip +      log = log.to_s.split ' ' +      if i = log.index(MSG[:ios]) +        ios ip, log,  i +      elsif i = log.index(MSG[:junos]) +        jnpr ip, log, i +      end +    end + +    def run io +      while true +        log = select [io] +        log, ip = log.first.first, nil +        if @mode == :udp +          log, ip = log.recvfrom_nonblock 2000 +          ip = ip.last +        else  +          begin +            log = log.read_nonblock 2000 +          rescue EOFError +            sleep 1 +            retry +          end +        end +        handle_log log, ip +      end +    end + +    def getname ip +      name = (Resolv.getname ip.to_s rescue ip) +      NAME_MAP.each { |re, sub| name.sub! re, sub } +      name +    end +  end +end + +Oxidized::SyslogMonitor.udp +#Oxidized::SyslogMonitor.file '/var/log/poop' diff --git a/lib/oxidized/api/rest.rb b/lib/oxidized/api/rest.rb index 4a5fbca..9e00837 100644 --- a/lib/oxidized/api/rest.rb +++ b/lib/oxidized/api/rest.rb @@ -3,6 +3,14 @@ module Oxidized    require 'json'    module API      class Rest +      module Helpers +        def send res, msg='OK', status=200 +          res['Content-Type'] = 'application/json' +          res.status = status +          res.body = JSON.dump msg +        end +      end +      include Oxidized::API::Rest::Helpers        def initialize nodes, listen          @nodes = nodes          addr, port = listen.to_s.split ':' @@ -18,26 +26,49 @@ module Oxidized          end        end        def maps +        @web.mount '/nodes/next', Next, @nodes          @web.mount_proc '/nodes' do |req, res|            #script_name, #path_info            case req.path_info[1..-1]            # /nodes/reload - reloads list of nodes            when 'reload'              @nodes.load -            res.body = JSON.dump 'OK' -          # /nodes/next/node - moves node to head of queue -          when /next\/(.*)/ -            @nodes.next $1 -            res.body = JSON.dump 'OK' +            send res            # /nodes/list - returns list of nodes            when 'list' -            res.body = JSON.dump @nodes.list +            send res, @nodes.list            # /nodes/show/node - returns data about node            when /show\/(.*)/ -            res.body = JSON.dump @nodes.show $1 +            send res, @nodes.show($1) +          end +        end +      end + +      # /nodes/next/node - moves node to head of queue +      class Next < WEBrick::HTTPServlet::AbstractServlet +        include Oxidized::API::Rest::Helpers +        def initialize server, nodes +          super server +          @nodes = nodes +        end +        def do_GET req, res +          @nodes.next req.path_info[1..-1] +          send res +        end +        def do_PUT req, res +          node = req.path_info[1..-1] +          begin +            opt = JSON.load req.body +            Log.debug "before: #{@nodes.list}" +            @nodes.next node, opt +            Log.debug "after: #{@nodes.list}" +            send res +          rescue JSON::ParserError +            send res, 'broken JSON', 400            end          end        end +      end    end  end diff --git a/lib/oxidized/config/bootstrap.rb b/lib/oxidized/config/bootstrap.rb index 2991fa3..c3ea09d 100644 --- a/lib/oxidized/config/bootstrap.rb +++ b/lib/oxidized/config/bootstrap.rb @@ -4,10 +4,10 @@ module Oxidized    CFG.username = 'username'    CFG.password = 'password'    CFG.model    = 'junos' -  CFG.interval = 30 +  CFG.interval = 60    CFG.log      = File.join Config::Root, 'log'    CFG.debug    = false -  CFG.threads  = 10 +  CFG.threads  = 30    CFG.timeout  = 5    CFG.prompt   = /^([\w\.\-@]{3,30}[#>]\s?)$/     CFG.rest     = 8888 diff --git a/lib/oxidized/config/defaults.rb b/lib/oxidized/config/defaults.rb index 943d90b..d189ca3 100644 --- a/lib/oxidized/config/defaults.rb +++ b/lib/oxidized/config/defaults.rb @@ -1,6 +1,7 @@  module Oxidized    class Config      Root      = File.join ENV['HOME'], '.config', 'oxidized' +    Crash     = File.join Root, 'crash'      InputDir  = File.join Directory, %w(lib oxidized input)      OutputDir = File.join Directory, %w(lib oxidized output)      ModelDir  = File.join Directory, %w(lib oxidized model) diff --git a/lib/oxidized/node.rb b/lib/oxidized/node.rb index de04c98..35a5948 100644 --- a/lib/oxidized/node.rb +++ b/lib/oxidized/node.rb @@ -4,7 +4,7 @@ module Oxidized    class ModelNotFound < StandardError; end    class Node      attr_reader :name, :ip, :model, :input, :output, :group, :auth, :prompt -    attr_accessor :last, :running +    attr_accessor :last, :running, :user, :msg, :from      alias :running? :running      def initialize opt        @name           = opt[:name] @@ -47,6 +47,10 @@ module Oxidized        h      end +    def reset +      @user = @msg = @from = nil +    end +      private      def resolve_prompt opt diff --git a/lib/oxidized/nodes.rb b/lib/oxidized/nodes.rb index 3e58348..e5e87e5 100644 --- a/lib/oxidized/nodes.rb +++ b/lib/oxidized/nodes.rb @@ -31,9 +31,15 @@ module Oxidized        delete_at i if i      end      # @param node [String] name of the node moved into the head of array -    def next node +    def next node, opt={} +      require 'pp'        n = del node -      put n if n +      if n +        n.user = opt['user'] +        n.msg  = opt['msg'] +        n.from = opt['from'] +        put n +      end      end      alias :top :next      # @return [String] node from the head of the array diff --git a/lib/oxidized/worker.rb b/lib/oxidized/worker.rb index adbaa0e..6966322 100644 --- a/lib/oxidized/worker.rb +++ b/lib/oxidized/worker.rb @@ -23,8 +23,12 @@ module Oxidized        node.last = job        @jobs.duration job.time        if job.status == :success +        msg = "update #{node.name}" +        msg += " from #{node.from}" if node.from +        msg += " with message '#{node.msg}'" if node.msg          node.output.new.update node.name, job.config,  -                               :msg => "update #{node.name}", :group => node.group +                               :msg => msg, :user => node.user, :group => node.group +        node.reset        else          Log.warn "#{node.name} status #{job.status}"        end @@ -1,10 +1,19 @@  #!/usr/bin/env ruby20  $: << '.' -require 'pry' -#require 'pp' -#require 'rubygems' +require 'pry' if ENV['DEV']  require 'oxidized' -k = Oxidized.new - +begin + Oxidized.new +rescue Exception => e + open Oxidized::Config::Crash, 'w' do |file| +   file.puts '-' * 50 +   file.puts Time.now.utc +   file.puts '-' * 50 +   file.puts e.backtrace +   file.puts '-' * 50 +   file.puts caller + end + raise +end | 
