oxidized 0.35.0 → 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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.coderabbit.yaml +21 -0
  3. data/.github/workflows/publishdocker.yml +11 -9
  4. data/.github/workflows/ruby.yml +1 -3
  5. data/.rubocop.yml +16 -2
  6. data/.rubocop_todo.yml +21 -2
  7. data/CHANGELOG.md +76 -3
  8. data/README.md +2 -3
  9. data/Rakefile +1 -1
  10. data/docs/Configuration.md +40 -2
  11. data/docs/Creating-Models.md +129 -14
  12. data/docs/Docker.md +2 -1
  13. data/docs/Hooks.md +92 -67
  14. data/docs/Inputs.md +44 -12
  15. data/docs/Model-Notes/APC.md +72 -0
  16. data/docs/Model-Notes/ExaLink.md +43 -0
  17. data/docs/Model-Notes/Fortinet.md +75 -0
  18. data/docs/Model-Notes/GrandstreamHT8xx.md +8 -0
  19. data/docs/Model-Notes/IvantiConnectSecure.md +59 -0
  20. data/docs/Model-Notes/RouterOS.md +13 -0
  21. data/docs/Model-Notes/TrueNAS.md +23 -0
  22. data/docs/ModelUnitTests.md +23 -0
  23. data/docs/Outputs.md +18 -4
  24. data/docs/Release.md +7 -2
  25. data/docs/Ruby-API.md +86 -5
  26. data/docs/Supported-OS-Types.md +21 -9
  27. data/docs/Troubleshooting.md +1 -1
  28. data/extra/device2yaml.rb +2 -3
  29. data/extra/hooks/modelrules.rb +55 -0
  30. data/extra/hooks/modelrulesadvanced.rb +167 -0
  31. data/extra/hooks/srcipmap.rb +54 -0
  32. data/lib/oxidized/cli/support.rb +152 -0
  33. data/lib/oxidized/cli.rb +9 -0
  34. data/lib/oxidized/hook/githubrepo.rb +2 -1
  35. data/lib/oxidized/hook.rb +58 -8
  36. data/lib/oxidized/input/debugtext.rb +40 -0
  37. data/lib/oxidized/input/debugyaml.rb +82 -0
  38. data/lib/oxidized/input/exec.rb +1 -10
  39. data/lib/oxidized/input/ftp.rb +0 -17
  40. data/lib/oxidized/input/http.rb +39 -21
  41. data/lib/oxidized/input/input.rb +33 -13
  42. data/lib/oxidized/input/scp.rb +10 -64
  43. data/lib/oxidized/input/ssh.rb +36 -79
  44. data/lib/oxidized/input/sshbase.rb +102 -0
  45. data/lib/oxidized/input/telnet.rb +12 -13
  46. data/lib/oxidized/input/tftp.rb +7 -7
  47. data/lib/oxidized/model/aoscx.rb +18 -12
  48. data/lib/oxidized/model/aosw.rb +10 -11
  49. data/lib/oxidized/model/apc_aos.rb +4 -0
  50. data/lib/oxidized/model/apcaos.rb +39 -0
  51. data/lib/oxidized/model/arubainstant.rb +11 -20
  52. data/lib/oxidized/model/asa.rb +7 -7
  53. data/lib/oxidized/model/comware.rb +3 -1
  54. data/lib/oxidized/model/cumulus.rb +3 -3
  55. data/lib/oxidized/model/defacto.rb +26 -0
  56. data/lib/oxidized/model/dlinknextgen.rb +1 -0
  57. data/lib/oxidized/model/dslcommands.rb +93 -0
  58. data/lib/oxidized/model/dslsetup.rb +102 -0
  59. data/lib/oxidized/model/efos.rb +5 -5
  60. data/lib/oxidized/model/exalink.rb +36 -0
  61. data/lib/oxidized/model/fastiron.rb +2 -2
  62. data/lib/oxidized/model/firelinuxos.rb +1 -3
  63. data/lib/oxidized/model/fortigate.rb +160 -0
  64. data/lib/oxidized/model/fortios.rb +28 -69
  65. data/lib/oxidized/model/fsos.rb +1 -3
  66. data/lib/oxidized/model/grandstreamht8xx.rb +19 -0
  67. data/lib/oxidized/model/h3c.rb +1 -1
  68. data/lib/oxidized/model/ios.rb +23 -15
  69. data/lib/oxidized/model/ironware.rb +5 -3
  70. data/lib/oxidized/model/ivanti.rb +54 -0
  71. data/lib/oxidized/model/junos.rb +2 -2
  72. data/lib/oxidized/model/linuxgeneric.rb +4 -2
  73. data/lib/oxidized/model/macros.rb +60 -0
  74. data/lib/oxidized/model/mlnxos.rb +11 -7
  75. data/lib/oxidized/model/model.rb +28 -126
  76. data/lib/oxidized/model/ndms.rb +6 -0
  77. data/lib/oxidized/model/netgear.rb +5 -3
  78. data/lib/oxidized/model/nxos.rb +6 -3
  79. data/lib/oxidized/model/outputs.rb +5 -0
  80. data/lib/oxidized/model/perle.rb +14 -8
  81. data/lib/oxidized/model/routeros.rb +4 -0
  82. data/lib/oxidized/model/smartbyte.rb +48 -0
  83. data/lib/oxidized/model/tplink.rb +4 -6
  84. data/lib/oxidized/model/truenas.rb +63 -3
  85. data/lib/oxidized/model/voss.rb +3 -0
  86. data/lib/oxidized/model/vyos.rb +4 -1
  87. data/lib/oxidized/node.rb +25 -23
  88. data/lib/oxidized/nodes.rb +2 -0
  89. data/lib/oxidized/output/file.rb +7 -1
  90. data/lib/oxidized/output/git.rb +11 -1
  91. data/lib/oxidized/output/gitcrypt.rb +1 -1
  92. data/lib/oxidized/output/http.rb +12 -3
  93. data/lib/oxidized/source/csv.rb +5 -0
  94. data/lib/oxidized/source/jsonfile.rb +5 -0
  95. data/lib/oxidized/source/sql.rb +5 -0
  96. data/lib/oxidized/version.rb +2 -2
  97. data/lib/oxidized/worker.rb +36 -15
  98. data/lib/refinements.rb +18 -0
  99. data/oxidized.gemspec +28 -24
  100. metadata +103 -55
  101. data/docs/Model-Notes/APC_AOS.md +0 -65
  102. data/docs/Model-Notes/FortiOS.md +0 -44
