oxidized 0.29.1 → 0.37.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.
Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/.coderabbit.yaml +21 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +45 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  5. data/.github/ISSUE_TEMPLATE/support-request.md +39 -0
  6. data/.github/workflows/codeql.yml +4 -4
  7. data/.github/workflows/publishdocker.yml +108 -13
  8. data/.github/workflows/ruby.yml +3 -12
  9. data/.github/workflows/stale.yml +6 -1
  10. data/.gitignore +2 -0
  11. data/.rubocop.yml +69 -13
  12. data/.rubocop_todo.yml +13 -143
  13. data/CHANGELOG.md +431 -4
  14. data/CONTRIBUTING.md +189 -0
  15. data/Dockerfile +89 -34
  16. data/README.md +59 -188
  17. data/Rakefile +58 -12
  18. data/bin/oxidized +2 -5
  19. data/docs/Configuration.md +382 -149
  20. data/docs/Creating-Models.md +262 -10
  21. data/docs/DeviceSimulation.md +196 -0
  22. data/docs/Docker.md +246 -0
  23. data/docs/Hooks.md +146 -51
  24. data/docs/Inputs.md +237 -0
  25. data/docs/Issues.md +123 -0
  26. data/docs/Model-Notes/APC.md +72 -0
  27. data/docs/Model-Notes/Cumulus.md +5 -0
  28. data/docs/Model-Notes/EatonNetwork.md +18 -0
  29. data/docs/Model-Notes/ExaLink.md +43 -0
  30. data/docs/Model-Notes/FSOS.md +17 -0
  31. data/docs/Model-Notes/Fortinet.md +75 -0
  32. data/docs/Model-Notes/GrandstreamHT8xx.md +8 -0
  33. data/docs/Model-Notes/HPEAruba.md +32 -0
  34. data/docs/Model-Notes/IvantiConnectSecure.md +59 -0
  35. data/docs/Model-Notes/OS6.md +10 -0
  36. data/docs/Model-Notes/README.md +1 -20
  37. data/docs/Model-Notes/RouterOS.md +28 -0
  38. data/docs/Model-Notes/SikluMHTG.md +7 -0
  39. data/docs/Model-Notes/TrueNAS.md +23 -0
  40. data/docs/Model-Notes/XGS4600-Zyxel.md +5 -0
  41. data/docs/ModelUnitTests.md +219 -0
  42. data/docs/Outputs.md +102 -5
  43. data/docs/Release.md +102 -0
  44. data/docs/Ruby-API.md +135 -2
  45. data/docs/Sources.md +34 -0
  46. data/docs/Supported-OS-Types.md +207 -270
  47. data/docs/Troubleshooting.md +43 -1
  48. data/extra/device2yaml.rb +259 -0
  49. data/extra/gitdiff-msteams.sh +32 -5
  50. data/extra/hooks/modelrules.rb +55 -0
  51. data/extra/hooks/modelrulesadvanced.rb +167 -0
  52. data/extra/hooks/srcipmap.rb +54 -0
  53. data/extra/nagios_check_failing_nodes.rb +1 -1
  54. data/extra/oxidized.runit +1 -1
  55. data/extra/rest_client.rb +5 -4
  56. data/extra/syslog.rb +11 -6
  57. data/lib/oxidized/cli/support.rb +152 -0
  58. data/lib/oxidized/cli.rb +21 -6
  59. data/lib/oxidized/config/vars.rb +33 -14
  60. data/lib/oxidized/config.rb +28 -17
  61. data/lib/oxidized/core.rb +50 -8
  62. data/lib/oxidized/hook/ciscosparkdiff.rb +11 -9
  63. data/lib/oxidized/hook/exec.rb +5 -4
  64. data/lib/oxidized/hook/githubrepo.rb +55 -18
  65. data/lib/oxidized/hook/noophook.rb +2 -2
  66. data/lib/oxidized/hook/slackdiff.rb +50 -14
  67. data/lib/oxidized/hook/xmppdiff.rb +9 -9
  68. data/lib/oxidized/hook.rb +68 -16
  69. data/lib/oxidized/input/cli.rb +8 -3
  70. data/lib/oxidized/input/debugtext.rb +40 -0
  71. data/lib/oxidized/input/debugyaml.rb +82 -0
  72. data/lib/oxidized/input/exec.rb +2 -11
  73. data/lib/oxidized/input/ftp.rb +2 -19
  74. data/lib/oxidized/input/http.rb +67 -17
  75. data/lib/oxidized/input/input.rb +35 -14
  76. data/lib/oxidized/input/scp.rb +27 -0
  77. data/lib/oxidized/input/ssh.rb +59 -83
  78. data/lib/oxidized/input/sshbase.rb +102 -0
  79. data/lib/oxidized/input/telnet.rb +20 -19
  80. data/lib/oxidized/input/tftp.rb +8 -8
  81. data/lib/oxidized/job.rb +18 -7
  82. data/lib/oxidized/jobs.rb +11 -1
  83. data/lib/oxidized/logger.rb +51 -0
  84. data/lib/oxidized/manager.rb +18 -17
  85. data/lib/oxidized/model/acos.rb +2 -1
  86. data/lib/oxidized/model/addpack.rb +26 -0
  87. data/lib/oxidized/model/adtran.rb +5 -1
  88. data/lib/oxidized/model/adva.rb +3 -3
  89. data/lib/oxidized/model/airfiber.rb +1 -1
  90. data/lib/oxidized/model/aos7.rb +11 -0
  91. data/lib/oxidized/model/aoscx.rb +37 -29
  92. data/lib/oxidized/model/aosw.rb +39 -29
  93. data/lib/oxidized/model/apc_aos.rb +7 -2
  94. data/lib/oxidized/model/apcaos.rb +39 -0
  95. data/lib/oxidized/model/aricentiss.rb +9 -2
  96. data/lib/oxidized/model/arubainstant.rb +93 -0
  97. data/lib/oxidized/model/asa.rb +11 -10
  98. data/lib/oxidized/model/asternos.rb +22 -0
  99. data/lib/oxidized/model/asyncos.rb +3 -3
  100. data/lib/oxidized/model/audiocodes.rb +2 -2
  101. data/lib/oxidized/model/awplus.rb +15 -12
  102. data/lib/oxidized/model/axos.rb +1 -1
  103. data/lib/oxidized/model/bdcom.rb +1 -0
  104. data/lib/oxidized/model/c4cmts.rb +1 -2
  105. data/lib/oxidized/model/ciscosma.rb +1 -1
  106. data/lib/oxidized/model/ciscosmb.rb +6 -1
  107. data/lib/oxidized/model/cnos.rb +13 -10
  108. data/lib/oxidized/model/comtrol.rb +1 -1
  109. data/lib/oxidized/model/comware.rb +5 -3
  110. data/lib/oxidized/model/cumulus.rb +14 -2
  111. data/lib/oxidized/model/defacto.rb +26 -0
  112. data/lib/oxidized/model/dellx.rb +1 -1
  113. data/lib/oxidized/model/dlink.rb +5 -2
  114. data/lib/oxidized/model/dlinknextgen.rb +55 -0
  115. data/lib/oxidized/model/dnos.rb +3 -0
  116. data/lib/oxidized/model/dslcommands.rb +93 -0
  117. data/lib/oxidized/model/dslsetup.rb +102 -0
  118. data/lib/oxidized/model/eatonnetwork.rb +70 -0
  119. data/lib/oxidized/model/edgecos.rb +3 -1
  120. data/lib/oxidized/model/edgeos.rb +7 -6
  121. data/lib/oxidized/model/edgeswitch.rb +3 -1
  122. data/lib/oxidized/model/efos.rb +41 -0
  123. data/lib/oxidized/model/eltex.rb +4 -2
  124. data/lib/oxidized/model/enterasys800.rb +2 -2
  125. data/lib/oxidized/model/enterprise_sonic.rb +46 -0
  126. data/lib/oxidized/model/eos.rb +3 -1
  127. data/lib/oxidized/model/exalink.rb +36 -0
  128. data/lib/oxidized/model/f5os.rb +17 -0
  129. data/lib/oxidized/model/fabricos.rb +2 -2
  130. data/lib/oxidized/model/fastiron.rb +5 -3
  131. data/lib/oxidized/model/firebrick.rb +2 -2
  132. data/lib/oxidized/model/firelinuxos.rb +13 -6
  133. data/lib/oxidized/model/firewareos.rb +10 -1
  134. data/lib/oxidized/model/fortigate.rb +160 -0
  135. data/lib/oxidized/model/fortios.rb +38 -48
  136. data/lib/oxidized/model/fsos.rb +46 -0
  137. data/lib/oxidized/model/gaiaos.rb +4 -4
  138. data/lib/oxidized/model/garderos.rb +38 -0
  139. data/lib/oxidized/model/grandstreamht8xx.rb +19 -0
  140. data/lib/oxidized/model/h3c.rb +1 -1
  141. data/lib/oxidized/model/ibos.rb +1 -0
  142. data/lib/oxidized/model/ingate.rb +47 -0
  143. data/lib/oxidized/model/ios.rb +57 -30
  144. data/lib/oxidized/model/iosxr.rb +2 -2
  145. data/lib/oxidized/model/ironware.rb +7 -5
  146. data/lib/oxidized/model/ivanti.rb +54 -0
  147. data/lib/oxidized/model/junos.rb +12 -5
  148. data/lib/oxidized/model/kornfeldos.rb +33 -0
  149. data/lib/oxidized/model/lenovonos.rb +2 -0
  150. data/lib/oxidized/model/linksyssrw.rb +3 -3
  151. data/lib/oxidized/model/linuxgeneric.rb +4 -2
  152. data/lib/oxidized/model/macros.rb +60 -0
  153. data/lib/oxidized/model/mimosab11.rb +34 -0
  154. data/lib/oxidized/model/ml66.rb +33 -0
  155. data/lib/oxidized/model/mlnxos.rb +18 -14
  156. data/lib/oxidized/model/model.rb +98 -103
  157. data/lib/oxidized/model/mtrlrfs.rb +1 -0
  158. data/lib/oxidized/model/ndms.rb +6 -0
  159. data/lib/oxidized/model/netgear.rb +14 -3
  160. data/lib/oxidized/model/netonix.rb +1 -1
  161. data/lib/oxidized/model/netscaler.rb +1 -1
  162. data/lib/oxidized/model/nodegrid.rb +1 -1
  163. data/lib/oxidized/model/nsxdfw.rb +31 -0
  164. data/lib/oxidized/model/nsxfirewall.rb +2 -1
  165. data/lib/oxidized/model/nxos.rb +20 -15
  166. data/lib/oxidized/model/ocnos.rb +42 -0
  167. data/lib/oxidized/model/onefinity.rb +18 -0
  168. data/lib/oxidized/model/openbsd.rb +1 -1
  169. data/lib/oxidized/model/opengear.rb +36 -1
  170. data/lib/oxidized/model/openwrt.rb +6 -6
  171. data/lib/oxidized/model/opnsense.rb +9 -1
  172. data/lib/oxidized/model/os6.rb +48 -0
  173. data/lib/oxidized/model/outputs.rb +5 -0
  174. data/lib/oxidized/model/panos.rb +2 -0
  175. data/lib/oxidized/model/perle.rb +41 -0
  176. data/lib/oxidized/model/pfsense.rb +9 -0
  177. data/lib/oxidized/model/powerconnect.rb +32 -11
  178. data/lib/oxidized/model/procurve.rb +6 -2
  179. data/lib/oxidized/model/qtech.rb +3 -1
  180. data/lib/oxidized/model/quantaos.rb +8 -6
  181. data/lib/oxidized/model/rgos.rb +33 -0
  182. data/lib/oxidized/model/riverbed.rb +104 -0
  183. data/lib/oxidized/model/routeros.rb +18 -11
  184. data/lib/oxidized/model/saos.rb +18 -1
  185. data/lib/oxidized/model/saos10.rb +38 -0
  186. data/lib/oxidized/model/siklu.rb +1 -1
  187. data/lib/oxidized/model/siklumhtg.rb +22 -0
  188. data/lib/oxidized/model/sixwind.rb +28 -0
  189. data/lib/oxidized/model/slxos.rb +3 -3
  190. data/lib/oxidized/model/smartbyte.rb +48 -0
  191. data/lib/oxidized/model/sonicos.rb +27 -20
  192. data/lib/oxidized/model/speedtouch.rb +1 -1
  193. data/lib/oxidized/model/sros.rb +3 -3
  194. data/lib/oxidized/model/srosmd.rb +1 -1
  195. data/lib/oxidized/model/supermicro.rb +1 -1
  196. data/lib/oxidized/model/telco.rb +1 -1
  197. data/lib/oxidized/model/tmos.rb +1 -0
  198. data/lib/oxidized/model/tnsr.rb +57 -0
  199. data/lib/oxidized/model/tplink.rb +5 -5
  200. data/lib/oxidized/model/trango.rb +4 -2
  201. data/lib/oxidized/model/truenas.rb +64 -3
  202. data/lib/oxidized/model/ucs.rb +1 -1
  203. data/lib/oxidized/model/unifiap.rb +144 -0
  204. data/lib/oxidized/model/uplinkolt.rb +46 -0
  205. data/lib/oxidized/model/voltaire.rb +1 -1
  206. data/lib/oxidized/model/voss.rb +3 -0
  207. data/lib/oxidized/model/vrp.rb +5 -1
  208. data/lib/oxidized/model/vyatta.rb +9 -3
  209. data/lib/oxidized/model/vyos.rb +37 -0
  210. data/lib/oxidized/model/xos.rb +10 -1
  211. data/lib/oxidized/model/zhoneolt.rb +4 -2
  212. data/lib/oxidized/model/zynos.rb +67 -3
  213. data/lib/oxidized/model/zynosadsl.rb +14 -0
  214. data/lib/oxidized/model/zynoscli.rb +1 -1
  215. data/lib/oxidized/model/zynosgs.rb +2 -0
  216. data/lib/oxidized/model/zynosmgs.rb +32 -0
  217. data/lib/oxidized/node.rb +99 -64
  218. data/lib/oxidized/nodes.rb +40 -19
  219. data/lib/oxidized/output/file.rb +80 -43
  220. data/lib/oxidized/output/git.rb +295 -151
  221. data/lib/oxidized/output/gitcrypt.rb +193 -185
  222. data/lib/oxidized/output/http.rb +63 -51
  223. data/lib/oxidized/output/output.rb +20 -4
  224. data/lib/oxidized/signals.rb +44 -0
  225. data/lib/oxidized/source/csv.rb +49 -49
  226. data/lib/oxidized/source/http.rb +76 -63
  227. data/lib/oxidized/source/jsonfile.rb +68 -0
  228. data/lib/oxidized/source/source.rb +73 -18
  229. data/lib/oxidized/source/sql.rb +71 -59
  230. data/lib/oxidized/version.rb +6 -4
  231. data/lib/oxidized/worker.rb +57 -29
  232. data/lib/oxidized.rb +4 -24
  233. data/lib/refinements.rb +21 -1
  234. data/oxidized.gemspec +45 -21
  235. metadata +248 -41
  236. data/lib/oxidized/model/timos.rb +0 -10
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/ssh'
5
+ require 'optparse'
6
+ require 'etc'
7
+ require 'timeout'
8
+
9
+ # This script logs in a network device and outputs a YAML file that can be
10
+ # used for model unit tests in spec/model/
11
+ # For more information, see docs/DeviceSimulation.md
12
+
13
+ ################# Methods
14
+ # Runs cmd in the ssh session, either im exec mode or with a tty
15
+ # saves the output to @output
16
+ def ssh_exec(cmd)
17
+ puts "\n### Sending #{cmd.dump}..."
18
+ @output&.puts " #{@sequence_prepend_command}#{cmd.dump}: |-"
19
+
20
+ if @exec_mode
21
+ @ssh_output = @ssh.exec! cmd
22
+ else
23
+ @ses.send_data cmd
24
+ shell_wait
25
+ end
26
+ yaml_output(' ')
27
+ end
28
+
29
+ # Wait for the ssh command to be executed, with an idle timout @idle_timeout
30
+ # Pressing CTRL-C exits the script
31
+ # Pressing ESC termiates the idle timeout
32
+ def shell_wait
33
+ @ssh_output = ''
34
+ # ssh_output gets appended by chanel.on-data (below)
35
+ # We store the current length of @ssh_output in @ssh_output_length
36
+ # if @ssh_output.length is bigger than @ssh_output_length, we got new data
37
+ @ssh_output_length = 0
38
+
39
+ # Keep track of time for idle timeout
40
+ start_time = Time.now
41
+
42
+ # Loop & wait for @idle_timeout seconds after last output
43
+ # 0.1 means that the loop should run at least once per 0.1 second
44
+ @ssh.loop(0.1) do
45
+ # if @ssh_output is longer than our saved length, we got new output
46
+ if @ssh_output_length < @ssh_output.length
47
+ # reset the timer and save the new output length
48
+ start_time = Time.now
49
+ @ssh_output_length = @ssh_output.length
50
+ end
51
+
52
+ # We wait for 0.1 seconds if a key was pressed
53
+ begin
54
+ Timeout.timeout(0.1) do
55
+ # Get input // this is a blocking call
56
+ char = $stdin.getch
57
+ # If ctrl-c is pressed, exit the script
58
+ if char == "\u0003"
59
+ puts '### CTRL-C pressed, exiting'
60
+ cleanup
61
+ exit
62
+ end
63
+ # If escape is pressed, terminate idle timeout
64
+ if char == "\e"
65
+ puts "\n### ESC pressed, skipping idle timeout"
66
+ return false
67
+ else
68
+ # if not, record the char and send the char through ssh
69
+ puts "\n### #{char.dump} pressed"
70
+ yaml_output(' ')
71
+ @output&.puts " #{@sequence_prepend_command}#{char.dump}: |-"
72
+ @ssh_output = ''
73
+ start_time = Time.now
74
+ @ssh_output_length = @ssh_output.length
75
+
76
+ @ses.send_data char
77
+ end
78
+ end
79
+ rescue Timeout::Error
80
+ # No key pressed
81
+ end
82
+
83
+ # exit the loop when the @idle_timeout has been reached (false = exit)
84
+ Time.now - start_time < @idle_timeout
85
+ end
86
+ end
87
+
88
+ def yaml_output(prepend = '')
89
+ # Now print the collected output to @output
90
+ firstline = true
91
+
92
+ prepend = @sequence_prepend_output + prepend
93
+
94
+ # each_line(chomp: true) would remove \r\n, so we prefer split
95
+ @ssh_output.split("\n", -1).each do |line|
96
+ # encode line and remove the first and the trailing double quote
97
+ line = line.dump[1..-2]
98
+ if firstline
99
+ # Make sure the leading space of the first line (if present)
100
+ # is coded with \0x20 or YAML block scalars won't work
101
+ line.sub!(/^\A /, '\x20')
102
+ firstline = false
103
+ end
104
+ # Make sure trailing white spaces are coded with \0x20
105
+ line.gsub!(/ $/, '\x20')
106
+ # prepend white spaces for the yaml block scalar
107
+ line = prepend + line
108
+ @output&.puts line
109
+ end
110
+ end
111
+
112
+ def cleanup
113
+ (@ssh.close rescue true) unless @ssh.closed? # rubocop:disable Style/RedundantParentheses
114
+ @output&.close
115
+ end
116
+
117
+ ################# Main loop
118
+
119
+ # Define options
120
+ options = {}
121
+ @sequence_prepend_command = '- '
122
+ @sequence_prepend_output = ' '
123
+
124
+ optparse = OptionParser.new do |opts|
125
+ opts.banner = <<~HEREDOC
126
+ Usages:
127
+ - device2yaml.rb [user@]host -i file [options]
128
+ - device2yaml.rb [user@]host -c "command1
129
+ command2
130
+ command3" [options]
131
+
132
+ -i and -c are mutualy exclusive, one must be specified
133
+
134
+ [options]:
135
+ HEREDOC
136
+
137
+ opts.on('-c', '--commands "command list"', 'specify the commands to be run') do |cmds|
138
+ options[:commands] = cmds
139
+ end
140
+ opts.on('-i', '--input file', 'Specify an input file for commands to be run') do |file|
141
+ options[:input] = file
142
+ end
143
+ opts.on('-o', '--output file', 'Specify an output YAML-file') do |file|
144
+ options[:output] = file
145
+ end
146
+ opts.on('-t', '--timeout value', Integer,
147
+ 'Specify the idle timeout beween commands (default: 5 seconds)') do |timeout|
148
+ options[:timeout] = timeout
149
+ end
150
+ opts.on('-e', '--exec-mode', 'Run ssh in exec mode (without tty)') { @exec_mode = true }
151
+ opts.on('-u', '--unordered', 'The YAML simulation should not enforce an order of the commands') do
152
+ @sequence_prepend_command = ''
153
+ @sequence_prepend_output = ''
154
+ end
155
+ opts.on '-h', '--help', 'Print this help' do
156
+ puts opts
157
+ exit
158
+ end
159
+ end
160
+
161
+ # Catch and parse the first argument
162
+ if ARGV[0] && ARGV[0][0] != '-'
163
+ argument = ARGV.shift
164
+ if argument.include?('@')
165
+ ssh_user, ssh_host = argument.split('@')
166
+ else
167
+ ssh_user = Etc.getlogin
168
+ ssh_host = argument
169
+ end
170
+ else
171
+ puts 'Missing a host to connect to...'
172
+ puts
173
+ puts optparse
174
+ exit 1
175
+ end
176
+
177
+ # Parse the options
178
+ optparse.parse!
179
+
180
+ # Get the commands to be run against ssh_host
181
+ # ^ = xor = exclusive or
182
+ unless options[:commands].nil? ^ options[:input].nil?
183
+ puts "Please provide commands to be run against #{ssh_host} with either option -c or -i"
184
+ puts
185
+ puts optparse
186
+ exit 1
187
+ end
188
+
189
+ if options[:commands]
190
+ ssh_commands = []
191
+ options[:commands].each_line(chomp: true) { |command| ssh_commands << command }
192
+ elsif options[:input]
193
+ ssh_commands = File.read(options[:input]).split(/\n+|\r+/)
194
+ end
195
+
196
+ puts "Running #{ssh_commands} on #{ssh_user}@#{ssh_host}"
197
+ # Add \n to each command
198
+ ssh_commands.map! { |s| s + "\n" }
199
+
200
+ # Defaut idle timeout: 5 seconds, as tests showed that 2 seconds is too short
201
+ @idle_timeout = options[:timeout] || 5
202
+
203
+ # We will use safe navifation (&.) to call the methods on @output only
204
+ # if @output is not nil
205
+ @output = options[:output] ? File.open(options[:output], 'w') : nil
206
+
207
+ @ssh = Net::SSH.start(ssh_host,
208
+ ssh_user,
209
+ { timeout: 10,
210
+ append_all_supported_algorithms: true })
211
+
212
+ @ssh_output = ''
213
+
214
+ unless @exec_mode
215
+ @ses = @ssh.open_channel do |ch|
216
+ ch.on_data do |_ch, data|
217
+ @ssh_output += data
218
+ # Output the data to stdout for interactive control
219
+ # remove ANSI escape codes, as they can produce problems
220
+ # The code will be printed as '\e[123m' in the output
221
+ print data.gsub("\e", '\e')
222
+ end
223
+ ch.request_pty(term: 'vt100') do |_ch, success_pty|
224
+ raise "Can't get PTY" unless success_pty
225
+
226
+ ch.send_channel_request 'shell' do |_ch, success_shell|
227
+ raise "Can't get shell" unless success_shell
228
+ end
229
+ end
230
+ ch.on_extended_data do |_ch, _type, data|
231
+ $stderr.print "Error: #{data}\n"
232
+ end
233
+ end
234
+ end
235
+
236
+ # YAML begin of file
237
+ @output&.puts '---'
238
+
239
+ if @exec_mode
240
+ # init prompt does not exist and is empty in exec mode
241
+ @output&.puts 'init_prompt:'
242
+ else
243
+ # get motd and first prompt
244
+ @output&.puts 'init_prompt: |-'
245
+ shell_wait
246
+ yaml_output ' '
247
+ end
248
+
249
+ @output&.puts 'commands:'
250
+
251
+ begin
252
+ ssh_commands.each do |cmd|
253
+ ssh_exec cmd
254
+ end
255
+ rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError => e
256
+ puts "### Connection closed with message: #{e.message}"
257
+ end
258
+
259
+ cleanup
@@ -15,12 +15,31 @@
15
15
  # timeout: 120
