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.
- checksums.yaml +4 -4
- data/.coderabbit.yaml +21 -0
- data/.github/workflows/publishdocker.yml +11 -9
- data/.github/workflows/ruby.yml +1 -3
- data/.rubocop.yml +16 -2
- data/.rubocop_todo.yml +21 -2
- data/CHANGELOG.md +76 -3
- data/README.md +2 -3
- data/Rakefile +1 -1
- data/docs/Configuration.md +40 -2
- data/docs/Creating-Models.md +129 -14
- data/docs/Docker.md +2 -1
- data/docs/Hooks.md +92 -67
- data/docs/Inputs.md +44 -12
- data/docs/Model-Notes/APC.md +72 -0
- data/docs/Model-Notes/ExaLink.md +43 -0
- data/docs/Model-Notes/Fortinet.md +75 -0
- data/docs/Model-Notes/GrandstreamHT8xx.md +8 -0
- data/docs/Model-Notes/IvantiConnectSecure.md +59 -0
- data/docs/Model-Notes/RouterOS.md +13 -0
- data/docs/Model-Notes/TrueNAS.md +23 -0
- data/docs/ModelUnitTests.md +23 -0
- data/docs/Outputs.md +18 -4
- data/docs/Release.md +7 -2
- data/docs/Ruby-API.md +86 -5
- data/docs/Supported-OS-Types.md +21 -9
- data/docs/Troubleshooting.md +1 -1
- data/extra/device2yaml.rb +2 -3
- data/extra/hooks/modelrules.rb +55 -0
- data/extra/hooks/modelrulesadvanced.rb +167 -0
- data/extra/hooks/srcipmap.rb +54 -0
- data/lib/oxidized/cli/support.rb +152 -0
- data/lib/oxidized/cli.rb +9 -0
- data/lib/oxidized/hook/githubrepo.rb +2 -1
- data/lib/oxidized/hook.rb +58 -8
- data/lib/oxidized/input/debugtext.rb +40 -0
- data/lib/oxidized/input/debugyaml.rb +82 -0
- data/lib/oxidized/input/exec.rb +1 -10
- data/lib/oxidized/input/ftp.rb +0 -17
- data/lib/oxidized/input/http.rb +39 -21
- data/lib/oxidized/input/input.rb +33 -13
- data/lib/oxidized/input/scp.rb +10 -64
- data/lib/oxidized/input/ssh.rb +36 -79
- data/lib/oxidized/input/sshbase.rb +102 -0
- data/lib/oxidized/input/telnet.rb +12 -13
- data/lib/oxidized/input/tftp.rb +7 -7
- data/lib/oxidized/model/aoscx.rb +18 -12
- data/lib/oxidized/model/aosw.rb +10 -11
- data/lib/oxidized/model/apc_aos.rb +4 -0
- data/lib/oxidized/model/apcaos.rb +39 -0
- data/lib/oxidized/model/arubainstant.rb +11 -20
- data/lib/oxidized/model/asa.rb +7 -7
- data/lib/oxidized/model/comware.rb +3 -1
- data/lib/oxidized/model/cumulus.rb +3 -3
- data/lib/oxidized/model/defacto.rb +26 -0
- data/lib/oxidized/model/dlinknextgen.rb +1 -0
- data/lib/oxidized/model/dslcommands.rb +93 -0
- data/lib/oxidized/model/dslsetup.rb +102 -0
- data/lib/oxidized/model/efos.rb +5 -5
- data/lib/oxidized/model/exalink.rb +36 -0
- data/lib/oxidized/model/fastiron.rb +2 -2
- data/lib/oxidized/model/firelinuxos.rb +1 -3
- data/lib/oxidized/model/fortigate.rb +160 -0
- data/lib/oxidized/model/fortios.rb +28 -69
- data/lib/oxidized/model/fsos.rb +1 -3
- data/lib/oxidized/model/grandstreamht8xx.rb +19 -0
- data/lib/oxidized/model/h3c.rb +1 -1
- data/lib/oxidized/model/ios.rb +23 -15
- data/lib/oxidized/model/ironware.rb +5 -3
- data/lib/oxidized/model/ivanti.rb +54 -0
- data/lib/oxidized/model/junos.rb +2 -2
- data/lib/oxidized/model/linuxgeneric.rb +4 -2
- data/lib/oxidized/model/macros.rb +60 -0
- data/lib/oxidized/model/mlnxos.rb +11 -7
- data/lib/oxidized/model/model.rb +28 -126
- data/lib/oxidized/model/ndms.rb +6 -0
- data/lib/oxidized/model/netgear.rb +5 -3
- data/lib/oxidized/model/nxos.rb +6 -3
- data/lib/oxidized/model/outputs.rb +5 -0
- data/lib/oxidized/model/perle.rb +14 -8
- data/lib/oxidized/model/routeros.rb +4 -0
- data/lib/oxidized/model/smartbyte.rb +48 -0
- data/lib/oxidized/model/tplink.rb +4 -6
- data/lib/oxidized/model/truenas.rb +63 -3
- data/lib/oxidized/model/voss.rb +3 -0
- data/lib/oxidized/model/vyos.rb +4 -1
- data/lib/oxidized/node.rb +25 -23
- data/lib/oxidized/nodes.rb +2 -0
- data/lib/oxidized/output/file.rb +7 -1
- data/lib/oxidized/output/git.rb +11 -1
- data/lib/oxidized/output/gitcrypt.rb +1 -1
- data/lib/oxidized/output/http.rb +12 -3
- data/lib/oxidized/source/csv.rb +5 -0
- data/lib/oxidized/source/jsonfile.rb +5 -0
- data/lib/oxidized/source/sql.rb +5 -0
- data/lib/oxidized/version.rb +2 -2
- data/lib/oxidized/worker.rb +36 -15
- data/lib/refinements.rb +18 -0
- data/oxidized.gemspec +28 -24
- metadata +103 -55
- data/docs/Model-Notes/APC_AOS.md +0 -65
- 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
|
-
|
|
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
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
data/lib/oxidized/input/exec.rb
CHANGED
|
@@ -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
|
data/lib/oxidized/input/ftp.rb
CHANGED
|
@@ -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
|
data/lib/oxidized/input/http.rb
CHANGED
|
@@ -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
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
85
|
-
|
|
104
|
+
def authorization_header_present?(headers)
|
|
105
|
+
headers.keys.any? { |key| key.to_s.downcase == 'authorization' }
|
|
86
106
|
end
|
|
87
107
|
|
|
88
|
-
def
|
|
89
|
-
@
|
|
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)
|
data/lib/oxidized/input/input.rb
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
warn
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|