@@ -0,0 +1,152 @@
1
+ require 'time'
2
+
3
+ module Oxidized
4
+ class CLI
5
+ module Support
6
+ SENSITIVE_NAME_RE = /(password|passphrase|secret|token|enable|
7
+ (private|api|access)_?key|
8
+ community|credential|auth
9
+ )/ix
10
+ ROOT_GEMS = %w[oxidized oxidized-web].freeze
11
+ EXPLICIT_ENV_KEYS = %w[
12
+ OXIDIZED_HOME
13
+ OXIDIZED_LOGS
14
+ CONFIG_RELOAD_INTERVAL
15
+ UPDATE_CA_CERTIFICATES
16
+ ].freeze
17
+
18
+ private
19
+
20
+ def show_support_details
21
+ print_intro
22
+ print_environment
23
+ print_config_files
24
+ print_rugged_support
25
+ print_installed_gems
26
+ end
27
+
28
+ def print_intro
29
+ os_release = read_os_release
30
+ runit_path = '/etc/service/oxidized/run'
31
+
32
+ puts '> :warning:'
33
+ puts '> The --support option is intended for diagnostic purposes and may include sensitive information.'
34
+ puts '> Remove any sensitive data before sharing this output.'
35
+ puts
36
+ puts '## Oxidized Support Data'
37
+ puts "- Timestamp: #{Time.now.utc.iso8601}"
38
+ puts "- Oxidized version: #{Oxidized::VERSION_FULL}"
39
+ puts "- OS release: #{os_release}" if os_release
40
+ puts "- Container hint (#{runit_path} exists): #{File.exist?(runit_path)}"
41
+ puts "- Ruby engine: #{defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby'}"
42
+ puts "- Ruby version: #{RUBY_VERSION}p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
43
+ puts "- Working directory: #{Dir.pwd}"
44
+ puts "- Gem paths: #{Gem.path.join(', ')}"
45
+ puts
46
+ end
47
+
48
+ def print_environment
49
+ puts '### Environment Variables'
50
+ keys = (ENV.keys.grep(/^OXIDIZED_/) + EXPLICIT_ENV_KEYS).uniq.sort
51
+
52
+ keys.each do |key|
53
+ next unless ENV.has_key?(key)
54
+
55
+ value = ENV.fetch(key)
56
+ puts key.match?(SENSITIVE_NAME_RE) ? "#{key}=[REDACTED]" : "#{key}=#{value}"
57
+ end
58
+
59
+ puts
60
+ end
61
+
62
+ def print_config_files
63
+ puts '### Configuration Files'
64
+ config_paths.each do |path|
65
+ config_path = File.expand_path(path)
66
+ exists = File.exist?(config_path)
67
+ puts "- #{config_path} exists: #{exists ? 'yes' : 'no'}"
68
+ next unless exists
69
+
70
+ print_sanitized_config(config_path)
71
+ end
72
+ puts
73
+ end
74
+
75
+ def print_rugged_support
76
+ puts '### Rugged'
77
+ begin
78
+ require 'rugged'
79
+ puts "- Rugged version: #{Rugged::VERSION}"
80
+
81
+ ssh_supported = Rugged.respond_to?(:features) && Rugged.features.include?(:ssh)
82
+ puts "- Rugged SSH support: #{ssh_supported}"
83
+ rescue LoadError
84
+ puts '- Rugged: not available'
85
+ puts '- Rugged SSH support: false'
86
+ end
87
+ puts
88
+ end
89
+
90
+ def print_installed_gems
91
+ puts '### Relevant Installed Gems'
92
+ relevant_gem_names.each do |name|
93
+ versions = Gem::Specification.find_all_by_name(name).sort_by(&:version).map { |s| s.version.to_s }
94
+ puts "- #{name} (#{versions.join(', ')})" unless versions.empty?
95
+ end
96
+ end
97
+
98
+ def relevant_gem_names
99
+ names = ROOT_GEMS.select { |name| Gem::Specification.any? { |s| s.name == name } }
100
+
101
+ root_specs = names.flat_map { |name| Gem::Specification.find_all_by_name(name) }
102
+ runtime_deps = root_specs.flat_map { |spec| spec.dependencies }
103
+ names.concat(runtime_deps.map(&:name))
104
+ names.sort.uniq
105
+ end
106
+
107
+ def config_paths
108
+ user_default = File.join(Dir.home, '.config', 'oxidized', 'config')
109
+ home_from_env = File.join(File.expand_path(Oxidized::Config::ROOT), 'config')
110
+
111
+ [
112
+ '/etc/oxidized/config',
113
+ user_default,
114
+ home_from_env
115
+ ].uniq
116
+ end
117
+
118
+ def print_sanitized_config(path)
119
+ content = File.read(path)
120
+ puts '```yaml'
121
+ content.each_line(chomp: true) do |line|
122
+ key, separator, = line.partition(':')
123
+
124
+ if separator.empty? || key.empty?
125
+ puts line
126
+ next
127
+ end
128
+
129
+ if key.match?(SENSITIVE_NAME_RE)
130
+ puts "#{key}: [REDACTED]"
131
+ else
132
+ puts line
133
+ end
134
+ end
135
+ puts '```'
136
+ rescue StandardError => e
137
+ puts " <failed to read: #{e.class}: #{e.message}>"
138
+ end
139
+
140
+ def read_os_release
141
+ return nil unless File.exist?('/etc/os-release')
142
+
143
+ line = File.foreach('/etc/os-release').find { |entry| entry.start_with?('PRETTY_NAME=') }
144
+ return nil unless line
145
+
146
+ line.split('=', 2).last.to_s.strip.gsub(/^"|"$/, '')
147
+ rescue StandardError
148
+ nil
149
+ end
150
+ end
151
+ end
152
+ end
data/lib/oxidized/cli.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'semantic_logger'
2
+ require_relative 'cli/support'
2
3
 
