oxidized 0.7.2 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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