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
@@ -1,18 +1,15 @@
1
1
  module Oxidized
2
- require "oxidized/input/cli"
3
2
  require "net/http"
4
3
  require "json"
4
+ require "net/http/digest_auth"
5
5
 
6
6
  class HTTP < Input
7
- include Input::CLI
8
-
9
7
  def connect(node)
10
8
  @node = node
11
9
  @secure = false
12
10
  @username = nil
13
11
  @password = nil
14
12
  @headers = {}
15
- @log = File.open(Oxidized::Config::Log + "/#{@node.ip}-http", "w") if Oxidized.config.input.debug?
16
13
  @node.model.cfg["http"].each { |cb| instance_exec(&cb) }
17
14
 
18
15
  return true unless @main_page && defined?(login)
@@ -47,26 +44,79 @@ module Oxidized
47
44
  private
48
45
 
49
46
  def get_http(path)
50
- schema = @secure ? "https://" : "http://"
51
- uri = URI("#{schema}#{@node.ip}#{path}")
52
- req = Net::HTTP::Get.new(uri)
53
- req.basic_auth @username, @password unless @username.nil?
54
- @headers.each do |header, value|
55
- req.add_field(header, value)
56
- end
47
+ res = perform_http_request(path, method: :get)
48
+ res.body
49
+ end
50
+
51
+ def post_http(path, body = nil, extra_headers = {})
52
+ res = perform_http_request(path, method: :post, body: body, extra_headers: extra_headers)
53
+ res.body
54
+ end
55
+
56
+ def perform_http_request(path, method: :get, body: nil, extra_headers: {})
57
+ uri = get_uri(path)
58
+ http_method = method.to_s.upcase
59
+
60
+ logger.debug "Making #{http_method} request to: #{uri}"
61
+
57
62
  ssl_verify = Oxidized.config.input.http.ssl_verify? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
58
- res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", verify_mode: ssl_verify) do |http|
63
+
64
+ res = make_request(uri, ssl_verify, extra_headers, method: method, body: body)
65
+
66
+ if res.code == '401' && res['www-authenticate']&.include?('Digest')
67
+ uri.user = @username
68
+ uri.password = URI.encode_www_form_component(@password)
69
+ logger.debug "Server requires Digest authentication"
70
+
71
+ auth = Net::HTTP::DigestAuth.new.auth_header(uri, res['www-authenticate'], http_method)
72
+ res = make_request(uri, ssl_verify, extra_headers.merge('Authorization' => auth),
73
+ method: method, body: body)
74
+
75
+ elsif @username && @password && !authorization_header_present?(extra_headers)
76
+ logger.debug "Falling back to Basic authentication"
77
+ res = make_request(uri, ssl_verify, extra_headers.merge('Authorization' => basic_auth_header),
78
+ method: method, body: body)
79
+ end
80
+
81
+ logger.debug "Response code: #{res.code}"
82
+ res
83
+ end
84
+
85
+ def make_request(uri, ssl_verify, extra_headers = {}, method: :get, body: nil)
86
+ Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", verify_mode: ssl_verify) do |http|
87
+ req_class = if method == :get
88
+ Net::HTTP::Get
89
+ elsif method == :post
90
+ Net::HTTP::Post
91
+ else
92
+ raise Oxidized::OxidizedError, "Unsupported HTTP method: #{method.inspect}. " \
93
+ "Only :get and :post are supported"
94
+ end
95
+ req = req_class.new(uri)
96
+ @headers.merge(extra_headers).each { |header, value| req.add_field(header, value) }
97
+ req.body = body if body
98
+
99
+ logger.debug "Sending #{method.to_s.upcase} request with headers: #{@headers.merge(extra_headers)}"
59
100
  http.request(req)
60
101
  end
61
- res.body
62
102
  end
63
103
 
64
- def log(str)
65
- @log&.write(str)
104
+ def authorization_header_present?(headers)
105
+ headers.keys.any? { |key| key.to_s.downcase == 'authorization' }
66
106
  end
67
107
 