3
4
  module Oxidized
4
5
  class CLI
5
6
  include SemanticLogger::Loggable
7
+ include Support
6
8
 
7
9
  require 'slop'
8
10
  require 'oxidized'
@@ -48,6 +50,7 @@ module Oxidized
48
50
  opts = Slop.parse do |opt|
49
51
  opt.on '-d', '--debug', 'turn on debugging'
50
52
  opt.on '--daemonize', 'Daemonize/fork the process'
53
+ opt.on '--support', 'show support diagnostics and exit'
51
54
  opt.string '--home-dir', 'Oxidized home dir', default: nil
52
55
  opt.string '--config-file', 'Oxidized config file', default: nil
53
56
  opt.on '-h', '--help', 'show usage' do
@@ -64,6 +67,12 @@ module Oxidized
64
67
  Kernel.exit
65
68
  end
66
69
  end
70
+
71
+ if opts[:support]
72
+ show_support_details
73
+ Kernel.exit
74
+ end
75
+
67
76
  [opts.arguments, opts]
68
77
  end
69
78
 
@@ -86,7 +86,7 @@ class GithubRepo < Oxidized::Hook
86
86
  private
87
87
 
88
88
  def credentials(node)
89
- Proc.new do |_url, username_from_url, _allowed_types| # rubocop:disable Style/Proc
89
+ proc do |_url, username_from_url, _allowed_types|
90
90
  git_user = cfg.has_key?('username') ? cfg.username : (username_from_url || 'git')