16
16
  #
17
17
  # Add webhook to your MS Teams channel and set the next variable to the full url
18
+ # If INCLUDE_GITHUB_LINK is set to true, there will be a button that links to GITURL in the bottom of each teams post. the commit id is added at the end of the url.
19
+ # MAXSIZE is set to respect the 28 KB limit for teams webhooks, but you might need to change it if you modify the Adaptive Card
18
20
 
19
21
  weburl="https://contoso.webhook.office.com/webhookb2/etc etc etc"
22
+ GITURL="https://github.example.com/My-org/oxidized/commit/"
23
+ INCLUDE_GITHUB_LINK=false
24
+ # Max size for summary text.
25
+ MAXSIZE=24500
26
+
27
+ if [ "$INCLUDE_GITHUB_LINK" = true ]; then
28
+ github_action=",
29
+ { \"type\": \"ActionSet\",
30
+ \"actions\":
31
+ [
32
+ {
33
+ \"type\": \"Action.OpenUrl\",
34
+ \"title\": \"Click to see diff in github\",
35
+ \"url\": \"${GITURL}${OX_REPO_COMMITREF}\"
36
+ }
37
+ ]
38
+ }"
39
+ fi
20
40
 
21
41
  postdata()
22
42
  {
23
- COMMIT=$(git --bare --git-dir="${OX_REPO_NAME}" show --pretty='' --no-color "${OX_REPO_COMMITREF}" | jq --raw-input --slurp --compact-output)
24
43
  cat <<EOF
25
44
  {
26
45
  "type":"message",
@@ -77,7 +96,7 @@ postdata()
77
96
  "size": "small"
78
97
  }
79
98
  ]
