summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md11
-rw-r--r--Dockerfile11
-rw-r--r--README.md164
-rw-r--r--extra/nagios_check_failing_nodes.rb22
-rw-r--r--extra/oxidized.service12
-rw-r--r--extra/syslog.rb50
-rw-r--r--lib/oxidized/config.rb3
-rw-r--r--lib/oxidized/core.rb5
-rw-r--r--lib/oxidized/hook.rb88
-rw-r--r--lib/oxidized/hook/exec.rb84
-rw-r--r--lib/oxidized/hook/noophook.rb9
-rw-r--r--lib/oxidized/input/ftp.rb54
-rw-r--r--lib/oxidized/input/ssh.rb3
-rw-r--r--lib/oxidized/input/telnet.rb7
-rw-r--r--lib/oxidized/manager.rb11
-rw-r--r--lib/oxidized/model/aosw.rb13
-rw-r--r--lib/oxidized/model/edgeos.rb27
-rw-r--r--lib/oxidized/model/ironware.rb7
-rw-r--r--lib/oxidized/model/masteros.rb46
-rw-r--r--lib/oxidized/model/routeros.rb8
-rw-r--r--lib/oxidized/model/xos.rb2
-rw-r--r--lib/oxidized/model/zynos.rb12
-rw-r--r--lib/oxidized/node.rb3
-rw-r--r--lib/oxidized/nodes.rb31
-rw-r--r--lib/oxidized/output/git.rb88
-rw-r--r--lib/oxidized/worker.rb6
-rw-r--r--oxidized.gemspec7
27 files changed, 743 insertions, 41 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 296c67d..1066372 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+# 0.8.0
+- FEATURE: hooks (by @aakso)
+- FEATURE: MRV MasterOS support (by @kwibbly)
+- FEATURE: EdgeOS support (by @laf)
+- FEATURE: FTP input and Zyxel ZynOS support (by @ytti)
+- FEATURE: version and diffs API For oxidized-web (by @FlorianDoublet)
+- BUGFIX: aosw, ironware, routeros, xos models
+- BUGFIX: crash with 0 nodes
+- BUGFIX: ssh auth fail without keyboard-interactive
+- Full changelog https://github.com/ytti/oxidized/compare/0.7.1...HEAD
+
# 0.7.0
- FEATURE: support http source (by @laf)
- FEATURE: support Palo Alto PANOS (by @rixxxx)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..b2aecfb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM debian:latest
+MAINTAINER Samer Abdel-Hafez <sam@arahant.net>
+
+RUN apt-get update && \
+ apt-get install -y ruby ruby-dev libsqlite3-dev libssl-dev pkg-config make cmake
+
+RUN gem install oxidized oxidized-web --no-ri --no-rdoc
+
+RUN apt-get remove -y ruby-dev pkg-config make cmake
+
+RUN apt-get -y autoremove \ No newline at end of file
diff --git a/README.md b/README.md
index a332444..f6ce764 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
* restful API to reload list of nodes (GET /reload)
* restful API to fetch configurations (/node/fetch/[NODE] or /node/fetch/group/[NODE])
* restful API to show list of nodes (GET /nodes)
+* restful API to show list of version for a node (/node/version[NODE]) and diffs
[Youtube Video: Oxidized TREX 2014 presentation](http://youtu.be/kBQ_CTUuqeU#t=3h)
@@ -22,7 +23,8 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
* [CentOS, Oracle Linux, Red Hat Linux version 6](#centos-oracle-linux-red-hat-linux-version 6)
3. [Initial Configuration](#configuration)
4. [Installing Ruby 2.1.2 using RVM](#installing-ruby-2.1.2-using-rvm)
-5. [Cookbook](#cookbook)
+5. [Running with Docker](#running-with-docker)
+6. [Cookbook](#cookbook)
* [Debugging](#debugging)
* [Privileged mode](#privileged-mode)
* [Source: CSV](#source-csv)
@@ -30,8 +32,9 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
* [Source: HTTP](#source-http)
* [Output: GIT](#output-git)
* [Output: File](#output-file)
+ * [Output types](#output-types)
* [Advanced Configuration](#advanced-configuration)
-6. [Ruby API](#ruby-api)
+7. [Ruby API](#ruby-api)
* [Input](#input)
* [Output](#output)
* [Source](#source)
@@ -67,8 +70,10 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
* Juniper JunOS
* Juniper ScreenOS (Netscreen)
* Mikrotik RouterOS
+ * MRV Master-OS
* Ubiquiti AirOS
* Palo Alto PAN-OS
+ * Zyxel ZyNOS
# Installation
@@ -159,6 +164,43 @@ rvm install 2.1.2
rvm use --default 2.1.2
```
+# Running with Docker
+1. clone git repo:
+
+```
+ root@bla:~# git clone https://github.com/ytti/oxidized
+```
+2. build container locally:
+```
+ root@bla:~# docker build -q -t oxidized/oxidized:latest oxidized/
+```
+3. create config directory in main system:
+```
+ root@bla~:# mkdir /etc/oxidized
+```
+4. run container the first time:
+```
+ root@bla:~# docker run -v /etc/oxidized:/root/.config/oxidized -p 8888:8888/tcp -t oxidized/oxidized:latest oxidized
+```
+5. add 'router.db' to /etc/oxidized:
+```
+ root@bla:~# vim /etc/oxidized/router.db
+ [ ... ]
+ root@bla:~#
+```
+6. run container again:
+```
+ root@bla:~# docker run -v /etc/oxidized:/root/.config/oxidized -p 8888:8888/tcp -t oxidized/oxidized:latest oxidized
+ oxidized[1]: Oxidized starting, running as pid 1
+ oxidized[1]: Loaded 1 nodes
+ Puma 2.13.4 starting...
+ * Min threads: 0, max threads: 16
+ * Environment: development
+ * Listening on tcp://0.0.0.0:8888
+ ^C
+
+ root@bla:~#
+```
## Cookbook
### Debugging
@@ -169,7 +211,7 @@ The following example will log an active ssh session to ```/home/fisakytt/.confi
```
input:
default: ssh, telnet
- debug: ~/.config/oxidized/log_input
+ debug: /tmp/oxidized_log_input
ssh:
secure: false
```
@@ -265,6 +307,52 @@ output:
repo: "/var/lib/oxidized/devices.git"
```
+### Output types
+
+If you prefer to have different outputs in different files and/or directories, you can easily do this by modifying the corresponding model. To change the behaviour for IOS, you would edit `lib/oxidized/model/ios.rb`.
+
+For example, let's say you want to split out `show version` and `show inventory` into separate files in a directory called `nodiff` which your tools will not send automated diffstats for. You can apply a patch along the lines of
+
+```
+- cmd 'show version' do |cfg|
+- comment cfg.lines.first
++ cmd 'show version' do |state|
++ state.type = 'nodiff'
++ state
+
+- cmd 'show inventory' do |cfg|
+- comment cfg
++ cmd 'show inventory' do |state|
++ state.type = 'nodiff'
++ state
++ end
+
+- cmd 'show running-config' do |cfg|
+- cfg = cfg.each_line.to_a[3..-1].join
+- cfg.gsub! /^Current configuration : [^\n]*\n/, ''
+- cfg.sub! /^(ntp clock-period).*/, '! \1'
+- cfg.gsub! /^\ tunnel\ mpls\ traffic-eng\ bandwidth[^\n]*\n*(
++ cmd 'show running-config' do |state|
++ state = state.each_line.to_a[3..-1].join
++ state.gsub! /^Current configuration : [^\n]*\n/, ''
++ state.sub! /^(ntp clock-period).*/, '! \1'
++ state.gsub! /^\ tunnel\ mpls\ traffic-eng\ bandwidth[^\n]*\n*(
+ (?:\ [^\n]*\n*)*
+ tunnel\ mpls\ traffic-eng\ auto-bw)/mx, '\1'
+- cfg
++ state = Oxidized::String.new state
++ state.type = 'nodiff'
++ state
+```
+
+which will result in the following layout
+
+```
+diff/$FQDN--show_running_config
+nodiff/$FQDN--show_version
+nodiff/$FQDN--show_inventory
+```
+
### Advanced Configuration
Below is an advanced example configuration. You will be able to (optinally) override options per device. The router.db format used is ```hostname:model:username:password:enable_password```. Hostname and model will be the only required options, all others override the global configuration sections.
@@ -313,6 +401,57 @@ model_map:
juniper: junos
```
+# Hooks
+You can define arbitrary number of hooks that subscribe different events. The hook system is modular and different kind of hook types can be enabled.
+
+## Configuration
+Following configuration keys need to be defined for all hooks:
+
+ * `events`: which events to subscribe. Needs to be an array. See below for the list of available events.
+ * `type`: what hook class to use. See below for the list of available hook types.
+
+### Events
+ * `node_success`: triggered when configuration is succesfully pulled from a node and right before storing the configuration.
+ * `node_fail`: triggered after `retries` amount of failed node pulls.
+ * `post_store`: triggered after node configuration is stored.
+
+## Hook type: exec
+The `exec` hook type allows users to run an arbitrary shell command or a binary when triggered.
+
+The command is executed on a separate child process either in synchronous or asynchronous fashion. Non-zero exit values cause errors to be logged. STDOUT and STDERR are currently not collected.
+
+Command is executed with the following environment:
+```
+OX_EVENT
+OX_NODE_NAME
+OX_NODE_FROM
+OX_NODE_MSG
+OX_NODE_GROUP
+OX_JOB_STATUS
+OX_JOB_TIME
+```
+
+Exec hook recognizes following configuration keys:
+
+ * `timeout`: hard timeout for the command execution. SIGTERM will be sent to the child process after the timeout has elapsed. Default: 60
+ * `async`: influences whether main thread will wait for the command execution. Set this true for long running commands so node pull is not blocked. Default: false
+ * `cmd`: command to run.
+
+
+## Hook configuration example
+```
+hooks:
+ name_for_example_hook1:
+ type: exec
+ events: [node_success]
+ cmd: 'echo "Node success $OX_NODE_NAME" >> /tmp/ox_node_success.log'
+ name_for_example_hook2:
+ type: exec
+ events: [post_store, node_fail]
+ cmd: 'echo "Doing long running stuff for $OX_NODE_NAME" >> /tmp/ox_node_stuff.log; sleep 60'
+ async: true
+ timeout: 120
+```
# Ruby API
@@ -345,3 +484,22 @@ The following objects exist in Oxidized.
* cfg is executed in input/output/source context
* cmd is executed in instance of model
* 'junos', 'ios', 'ironware' and 'powerconnect' implemented
+
+
+# License and Copyright
+
+Copyright 2013-2015 Saku Ytti <saku@ytti.fi>
+ 2013-2015 Samer Abdel-Hafez <sam@arahant.net>
+
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/extra/nagios_check_failing_nodes.rb b/extra/nagios_check_failing_nodes.rb
index 1c81f66..27a5c66 100644
--- a/extra/nagios_check_failing_nodes.rb
+++ b/extra/nagios_check_failing_nodes.rb
@@ -6,20 +6,30 @@ require 'open-uri'
require 'json'
critical = false
+pending = false
critical_nodes = []
+pending_nodes = []
json = JSON.load(open("http://localhost:8888/nodes.json"))
json.each do |node|
- if node['last']['status'] != 'success'
- critical_nodes << node['name']
- critical = true
+ if not node['last'].nil?
+ if node['last']['status'] != 'success'
+ critical_nodes << node['name']
+ critical = true
+ end
+ else
+ pending_nodes << node['name']
+ pending = true
end
end
-if critical
- puts 'Unable to backup: ' + critical_nodes.join(' ')
+if pending
+ puts '[WARN] Pending backup: ' + pending_nodes.join(',')
+ exit 1
+elsif critical
+ puts '[CRIT] Unable to backup: ' + critical_nodes.join(',')
exit 2
else
- puts 'Backup of all nodes completed successfully.'
+ puts '[OK] Backup of all nodes completed successfully.'
exit 0
end
diff --git a/extra/oxidized.service b/extra/oxidized.service
new file mode 100644
index 0000000..65063b7
--- /dev/null
+++ b/extra/oxidized.service
@@ -0,0 +1,12 @@
+#For debian 8 put it in /lib/systemd/system/
+#and call it with systemctl start oxidized.service
+
+[Unit]
+Description=Oxidized - Network Device Configuration Backup Tool
+
+[Service]
+ExecStart=/usr/local/bin/oxidized
+User=root
+
+[Install]
+WantedBy=multi-user.target
diff --git a/extra/syslog.rb b/extra/syslog.rb
index 20e439e..e364cf9 100644
--- a/extra/syslog.rb
+++ b/extra/syslog.rb
@@ -8,10 +8,18 @@
# set system syslog host SERVER interactive-commands notice
# set system syslog host SERVER match "^mgd\[[0-9]+\]: UI_COMMIT: .*"
-# Ports < 1024 need extra privileges, use a port higher than this by passing the first argument a number
-# To use the default port for syslog (514) you shouldnt pass an argument, but you will need to allow this with:
+# Ports < 1024 need extra privileges, use a port higher than this by setting the port option in your oxidized config file.
+# To use the default port for syslog (514) you shouldn't pass an argument, but you will need to allow this with:
# sudo setcap 'cap_net_bind_service=+ep' /usr/bin/ruby
+# Config options are:
+# syslogd
+# port (Default = 514)
+# file (Default = messages)
+# resolve (Default = true)
+
+# To stop the resolution of IP's to PTR you can set resolve to false
+
# exit if fork ## TODO: proper daemonize
require 'socket'
@@ -19,13 +27,30 @@ require 'resolv'
require_relative 'rest_client'
module Oxidized
+
+ require 'asetus'
+ class Config
+ Root = File.join ENV['HOME'], '.config', 'oxidized'
+ end
+
+ CFGS = Asetus.new :name=>'oxidized', :load=>false, :key_to_s=>true
+ CFGS.default.syslogd.port = 514
+ CFGS.default.syslogd.file = 'messages'
+ CFGS.default.syslogd.resolve = true
+
+ begin
+ CFGS.load
+ rescue => error
+ raise InvalidConfig, "Error loading config: #{error.message}"
+ ensure
+ CFG = CFGS.cfg # convenienence, instead of Config.cfg.password, CFG.password
+ end
+
class SyslogMonitor
NAME_MAP = {
/(.*)\.ip\.tdc\.net/ => '\1',
/(.*)\.ip\.fi/ => '\1',
}
- PORT = 514
- FILE = 'messages'
MSG = {
:ios => /%SYS-(SW[0-9]+-)?5-CONFIG_I:/,
:junos => 'UI_COMMIT:',
@@ -34,13 +59,12 @@ module Oxidized
}
class << self
- def udp port=PORT, listen=0
- port ||= PORT
+ def udp port=Oxidized::CFG.syslogd.port, listen=0
io = UDPSocket.new
io.bind listen, port
new io, :udp
end
- def file syslog_file=FILE
+ def file syslog_file=Oxidized::CFG.syslogd.file
io = open syslog_file, 'r'
io.seek 0, IO::SEEK_END
new io, :file
@@ -104,12 +128,16 @@ module Oxidized
end
def getname ip
- name = (Resolv.getname ip.to_s rescue ip)
- NAME_MAP.each { |re, sub| name.sub! re, sub }
- name
+ if Oxidized::CFG.syslogd.resolve == false
+ ip
+ else
+ name = (Resolv.getname ip.to_s rescue ip)
+ NAME_MAP.each { |re, sub| name.sub! re, sub }
+ name
+ end
end
end
end
-Oxidized::SyslogMonitor.udp ARGV[0]
+Oxidized::SyslogMonitor.udp
#Oxidized::SyslogMonitor.file '/var/log/poop'
diff --git a/lib/oxidized/config.rb b/lib/oxidized/config.rb
index 96c8fbf..f45004a 100644
--- a/lib/oxidized/config.rb
+++ b/lib/oxidized/config.rb
@@ -9,10 +9,11 @@ module Oxidized
OutputDir = File.join Directory, %w(lib oxidized output)
ModelDir = File.join Directory, %w(lib oxidized model)
SourceDir = File.join Directory, %w(lib oxidized source)
+ HookDir = File.join Directory, %w(lib oxidized hook)
Sleep = 1
end
class << self
- attr_accessor :mgr
+ attr_accessor :mgr, :Hooks
end
CFGS = Asetus.new :name=>'oxidized', :load=>false, :key_to_s=>true
CFGS.default.username = 'username'
diff --git a/lib/oxidized/core.rb b/lib/oxidized/core.rb
index 71267dd..6e7a352 100644
--- a/lib/oxidized/core.rb
+++ b/lib/oxidized/core.rb
@@ -6,6 +6,7 @@ module Oxidized
require 'oxidized/worker'
require 'oxidized/nodes'
require 'oxidized/manager'
+ require 'oxidized/hook'
class << self
def new *args
Core.new args
@@ -13,9 +14,13 @@ module Oxidized
end
class Core
+ class NoNodesFound < OxidizedError; end
+
def initialize args
Oxidized.mgr = Manager.new
+ Oxidized.Hooks = HookManager.from_config CFG
nodes = Nodes.new
+ raise NoNodesFound, 'source returns no usable nodes' if nodes.size == 0
@worker = Worker.new nodes
trap('HUP') { nodes.load }
if CFG.rest?
diff --git a/lib/oxidized/hook.rb b/lib/oxidized/hook.rb
new file mode 100644
index 0000000..2763c4f
--- /dev/null
+++ b/lib/oxidized/hook.rb
@@ -0,0 +1,88 @@
+module Oxidized
+class HookManager
+ class << self
+ def from_config cfg
+ mgr = new
+ cfg.hooks.each do |name,h_cfg|
+ h_cfg.events.each do |event|
+ mgr.register event.to_sym, name, h_cfg.type, h_cfg
+ end
+ end
+ mgr
+ end
+ end
+
+ # HookContext is passed to each hook. It can contain anything related to the
+ # event in question. At least it contains the event name
+ class HookContext < OpenStruct; end
+
+ # RegisteredHook is a container for a Hook instance
+ class RegisteredHook < Struct.new(:name, :hook); end
+
+ Events = [
+ :node_success,
+ :node_fail,
+ :post_store,
+ ]
+ attr_reader :registered_hooks
+
+ def initialize
+ @registered_hooks = Hash.new {|h,k| h[k] = []}
+ end
+
+ def register event, name, hook_type, cfg
+ unless Events.include? event
+ raise ArgumentError,
+ "unknown event #{event}, available: #{Events.join ','}"
+ end
+
+ Oxidized.mgr.add_hook hook_type
+ begin
+ hook = Oxidized.mgr.hook.fetch(hook_type).new
+ rescue KeyError
+ raise KeyError, "cannot find hook #{hook_type.inspect}"
+ end
+
+ hook.cfg = cfg
+
+ @registered_hooks[event] << RegisteredHook.new(name, hook)
+ Log.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
+ end
+
+ def handle event, **ctx_params
+ ctx = HookContext.new ctx_params
+ ctx.event = event
+
+ @registered_hooks[event].each do |r_hook|
+ begin
+ r_hook.hook.run_hook ctx
+ rescue => e
+ Log.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " +
+ "(#{e.inspect}) for event #{event.inspect}"
+ end
+ end
+ end
+end
+
+# Hook abstract base class
+class Hook
+ attr_accessor :cfg
+
+ def initialize
+ end
+
+ def cfg=(cfg)
+ @cfg = cfg
+ validate_cfg! if self.respond_to? :validate_cfg!
+ end
+
+ def run_hook ctx
+ raise NotImplementedError
+ end
+
+ def log(msg, level=:info)
+ Log.send(level, "#{self.class.name}: #{msg}")
+ end
+
+end
+end
diff --git a/lib/oxidized/hook/exec.rb b/lib/oxidized/hook/exec.rb
new file mode 100644
index 0000000..eb71466
--- /dev/null
+++ b/lib/oxidized/hook/exec.rb
@@ -0,0 +1,84 @@
+class Exec < Oxidized::Hook
+ include Process
+
+ def initialize
+ super
+ @timeout = 60
+ @async = false
+ end
+
+ def validate_cfg!
+ # Syntax check
+ if cfg.has_key? "timeout"
+ @timeout = cfg.timeout
+ raise "invalid timeout value" unless @timeout.is_a?(Integer) &&
+ @timeout > 0
+ end
+
+ if cfg.has_key? "async"
+ @async = !!cfg.async
+ end
+
+ if cfg.has_key? "cmd"
+ @cmd = cfg.cmd
+ raise "invalid cmd value" unless @cmd.is_a?(String) || @cmd.is_a?(Array)
+ end
+
+ rescue RuntimeError => e
+ raise ArgumentError,
+ "#{self.class.name}: configuration invalid: #{e.message}"
+ end
+
+ def run_hook ctx
+ env = make_env ctx
+ log "Execute: #{@cmd.inspect}", :debug
+ th = Thread.new do
+ begin
+ run_cmd! env
+ rescue => e
+ raise e unless @async
+ end
+ end
+ th.join unless @async
+ end
+
+ def run_cmd! env
+ pid, status = nil, nil
+ Timeout.timeout(@timeout) do
+ pid = spawn env, @cmd , :unsetenv_others => true
+ pid, status = wait2 pid
+ unless status.exitstatus.zero?
+ msg = "#{@cmd.inspect} failed with exit value #{status.exitstatus}"
+ log msg, :error
+ raise msg
+ end
+ end
+ rescue TimeoutError
+ kill "TERM", pid
+ msg = "#{@cmd} timed out"
+ log msg, :error
+ raise TimeoutError, msg
+ end
+
+ def make_env ctx
+ env = {
+ "OX_EVENT" => ctx.event.to_s
+ }
+ if ctx.node
+ env.merge!(
+ "OX_NODE_NAME" => ctx.node.name.to_s,
+ "OX_NODE_FROM" => ctx.node.from.to_s,
+ "OX_NODE_MSG" => ctx.node.msg.to_s,
+ "OX_NODE_GROUP" => ctx.node.group.to_s,
+ "OX_EVENT" => ctx.event.to_s,
+ )
+ end
+ if ctx.job
+ env.merge!(
+ "OX_JOB_STATUS" => ctx.job.status.to_s,
+ "OX_JOB_TIME" => ctx.job.time.to_s,
+ )
+ end
+ env
+ end
+end
diff --git a/lib/oxidized/hook/noophook.rb b/lib/oxidized/hook/noophook.rb
new file mode 100644
index 0000000..d4673ba
--- /dev/null
+++ b/lib/oxidized/hook/noophook.rb
@@ -0,0 +1,9 @@
+class NoopHook < Oxidized::Hook
+ def validate_cfg!
+ log "Validate config"
+ end
+
+ def run_hook ctx
+ log "Run hook with context: #{ctx}"
+ end
+end
diff --git a/lib/oxidized/input/ftp.rb b/lib/oxidized/input/ftp.rb
new file mode 100644
index 0000000..ccbf5ef
--- /dev/null
+++ b/lib/oxidized/input/ftp.rb
@@ -0,0 +1,54 @@
+module Oxidized
+ require 'net/ftp'
+ require 'timeout'
+ require_relative 'cli'
+
+ class FTP < Input
+ RescueFail = {
+ :debug => [
+ #Net::SSH::Disconnect,
+ ],
+ :warn => [
+ #RuntimeError,
+ #Net::SSH::AuthenticationFailed,
+ ],
+ }
+ include Input::CLI
+
+ def connect node
+ @node = node
+ @node.model.cfg['ftp'].each { |cb| instance_exec(&cb) }
+ @log = File.open(CFG.input.debug?.to_s + '-ftp', 'w') if CFG.input.debug?
+ @ftp = Net::FTP.new @node.ip, @node.auth[:username], @node.auth[:password]
+ connected?
+ end
+
+ def connected?
+ @ftp and not @ftp.closed?
+ end
+
+ def cmd file
+ Log.debug "FTP: #{file} @ #{@node.name}"
+ @ftp.getbinaryfile file, nil
+ end
+
+ # meh not sure if this is the best way, but perhaps better than not implementing send
+ def send my_proc
+ my_proc.call
+ end
+
+ def output
+ ""
+ end
+
+ private
+
+ def disconnect
+ @ftp.close
+ #rescue Errno::ECONNRESET, IOError
+ ensure
+ @log.close if CFG.input.debug?
+ end
+
+ end
+end
diff --git a/lib/oxidized/input/ssh.rb b/lib/oxidized/input/ssh.rb
index b1f109b..d6b4e1f 100644
--- a/lib/oxidized/input/ssh.rb
+++ b/lib/oxidized/input/ssh.rb
@@ -21,7 +21,8 @@ module Oxidized
@node.model.cfg['ssh'].each { |cb| instance_exec(&cb) }
secure = CFG.input.ssh.secure
@log = File.open(CFG.input.debug?.to_s + '-ssh', 'w') if CFG.input.debug?
- @ssh = Net::SSH.start @node.ip, @node.auth[:username],
+ port = vars(:ssh_port) || 22
+ @ssh = Net::SSH.start @node.ip, @node.auth[:username], :port => port.to_i,
:password => @node.auth[:password], :timeout => CFG.timeout,
:paranoid => secure,
:auth_methods => %w(none publickey password keyboard-interactive),
diff --git a/lib/oxidized/input/telnet.rb b/lib/oxidized/input/telnet.rb
index 13fccf7..d305fcd 100644
--- a/lib/oxidized/input/telnet.rb
+++ b/lib/oxidized/input/telnet.rb
@@ -10,9 +10,12 @@ module Oxidized
@node = node
@timeout = CFG.timeout
@node.model.cfg['telnet'].each { |cb| instance_exec(&cb) }
+ port = vars(:telnet_port) || 23
- opt = { 'Host' => @node.ip, 'Timeout' => @timeout,
- 'Model' => @node.model }
+ opt = { 'Host' => @node.ip,
+ 'Port' => port.to_i,
+ 'Timeout' => @timeout,
+ 'Model' => @node.model }
opt['Output_log'] = CFG.input.debug?.to_s + '-telnet' if CFG.input.debug?
@telnet = Net::Telnet.new opt
diff --git a/lib/oxidized/manager.rb b/lib/oxidized/manager.rb
index b4eaecd..bf28ae7 100644
--- a/lib/oxidized/manager.rb
+++ b/lib/oxidized/manager.rb
@@ -23,12 +23,13 @@ module Oxidized
end
end
end
- attr_reader :input, :output, :model, :source
+ attr_reader :input, :output, :model, :source, :hook
def initialize
@input = {}
@output = {}
@model = {}
@source = {}
+ @hook = {}
end
def add_input method
method = Manager.load Config::InputDir, method
@@ -53,5 +54,13 @@ module Oxidized
return false if _source.empty?
@source.merge! _source
end
+ def add_hook _hook
+ return nil if @hook.key? _hook
+ name = _hook
+ _hook = Manager.load File.join(Config::Root, 'hook'), name
+ _hook = Manager.load Config::HookDir, name if _hook.empty?
+ return false if _hook.empty?
+ @hook.merge! _hook
+ end
end
end
diff --git a/lib/oxidized/model/aosw.rb b/lib/oxidized/model/aosw.rb
index 203c5e0..43e1cff 100644
--- a/lib/oxidized/model/aosw.rb
+++ b/lib/oxidized/model/aosw.rb
@@ -5,7 +5,7 @@ class AOSW < Oxidized::Model
# Also Dell controllers
comment '# '
- prompt /^\([^)]+\) #/
+ prompt /^\([^)]+\) [#>]/
cmd :all do |cfg|
cfg.each_line.to_a[1..-2].join
@@ -36,7 +36,16 @@ class AOSW < Oxidized::Model
end
cfg :telnet, :ssh do
+ if vars :enable
+ post_login do
+ send 'enable\n'
+ send vars(:enable) + '\n'
+ end
+ end
post_login 'no paging'
+ if vars :enable
+ pre_logout 'exit'
+ end
pre_logout 'exit'
end
@@ -50,7 +59,7 @@ class AOSW < Oxidized::Model
next if line.match /[0-9]+ (RPM|mV|C)$/
out << line.strip
end
- out = out.join "\n"
+ out = comment out.join "\n"
out << "\n"
end
diff --git a/lib/oxidized/model/edgeos.rb b/lib/oxidized/model/edgeos.rb
new file mode 100644
index 0000000..2a8d663
--- /dev/null
+++ b/lib/oxidized/model/edgeos.rb
@@ -0,0 +1,27 @@
+class Edgeos < Oxidized::Model
+
+ # EdgeOS #
+
+ prompt /\@.*?\:~\$\s/
+
+ cmd :all do |cfg|
+ cfg = cfg.lines.to_a[1..-2].join
+ end
+
+ cmd :secret do |cfg|
+ cfg.gsub! /community (\S+) {/, 'community <hidden> {'
+ cfg
+ end
+
+ cmd 'show configuration | no-more'
+
+ cfg :telnet do
+ username /login:\s/
+ password /^Password:\s/
+ end
+
+ cfg :telnet, :ssh do
+ pre_logout 'exit'
+ end
+
+end
diff --git a/lib/oxidized/model/ironware.rb b/lib/oxidized/model/ironware.rb
index 1ec8b80..805e07b 100644
--- a/lib/oxidized/model/ironware.rb
+++ b/lib/oxidized/model/ironware.rb
@@ -1,6 +1,6 @@
class IronWare < Oxidized::Model
- prompt /^.+[>#]\s?$/
+ prompt /^.*(telnet|ssh)\@.+[>#]\s?$/i
comment '! '
#to handle pager without enable
@@ -26,13 +26,13 @@ class IronWare < Oxidized::Model
cmd 'show version' do |cfg|
cfg.gsub! /(^((.*)[Ss]ystem uptime(.*))$)/, '' #remove unwanted line system uptime
- cfg.gsub! /uptime is .*/,''
+ cfg.gsub! /[Uu]p\s?[Tt]ime is .*/,''
comment cfg
end
cmd 'show chassis' do |cfg|
- cfg.gsub! "\xFF", '' # ugly hack - avoids JSON.dump utf-8 breakage on 1.9..
+ cfg.encode!("UTF-8", :invalid => :replace) #sometimes ironware returns broken encoding
cfg.gsub! /(^((.*)Current temp(.*))$)/, '' #remove unwanted lines current temperature
cfg.gsub! /Speed = [A-Z]{3} \(\d{2}\%\)/, '' #remove unwanted lines Speed Fans
cfg.gsub! /current speed is [A-Z]{3} \(\d{2}\%\)/, ''
@@ -71,6 +71,7 @@ class IronWare < Oxidized::Model
send vars(:enable) + "\n"
end
end
+ post_login ''
post_login 'skip-page-display'
post_login 'terminal length 0'
pre_logout 'logout'
diff --git a/lib/oxidized/model/masteros.rb b/lib/oxidized/model/masteros.rb
new file mode 100644
index 0000000..3f5a2fc
--- /dev/null
+++ b/lib/oxidized/model/masteros.rb
@@ -0,0 +1,46 @@
+class MasterOS < Oxidized::Model
+
+ # MRV MasterOS model #
+
+comment '!'
+
+ cmd :secret do |cfg|
+ cfg.gsub! /^(snmp-server community).*/, '\\1 <configuration removed>'
+ cfg.gsub! /username (\S+) password encrypted (\S+) class (\S+).*/, '<secret hidden>'
+ cfg
+ end
+
+ cmd :all do |cfg|
+ cfg.each_line.to_a[1..-2].join
+ end
+
+ cmd 'show inventory' do |cfg|
+ cfg = cfg.each_line.to_a[0..-2].join
+ comment cfg
+ end
+
+ cmd 'show plugins' do |cfg|
+ comment cfg
+ end
+
+ cmd 'show hw-config' do |cfg|
+ comment cfg
+ end
+
+ cmd 'show running-config' do |cfg|
+ cfg = cfg.each_line.to_a[3..-1].join
+ cfg
+ end
+
+ cfg :telnet, :ssh do
+ post_login 'no pager'
+ if vars :enable
+ post_login do
+ send "enable\n"
+ send vars(:enable) + "\n"
+ end
+ end
+ pre_logout 'exit'
+ end
+
+end \ No newline at end of file
diff --git a/lib/oxidized/model/routeros.rb b/lib/oxidized/model/routeros.rb
index bd588b7..4822500 100644
--- a/lib/oxidized/model/routeros.rb
+++ b/lib/oxidized/model/routeros.rb
@@ -1,5 +1,5 @@
class RouterOS < Oxidized::Model
- prompt /^\[\w+@\S+\]\s?>\s?$/
+ prompt /\[\w+@\S+\]\s?>\s?$/
comment "# "
cmd '/system routerboard print' do |cfg|
@@ -7,10 +7,16 @@ class RouterOS < Oxidized::Model
end
cmd '/export' do |cfg|
+ cfg.gsub! /\x1B\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[m|K]/, '' # strip ANSI colours
cfg = cfg.split("\n").select { |line| not line[/^\#\s\w{3}\/\d{2}\/\d{4}.*$/] }
cfg.join("\n") + "\n"
end
+ cfg :telnet do
+ username /^Login:/
+ password /^Password:/
+ end
+
cfg :ssh do
exec true
end
diff --git a/lib/oxidized/model/xos.rb b/lib/oxidized/model/xos.rb
index 88c81ed..309340f 100644
--- a/lib/oxidized/model/xos.rb
+++ b/lib/oxidized/model/xos.rb
@@ -6,7 +6,7 @@ class XOS < Oxidized::Model
comment '# '
cmd :all do |cfg|
- cfg.each_line.to_a[1..-2].join.rstrip
+ cfg.each_line.to_a[1..-2].join
end
cmd 'show version' do |cfg|
diff --git a/lib/oxidized/model/zynos.rb b/lib/oxidized/model/zynos.rb
new file mode 100644
index 0000000..89be8af
--- /dev/null
+++ b/lib/oxidized/model/zynos.rb
@@ -0,0 +1,12 @@
+class ZyNOS < Oxidized::Model
+
+ # Used in Zyxel DSLAMs, such as SAM1316
+
+ comment '! '
+
+ cmd 'config-0'
+
+ cfg :ftp do
+ end
+
+end
diff --git a/lib/oxidized/node.rb b/lib/oxidized/node.rb
index 253de53..d50317f 100644
--- a/lib/oxidized/node.rb
+++ b/lib/oxidized/node.rb
@@ -29,6 +29,9 @@ module Oxidized
def run
status, config = :fail, nil
@input.each do |input|
+ # don't try input if model is missing config block, we may need strong config to class_name map
+ cfg_name = input.to_s.split('::').last.downcase
+ next unless @model.cfg[cfg_name] and not @model.cfg[cfg_name].empty?
@model.input = input = input.new
if config=run_input(input)
status = :success
diff --git a/lib/oxidized/nodes.rb b/lib/oxidized/nodes.rb
index cb2fbc5..cb2ce7b 100644
--- a/lib/oxidized/nodes.rb
+++ b/lib/oxidized/nodes.rb
@@ -111,7 +111,7 @@ module Oxidized
end
def find_index node
- index { |e| e.name == node }
+ index { |e| e.name == node or e.ip == node}
end
# @param node node which is removed from nodes list
@@ -148,6 +148,35 @@ module Oxidized
end
end
end
+
+ public
+
+ def version node, group
+ with_lock do
+ i = find_node_index node
+ output = self[i].output.new
+ raise Oxidized::NotSupported unless output.respond_to? :fetch
+ output.version node, group
+ end
+ end
+
+ def get_version node, group, oid
+ with_lock do
+ i = find_node_index node
+ output = self[i].output.new
+ raise Oxidized::NotSupported unless output.respond_to? :fetch
+ output.get_version node, group, oid
+ end
+ end
+
+ def get_diff node, group, oid1, oid2
+ with_lock do
+ i = find_node_index node
+ output = self[i].output.new
+ raise Oxidized::NotSupported unless output.respond_to? :fetch
+ output.get_diff node, group, oid1, oid2
+ end
+ end
end
end
diff --git a/lib/oxidized/output/git.rb b/lib/oxidized/output/git.rb
index 57b6f62..3757cfc 100644
--- a/lib/oxidized/output/git.rb
+++ b/lib/oxidized/output/git.rb
@@ -19,6 +19,7 @@ class Git < Output
CFGS.save :user
raise NoConfig, 'no output git config, edit ~/.config/oxidized/config'
end
+ @cfg.repo = File.expand_path @cfg.repo
end
def store file, outputs, opt={}
@@ -50,18 +51,95 @@ class Git < Output
def fetch node, group
begin
repo = @cfg.repo
- if group
- repo = File.join File.dirname(repo), group + '.git'
- end
+ repo = File.join File.dirname(repo), group + '.git' if group and not @cfg.single_repo?
repo = Rugged::Repository.new repo
index = repo.index
index.read_tree repo.head.target.tree unless repo.empty?
- repo.read(index.get(node)[:oid]).data
+ file = node
+ file = File.join(group, node) if group and @cfg.single_repo?
+ repo.read(index.get(file)[:oid]).data
rescue
'node not found'
end
end
+ #give a hash of all oid revision for the givin node, and the date of the commit
+ def version node, group
+ begin
+ repo = @cfg.repo
+ if group
+ repo = File.join File.dirname(repo), group + '.git'
+ end
+ repo = Rugged::Repository.new repo
+ walker = Rugged::Walker.new(repo)
+ walker.sorting(Rugged::SORT_DATE)
+ walker.push(repo.head.target)
+ i = -1
+ tab = []
+ walker.each do |commit|
+ if commit.diff(paths: [node]).size > 0
+ hash = {}
+ hash[:date] = commit.time.to_s
+ hash[:oid] = commit.oid
+ hash[:author] = commit.author
+ hash[:message] = commit.message
+ tab[i += 1] = hash
+ end
+ end
+ walker.reset
+ tab
+ rescue
+ 'node not found'
+ end
+ end
+
+ #give the blob of a specific revision
+ def get_version node, group, oid
+ begin
+ repo = @cfg.repo
+ if group && group != ''
+ repo = File.join File.dirname(repo), group + '.git'
+ end
+ repo = Rugged::Repository.new repo
+ repo.blob_at(oid,node).content
+ rescue
+ 'version not found'
+ end
+ end
+
+ #give a hash with the patch of a diff between 2 revision and the stats (added and deleted lines)
+ def get_diff node, group, oid1, oid2
+ begin
+ repo = @cfg.repo
+ diff_commits = nil
+ if group && group != ''
+ repo = File.join File.dirname(repo), group + '.git'
+ end
+ repo = Rugged::Repository.new repo
+ commit = repo.lookup(oid1)
+ #if the second revision is precised
+ if oid2
+ commit_old = repo.lookup(oid2)
+ diff = repo.diff(commit_old, commit)
+ diff.each do |patch|
+ if /#{node}\s+/.match(patch.to_s.lines.first)
+ diff_commits = {:patch => patch.to_s, :stat => patch.stat}
+ break
+ end
+ end
+ #else gives the diffs between the first oid and his first parrent
+ else
+ stat = commit.parents[0].diff(commit).stat
+ stat = [stat[1],stat[2]]
+ patch = commit.parents[0].diff(commit).patch
+ diff_commits = {:patch => patch, :stat => stat}
+ end
+ diff_commits
+ rescue
+ 'no diffs'
+ end
+ end
+
private
def update repo, file, data
@@ -104,7 +182,7 @@ class Git < Output
:parents => repo.empty? ? [] : [repo.head.target].compact,
:update_ref => 'HEAD',
)
-
+
index.write
true
end
diff --git a/lib/oxidized/worker.rb b/lib/oxidized/worker.rb
index 6bb2a22..eea747e 100644
--- a/lib/oxidized/worker.rb
+++ b/lib/oxidized/worker.rb
@@ -34,12 +34,16 @@ module Oxidized
@jobs.duration job.time
node.running = false
if job.status == :success
+ Oxidized.Hooks.handle :node_success, :node => node,
+ :job => job
msg = "update #{node.name}"
msg += " from #{node.from}" if node.from
msg += " with message '#{node.msg}'" if node.msg
if node.output.new.store node.name, job.config,
:msg => msg, :user => node.user, :group => node.group
Log.info "Configuration updated for #{node.group}/#{node.name}"
+ Oxidized.Hooks.handle :post_store, :node => node,
+ :job => job
end
node.reset
else
@@ -51,6 +55,8 @@ module Oxidized
else
msg += ", retries exhausted, giving up"
node.retry = 0
+ Oxidized.Hooks.handle :node_fail, :node => node,
+ :job => job
end
Log.warn msg
end
diff --git a/oxidized.gemspec b/oxidized.gemspec
index 62c3e73..15e4e41 100644
--- a/oxidized.gemspec
+++ b/oxidized.gemspec
@@ -1,10 +1,10 @@
Gem::Specification.new do |s|
s.name = 'oxidized'
- s.version = '0.7.1'
+ s.version = '0.8.0'
s.licenses = %w( Apache-2.0 )
s.platform = Gem::Platform::RUBY
- s.authors = [ 'Saku Ytti', 'Samer Abdel-Hafez' ]
- s.email = %w( saku@ytti.fi sam@arahant.net )
+ s.authors = [ 'Saku Ytti', 'Samer Abdel-Hafez', 'Anton Aksola' ]
+ s.email = %w( saku@ytti.fi sam@arahant.net aakso@iki.fi)
s.homepage = 'http://github.com/ytti/oxidized'
s.summary = 'feeble attempt at rancid'
s.description = 'software to fetch configuration from network devices and store them'
@@ -18,4 +18,5 @@ Gem::Specification.new do |s|
s.add_runtime_dependency 'slop', '~> 3.5'
s.add_runtime_dependency 'net-ssh', '~> 2.8'
s.add_runtime_dependency 'rugged', '~> 0.21', '>= 0.21.4'
+ s.add_development_dependency 'pry', '~> 0'
end