91
91
  if cfg.has_key?('password')
92
92
  logger.debug "Authenticating using username and password as '#{git_user}'"
@@ -96,6 +96,7 @@ class GithubRepo < Oxidized::Hook
96
96
  logger.debug "Authenticating using ssh keys as '#{git_user}'"
97
97
  rugged_sshkey(git_user: git_user, privkey: cfg.privatekey, pubkey: pubkey)
98
98
  elsif cfg.has_key?('remote_repo') &&
99
+ cfg.remote_repo.respond_to?(:has_key?) &&
99
100
  cfg.remote_repo.has_key?(node.group) &&
100
101
  cfg.remote_repo[node.group].has_key?('privatekey')
101
102
  pubkey = cfg.remote_repo[node.group].has_key?('publickey') ? cfg.remote_repo[node.group].publickey : nil
data/lib/oxidized/hook.rb CHANGED
@@ -6,6 +6,8 @@ module Oxidized
6
6
  def from_config(cfg)
7
7
  mgr = new
8
8
  cfg.hooks.each do |name, h_cfg|
9
+ raise("Please specify an hook type in the configuration") unless h_cfg.type?
10
+
9
11
  h_cfg.events.each do |event|
10
12
  mgr.register event.to_sym, name, h_cfg.type, h_cfg
11
13
  end
@@ -14,11 +16,14 @@ module Oxidized
14
16
  end
15
17
  end
16
18
 
17
- # HookContext is passed to each hook. It can contain anything related to the
18
- # event in question. At least it contains the event name
19
- # The argument keyword_init: true is needed for ruby < 3.2 and can be
20
- # dropped with the support of ruby 3.1
21
- HookContext = Struct.new(:event, :node, :job, :commitref, keyword_init: true)
19
+ # HookContext is passed to each hook. It always carries the event name.
20
+ # The keyword_init: true argument forces keyword-argument initialization.
21
+ HookContext = Struct.new(
22
+ :event, :node, :job, :commitref,
23
+ :node_raw, # raw source record: JSON hash, SQL row hash, CSV field array
24
+ :context, # self from call site, to access methods/bindings of call site
25
+ keyword_init: true
26
+ )
22
27
 
23
28
  # RegisteredHook is a container for a Hook instance
24
29
  RegisteredHook = Struct.new(:name, :hook)
@@ -28,6 +33,7 @@ module Oxidized
28
33
  node_fail
29
34
  post_store
30
35
  nodes_done
36
+ source_node_transform
31
37
  ].freeze
32
38
  attr_reader :registered_hooks
33
39
 
@@ -54,16 +60,60 @@ module Oxidized
54
60
  logger.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
55
61
  end
56
62
 
