oxidized 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c94960c6cdf5ebe4dc6910df211a29a55e3e3ecd
4
- data.tar.gz: 9f8a27d266ac4e52519017bce1502df6b87fe802
3
+ metadata.gz: 92cfff2bf262414c347bf28d769b62618495327b
4
+ data.tar.gz: f82dbcaaaa017cf3359a5ffae022d73789a174c8
5
5
  SHA512:
6
- metadata.gz: 1e005677f0702e5538645a4489ec4e123dad5ffc88386dc9186053e0db6f08c0321db0df1e945b84d5bb57cecf92c750a25eb3d1b82970496c8fa423f70be083
7
- data.tar.gz: 16b8495f91c14040ca26c7f5028250a23ffa54aa7268ea5420ced503ef9fd4027ec46e959801854f44ca03f4cee64fdbd2fa787dca4a6decc69e8e9833a24931
6
+ metadata.gz: bcb49a619bf2d17d457e800e6a4a285d9d05907d52aec1e03ad2605f210ba2e1ef1ef75ce613f538e28135d8a37f99c9f482faac4acf7e583d90b3474c538585
7
+ data.tar.gz: 2dd8a40b4a6b12cef3db4e74164533379c9a7192f1e6dab929722595f7352b85fadeb1b8514c8aad2e9c23eb1be0f7d54a70fd52b9b524198e6fa219e20d1b20
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ # 0.8.0
2
+ - FEATURE: hooks (by @aakso)
3
+ - FEATURE: MRV MasterOS support (by @kwibbly)
4
+ - FEATURE: EdgeOS support (by @laf)
5
+ - FEATURE: FTP input and Zyxel ZynOS support (by @ytti)
6
+ - FEATURE: version and diffs API For oxidized-web (by @FlorianDoublet)
7
+ - BUGFIX: aosw, ironware, routeros, xos models
8
+ - BUGFIX: crash with 0 nodes
9
+ - BUGFIX: ssh auth fail without keyboard-interactive
10
+ - Full changelog https://github.com/ytti/oxidized/compare/0.7.1...HEAD
11
+
1
12
  # 0.7.0
2
13
  - FEATURE: support http source (by @laf)
3
14
  - FEATURE: support Palo Alto PANOS (by @rixxxx)
data/Dockerfile ADDED
@@ -0,0 +1,11 @@
1
+ FROM debian:latest
2
+ MAINTAINER Samer Abdel-Hafez <sam@arahant.net>
3
+
4
+ RUN apt-get update && \
5
+ apt-get install -y ruby ruby-dev libsqlite3-dev libssl-dev pkg-config make cmake
6
+
7
+ RUN gem install oxidized oxidized-web --no-ri --no-rdoc
8
+
9
+ RUN apt-get remove -y ruby-dev pkg-config make cmake
10
+
11
+ RUN apt-get -y autoremove
data/README.md CHANGED
@@ -12,6 +12,7 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
12
12
  * restful API to reload list of nodes (GET /reload)
13
13
  * restful API to fetch configurations (/node/fetch/[NODE] or /node/fetch/group/[NODE])
14
14
  * restful API to show list of nodes (GET /nodes)
15
+ * restful API to show list of version for a node (/node/version[NODE]) and diffs
15
16
 