80
- }
99
+ }${github_action}
81
100
  ]
82
101
  }
83
102
  }
@@ -86,6 +105,14 @@ postdata()
86
105
  EOF
87
106
  }
88
107
 
89
- curl -i \
90
- -H "Content-Type: application/json" \
91
- -X POST --data "$(postdata)" "${weburl}"
108
+ COMMIT=$(git --bare --git-dir="${OX_REPO_NAME}" show --pretty='' --no-color "${OX_REPO_COMMITREF}" | jq --raw-input --slurp --compact-output)
109
+ URL=""
110
+
111
+ size=$(postdata | wc -c)
112
+ if [ "$size" -gt "$MAXSIZE" ]; then
113
+ COMMIT=$(git --bare --git-dir="${OX_REPO_NAME}" show --pretty='' --no-color "${OX_REPO_COMMITREF}" | head -c $MAXSIZE)
114
+ COMMIT+="$NEWLINE...$NEWLINE Shortened because of length"
115
+ COMMIT=$(echo "${COMMIT}" | jq --raw-input --slurp --compact-output )
116
+ fi
117
+
118
+ curl -i -H "Content-Type: application/json" -X POST --data "$(postdata)" "${weburl}"
@@ -0,0 +1,55 @@
1
+ ### script in ~/config/oxidized/hook/modelrules.rb ## or OXDIZED_HOME equivalent
2
+ ###
3
+ ### router.db:
4
+ ### router1:1.1.1.1:routeros::mikrotik
5
+ ### router2:2.2.2.2:ios:switch:cisco
6
+ ### router3:3.3.3.3:routeros:switch:mikrotik
7
+ ###
8
+ ### config:
9
+ ### source:
10
+ ### default: csv
11
+ ### csv:
12
+ ### file: "/Users/ytti/.config/oxidized/router.db"
13
+ ### delimiter: !ruby/regexp /:/
14
+ ### map:
15
+ ### name: 0
16
+ ### ip: 1
17
+ ### model: 2
18
+ ### group: 3
19
+ ### hooks:
20
+ ### somename:
21
+ ### type: modelrules
22
+ ### events: ["source_node_transform"]
23
+ ### rules:
24
+ ### - vendor: mikrotik
25
+ ### group: switch
26
+ ### model: eltex
27
+ ###
28
+ ### Nodes BEFORE script:
29
+ ### {name: "router1", ip: "1.1.1.1", model: "routeros", group: ""}
30
+ ### {name: "router2", ip: "2.2.2.2", model: "ios", group: "switch"}
31
+ ### {name: "router3", ip: "3.3.3.3", model: "routeros", group: "switch"}
32
+ ###
33
+ ### Nodes AFTER script:
34
+ ### {name: "router1", ip: "1.1.1.1", model: "routeros", group: ""}
35
+ ### {name: "router2", ip: "2.2.2.2", model: "ios", group: "switch"}
36
+ ### {name: "router3", ip: "3.3.3.3", model: "eltex", group: "switch"}
37
+ class ModelRules < Oxidized::Hook
38
+ def validate_cfg!
39
+ raise KeyError, 'hook.rules is required' unless cfg.has_key?('rules')
40
+ end
41
+
42
+ def run_hook(ctx)
43
+ # node is the node[key] that we'd return without manipulation
44
+ node = ctx.node ## e.g. node[:ip], node[:model] - what ever config maps
45
+
46
+ ## node_raw is source specific, in CSV it is just the field number, in HTTP it is JSON
47
+ vendor = ctx.node_raw[4]
48
+
49
+ cfg.rules.each do |rule|
50
+ node[:model] = rule['model'] if node[:group] == rule['group'] && vendor == rule['vendor']
51
+ end
52
+
53
+ node
54
+ end
55
+ end
@@ -0,0 +1,167 @@
1
+ # Advanced dynamic model selection hook for Oxidized
2
+ # --------------------------------------------------
3
+ # This hook allows you to assign Oxidized models based on any device attributes
4
+ # (name, vendor, group, type, IP, etc.) using flexible rules defined in the config.
5
+ # It uses the `source_node_transform` event and works with any source (CSV, HTTP, SQL).
6
+ #
7
+ # The hook is designed to be generic: you can specify any field that exists in the
8
+ # node data after source mapping (see `map` section in the source configuration).
9
+ # Rules are evaluated in order; the first matching rule assigns its `model`.
10
+ #
11
+ # ⚠️ IMPORTANT: For the hook to load correctly:
12
+ # - The filename must match the hook type (e.g., `modelrulesadvanced.rb`).
13
+ # - The class name must be the CamelCase version of the filename without underscores
14
+ # (e.g., filename `modelrulesadvanced.rb` → class `Modelrulesadvanced`).
15
+ # - In the Oxidized config, specify `type: modelrulesadvanced` (the filename without .rb).
16
+ #
17
+ # ⚠️ DOCKER NOTE: This hook was developed and tested in a Docker container.
18
+ # Ensure that your hooks directory inside the container is
19
+ # `/home/oxidized/.config/oxidized/hook` (singular "hook"), not "hooks".
20
+ # In docker-compose, mount your local hooks folder to this path, e.g.:
21
+ # volumes:
22
+ # - ./extra/hooks:/home/oxidized/.config/oxidized/hook
23
+ #
24
+ # Example scenario with NetBox:
25
+ # Suppose your NetBox instance returns devices with the following data (simplified):
26
+ # gw-001 | Mikrotik | RB750 | ro_ud | 192.168.0.167/24
27
+ # gw-002 | Mikrotik | RB750 | ro_ud | 192.168.0.177/24
28
+ # gw-003 | Mikrotik | RB750 | ro_bd | 192.168.0.178/24
29
+ # gw-004 | Mikrotik | RB750 | switch | 192.168.0.181/24
30
+ # gw-005 | Cisco | 2960 | switch | 192.168.0.179/24
31
+ # gw-006 | Mikrotik | RB750 | switch | 192.168.0.180/24
32
+ # gw-007 | Arista | 3456 | ro_ud | 192.168.0.190/24
33
+ #
34
+ # Desired model assignment:
35
+ # - gw-001 (exception by name) → asa
36
+ # - MikroTik in group `switch` with IP 192.168.0.180/24 → eltex
37
+ # - All other MikroTik → routeros
38
+ # - Cisco switches → ios
39
+ # - Arista devices → eos
40
+ #
41
+ # Example configuration (top-level in Oxidized config):
42
+ # ---
43
+ # username: oxidized_ssh_user
44
+ # password: oxidized_ssh_password
45
+ #
46
+ # hooks:
47
+ # modelrulesadvanced:
48
+ # type: modelrulesadvanced
49
+ # events: [source_node_transform]
50
+ # rules:
51
+ # - description: "Exception: gw-001 uses ASA model"
52
+ # name: gw-001
53
+ # model: asa
54
+ # - description: "Mikrotik switch with IP 192.168.0.180/24 uses eltex"
55
+ # vendor: Mikrotik
56
+ # group: switch
57
+ # ip: 192.168.0.180/24
58
+ # model: eltex
59
+ # - description: "All other Mikrotik devices"
60
+ # vendor: Mikrotik
61
+ # model: routeros
62
+ # - description: "Cisco switches"
63
+ # vendor: Cisco
64
+ # group: switch
65
+ # model: ios
66
+ # - description: "Arista devices"
67
+ # vendor: Arista
68
+ # model: eos
69
+ #
70
+ # resolve_dns: false
71
+ # interval: 3600
72
+ # rest: 0.0.0.0:8888
73
+ # debug: true # optional – enables detailed logging from the hook
74
+ #
75
+ # source:
76
+ # default: http
77
+ # http:
78
+ # url: http://netbox.test/api/dcim/devices/?status=active&has_primary_ip=true
79
+ # headers:
80
+ # Authorization: Token YOUR_API_TOKEN
81
+ # map:
82
+ # name: name
83
+ # vendor: device_type.manufacturer.name
84
+ # type: device_type.model
85
+ # group: role.slug
86
+ # ip: primary_ip.address
87
+ # secure: false
88
+ # hosts_location: results
89
+ # pagination: true
90
+ # pagination_key_name: next
91
+ #
92
+ # output:
93
+ # default: file
94
+ # file:
95
+ # directory: "/home/oxidized/.config/oxidized/configs"
96
+ #
97
+ # groups:
98
+ # ro_ud: {}
99
+ # ro_bd: {}
100
+ # switch: {}
101
+ #
102
+ # How it works:
103
+ # 1. Oxidized loads node data from the source (NetBox) and applies the `map`.
104
+ # 2. For each node, the `source_node_transform` event is triggered.
105
+ # 3. This hook receives a context object `ctx` containing:
106
+ # - `ctx.node` – the mapped node attributes (hash)
107
+ # - `ctx.node_raw` – the original source record (useful for unmapped fields)
108
+ # 4. The hook iterates through the rules defined in `cfg.rules`.
109
+ # 5. For each rule, it checks that every specified key (except `model` and `description`)
110
+ # matches the corresponding value in `ctx.node`. Comparison is case‑insensitive and
111
+ # strips surrounding spaces.
112
+ # 6. The first matching rule assigns its `model` to `ctx.node[:model]`.
113
+ # 7. If no rule matches, the node's `model` remains unchanged.
114
+ # 8. The modified (or original) node is returned; returning `nil` would exclude the node.
115
+ #
116
+ # Notes:
117
+ # - Rule order is crucial: place more specific rules (e.g., with IP or name) before generic ones.
118
+ # - The `description` field is optional and only appears in debug logs.
119
+ # - Any field present in `ctx.node` after mapping can be used as a match key.
120
+ # - For HTTP sources (NetBox), you can access additional fields via `ctx.node_raw["field"]`.
121
+ # - Debug logging requires `debug: true` in the global config.
122
+
123
+ class Modelrulesadvanced < Oxidized::Hook
124
+ # Validate that the hook configuration contains a 'rules' array.
125
+ def validate_cfg!
126
+ raise KeyError, "hook.rules is required" unless cfg.has_key?("rules")
127
+ end
128
+
129
+ # Main hook method – called for each node during source_node_transform event.
130
+ def run_hook(ctx)
131
+ node = ctx.node
132
+ rules = cfg.rules || []
133
+
134
+ matched_model = nil
135
+ rules.each_with_index do |rule, idx|
136
+ match = true
137
+ rule.each do |key, value|
138
+ next if %w[model description].include?(key)
139
+
140
+ # Retrieve the corresponding value from the node (symbol or string key)
141
+ node_value = node[key.to_sym] || node[key.to_s]
142
+ if node_value.to_s.strip.downcase != value.to_s.strip.downcase
143
+ match = false
144
+ break
145
+ end
146
+ end
147
+ next unless match
148
+
149
+ matched_model = rule["model"]
150
+ desc = rule["description"] ? " (#{rule['description']})" : ""
151
+ logger.debug "ModelRulesAdvanced: rule #{idx + 1}#{desc} matched -> #{matched_model}"
152
+ break
153
+ end
154
+
155
+ if matched_model
156
+ old_model = node[:model] || node["model"]
157
+ node[:model] = matched_model
158
+ logger.debug "ModelRulesAdvanced: changed model from #{old_model.inspect} to #{matched_model.inspect}"
159
+ else
160
+ # rubocop:disable Layout/LineLength
161
+ logger.debug "ModelRulesAdvanced: no rule matched, keeping existing model: #{node[:model] || node['model'].inspect}"
162
+ # rubocop:enable Layout/LineLength
163
+ end
164
+
165
+ node
166
+ end
167
+ end
@@ -0,0 +1,54 @@
1
+ ### script in ~/config/oxidized/hook/srcipmap.rb ## or OXDIZED_HOME equivalent
2
+ ###
3
+ ### router.db:
4
+ ### router1:1.1.1.1:cisco:c7200:10.10.10.1:somerole
5
+ ### router2:2.2.2.2:juniper:mx80:10.10.10.2:wlc
6
+ ### router3:3.3.3.3:juniper:mx2020:10.10.10.3:anotherrole
7
+ ###
8
+ ### config:
9
+ ### source:
10
+ ### default: csv
11
+ ### csv:
12
+ ### file: "/Users/ytti/.config/oxidized/router.db"
13
+ ### delimiter: !ruby/regexp /:/
14
+ ### map:
15
+ ### name: 0
16
+ ### ip: 1
17
+ ### model: 2
18
+ ### model_map:
19
+ ### juniper: junos
20
+ ### cisco: ios
21
+ ### hooks:
22
+ ### somename:
23
+ ### type: srcipmap
24
+ ### events: ["source_node_transform"]
25
+ ###
26
+ ###
27
+ ###
28
+ ### Nodes BEFORE script:
29
+ ### {name: "router1", ip: "1.1.1.1", model: "ios"}
30
+ ### {name: "router2", ip: "2.2.2.2", model: "junos"}
31
+ ### {name: "router3", ip: "3.3.3.3", model: "junos"
32
+ ###
33
+ ### Nodes AFTER script:
34
+ ### {name: "router1", ip: "1.1.1.1", model: "ios"}
35
+ ### {name: "router2", ip: "10.10.10.2", model: "junos"}
36
+ ### {name: "router3", ip: "3.3.3.3", model: "junos"}
37
+
38
+ class SrcIpMap < Oxidized::Hook
39
+ def run_hook(ctx)
40
+ # node is the node[key] that we'd return without manipulation
41
+ node = ctx.node
42
+
43
+ ## node_raw is source specific, in CSV it is just the field number
44
+ _platform = ctx.node_raw[3]
45
+ oob_ip = ctx.node_raw[4]
46
+ role = ctx.node_raw[5]
47
+
48
+ ### the magic
49
+ node[:ip] = oob_ip if role == 'wlc'
50
+
51
+ ### remember to return the manipulated object, nil if you want to ignore loading node
52
+ node
53
+ end
54
+ end
@@ -10,7 +10,7 @@ pending = false
10
10
  critical_nodes = []