63
+ # --- Transform events ---
64
+
65
+ # Runs source_node_transform hooks in sequence, passing the return value of
66
+ # each hook as node to the next. Returns the final node, or nil
67
+ # to signal that the node should be excluded.
68
+ def source_node_transform(node:, node_raw:, context:)
69
+ ctx = HookContext.new(
70
+ event: :source_node_transform,
71
+ node: node,
72
+ node_raw: node_raw,
73
+ context: context
74
+ )
75
+ @registered_hooks[:source_node_transform].each do |r_hook|
76
+ ctx.node = r_hook.hook.run_hook(ctx)
77
+ rescue StandardError => e
78
+ logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
79
+ "(#{e.inspect}) for event :source_node_transform"
80
+ end
81
+ ctx.node
82
+ end
83
+
84
+ # --- Fire-and-forget events ---
85
+
86
+ def node_success(node:, job: nil)
87
+ handle(:node_success, node: node, job: job)
88
+ end
89
+
90
+ def node_fail(node:, job: nil)
91
+ handle(:node_fail, node: node, job: job)
92
+ end
93
+
94
+ def post_store(node:, job: nil, commitref: nil)
95
+ handle(:post_store, node: node, job: job, commitref: commitref)
96
+ end
97
+
98
+ def nodes_done
99
+ handle(:nodes_done)
100
+ end
101
+
102
+ private
103
+
104
+ # Shared implementation for fire-and-forget events: runs all registered
105
+ # hooks for the event, ignores return values, logs errors.
57
106
  def handle(event, ctx_params = {})
58
- ctx = HookContext.new ctx_params
59
- ctx.event = event
107
+ ctx = HookContext.new(event: event, **ctx_params)
60
108
 
61
109
  @registered_hooks[event].each do |r_hook|
62
- r_hook.hook.run_hook ctx
110
+ r_hook.hook.run_hook(ctx)
63
111
  rescue StandardError => e
64
112
  logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
65
113
  "(#{e.inspect}) for event #{event.inspect}"
66
114
  end
115
+
116
+ nil
67
117
  end
68
118
  end
69
119
 
@@ -0,0 +1,40 @@
1
+ module Oxidized
2
+ class DebugText
3
+ include SemanticLogger::Loggable
4
+
5
+ def initialize(config_debug, node, input_name)
6
+ return unless config_debug == true ||
7
+ (config_debug.is_a?(String) && config_debug.downcase.include?('text'))
8
+
9
+ @log = File.open(logfile(node, input_name), 'w')
10
+ end
11
+
12
+ # Separate method to ease unit tests
13
+ def logfile(node, input_name)
14
+ timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
15
+ file = Oxidized::Config::LOG + "/#{node&.ip}-#{input_name}-#{timestamp}.txt"
16
+ logger.debug "Writing I/O Debugging to #{file}"
17
+ file
18
+ end
19
+
20
+ def send_data(data)
21
+ return unless @log
22
+
23
+ @log.puts "sent cmd #{data.dump}"
24
+ @log.flush
25
+ end
26
+
27
+ def receive_data(data)
28
+ return unless @log
29
+
30
+ @log.puts "received #{data.dump}"
31
+ @log.flush
32
+ end
33
+
34
+ def close
35
+ return unless @log
36
+
37
+ @log.close
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,82 @@
1
+ module Oxidized
2
+ class DebugYAML
3
+ include SemanticLogger::Loggable
4
+
5
+ def initialize(config_debug, node, input_name)
6
+ return unless config_debug == true ||
7
+ (config_debug.is_a?(String) && config_debug.downcase.include?('yaml'))
8
+
9
+ @log = File.open(logfile(node, input_name), 'w')
10
+
11
+ @partial_line = false
12
+ @first_line = true
13
+ @commands_started = false
14
+
15
+ @log.puts '---'
16
+ @log.puts 'init_prompt: |-'
17
+ @log.flush
18
+ end
19
+
20
+ # Separate method to ease unit tests
21
+ def logfile(node, input_name)
22
+ timestamp = Time.now.strftime('%Y%m%d-%H%M%S')
23
+ file = Oxidized::Config::LOG + "/#{node&.ip}-#{input_name}-#{timestamp}.yaml"
24
+ logger.debug "Writing YAML Simulation to #{file}"
25
+ file
26
+ end
27
+
28
+ def send_data(data)
29
+ return unless @log
30
+
31
+ @log.puts
32
+ @log.puts 'commands:' unless @commands_started
33
+ @log.puts " - #{data.dump}: |-"
34
+ @first_line = true
35
+ @partial_line = false
36
+ @commands_started = true
37
+ @log.flush
38
+ end
39
+
40
+ def receive_data(data)
41
+ return unless @log
42
+ return if data.empty?
43
+
44
+ lines = data.split("\n", -1)
45
+
46
+ lines.each_with_index do |line, idx|
47
+ is_last = idx == lines.length - 1
48
+ full_line = is_last ? (data[-1] == "\n") : true
49
+ # Escape line and strip surrounding double quotes
50
+ line = line.dump[1..-2]
51
+ if @first_line
52
+ # Make sure the leading space of the first line (if present)
53
+ # is coded with \0x20 or YAML block scalars won't work
54
+ line.sub!(/^ /, '\x20')
55
+ @first_line = false
56
+ end
57
+
58
+ # Make sure trailing white spaces are coded with \0x20
59
+ line.gsub!(/ $/, '\x20')
60
+
61
+ output = @partial_line ? line : (' ' + line)
62
+ @partial_line = false
63
+
64
+ if full_line
65
+ @log.puts output
66
+ else
67
+ @log.write output
68
+ end
69
+ end
70
+
71
+ @partial_line = data[-1] != "\n"
72
+
73
+ @log.flush
74
+ end
75
+
76
+ def close
77
+ return unless @log
78
+
79
+ @log.close
80
+ end
81
+ end
82
+ end
@@ -1,12 +1,7 @@
1
1
  module Oxidized