68
- def disconnect
69
- @log.close if Oxidized.config.input.debug?
108
+ def basic_auth_header
109
+ "Basic " + ["#{@username}:#{@password}"].pack('m').delete("\r\n")
110
+ end
111
+
112
+ def disconnect; end
113
+
114
+ def get_uri(path)
115
+ path = URI.parse(path)
116
+ uri_class = @secure ? URI::HTTPS : URI::HTTP
117
+ uri_class.build(host: @node.ip,
118
+ path: path.path,
119
+ query: path.query)
70
120
  end
71
121
  end
72
122
  end
@@ -1,23 +1,44 @@
1
+ require_relative 'cli'
2
+
1
3
  module Oxidized
2
4
  class PromptUndetect < OxidizedError; end
3
5
 
4
6
  class Input
7
+ include SemanticLogger::Loggable
5
8
  include Oxidized::Config::Vars
9
+ include Oxidized::Input::CLI
6
10
 
7
- RescueFail = {
8
- debug: [
9
- Errno::ECONNREFUSED
10
- ],
11
- warn: [
12
- IOError,
13
- PromptUndetect,
14
- Timeout::Error,
15
- Errno::ECONNRESET,
16
- Errno::EHOSTUNREACH,
17
- Errno::ENETUNREACH,
18
- Errno::EPIPE,
19
- Errno::ETIMEDOUT
20
- ]
11
+ RESCUE_FAIL = {
12
+ Errno::ECONNREFUSED => :debug,
13
+ IOError => :warn,
14
+ PromptUndetect => :warn,
15
+ Timeout::Error => :warn,
16
+ Errno::ECONNRESET => :warn,
17
+ Errno::EHOSTUNREACH => :warn,
18
+ Errno::ENETUNREACH => :warn,
19
+ Errno::EPIPE => :warn,
20
+ Errno::ETIMEDOUT => :warn
21
21
  }.freeze
22
+
23
+ # Returns a hash mapping exception classes to their log level
24
+ def self.rescue_fail
25
+ RESCUE_FAIL.dup
26
+ end
27
+
28
+ def self.config_name
29
+ name.split('::').last.downcase
30
+ end
31
+
32
+ def self.to_sym
33
+ config_name.to_sym
34
+ end
35
+
36
+ def config_name
37
+ self.class.config_name
38
+ end
39
+
40
+ def to_sym
41
+ self.class.to_sym
42
+ end
22
43
  end
23
44
  end
@@ -0,0 +1,27 @@
1
+ module Oxidized
2
+ require 'net/ssh'
3
+ begin
4
+ require 'net/scp'
5
+ rescue LoadError
6
+ raise OxidizedError, 'net/scp not found: sudo gem install net-scp'
7
+ end
8
+ require 'timeout'
9
+ require_relative 'sshbase'
10
+
11
+ class SCP < SSHBase
12
+ RESCUE_FAIL = {
13
+ Net::SCP::Error => :warn
14
+ }.freeze
15
+
16
+ def self.rescue_fail
17
+ super.merge(RESCUE_FAIL)
18
+ end
19
+
20
+ def cmd(file)
21
+ logger.debug "SCP: #{file} @ #{@node.name}"
22
+ Timeout.timeout(@node.timeout) do
23
+ @ssh.scp.download!(file)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,29 +1,30 @@
1
+ require 'timeout'
2
+ require_relative 'sshbase'
3
+ require_relative 'debugyaml'
4
+ require_relative 'debugtext'
5
+
1
6
  module Oxidized
2
- require 'net/ssh'
3
- require 'net/ssh/proxy/command'
4
- require 'timeout'
5
- require 'oxidized/input/cli'
6
- class SSH < Input
7
- RescueFail = {
8
- debug: [
9
- Net::SSH::Disconnect
10
- ],
11
- warn: [
12
- RuntimeError,
13
- Net::SSH::AuthenticationFailed
14
- ]
15
- }.freeze
16
- include Input::CLI
7
+ class SSH < SSHBase
17
8
  class NoShell < OxidizedError; end
18
9
 
19
- def connect(node)
10
+ RESCUE_FAIL = {
11
+ RuntimeError => :warn
12
+ }.freeze
13
+
14
+ def self.rescue_fail
15
+ super.merge(RESCUE_FAIL)
16
+ end
17
+
18
+ def connect(node) # rubocop:disable Naming/PredicateMethod
20
19
  @node = node
21
- @output = ''
20
+ @output = String.new('')
22
21
  @pty_options = { term: "vt100" }
23
22
  @node.model.cfg['ssh'].each { |cb| instance_exec(&cb) }
24
- @log = File.open(Oxidized::Config::Log + "/#{@node.ip}-ssh", 'w') if Oxidized.config.input.debug?
25
23
 
26
- Oxidized.logger.debug "lib/oxidized/input/ssh.rb: Connecting to #{@node.name}"
24
+ @yaml_debug = DebugYAML.new(Oxidized.config.input.debug, @node, config_name)
25
+ @text_debug = DebugText.new(Oxidized.config.input.debug, @node, config_name)
26
+
27
+ logger.debug "Connecting to #{@node.name}"
27
28
  @ssh = Net::SSH.start(@node.ip, @node.auth[:username], make_ssh_opts)
28
29
  unless @exec
29
30
  shell_open @ssh
@@ -36,20 +37,31 @@ module Oxidized
36
37
  connected?
37
38
  end
38
39
 
39
- def connected?
40
- @ssh && (not @ssh.closed?)
41
- end
42
-
43
40
  def cmd(cmd, expect = node.prompt)
44
- Oxidized.logger.debug "lib/oxidized/input/ssh.rb #{cmd} @ #{node.name} with expect: #{expect.inspect}"
45
- if @exec
46
- @ssh.exec! cmd
47
- else
48
- cmd_shell(cmd, expect).gsub(/\r\n/, "\n")
41
+ unless cmd.is_a?(String)
42
+ logger.error "cmd must be a String (#{cmd.class}): #{cmd.inspect} @ #{node.name}"
43
+ raise ArgumentError, "cmd must be a String"
49
44
  end
45
+ logger.debug "Sending '#{cmd.dump}' @ #{node.name} with expect: #{expect.inspect}"
46
+ cmd_output = if @exec
47
+ @yaml_debug&.send_data(cmd)
48
+ @text_debug&.send_data(cmd)
49
+ @ssh.exec! cmd
50
+ else
51
+ cmd_shell(cmd, expect).gsub("\r\n", "\n")
52
+ end
53
+
54
+ # only logging @exec as cmd_shell is handled in the ssh loop
55
+ @yaml_debug&.receive_data(cmd_output) if @exec
56
+ @text_debug&.receive_data(cmd_output) if @exec
57
+
58
+ # Make sure we return a String
59
+ cmd_output.to_s
50
60
  end
51
61
 
52
62
  def send(data)
63
+ @yaml_debug&.send_data(data)
64
+ @text_debug&.send_data(data)
53
65
  @ses.send_data data
54
66
  end
55
67
 
@@ -61,23 +73,27 @@ module Oxidized
61
73
 
62
74
  private
63
75
 
76
+ # We need a specific disconnect for SSH in shell mode, see issue #3725
64
77
  def disconnect
65
78
  disconnect_cli
66
79
  # if disconnect does not disconnect us, give up after timeout
67
- Timeout.timeout(Oxidized.config.timeout) { @ssh.loop }
68
- rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError
80
+ Timeout.timeout(@node.timeout) { @ssh.loop }
81
+ rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError => e
82
+ logger.debug 'The other side closed the connection while ' \
83
+ "disconnecting, raising #{e.class} with #{e.message}"
84
+ rescue Timeout::Error
85
+ logger.debug "#{@node.name} timed out while disconnecting"
69
86
  ensure
70
- @log.close if Oxidized.config.input.debug?
71
- (@ssh.close rescue true) unless @ssh.closed?
87
+ @yaml_debug&.close
88
+ @text_debug&.close
89
+ (@ssh.close rescue true) unless @ssh.closed? # rubocop:disable Style/RedundantParentheses
72
90
  end
73
91
 
74
92
  def shell_open(ssh)
75
93
  @ses = ssh.open_channel do |ch|
76
94
  ch.on_data do |_ch, data|
77
- if Oxidized.config.input.debug?
78
- @log.print data
79
- @log.flush
80
- end
95
+ @yaml_debug&.receive_data(data)
96
+ @text_debug&.receive_data(data)
81
97
  @output << data
82
98
  @output = @node.model.expects @output
83
99
  end
@@ -98,8 +114,11 @@ module Oxidized
98
114
  end
99
115
 
100
116
  def cmd_shell(cmd, expect_re)
101
- @output = ''
102
- @ses.send_data cmd + "\n"
117
+ @output = String.new('')
118
+
119
+ @yaml_debug&.send_data(cmd + newline)
120
+ @text_debug&.send_data(cmd + newline)
121
+ @ses.send_data cmd + newline
103
122
  @ses.process
104
123
  expect expect_re if expect_re
105
124
  @output
@@ -107,8 +126,8 @@ module Oxidized
107
126
 
108
127
  def expect(*regexps)
109
128
  regexps = [regexps].flatten
110
- Oxidized.logger.debug "lib/oxidized/input/ssh.rb: expecting #{regexps.inspect} at #{node.name}"
111
- Timeout.timeout(Oxidized.config.timeout) do
129
+ logger.debug "Expecting #{regexps.inspect} at #{node.name}"
130
+ Timeout.timeout(@node.timeout) do
112
131
  @ssh.loop(0.1) do
113
132
  sleep 0.1
114
133
  match = regexps.find { |regexp| @output.match regexp }
@@ -118,48 +137,5 @@ module Oxidized
118
137
  end
119
138
  end
120
139
  end
121
-
122
- def make_ssh_opts
123
- secure = Oxidized.config.input.ssh.secure?
124
- ssh_opts = {
125
- number_of_password_prompts: 0,
126
- keepalive: vars(:ssh_no_keepalive) ? false : true,
127
- verify_host_key: secure ? :always : :never,
128
- append_all_supported_algorithms: true,
129
- password: @node.auth[:password],
130
- timeout: Oxidized.config.timeout,
131
- port: (vars(:ssh_port) || 22).to_i,
132
- forward_agent: false
133
- }
134
-
135
- auth_methods = vars(:auth_methods) || %w[none publickey password]
136
- ssh_opts[:auth_methods] = auth_methods
137
- Oxidized.logger.debug "AUTH METHODS::#{auth_methods}"
138
-
139
- ssh_opts[:proxy] = make_ssh_proxy_command(vars(:ssh_proxy), vars(:ssh_proxy_port), secure) if vars(:ssh_proxy)
140
-
141
- ssh_opts[:keys] = [vars(:ssh_keys)].flatten if vars(:ssh_keys)
142
- ssh_opts[:kex] = vars(:ssh_kex).split(/,\s*/) if vars(:ssh_kex)
143
- ssh_opts[:encryption] = vars(:ssh_encryption).split(/,\s*/) if vars(:ssh_encryption)
144
- ssh_opts[:host_key] = vars(:ssh_host_key).split(/,\s*/) if vars(:ssh_host_key)
145
- ssh_opts[:hmac] = vars(:ssh_hmac).split(/,\s*/) if vars(:ssh_hmac)
146
-
147
- if Oxidized.config.input.debug?
148
- ssh_opts[:logger] = Oxidized.logger
149
- ssh_opts[:verbose] = Logger::DEBUG
150
- end
151
-
152
- ssh_opts
153
- end
154
-
155
- def make_ssh_proxy_command(proxy_host, proxy_port, secure)
156
- return nil unless !proxy_host.nil? && !proxy_host.empty?
157
-
158
- proxy_command = "ssh "
159
- proxy_command += "-o StrictHostKeyChecking=no " unless secure
160
- proxy_command += "-p #{proxy_port} " if proxy_port
161
- proxy_command += "#{proxy_host} -W [%h]:%p"
162
- Net::SSH::Proxy::Command.new(proxy_command)
163
- end
164
140
  end
165
141
  end
@@ -0,0 +1,102 @@
1
+ module Oxidized
2
+ require 'net/ssh'
3
+ require 'net/ssh/proxy/command'
4
+ require 'timeout'
5
+
6
+ class SSHBase < Input
7
+ RESCUE_FAIL = {
8
+ Net::SSH::Disconnect => :debug,
9
+ Net::SSH::ConnectionTimeout => :debug,
10
+ Net::SSH::AuthenticationFailed => :warn,
11
+ Net::SSH::HostKeyUnknown => :warn
12
+ }.freeze
13
+
14
+ def self.rescue_fail
15
+ super.merge(RESCUE_FAIL)
16
+ end
17
+
18
+ def connect(node) # rubocop:disable Naming/PredicateMethod
19
+ @node = node
20
+ @node.model.cfg[config_name].each { |cb| instance_exec(&cb) }
21
+ logger.debug "Connecting to #{@node.name}"
22
+ @ssh = Net::SSH.start(@node.ip, @node.auth[:username], make_ssh_opts)
23
+ connected?
24
+ end
25
+
26
+ def connected?
27
+ @ssh && (not @ssh.closed?)
28
+ end
29
+
30
+ def make_ssh_opts
31
+ ssh_opts = {
32
+ number_of_password_prompts: 0,
33
+ keepalive: vars(:ssh_no_keepalive) ? false : true,
34
+ verify_host_key: must_secure? ? :always : :never,
35
+ append_all_supported_algorithms: true,
36
+ password: @node.auth[:password],
37
+ timeout: @node.timeout,
38
+ port: (vars(:ssh_port) || 22).to_i,
39
+ forward_agent: false
40
+ }
41
+
42
+ auth_methods = vars(:auth_methods) || %w[none publickey password]
43
+ ssh_opts[:auth_methods] = auth_methods
44
+ logger.debug "AUTH METHODS::#{auth_methods}"
45
+
46
+ if vars(:ssh_proxy)
47
+ ssh_opts[:proxy] = make_ssh_proxy_command(
48
+ vars(:ssh_proxy), vars(:ssh_proxy_port), must_secure?
49
+ )
50
+ end
51
+ ssh_opts[:keys] = [vars(:ssh_keys)].flatten if vars(:ssh_keys)
52
+ ssh_opts[:kex] = vars(:ssh_kex).split(/,\s*/) if vars(:ssh_kex)
53
+ ssh_opts[:encryption] = vars(:ssh_encryption).split(/,\s*/) if vars(:ssh_encryption)
54
+ ssh_opts[:host_key] = vars(:ssh_host_key).split(/,\s*/) if vars(:ssh_host_key)
55
+ ssh_opts[:hmac] = vars(:ssh_hmac).split(/,\s*/) if vars(:ssh_hmac)
56
+
57
+ # Use our logger for Net::SSH
58
+ ssh_logger = SemanticLogger[Net::SSH]
59
+ config_debug = Oxidized.config.input.debug
60
+ if config_debug == true ||
61
+ (config_debug.is_a?(String) && config_debug.downcase.include?('library'))
62
+ ssh_logger.level = :debug
63
+ else
64
+ ssh_logger.level = :fatal
65
+ end
66
+ ssh_opts[:logger] = ssh_logger
67
+
68
+ ssh_opts
69
+ end
70
+
71
+ def must_secure?
72
+ Oxidized.config.input[config_name].secure? == true
73
+ end
74
+
75
+ def make_ssh_proxy_command(proxy_host, proxy_port, secure)
76
+ return nil unless !proxy_host.nil? && !proxy_host.empty?
77
+
78
+ proxy_command = "ssh "
79
+ proxy_command += "-o StrictHostKeyChecking=no " unless secure
80
+ proxy_command += "-p #{proxy_port} " if proxy_port
81
+ proxy_command += "#{proxy_host} -W [%h]:%p"
82
+ Net::SSH::Proxy::Command.new(proxy_command)
83
+ end
84
+
85
+ def disconnect
86
+ disconnect_cli
87
+ Timeout.timeout(@node.timeout) do
88
+ @ssh.close
89
+ end
90
+ rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError => e
91
+ logger.debug 'The other side closed the connection while ' \
92
+ "disconnecting, raising #{e.class} with #{e.message}"
93
+ rescue Timeout::Error
94
+ logger.debug "#{@node.name} timed out while disconnecting"
95
+ end
96
+
97
+ # Methods to implement in subclasses
98
+ def cmd(**_args)
99
+ raise NotImplementedError, "Subclasses must implement cmd"
100
+ end
101
+ end
102
+ end
@@ -1,16 +1,17 @@
1
+ require_relative 'debugtext'
2
+
1
3
  module Oxidized
2
4
  require 'net/telnet'
3
- require 'oxidized/input/cli'
4
5
  class Telnet < Input
5
- RescueFail = {}.freeze
6
- include Input::CLI
7
6
  attr_reader :telnet
8
7
 
9
- def connect(node)
8
+ def connect(node) # rubocop:disable Naming/PredicateMethod
10
9
  @node = node
11
- @timeout = Oxidized.config.timeout
10
+ @timeout = @node.timeout
12
11
  @node.model.cfg['telnet'].each { |cb| instance_exec(&cb) }
13
- @log = File.open(Oxidized::Config::Log + "/#{@node.ip}-telnet", 'w') if Oxidized.config.input.debug?
12
+
13
+ @text_debug = DebugText.new(Oxidized.config.input.debug, @node, config_name)
14
+
14
15
  port = vars(:telnet_port) || 23
15
16
 
16
17
  telnet_opts = {
@@ -18,7 +19,7 @@ module Oxidized
18
19
  'Port' => port.to_i,
19
20
  'Timeout' => @timeout,
20
21
  'Model' => @node.model,
21
- 'Log' => @log
22
+ 'Log' => @text_debug
22
23
  }
23
24
 
24
25
  @telnet = Net::Telnet.new telnet_opts
@@ -35,18 +36,20 @@ module Oxidized
35
36
  end
36
37
 
37
38
  def cmd(cmd_str, expect = @node.prompt)
38
- Oxidized.logger.debug "Telnet: #{cmd_str} @#{@node.name}"
39
+ logger.debug "Telnet: #{cmd_str} @#{@node.name}"
39
40
  return send(cmd_str + "\r\n") unless expect
40
41
 
41
42
  # create a string to be passed to oxidized_expect and modified _there_
42
43
  # default to a single space so it shouldn't be coerced to nil by any models.
43
44
  out = String(' ')
45
+ @text_debug&.send_data(cmd_str)
44
46
  @telnet.puts(cmd_str)
45
47
  @telnet.oxidized_expect(timeout: @timeout, expect: expect, out: out)
46
48
  out
47
49
  end
48
50
 
49
51
  def send(data)
52
+ @text_debug&.send_data(data)
50
53
  @telnet.write data
51
54
  end
52
55
 
@@ -63,10 +66,11 @@ module Oxidized
63
66
  def disconnect
64
67
  disconnect_cli
65
68
  @telnet.close
66
- rescue Errno::ECONNRESET
69
+ rescue Errno::ECONNRESET, IOError
70
+ # This exception is intented and therefore not handled here
67
71
  ensure
68
- @log.close if Oxidized.config.input.debug?
69
- (@telnet.close rescue true) unless @telnet.sock.closed?
72
+ @text_debug&.close
73
+ (@telnet.close rescue true) unless @telnet.sock.closed? # rubocop:disable Style/RedundantParentheses
70
74
  end
71
75
  end
72
76
  end
@@ -77,12 +81,12 @@ module Net
77
81
  ## FIXME: we also need output (not sure I'm going to support this)
78
82
  attr_reader :output
79
83
 
80
- def oxidized_expect(options) ## rubocop:disable Metrics/PerceivedComplexity
81
- model = @options["Model"]
82
- @log = @options["Log"]
84
+ def oxidized_expect(options)
85
+ model = @options["Model"]
86
+ text_debug = @options["Log"]
83
87
 
84
88
  expects = [options[:expect]].flatten
85
- time_out = options[:timeout] || @options["Timeout"] || Oxidized.config.timeout?
89
+ time_out = options[:timeout] || @options["Timeout"]
86
90
 
87
91
  Timeout.timeout(time_out) do
88
92
  line = ""
@@ -105,10 +109,7 @@ module Net
105
109
  buf = preprocess(c)
106
110
  rest = ''
107
111
  end
108
- if Oxidized.config.input.debug?
109
- @log.print buf
110
- @log.flush
111
- end
112
+ text_debug&.receive_data buf
112
113
  line += buf
113
114
  line = model.expects line
114
115
  # match is a regexp object. we need to return that for logins to work.
@@ -1,7 +1,5 @@
1
1
  module Oxidized
2
2
  require 'stringio'
3
- require_relative 'cli'
4
-
5
3
  begin
6
4
  require 'net/tftp'
7
5
  rescue LoadError
@@ -9,19 +7,24 @@ module Oxidized
9
7
  end
10
8
 
11
9
  class TFTP < Input
12
- include Input::CLI
10
+ RESCUE_FAIL = {
11
+ Net::TFTPTimeout => :warn
12
+ }.freeze
13
+
14
+ def self.rescue_fail
15
+ super.merge(RESCUE_FAIL)
16
+ end
13
17
 
14
18
  # TFTP utilizes UDP, there is not a connection. We simply specify an IP and send/receive data.
15
19
  def connect(node)
16
20
  @node = node
17
21
 
18
22
  @node.model.cfg['tftp'].each { |cb| instance_exec(&cb) }
19
- @log = File.open(Oxidized::Config::Log + "/#{@node.ip}-tftp", 'w') if Oxidized.config.input.debug?
20
23
  @tftp = Net::TFTP.new @node.ip
21
24
  end
22
25
 
23
26
  def cmd(file)
24
- Oxidized.logger.debug "TFTP: #{file} @ #{@node.name}"
27
+ logger.debug "TFTP: #{file} @ #{@node.name}"
25
28
  config = StringIO.new
26
29
  @tftp.getbinary file, config
27
30
  config.rewind
@@ -32,9 +35,6 @@ module Oxidized
32
35
 
33
36
  def disconnect
34
37
  # TFTP uses UDP, there is no connection to close
35
- true
36
- ensure
37
- @log.close if Oxidized.config.input.debug?
38
38
  end
39
39
  end
40
40
  end
data/lib/oxidized/job.rb CHANGED
@@ -1,16 +1,27 @@
1
1
  module Oxidized
2
2
  class Job < Thread
3
+ include SemanticLogger::Loggable
4
+
3
5
  attr_reader :start, :end, :status, :time, :node, :config
4
6
 
5
7
  def initialize(node)
6
- @node = node
7
- @start = Time.now.utc
8
+ @node = node
9
+ @start = Time.now.utc
10
+ self.name = "Job '#{@node.name}'"
8
11
  super do
9
- Oxidized.logger.debug "lib/oxidized/job.rb: Starting fetching process for #{@node.name} at #{Time.now.utc}"
10
- @status, @config = @node.run
11
- @end = Time.now.utc
12
- @time = @end - @start
13
- Oxidized.logger.debug "lib/oxidized/job.rb: Config fetched for #{@node.name} at #{@end}"
12
+ logger.debug "Starting fetching process for #{@node.name}"
13
+ begin
14
+ Timeout.timeout(Oxidized.config.timelimit) do
15
+ @status, @config = @node.run
16
+ end
17
+ logger.debug "Config fetched for #{@node.name}"
18
+ rescue Timeout::Error
19
+ logger.warn "Job timelimit reached for #{@node.name}"
20
+ @status = :timelimit
21
+ ensure
22
+ @end = Time.now.utc
23
+ @time = @end - @start
24
+ end
14
25
  end
15
26
  end
16
27
  end