16
17
  [Youtube Video: Oxidized TREX 2014 presentation](http://youtu.be/kBQ_CTUuqeU#t=3h)
17
18
 
@@ -22,7 +23,8 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
22
23
  * [CentOS, Oracle Linux, Red Hat Linux version 6](#centos-oracle-linux-red-hat-linux-version 6)
23
24
  3. [Initial Configuration](#configuration)
24
25
  4. [Installing Ruby 2.1.2 using RVM](#installing-ruby-2.1.2-using-rvm)
25
- 5. [Cookbook](#cookbook)
26
+ 5. [Running with Docker](#running-with-docker)
27
+ 6. [Cookbook](#cookbook)
26
28
  * [Debugging](#debugging)
27
29
  * [Privileged mode](#privileged-mode)
28
30
  * [Source: CSV](#source-csv)
@@ -30,8 +32,9 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
30
32
  * [Source: HTTP](#source-http)
31
33
  * [Output: GIT](#output-git)
32
34
  * [Output: File](#output-file)
35
+ * [Output types](#output-types)
33
36
  * [Advanced Configuration](#advanced-configuration)
34
- 6. [Ruby API](#ruby-api)
37
+ 7. [Ruby API](#ruby-api)
35
38
  * [Input](#input)
36
39
  * [Output](#output)
37
40
  * [Source](#source)
@@ -67,8 +70,10 @@ Oxidized is a network device configuration backup tool. It's a RANCID replacemen
67
70
  * Juniper JunOS
68
71
  * Juniper ScreenOS (Netscreen)
69
72
  * Mikrotik RouterOS
73
+ * MRV Master-OS
70
74
  * Ubiquiti AirOS
71
75
  * Palo Alto PAN-OS
76
+ * Zyxel ZyNOS
72
77
 
73
78
 
74
79
  # Installation
@@ -159,6 +164,43 @@ rvm install 2.1.2
159
164
  rvm use --default 2.1.2
160
165
  ```
161
166
 
167
+ # Running with Docker
168
+ 1. clone git repo:
169
+
170
+ ```
171
+ root@bla:~# git clone https://github.com/ytti/oxidized
172
+ ```
173
+ 2. build container locally:
174
+ ```
175
+ root@bla:~# docker build -q -t oxidized/oxidized:latest oxidized/
176
+ ```
177
+ 3. create config directory in main system:
178
+ ```
179
+ root@bla~:# mkdir /etc/oxidized
180
+ ```
181
+ 4. run container the first time:
182
+ ```
183
+ root@bla:~# docker run -v /etc/oxidized:/root/.config/oxidized -p 8888:8888/tcp -t oxidized/oxidized:latest oxidized
184
+ ```
185
+ 5. add 'router.db' to /etc/oxidized:
186
+ ```
187
+ root@bla:~# vim /etc/oxidized/router.db
188
+ [ ... ]
189
+ root@bla:~#
190
+ ```
191
+ 6. run container again:
192
+ ```
193
+ root@bla:~# docker run -v /etc/oxidized:/root/.config/oxidized -p 8888:8888/tcp -t oxidized/oxidized:latest oxidized
194
+ oxidized[1]: Oxidized starting, running as pid 1
195
+ oxidized[1]: Loaded 1 nodes
196
+ Puma 2.13.4 starting...
197
+ * Min threads: 0, max threads: 16
198
+ * Environment: development
199
+ * Listening on tcp://0.0.0.0:8888
200
+ ^C
201
+
202
+ root@bla:~#
203
+ ```
162
204
 
163
205
  ## Cookbook
164
206
  ### Debugging
@@ -169,7 +211,7 @@ The following example will log an active ssh session to ```/home/fisakytt/.confi
169
211
  ```
170
212
  input:
171
213
  default: ssh, telnet
172
- debug: ~/.config/oxidized/log_input
214
+ debug: /tmp/oxidized_log_input
173
215
  ssh:
174
216
  secure: false
175
217
  ```
@@ -265,6 +307,52 @@ output:
265
307
  repo: "/var/lib/oxidized/devices.git"
266
308
  ```
267
309
 
310
+ ### Output types
311
+
312
+ 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`.
313
+
314
+ 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
315
+
316
+ ```
317
+ - cmd 'show version' do |cfg|
318
+ - comment cfg.lines.first
319
+ + cmd 'show version' do |state|
320
+ + state.type = 'nodiff'
321
+ + state
322
+
323
+ - cmd 'show inventory' do |cfg|
324
+ - comment cfg
325
+ + cmd 'show inventory' do |state|
326
+ + state.type = 'nodiff'
327
+ + state
328
+ + end
329
+
330
+ - cmd 'show running-config' do |cfg|
331
+ - cfg = cfg.each_line.to_a[3..-1].join
332
+ - cfg.gsub! /^Current configuration : [^\n]*\n/, ''
333
+ - cfg.sub! /^(ntp clock-period).*/, '! \1'
334
+ - cfg.gsub! /^\ tunnel\ mpls\ traffic-eng\ bandwidth[^\n]*\n*(
335
+ + cmd 'show running-config' do |state|
336
+ + state = state.each_line.to_a[3..-1].join
337
+ + state.gsub! /^Current configuration : [^\n]*\n/, ''
338
+ + state.sub! /^(ntp clock-period).*/, '! \1'
339
+ + state.gsub! /^\ tunnel\ mpls\ traffic-eng\ bandwidth[^\n]*\n*(
340
+ (?:\ [^\n]*\n*)*
341
+ tunnel\ mpls\ traffic-eng\ auto-bw)/mx, '\1'
342
+ - cfg
343
+ + state = Oxidized::String.new state
344
+ + state.type = 'nodiff'
345
+ + state
346
+ ```
347
+
348
+ which will result in the following layout
349
+
350
+ ```
351
+ diff/$FQDN--show_running_config
352
+ nodiff/$FQDN--show_version
353
+ nodiff/$FQDN--show_inventory
354
+ ```
355
+
268
356
  ### Advanced Configuration
269
357
 
270
358
  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:
313
401
  juniper: junos
314
402
  ```
315
403
 
404
+ # Hooks
405
+ 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.
406
+
407
+ ## Configuration
408
+ Following configuration keys need to be defined for all hooks:
409
+
410
+ * `events`: which events to subscribe. Needs to be an array. See below for the list of available events.
411
+ * `type`: what hook class to use. See below for the list of available hook types.
412
+
413
+ ### Events
414
+ * `node_success`: triggered when configuration is succesfully pulled from a node and right before storing the configuration.
415
+ * `node_fail`: triggered after `retries` amount of failed node pulls.
416
+ * `post_store`: triggered after node configuration is stored.
417
+
418
+ ## Hook type: exec
419
+ The `exec` hook type allows users to run an arbitrary shell command or a binary when triggered.
420
+
421
+ 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.
422
+
423
+ Command is executed with the following environment:
424
+ ```
425
+ OX_EVENT
426
+ OX_NODE_NAME
427
+ OX_NODE_FROM
428
+ OX_NODE_MSG
429
+ OX_NODE_GROUP
430
+ OX_JOB_STATUS
431
+ OX_JOB_TIME
432
+ ```
433
+
434
+ Exec hook recognizes following configuration keys:
435
+
436
+ * `timeout`: hard timeout for the command execution. SIGTERM will be sent to the child process after the timeout has elapsed. Default: 60
437
+ * `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
438
+ * `cmd`: command to run.
439
+
440
+
441
+ ## Hook configuration example
442
+ ```
443
+ hooks:
444
+ name_for_example_hook1:
445
+ type: exec
446
+ events: [node_success]
447
+ cmd: 'echo "Node success $OX_NODE_NAME" >> /tmp/ox_node_success.log'
448
+ name_for_example_hook2:
449
+ type: exec
450
+ events: [post_store, node_fail]
451
+ cmd: 'echo "Doing long running stuff for $OX_NODE_NAME" >> /tmp/ox_node_stuff.log; sleep 60'
452
+ async: true
453
+ timeout: 120
454
+ ```
316
455
 
317
456
  # Ruby API
318
457
 
@@ -345,3 +484,22 @@ The following objects exist in Oxidized.
345
484
  * cfg is executed in input/output/source context
346
485
  * cmd is executed in instance of model
347
486
  * 'junos', 'ios', 'ironware' and 'powerconnect' implemented
487
+
488
+
489
+ # License and Copyright
490
+
491
+ Copyright 2013-2015 Saku Ytti <saku@ytti.fi>
492
+ 2013-2015 Samer Abdel-Hafez <sam@arahant.net>
493
+
494
+
495
+ Licensed under the Apache License, Version 2.0 (the "License");
496
+ you may not use this file except in compliance with the License.
497
+ You may obtain a copy of the License at
498
+
499
+ http://www.apache.org/licenses/LICENSE-2.0
500
+
501
+ Unless required by applicable law or agreed to in writing, software
502
+ distributed under the License is distributed on an "AS IS" BASIS,
503
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
504
+ See the License for the specific language governing permissions and
505
+ limitations under the License.
@@ -6,20 +6,30 @@ require 'open-uri'
6
6
  require 'json'
7
7
 
8
8
  critical = false
9
+ pending = false
9
10
  critical_nodes = []
11
+ pending_nodes = []
10
12
 
11
13
  json = JSON.load(open("http://localhost:8888/nodes.json"))
12
14
  json.each do |node|
13
- if node['last']['status'] != 'success'
14
- critical_nodes << node['name']
15
- critical = true
15
+ if not node['last'].nil?
16
+ if node['last']['status'] != 'success'
17
+ critical_nodes << node['name']
18
+ critical = true
19
+ end
20
+ else
21
+ pending_nodes << node['name']
22
+ pending = true
16
23
  end
17
24
  end
18
25
 
19
- if critical
20
- puts 'Unable to backup: ' + critical_nodes.join(' ')
26
+ if pending
27
+ puts '[WARN] Pending backup: ' + pending_nodes.join(',')
28
+ exit 1
29
+ elsif critical
30
+ puts '[CRIT] Unable to backup: ' + critical_nodes.join(',')
21
31
  exit 2
22
32
  else
23
- puts 'Backup of all nodes completed successfully.'
33
+ puts '[OK] Backup of all nodes completed successfully.'
24
34
  exit 0
25
35
  end
data/extra/syslog.rb CHANGED
@@ -8,10 +8,18 @@
8
8
  # set system syslog host SERVER interactive-commands notice
9
9
  # set system syslog host SERVER match "^mgd\[[0-9]+\]: UI_COMMIT: .*"
10
10
 
11
- # Ports < 1024 need extra privileges, use a port higher than this by passing the first argument a number
12
- # To use the default port for syslog (514) you shouldnt pass an argument, but you will need to allow this with:
11
+ # Ports < 1024 need extra privileges, use a port higher than this by setting the port option in your oxidized config file.
12
+ # To use the default port for syslog (514) you shouldn't pass an argument, but you will need to allow this with:
13
13
  # sudo setcap 'cap_net_bind_service=+ep' /usr/bin/ruby
14
14
 
15
+ # Config options are:
16
+ # syslogd
17
+ # port (Default = 514)
18
+ # file (Default = messages)
19
+ # resolve (Default = true)
20
+
21
+ # To stop the resolution of IP's to PTR you can set resolve to false
22
+
15
23
  # exit if fork ## TODO: proper daemonize
16
24
 
17
25
  require 'socket'
@@ -19,26 +27,42 @@ require 'resolv'
19
27
  require_relative 'rest_client'
20
28
 
21
29
  module Oxidized
30
+
31
+ require 'asetus'
32
+ class Config
33
+ Root = File.join ENV['HOME'], '.config', 'oxidized'
34
+ end
35
+
36
+ CFGS = Asetus.new :name=>'oxidized', :load=>false, :key_to_s=>true
37
+ CFGS.default.syslogd.port = 514
38
+ CFGS.default.syslogd.file = 'messages'
39
+ CFGS.default.syslogd.resolve = true
40
+
41
+ begin
42
+ CFGS.load
43
+ rescue => error
44
+ raise InvalidConfig, "Error loading config: #{error.message}"
45
+ ensure
46
+ CFG = CFGS.cfg # convenienence, instead of Config.cfg.password, CFG.password
47
+ end
48
+
22
49
  class SyslogMonitor
23
50
  NAME_MAP = {
24
51
  /(.*)\.ip\.tdc\.net/ => '\1',
25
52
  /(.*)\.ip\.fi/ => '\1',
26
53
  }
27
- PORT = 514
28
- FILE = 'messages'
29
54
  MSG = {
30
55
  :ios => /%SYS-(SW[0-9]+-)?5-CONFIG_I:/,
31
56
  :junos => 'UI_COMMIT:',
32
57
  }
33
58
 
34
59
  class << self
35
- def udp port=PORT, listen=0
36
- port ||= PORT
60
+ def udp port=Oxidized::CFG.syslogd.port, listen=0
37
61
  io = UDPSocket.new
38
62
  io.bind listen, port
39
63
  new io, :udp
40
64
  end
41
- def file syslog_file=FILE
65
+ def file syslog_file=Oxidized::CFG.syslogd.file
42
66
  io = open syslog_file, 'r'
43
67
  io.seek 0, IO::SEEK_END
44
68
  new io, :file
@@ -102,12 +126,16 @@ module Oxidized
102
126
  end
103
127
 
104
128
  def getname ip
105
- name = (Resolv.getname ip.to_s rescue ip)
106
- NAME_MAP.each { |re, sub| name.sub! re, sub }
107
- name
129
+ if Oxidized::CFG.syslogd.resolve == false
130
+ ip
131
+ else
132
+ name = (Resolv.getname ip.to_s rescue ip)
133
+ NAME_MAP.each { |re, sub| name.sub! re, sub }
134
+ name
135
+ end
108
136
  end
109
137
  end
110
138
  end
111
139
 
112
- Oxidized::SyslogMonitor.udp ARGV[0]
140
+ Oxidized::SyslogMonitor.udp
113
141
  #Oxidized::SyslogMonitor.file '/var/log/poop'
@@ -9,10 +9,11 @@ module Oxidized
9
9
  OutputDir = File.join Directory, %w(lib oxidized output)
10
10
  ModelDir = File.join Directory, %w(lib oxidized model)
11
11
  SourceDir = File.join Directory, %w(lib oxidized source)
12
+ HookDir = File.join Directory, %w(lib oxidized hook)
12
13
  Sleep = 1
13
14
  end
14
15
  class << self
15
- attr_accessor :mgr
16
+ attr_accessor :mgr, :Hooks
16
17
  end
17
18
  CFGS = Asetus.new :name=>'oxidized', :load=>false, :key_to_s=>true
18
19
  CFGS.default.username = 'username'
data/lib/oxidized/core.rb CHANGED
@@ -6,6 +6,7 @@ module Oxidized
6
6
  require 'oxidized/worker'
7
7
  require 'oxidized/nodes'
8
8
  require 'oxidized/manager'
9
+ require 'oxidized/hook'
9
10
  class << self
10
11
  def new *args
11
12
  Core.new args
@@ -13,9 +14,13 @@ module Oxidized
13
14
  end
14
15
 
15
16
  class Core
17
+ class NoNodesFound < OxidizedError; end
18
+
16
19
  def initialize args
17
20
  Oxidized.mgr = Manager.new
21
+ Oxidized.Hooks = HookManager.from_config CFG
18
22
  nodes = Nodes.new
23
+ raise NoNodesFound, 'source returns no usable nodes' if nodes.size == 0
19
24
  @worker = Worker.new nodes
20
25
  trap('HUP') { nodes.load }
21
26
  if CFG.rest?
@@ -0,0 +1,88 @@
1
+ module Oxidized
2
+ class HookManager
3
+ class << self
4
+ def from_config cfg
5
+ mgr = new
6
+ cfg.hooks.each do |name,h_cfg|
7
+ h_cfg.events.each do |event|
8
+ mgr.register event.to_sym, name, h_cfg.type, h_cfg
9
+ end
10
+ end
11
+ mgr
12
+ end
13
+ end
14
+
15
+ # HookContext is passed to each hook. It can contain anything related to the
16
+ # event in question. At least it contains the event name
17
+ class HookContext < OpenStruct; end
18
+
19
+ # RegisteredHook is a container for a Hook instance
20
+ class RegisteredHook < Struct.new(:name, :hook); end
21
+
22
+ Events = [
23
+ :node_success,
24
+ :node_fail,
25
+ :post_store,
26
+ ]
27
+ attr_reader :registered_hooks
28
+
29
+ def initialize
30
+ @registered_hooks = Hash.new {|h,k| h[k] = []}
31
+ end
32
+
33
+ def register event, name, hook_type, cfg
34
+ unless Events.include? event
35
+ raise ArgumentError,
36
+ "unknown event #{event}, available: #{Events.join ','}"
37
+ end
38
+
39
+ Oxidized.mgr.add_hook hook_type
40
+ begin
41
+ hook = Oxidized.mgr.hook.fetch(hook_type).new
42
+ rescue KeyError
43
+ raise KeyError, "cannot find hook #{hook_type.inspect}"
44
+ end
45
+
46
+ hook.cfg = cfg
47
+
48
+ @registered_hooks[event] << RegisteredHook.new(name, hook)
49
+ Log.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
50
+ end
51
+
52
+ def handle event, **ctx_params
53
+ ctx = HookContext.new ctx_params
54
+ ctx.event = event
55
+
56
+ @registered_hooks[event].each do |r_hook|
57
+ begin
58
+ r_hook.hook.run_hook ctx
59
+ rescue => e
60
+ Log.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " +
61
+ "(#{e.inspect}) for event #{event.inspect}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # Hook abstract base class
68
+ class Hook
69
+ attr_accessor :cfg
70
+
71
+ def initialize
72
+ end
73
+
74
+ def cfg=(cfg)
75
+ @cfg = cfg
76
+ validate_cfg! if self.respond_to? :validate_cfg!
77
+ end
78
+
79
+ def run_hook ctx
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def log(msg, level=:info)
84
+ Log.send(level, "#{self.class.name}: #{msg}")
85
+ end
86
+
87
+ end
88
+ end
@@ -0,0 +1,84 @@
1
+ class Exec < Oxidized::Hook
2
+ include Process
3
+
4
+ def initialize
5
+ super
6
+ @timeout = 60
7
+ @async = false
8
+ end
9
+
10
+ def validate_cfg!
11
+ # Syntax check
12
+ if cfg.has_key? "timeout"
13
+ @timeout = cfg.timeout
14
+ raise "invalid timeout value" unless @timeout.is_a?(Integer) &&
15
+ @timeout > 0
16
+ end
17
+
18
+ if cfg.has_key? "async"
19
+ @async = !!cfg.async
20
+ end
21
+
22
+ if cfg.has_key? "cmd"
23
+ @cmd = cfg.cmd
24
+ raise "invalid cmd value" unless @cmd.is_a?(String) || @cmd.is_a?(Array)
25
+ end
26
+
27
+ rescue RuntimeError => e
28
+ raise ArgumentError,
29
+ "#{self.class.name}: configuration invalid: #{e.message}"
30
+ end
31
+
32
+ def run_hook ctx
33
+ env = make_env ctx
34
+ log "Execute: #{@cmd.inspect}", :debug
35
+ th = Thread.new do
36
+ begin
37
+ run_cmd! env
38
+ rescue => e
39
+ raise e unless @async
40
+ end
41
+ end
42
+ th.join unless @async
43
+ end
44
+
45
+ def run_cmd! env
46
+ pid, status = nil, nil
47
+ Timeout.timeout(@timeout) do
48
+ pid = spawn env, @cmd , :unsetenv_others => true
49
+ pid, status = wait2 pid
50
+ unless status.exitstatus.zero?
51
+ msg = "#{@cmd.inspect} failed with exit value #{status.exitstatus}"
52
+ log msg, :error
53
+ raise msg
54
+ end
55
+ end
56
+ rescue TimeoutError
57
+ kill "TERM", pid
58
+ msg = "#{@cmd} timed out"
59
+ log msg, :error
60
+ raise TimeoutError, msg
61
+ end
62
+
63
+ def make_env ctx
64
+ env = {
65
+ "OX_EVENT" => ctx.event.to_s
66
+ }
67
+ if ctx.node
68
+ env.merge!(
69
+ "OX_NODE_NAME" => ctx.node.name.to_s,
70
+ "OX_NODE_FROM" => ctx.node.from.to_s,
71
+ "OX_NODE_MSG" => ctx.node.msg.to_s,
72
+ "OX_NODE_GROUP" => ctx.node.group.to_s,
73
+ "OX_EVENT" => ctx.event.to_s,
74
+ )
75
+ end
76
+ if ctx.job
77
+ env.merge!(
78
+ "OX_JOB_STATUS" => ctx.job.status.to_s,
79
+ "OX_JOB_TIME" => ctx.job.time.to_s,
80
+ )
81
+ end
82
+ env
83
+ end
84
+ end
@@ -0,0 +1,9 @@
1
+ class NoopHook < Oxidized::Hook
2
+ def validate_cfg!
3
+ log "Validate config"
4
+ end
5
+
6
+ def run_hook ctx
7
+ log "Run hook with context: #{ctx}"
8
+ end
9
+ end
@@ -0,0 +1,54 @@
1
+ module Oxidized
2
+ require 'net/ftp'
3
+ require 'timeout'
4
+ require_relative 'cli'
5
+
6
+ class FTP < Input
7
+ RescueFail = {
8
+ :debug => [
9
+ #Net::SSH::Disconnect,
10
+ ],
11
+ :warn => [
12
+ #RuntimeError,
13
+ #Net::SSH::AuthenticationFailed,
14
+ ],
15
+ }
16
+ include Input::CLI
17
+
18
+ def connect node
19
+ @node = node
20
+ @node.model.cfg['ftp'].each { |cb| instance_exec(&cb) }
21
+ @log = File.open(CFG.input.debug?.to_s + '-ftp', 'w') if CFG.input.debug?
22
+ @ftp = Net::FTP.new @node.ip, @node.auth[:username], @node.auth[:password]
23
+ connected?
24
+ end
25
+
26
+ def connected?
27
+ @ftp and not @ftp.closed?
28
+ end
29
+
30
+ def cmd file
31
+ Log.debug "FTP: #{file} @ #{@node.name}"
32
+ @ftp.getbinaryfile file, nil
33
+ end
34
+
35
+ # meh not sure if this is the best way, but perhaps better than not implementing send
36
+ def send my_proc
37
+ my_proc.call
38
+ end
39
+
40
+ def output
41
+ ""
42
+ end
43
+
44
+ private
45
+
46
+ def disconnect
47
+ @ftp.close
48
+ #rescue Errno::ECONNRESET, IOError
49
+ ensure
50
+ @log.close if CFG.input.debug?
51
+ end
52
+
53
+ end
54
+ end
@@ -21,7 +21,8 @@ module Oxidized
21
21
  @node.model.cfg['ssh'].each { |cb| instance_exec(&cb) }
22
22
  secure = CFG.input.ssh.secure
23
23
  @log = File.open(CFG.input.debug?.to_s + '-ssh', 'w') if CFG.input.debug?
24
- @ssh = Net::SSH.start @node.ip, @node.auth[:username],
24
+ port = vars(:ssh_port) || 22
25
+ @ssh = Net::SSH.start @node.ip, @node.auth[:username], :port => port.to_i,
25
26
  :password => @node.auth[:password], :timeout => CFG.timeout,
26
27
  :paranoid => secure,
27
28
  :auth_methods => %w(none publickey password keyboard-interactive),
@@ -10,9 +10,12 @@ module Oxidized
10
10
  @node = node
11
11
  @timeout = CFG.timeout
12
12
  @node.model.cfg['telnet'].each { |cb| instance_exec(&cb) }
13
+ port = vars(:telnet_port) || 23
13
14
 
14
- opt = { 'Host' => @node.ip, 'Timeout' => @timeout,
15
- 'Model' => @node.model }
15
+ opt = { 'Host' => @node.ip,
16
+ 'Port' => port.to_i,
17
+ 'Timeout' => @timeout,
18
+ 'Model' => @node.model }
16
19
  opt['Output_log'] = CFG.input.debug?.to_s + '-telnet' if CFG.input.debug?
17
20
 
18
21
  @telnet = Net::Telnet.new opt
@@ -23,12 +23,13 @@ module Oxidized
23
23
  end
24
24
  end
25
25
  end
26
- attr_reader :input, :output, :model, :source
26
+ attr_reader :input, :output, :model, :source, :hook
27
27
  def initialize
28
28
  @input = {}
29
29
  @output = {}
30
30
  @model = {}
31
31
  @source = {}
32
+ @hook = {}
32
33
  end
33
34
  def add_input method
34
35
  method = Manager.load Config::InputDir, method
@@ -53,5 +54,13 @@ module Oxidized
53
54
  return false if _source.empty?
54
55
  @source.merge! _source
55
56
  end
57
+ def add_hook _hook
58
+ return nil if @hook.key? _hook
59
+ name = _hook
60
+ _hook = Manager.load File.join(Config::Root, 'hook'), name
61
+ _hook = Manager.load Config::HookDir, name if _hook.empty?
62
+ return false if _hook.empty?
63
+ @hook.merge! _hook
64
+ end
56
65
  end
57
66
  end
@@ -5,7 +5,7 @@ class AOSW < Oxidized::Model
5
5
  # Also Dell controllers
6
6
 
7
7
  comment '# '
8
- prompt /^\([^)]+\) #/
8
+ prompt /^\([^)]+\) [#>]/
9
9
 
10
10
  cmd :all do |cfg|
11
11
  cfg.each_line.to_a[1..-2].join
@@ -36,7 +36,16 @@ class AOSW < Oxidized::Model
36
36
  end
37
37
 
38
38
  cfg :telnet, :ssh do
39
+ if vars :enable
40
+ post_login do
41
+ send 'enable\n'
42
+ send vars(:enable) + '\n'
43
+ end
44
+ end
39
45
  post_login 'no paging'
46
+ if vars :enable
47
+ pre_logout 'exit'
48
+ end
40
49
  pre_logout 'exit'
41
50
  end
42
51
 
@@ -50,7 +59,7 @@ class AOSW < Oxidized::Model
50
59
  next if line.match /[0-9]+ (RPM|mV|C)$/
51
60
  out << line.strip
52
61
  end
53
- out = out.join "\n"
62
+ out = comment out.join "\n"
54
63
  out << "\n"
55
64
  end
56
65
 
@@ -0,0 +1,27 @@
1
+ class Edgeos < Oxidized::Model
2
+
3
+ # EdgeOS #
4
+
5
+ prompt /\@.*?\:~\$\s/
6
+
7
+ cmd :all do |cfg|
8
+ cfg = cfg.lines.to_a[1..-2].join
9
+ end
10
+
11
+ cmd :secret do |cfg|
12
+ cfg.gsub! /community (\S+) {/, 'community <hidden> {'
13
+ cfg
14
+ end
15
+
16
+ cmd 'show configuration | no-more'
17
+
18
+ cfg :telnet do
19
+ username /login:\s/
20
+ password /^Password:\s/
21
+ end
22
+
23
+ cfg :telnet, :ssh do
24
+ pre_logout 'exit'
25
+ end
26
+
27
+ end
@@ -1,6 +1,6 @@
1
1
  class IronWare < Oxidized::Model
2
2
 
3
- prompt /^.+[>#]\s?$/
3
+ prompt /^.*(telnet|ssh)\@.+[>#]\s?$/i
4
4
  comment '! '
5
5
 
6
6
  #to handle pager without enable
@@ -26,13 +26,13 @@ class IronWare < Oxidized::Model
26
26
 
27
27
  cmd 'show version' do |cfg|
28
28
  cfg.gsub! /(^((.*)[Ss]ystem uptime(.*))$)/, '' #remove unwanted line system uptime
29
- cfg.gsub! /uptime is .*/,''
29
+ cfg.gsub! /[Uu]p\s?[Tt]ime is .*/,''
30
30
 
31
31
  comment cfg
32
32
  end
33
33
 
34
34
  cmd 'show chassis' do |cfg|
35
- cfg.gsub! /\xFF/n, '' # ugly hack - avoids JSON.dump utf-8 breakage on 1.9..
35
+ cfg.encode!("UTF-8", :invalid => :replace) #sometimes ironware returns broken encoding
36
36
  cfg.gsub! /(^((.*)Current temp(.*))$)/, '' #remove unwanted lines current temperature
37
37
  cfg.gsub! /Speed = [A-Z]{3} \(\d{2}\%\)/, '' #remove unwanted lines Speed Fans
38
38
  cfg.gsub! /current speed is [A-Z]{3} \(\d{2}\%\)/, ''
@@ -71,6 +71,7 @@ class IronWare < Oxidized::Model
71
71
  send vars(:enable) + "\n"
72
72
  end
73
73
  end
74
+ post_login ''
74
75
  post_login 'skip-page-display'
75
76
  post_login 'terminal length 0'
76
77
  pre_logout 'logout'
@@ -0,0 +1,46 @@
1
+ class MasterOS < Oxidized::Model
2
+
3
+ # MRV MasterOS model #
4
+
5
+ comment '!'
6
+
7
+ cmd :secret do |cfg|
8
+ cfg.gsub! /^(snmp-server community).*/, '\\1 <configuration removed>'
9
+ cfg.gsub! /username (\S+) password encrypted (\S+) class (\S+).*/, '<secret hidden>'
10
+ cfg
11
+ end
12
+
13
+ cmd :all do |cfg|
14
+ cfg.each_line.to_a[1..-2].join
15
+ end
16
+
17
+ cmd 'show inventory' do |cfg|
18
+ cfg = cfg.each_line.to_a[0..-2].join
19
+ comment cfg
20
+ end
21
+
22
+ cmd 'show plugins' do |cfg|
23
+ comment cfg
24
+ end
25
+
26
+ cmd 'show hw-config' do |cfg|
27
+ comment cfg
28
+ end
29
+
30
+ cmd 'show running-config' do |cfg|
31
+ cfg = cfg.each_line.to_a[3..-1].join
32
+ cfg
33
+ end
34
+
35
+ cfg :telnet, :ssh do
36
+ post_login 'no pager'
37
+ if vars :enable
38
+ post_login do
39
+ send "enable\n"
40
+ send vars(:enable) + "\n"
41
+ end
42
+ end
43
+ pre_logout 'exit'
44
+ end
45
+
46
+ end
@@ -1,5 +1,5 @@
1
1
  class RouterOS < Oxidized::Model
2
- prompt /^\[\w+@\S+\]\s?>\s?$/
2
+ prompt /\[\w+@\S+\]\s?>\s?$/
3
3
  comment "# "
4
4
 
5
5
  cmd '/system routerboard print' do |cfg|
@@ -7,10 +7,16 @@ class RouterOS < Oxidized::Model
7
7
  end
8
8
 
9
9
  cmd '/export' do |cfg|
10
+ cfg.gsub! /\x1B\[([0-9]{1,3}((;[0-9]{1,3})*)?)?[m|K]/, '' # strip ANSI colours
10
11
  cfg = cfg.split("\n").select { |line| not line[/^\#\s\w{3}\/\d{2}\/\d{4}.*$/] }
11
12
  cfg.join("\n") + "\n"
12
13
  end
13
14
 
15
+ cfg :telnet do
16
+ username /^Login:/
17
+ password /^Password:/
18
+ end
19
+
14
20
  cfg :ssh do
15
21
  exec true
16
22
  end
@@ -6,7 +6,7 @@ class XOS < Oxidized::Model
6
6
  comment '# '
7
7
 
8
8
  cmd :all do |cfg|
9
- cfg.each_line.to_a[1..-2].join.rstrip
9
+ cfg.each_line.to_a[1..-2].join
10
10
  end
11
11
 
12
12
  cmd 'show version' do |cfg|
@@ -0,0 +1,12 @@
1
+ class ZyNOS < Oxidized::Model
2
+
3
+ # Used in Zyxel DSLAMs, such as SAM1316
4
+
5
+ comment '! '
6
+
7
+ cmd 'config-0'
8
+
9
+ cfg :ftp do
10
+ end
11
+
12
+ end
data/lib/oxidized/node.rb CHANGED
@@ -29,6 +29,9 @@ module Oxidized
29
29
  def run
30
30
  status, config = :fail, nil
31
31
  @input.each do |input|
32
+ # don't try input if model is missing config block, we may need strong config to class_name map
33
+ cfg_name = input.to_s.split('::').last.downcase
34
+ next unless @model.cfg[cfg_name] and not @model.cfg[cfg_name].empty?
32
35
  @model.input = input = input.new
33
36
  if config=run_input(input)
34
37
  status = :success
@@ -111,7 +111,7 @@ module Oxidized
111
111
  end
112
112
 
113
113
  def find_index node
114
- index { |e| e.name == node }
114
+ index { |e| e.name == node or e.ip == node}
115
115
  end
116
116
 
117
117
  # @param node node which is removed from nodes list
@@ -19,6 +19,7 @@ class Git < Output
19
19
  CFGS.save :user
20
20
  raise NoConfig, 'no output git config, edit ~/.config/oxidized/config'
21
21
  end
22
+ @cfg.repo = File.expand_path @cfg.repo
22
23
  end
23
24
 
24
25
  def store file, outputs, opt={}
@@ -50,18 +51,18 @@ class Git < Output
50
51
  def fetch node, group
51
52
  begin
52
53
  repo = @cfg.repo
53
- if group
54
- repo = File.join File.dirname(repo), group + '.git'
55
- end
54
+ repo = File.join File.dirname(repo), group + '.git' if group and not @cfg.single_repo?
56
55
  repo = Rugged::Repository.new repo
57
56
  index = repo.index
58
57
  index.read_tree repo.head.target.tree unless repo.empty?
59
- repo.read(index.get(node)[:oid]).data
58
+ file = node
59
+ file = File.join(group, node) if group and @cfg.single_repo?
60
+ repo.read(index.get(file)[:oid]).data
60
61
  rescue
61
62
  'node not found'
62
63
  end
63
64
  end
64
-
65
+
65
66
  #give a hash of all oid revision for the givin node, and the date of the commit
66
67
  def version node, group
67
68
  begin
@@ -69,7 +70,7 @@ class Git < Output
69
70
  if group
70
71
  repo = File.join File.dirname(repo), group + '.git'
71
72
  end
72
- repo = Rugged::Repository.new repo
73
+ repo = Rugged::Repository.new repo
73
74
  walker = Rugged::Walker.new(repo)
74
75
  walker.sorting(Rugged::SORT_DATE)
75
76
  walker.push(repo.head.target)
@@ -78,8 +79,10 @@ class Git < Output
78
79
  walker.each do |commit|
79
80
  if commit.diff(paths: [node]).size > 0
80
81
  hash = {}
81
- hash[:date] = commit.time.to_s
82
+ hash[:date] = commit.time.to_s
82
83
  hash[:oid] = commit.oid
84
+ hash[:author] = commit.author
85
+ hash[:message] = commit.message
83
86
  tab[i += 1] = hash
84
87
  end
85
88
  end
@@ -89,7 +92,7 @@ class Git < Output
89
92
  'node not found'
90
93
  end
91
94
  end
92
-
95
+
93
96
  #give the blob of a specific revision
94
97
  def get_version node, group, oid
95
98
  begin
@@ -97,13 +100,13 @@ class Git < Output
97
100
  if group && group != ''
98
101
  repo = File.join File.dirname(repo), group + '.git'
99
102
  end
100
- repo = Rugged::Repository.new repo
103
+ repo = Rugged::Repository.new repo
101
104
  repo.blob_at(oid,node).content
102
105
  rescue
103
106
  'version not found'
104
107
  end
105
108
  end
106
-
109
+
107
110
  #give a hash with the patch of a diff between 2 revision and the stats (added and deleted lines)
108
111
  def get_diff node, group, oid1, oid2
109
112
  begin
@@ -112,10 +115,10 @@ class Git < Output
112
115
  if group && group != ''
113
116
  repo = File.join File.dirname(repo), group + '.git'
114
117
  end
115
- repo = Rugged::Repository.new repo
118
+ repo = Rugged::Repository.new repo
116
119
  commit = repo.lookup(oid1)
117
- #if the second revision is precised
118
- if oid2
120
+ #if the second revision is precised
121
+ if oid2
119
122
  commit_old = repo.lookup(oid2)
120
123
  diff = repo.diff(commit_old, commit)
121
124
  diff.each do |patch|
@@ -179,10 +182,10 @@ class Git < Output
179
182
  :parents => repo.empty? ? [] : [repo.head.target].compact,
180
183
  :update_ref => 'HEAD',
181
184
  )
182
-
185
+
183
186
  index.write
184
187
  true
185
188
  end
186
189
  end
187
190
  end
188
- end
191
+ end
@@ -34,12 +34,16 @@ module Oxidized
34
34
  @jobs.duration job.time
35
35
  node.running = false
36
36
  if job.status == :success
37
+ Oxidized.Hooks.handle :node_success, :node => node,
38
+ :job => job
37
39
  msg = "update #{node.name}"
38
40
  msg += " from #{node.from}" if node.from
39
41
  msg += " with message '#{node.msg}'" if node.msg
40
42
  if node.output.new.store node.name, job.config,
41
43
  :msg => msg, :user => node.user, :group => node.group
42
44
  Log.info "Configuration updated for #{node.group}/#{node.name}"
45
+ Oxidized.Hooks.handle :post_store, :node => node,
46
+ :job => job
43
47
  end
44
48
  node.reset
45
49
  else
@@ -51,6 +55,8 @@ module Oxidized
51
55
  else
52
56
  msg += ", retries exhausted, giving up"
53
57
  node.retry = 0
58
+ Oxidized.Hooks.handle :node_fail, :node => node,
59
+ :job => job
54
60
  end
55
61
  Log.warn msg
56
62
  end
data/oxidized.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'oxidized'
3
- s.version = '0.7.2'
3
+ s.version = '0.8.0'
4
4
  s.licenses = %w( Apache-2.0 )
5
5
  s.platform = Gem::Platform::RUBY
6
- s.authors = [ 'Saku Ytti', 'Samer Abdel-Hafez' ]
7
- s.email = %w( saku@ytti.fi sam@arahant.net )
6
+ s.authors = [ 'Saku Ytti', 'Samer Abdel-Hafez', 'Anton Aksola' ]
7
+ s.email = %w( saku@ytti.fi sam@arahant.net aakso@iki.fi)
8
8
  s.homepage = 'http://github.com/ytti/oxidized'
9
9
  s.summary = 'feeble attempt at rancid'
10
10
  s.description = 'software to fetch configuration from network devices and store them'
@@ -18,4 +18,5 @@ Gem::Specification.new do |s|
18
18
  s.add_runtime_dependency 'slop', '~> 3.5'
19
19
  s.add_runtime_dependency 'net-ssh', '~> 2.8'
20
20
  s.add_runtime_dependency 'rugged', '~> 0.21', '>= 0.21.4'
21
+ s.add_development_dependency 'pry', '~> 0'
21
22
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oxidized
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saku Ytti
8
8
  - Samer Abdel-Hafez
9
+ - Anton Aksola
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2015-06-17 00:00:00.000000000 Z
13
+ date: 2015-09-14 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: asetus
@@ -73,16 +74,32 @@ dependencies:
73
74
  - - ">="
74
75
  - !ruby/object:Gem::Version
75
76
  version: 0.21.4
77
+ - !ruby/object:Gem::Dependency
78
+ name: pry
79
+ requirement: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ type: :development
85
+ prerelease: false
86
+ version_requirements: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
76
91
  description: software to fetch configuration from network devices and store them
77
92
  email:
78
93
  - saku@ytti.fi
79
94
  - sam@arahant.net
95
+ - aakso@iki.fi
80
96
  executables:
81
97
  - oxidized
82
98
  extensions: []
83
99
  extra_rdoc_files: []
84
100
  files:
85
101
  - CHANGELOG.md
102
+ - Dockerfile
86
103
  - Gemfile
87
104
  - README.md
88
105
  - Rakefile
@@ -100,7 +117,11 @@ files:
100
117
  - lib/oxidized/config.rb
101
118
  - lib/oxidized/config/vars.rb
102
119
  - lib/oxidized/core.rb
120
+ - lib/oxidized/hook.rb
121
+ - lib/oxidized/hook/exec.rb
122
+ - lib/oxidized/hook/noophook.rb
103
123
  - lib/oxidized/input/cli.rb
124
+ - lib/oxidized/input/ftp.rb
104
125
  - lib/oxidized/input/input.rb
105
126
  - lib/oxidized/input/ssh.rb
106
127
  - lib/oxidized/input/telnet.rb
@@ -118,6 +139,7 @@ files:
118
139
  - lib/oxidized/model/ciscosmb.rb
119
140
  - lib/oxidized/model/comware.rb
120
141
  - lib/oxidized/model/cumulus.rb
142
+ - lib/oxidized/model/edgeos.rb
121
143
  - lib/oxidized/model/eos.rb
122
144
  - lib/oxidized/model/fabricos.rb
123
145
  - lib/oxidized/model/fortios.rb
@@ -127,6 +149,7 @@ files:
127
149
  - lib/oxidized/model/ironware.rb
128
150
  - lib/oxidized/model/isam.rb
129
151
  - lib/oxidized/model/junos.rb
152
+ - lib/oxidized/model/masteros.rb
130
153
  - lib/oxidized/model/model.rb
131
154
  - lib/oxidized/model/nos.rb
132
155
  - lib/oxidized/model/nxos.rb
@@ -140,6 +163,7 @@ files:
140
163
  - lib/oxidized/model/vrp.rb
141
164
  - lib/oxidized/model/vyatta.rb
142
165
  - lib/oxidized/model/xos.rb
166
+ - lib/oxidized/model/zynos.rb
143
167
  - lib/oxidized/node.rb
144
168
  - lib/oxidized/node/stats.rb
145
169
  - lib/oxidized/nodes.rb
@@ -174,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
198
  version: '0'
175
199
  requirements: []
176
200
  rubyforge_project: oxidized
177
- rubygems_version: 2.4.5
201
+ rubygems_version: 2.2.2
178
202
  signing_key:
179
203
  specification_version: 4
180
204
  summary: feeble attempt at rancid