2
- require "oxidized/input/cli"
3
-
4
2
  class Exec < Input
5
- include Input::CLI
6
-
7
3
  def connect(node)
8
4
  @node = node
9
- @log = File.open(Oxidized::Config::LOG + "/#{@node.ip}-exec", "w") if Oxidized.config.input.debug?
10
5
  @node.model.cfg["exec"].each { |cb| instance_exec(&cb) }
11
6
  end
12
7
 
@@ -19,10 +14,6 @@ module Oxidized
19
14
 
20
15
  private
21
16
 
22
- def disconnect
23
- true
24
- ensure
25
- @log.close if Oxidized.config.input.debug?
26
- end
17
+ def disconnect; end
27
18
  end
28
19
  end
@@ -1,24 +1,10 @@
1
1
  module Oxidized
2
2
  require 'net/ftp'
3
3
  require 'timeout'
4
- require_relative 'cli'
5
-
6
4
  class FTP < Input
7
- RESCUE_FAIL = {
8
- debug: [
9
- # Net::SSH::Disconnect,
10
- ],
11
- warn: [
12
- # RuntimeError,
13
- # Net::SSH::AuthenticationFailed,
14
- ]
15
- }.freeze
16
- include Input::CLI
17
-
18
5
  def connect(node) # rubocop:disable Naming/PredicateMethod
19
6
  @node = node
20
7
  @node.model.cfg['ftp'].each { |cb| instance_exec(&cb) }
21
- @log = File.open(Oxidized::Config::LOG + "/#{@node.ip}-ftp", 'w') if Oxidized.config.input.debug?
22
8
  @ftp = Net::FTP.new(@node.ip)
23
9
  @ftp.passive = Oxidized.config.input.ftp.passive
24
10
  @ftp.login @node.auth[:username], @node.auth[:password]
@@ -47,9 +33,6 @@ module Oxidized
47
33
 
48
34
  def disconnect
49
35
  @ftp.close
50
- # rescue Errno::ECONNRESET, IOError
51
- ensure
52
- @log.close if Oxidized.config.input.debug?
53
36
  end
54
37
  end
55
38
  end
@@ -1,19 +1,15 @@
1
1
  module Oxidized
2
- require "oxidized/input/cli"
3
2
  require "net/http"
4
3
  require "json"
5
4
  require "net/http/digest_auth"
6
5
 
7
6
  class HTTP < Input
8
- include Input::CLI
9
-
10
7
  def connect(node)
11
8
  @node = node
12
9
  @secure = false
13
10
  @username = nil