11
11
  pending_nodes = []
12
12
 
13
- json = JSON.parse(open("http://localhost:8888/nodes.json").read)
13
+ json = JSON.parse(URI("http://localhost:8888/nodes.json").open(&:read))
14
14
  json.each do |node|
15
15
  next if !ARGV.empty? && (ARGV[0] != node['name'])
16
16
 
data/extra/oxidized.runit CHANGED
@@ -2,4 +2,4 @@
2
2
  [ ! -d /home/oxidized/.config/oxidized ] && mkdir -p /home/oxidized/.config/oxidized
3
3
  [ -f /home/oxidized/.config/oxidized/pid ] && rm /home/oxidized/.config/oxidized/pid
4
4
  chown -R oxidized:oxidized /home/oxidized/.config/oxidized
5
- exec setuser oxidized oxidized
5
+ exec gosu oxidized oxidized
data/extra/rest_client.rb CHANGED
@@ -6,7 +6,7 @@ module Oxidized
6
6
  require 'asetus'
7
7
 
8
8
  class Config
9
- Root = Root = ENV['OXIDIZED_HOME'] || File.join(Dir.home, '.config', 'oxidized')
9
+ ROOT = ENV['OXIDIZED_HOME'] || File.join(Dir.home, '.config', 'oxidized')
10
10
  end
11
11
 
12
12
  CFGS = Asetus.new name: 'oxidized', load: false, key_to_s: true
@@ -14,8 +14,8 @@ module Oxidized
14
14
 
15
15
  begin
16
16
  CFGS.load
17
- rescue StandardError => error
18
- raise InvalidConfig, "Error loading config: #{error.message}"
17
+ rescue StandardError => e
18
+ raise InvalidConfig, "Error loading config: #{e.message}"
19
19
  end
20
20
 
21
21
  restcfg = CFGS.cfg.rest
@@ -38,7 +38,8 @@ module Oxidized
38
38
 
39
39
  def next(opt)
40
40
  data = JSON.dump opt
41
- @web.put PATH + '/node/next/' + opt[:name].to_s, data
41
+ headers = { 'content-type': 'application/json' }
42
+ @web.put PATH + '/node/next/' + opt[:name].to_s, data, headers
42
43
  end
43
44
  end
44
45
  end