14
11
  @password = nil
15
12
  @headers = {}
16
- @log = File.open(Oxidized::Config::LOG + "/#{@node.ip}-http", "w") if Oxidized.config.input.debug?
17
13
  @node.model.cfg["http"].each { |cb| instance_exec(&cb) }
18
14
 
19
15
  return true unless @main_page && defined?(login)
@@ -48,50 +44,72 @@ module Oxidized
48
44
  private
49
45
 
50
46
  def get_http(path)
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: {})
51
57
  uri = get_uri(path)
58
+ http_method = method.to_s.upcase
52
59
 
53
- logger.debug "Making request to: #{uri}"
60
+ logger.debug "Making #{http_method} request to: #{uri}"
54
61
 
55
62
  ssl_verify = Oxidized.config.input.http.ssl_verify? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
56
63
 
57
- res = make_request(uri, ssl_verify)
64
+ res = make_request(uri, ssl_verify, extra_headers, method: method, body: body)
58
65
 
59
66
  if res.code == '401' && res['www-authenticate']&.include?('Digest')
60
67
  uri.user = @username
61
68
  uri.password = URI.encode_www_form_component(@password)
62
69
  logger.debug "Server requires Digest authentication"
63
- auth = Net::HTTP::DigestAuth.new.auth_header(uri, res['www-authenticate'], 'GET')
64
70
 
65
- res = make_request(uri, ssl_verify, 'Authorization' => auth)
66
- elsif @username && @password
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)
67
76
  logger.debug "Falling back to Basic authentication"
68
- res = make_request(uri, ssl_verify, 'Authorization' => basic_auth_header)
77
+ res = make_request(uri, ssl_verify, extra_headers.merge('Authorization' => basic_auth_header),
78
+ method: method, body: body)
69
79
  end
70
80
 
71
81
  logger.debug "Response code: #{res.code}"
72
- res.body
82
+ res
73
83
  end
74
84
 
75
- def make_request(uri, ssl_verify, extra_headers = {})
85
+ def make_request(uri, ssl_verify, extra_headers = {}, method: :get, body: nil)
76
86
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", verify_mode: ssl_verify) do |http|
77
- req = Net::HTTP::Get.new(uri)
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)
78
96
  @headers.merge(extra_headers).each { |header, value| req.add_field(header, value) }
79
- logger.debug "Sending request with headers: #{@headers.merge(extra_headers)}"
97
+ req.body = body if body
98
+
99
+ logger.debug "Sending #{method.to_s.upcase} request with headers: #{@headers.merge(extra_headers)}"
80
100
  http.request(req)
81
101
  end
82
102
  end
83
103
 
84
- def basic_auth_header
85
- "Basic " + ["#{@username}:#{@password}"].pack('m').delete("\r\n")
104
+ def authorization_header_present?(headers)
105
+ headers.keys.any? { |key| key.to_s.downcase == 'authorization' }
86
106
  end
87
107
 
88
- def log(str)
89
- @log&.write(str)
108
+ def basic_auth_header
109
+ "Basic " + ["#{@username}:#{@password}"].pack('m').delete("\r\n")
90
110
  end
91
111
 
92
- def disconnect
93
- @log.close if Oxidized.config.input.debug?
94
- end
112
+ def disconnect; end
95
113
 
96
114
  def get_uri(path)
97
115
  path = URI.parse(path)
@@ -1,24 +1,44 @@
1
+ require_relative 'cli'
2
+
1
3
  module Oxidized
2
4
  class PromptUndetect < OxidizedError; end
3
5
 
4
6
  class Input
5
7
  include SemanticLogger::Loggable
6
8
  include Oxidized::Config::Vars
9
+ include Oxidized::Input::CLI
7
10
 
8
11
  RESCUE_FAIL = {
9
- debug: [
10
- Errno::ECONNREFUSED
11
- ],
12
- warn: [
13
- IOError,
14
- PromptUndetect,
15
- Timeout::Error,
16
- Errno::ECONNRESET,
17
- Errno::EHOSTUNREACH,
18
- Errno::ENETUNREACH,
19
- Errno::EPIPE,
20
- Errno::ETIMEDOUT
21
- ]
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
22
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
23
43
  end
24
44
  end