oxidized 0.35.0 → 0.36.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 +13 -2
- data/.rubocop_todo.yml +21 -2
- data/CHANGELOG.md +50 -3
- data/README.md +2 -3
- data/docs/Configuration.md +30 -1
- data/docs/Creating-Models.md +128 -13
- data/docs/Docker.md +2 -1
- data/docs/Inputs.md +29 -0
- 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/IvantiConnectSecure.md +59 -0
- data/docs/Model-Notes/TrueNAS.md +19 -0
- data/docs/ModelUnitTests.md +23 -0
- data/docs/Outputs.md +18 -4
- data/docs/Release.md +1 -1
- data/docs/Ruby-API.md +86 -5
- data/docs/Supported-OS-Types.md +20 -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 +168 -0
- data/extra/hooks/srcipmap.rb +54 -0
- data/lib/oxidized/hook/githubrepo.rb +2 -1
- data/lib/oxidized/hook.rb +56 -8
- data/lib/oxidized/input/exec.rb +0 -4
- data/lib/oxidized/input/ftp.rb +0 -13
- data/lib/oxidized/input/http.rb +38 -13
- data/lib/oxidized/input/input.rb +33 -13
- data/lib/oxidized/input/scp.rb +10 -64
- data/lib/oxidized/input/ssh.rb +10 -60
- data/lib/oxidized/input/sshbase.rb +107 -0
- data/lib/oxidized/input/telnet.rb +0 -4
- data/lib/oxidized/input/tftp.rb +7 -3
- data/lib/oxidized/model/aoscx.rb +5 -3
- 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/defacto.rb +26 -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/h3c.rb +1 -1
- data/lib/oxidized/model/ios.rb +21 -15
- data/lib/oxidized/model/ironware.rb +5 -3
- data/lib/oxidized/model/ivanti.rb +54 -0
- 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 +2 -2
- data/lib/oxidized/model/outputs.rb +5 -0
- data/lib/oxidized/model/perle.rb +14 -8
- data/lib/oxidized/model/smartbyte.rb +48 -0
- data/lib/oxidized/model/truenas.rb +10 -1
- data/lib/oxidized/model/voss.rb +3 -0
- data/lib/oxidized/model/vyos.rb +3 -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 +98 -55
- data/docs/Model-Notes/APC_AOS.md +0 -65
- data/docs/Model-Notes/FortiOS.md +0 -44
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
# log: /home/oxidized/.config/oxidized/logs/oxidized.log
|
|
74
|
+
# debug: true # optional – enables detailed logging from the hook
|
|
75
|
+
#
|
|
76
|
+
# source:
|
|
77
|
+
# default: http
|
|
78
|
+
# http:
|
|
79
|
+
# url: http://netbox.test/api/dcim/devices/?status=active&has_primary_ip=true
|
|
80
|
+
# headers:
|
|
81
|
+
# Authorization: Token YOUR_API_TOKEN
|
|
82
|
+
# map:
|
|
83
|
+
# name: name
|
|
84
|
+
# vendor: device_type.manufacturer.name
|
|
85
|
+
# type: device_type.model
|
|
86
|
+
# group: role.slug
|
|
87
|
+
# ip: primary_ip.address
|
|
88
|
+
# secure: false
|
|
89
|
+
# hosts_location: results
|
|
90
|
+
# pagination: true
|
|
91
|
+
# pagination_key_name: next
|
|
92
|
+
#
|
|
93
|
+
# output:
|
|
94
|
+
# default: file
|
|
95
|
+
# file:
|
|
96
|
+
# directory: "/home/oxidized/.config/oxidized/configs"
|
|
97
|
+
#
|
|
98
|
+
# groups:
|
|
99
|
+
# ro_ud: {}
|
|
100
|
+
# ro_bd: {}
|
|
101
|
+
# switch: {}
|
|
102
|
+
#
|
|
103
|
+
# How it works:
|
|
104
|
+
# 1. Oxidized loads node data from the source (NetBox) and applies the `map`.
|
|
105
|
+
# 2. For each node, the `source_node_transform` event is triggered.
|
|
106
|
+
# 3. This hook receives a context object `ctx` containing:
|
|
107
|
+
# - `ctx.node` – the mapped node attributes (hash)
|
|
108
|
+
# - `ctx.node_raw` – the original source record (useful for unmapped fields)
|
|
109
|
+
# 4. The hook iterates through the rules defined in `cfg.rules`.
|
|
110
|
+
# 5. For each rule, it checks that every specified key (except `model` and `description`)
|
|
111
|
+
# matches the corresponding value in `ctx.node`. Comparison is case‑insensitive and
|
|
112
|
+
# strips surrounding spaces.
|
|
113
|
+
# 6. The first matching rule assigns its `model` to `ctx.node[:model]`.
|
|
114
|
+
# 7. If no rule matches, the node's `model` remains unchanged.
|
|
115
|
+
# 8. The modified (or original) node is returned; returning `nil` would exclude the node.
|
|
116
|
+
#
|
|
117
|
+
# Notes:
|
|
118
|
+
# - Rule order is crucial: place more specific rules (e.g., with IP or name) before generic ones.
|
|
119
|
+
# - The `description` field is optional and only appears in debug logs.
|
|
120
|
+
# - Any field present in `ctx.node` after mapping can be used as a match key.
|
|
121
|
+
# - For HTTP sources (NetBox), you can access additional fields via `ctx.node_raw["field"]`.
|
|
122
|
+
# - Debug logging requires `debug: true` in the global config.
|
|
123
|
+
|
|
124
|
+
class Modelrulesadvanced < Oxidized::Hook
|
|
125
|
+
# Validate that the hook configuration contains a 'rules' array.
|
|
126
|
+
def validate_cfg!
|
|
127
|
+
raise KeyError, "hook.rules is required" unless cfg.has_key?("rules")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Main hook method – called for each node during source_node_transform event.
|
|
131
|
+
def run_hook(ctx)
|
|
132
|
+
node = ctx.node
|
|
133
|
+
rules = cfg.rules || []
|
|
134
|
+
|
|
135
|
+
matched_model = nil
|
|
136
|
+
rules.each_with_index do |rule, idx|
|
|
137
|
+
match = true
|
|
138
|
+
rule.each do |key, value|
|
|
139
|
+
next if %w[model description].include?(key)
|
|
140
|
+
|
|
141
|
+
# Retrieve the corresponding value from the node (symbol or string key)
|
|
142
|
+
node_value = node[key.to_sym] || node[key.to_s]
|
|
143
|
+
if node_value.to_s.strip.downcase != value.to_s.strip.downcase
|
|
144
|
+
match = false
|
|
145
|
+
break
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
next unless match
|
|
149
|
+
|
|
150
|
+
matched_model = rule["model"]
|
|
151
|
+
desc = rule["description"] ? " (#{rule['description']})" : ""
|
|
152
|
+
logger.debug "ModelRulesAdvanced: rule #{idx + 1}#{desc} matched -> #{matched_model}"
|
|
153
|
+
break
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if matched_model
|
|
157
|
+
old_model = node[:model] || node["model"]
|
|
158
|
+
node[:model] = matched_model
|
|
159
|
+
logger.debug "ModelRulesAdvanced: changed model from #{old_model.inspect} to #{matched_model.inspect}"
|
|
160
|
+
else
|
|
161
|
+
# rubocop:disable Layout/LineLength
|
|
162
|
+
logger.debug "ModelRulesAdvanced: no rule matched, keeping existing model: #{node[:model] || node['model'].inspect}"
|
|
163
|
+
# rubocop:enable Layout/LineLength
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
node
|
|
167
|
+
end
|
|
168
|
+
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
|
|
@@ -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
|
@@ -14,11 +14,14 @@ module Oxidized
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
# HookContext is passed to each hook. It
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# HookContext is passed to each hook. It always carries the event name.
|
|
18
|
+
# The keyword_init: true argument forces keyword-argument initialization.
|
|
19
|
+
HookContext = Struct.new(
|
|
20
|
+
:event, :node, :job, :commitref,
|
|
21
|
+
:node_raw, # raw source record: JSON hash, SQL row hash, CSV field array
|
|
22
|
+
:context, # self from call site, to access methods/bindings of call site
|
|
23
|
+
keyword_init: true
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
# RegisteredHook is a container for a Hook instance
|
|
24
27
|
RegisteredHook = Struct.new(:name, :hook)
|
|
@@ -28,6 +31,7 @@ module Oxidized
|
|
|
28
31
|
node_fail
|
|
29
32
|
post_store
|
|
30
33
|
nodes_done
|
|
34
|
+
source_node_transform
|
|
31
35
|
].freeze
|
|
32
36
|
attr_reader :registered_hooks
|
|
33
37
|
|
|
@@ -54,16 +58,60 @@ module Oxidized
|
|
|
54
58
|
logger.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
|
|
55
59
|
end
|
|
56
60
|
|
|
61
|
+
# --- Transform events ---
|
|
62
|
+
|
|
63
|
+
# Runs source_node_transform hooks in sequence, passing the return value of
|
|
64
|
+
# each hook as node to the next. Returns the final node, or nil
|
|
65
|
+
# to signal that the node should be excluded.
|
|
66
|
+
def source_node_transform(node:, node_raw:, context:)
|
|
67
|
+
ctx = HookContext.new(
|
|
68
|
+
event: :source_node_transform,
|
|
69
|
+
node: node,
|
|
70
|
+
node_raw: node_raw,
|
|
71
|
+
context: context
|
|
72
|
+
)
|
|
73
|
+
@registered_hooks[:source_node_transform].each do |r_hook|
|
|
74
|
+
ctx.node = r_hook.hook.run_hook(ctx)
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
|
|
77
|
+
"(#{e.inspect}) for event :source_node_transform"
|
|
78
|
+
end
|
|
79
|
+
ctx.node
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- Fire-and-forget events ---
|
|
83
|
+
|
|
84
|
+
def node_success(node:, job: nil)
|
|
85
|
+
handle(:node_success, node: node, job: job)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def node_fail(node:, job: nil)
|
|
89
|
+
handle(:node_fail, node: node, job: job)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def post_store(node:, job: nil, commitref: nil)
|
|
93
|
+
handle(:post_store, node: node, job: job, commitref: commitref)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def nodes_done
|
|
97
|
+
handle(:nodes_done)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Shared implementation for fire-and-forget events: runs all registered
|
|
103
|
+
# hooks for the event, ignores return values, logs errors.
|
|
57
104
|
def handle(event, ctx_params = {})
|
|
58
|
-
ctx = HookContext.new ctx_params
|
|
59
|
-
ctx.event = event
|
|
105
|
+
ctx = HookContext.new(event: event, **ctx_params)
|
|
60
106
|
|
|
61
107
|
@registered_hooks[event].each do |r_hook|
|
|
62
|
-
r_hook.hook.run_hook
|
|
108
|
+
r_hook.hook.run_hook(ctx)
|
|
63
109
|
rescue StandardError => e
|
|
64
110
|
logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
|
|
65
111
|
"(#{e.inspect}) for event #{event.inspect}"
|
|
66
112
|
end
|
|
113
|
+
|
|
114
|
+
nil
|
|
67
115
|
end
|
|
68
116
|
end
|
|
69
117
|
|
data/lib/oxidized/input/exec.rb
CHANGED
data/lib/oxidized/input/ftp.rb
CHANGED
|
@@ -1,20 +1,7 @@
|
|
|
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) }
|
data/lib/oxidized/input/http.rb
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
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
|
|
@@ -48,39 +45,67 @@ module Oxidized
|
|
|
48
45
|
private
|
|
49
46
|
|
|
50
47
|
def get_http(path)
|
|
48
|
+
res = perform_http_request(path, method: :get)
|
|
49
|
+
res.body
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def post_http(path, body = nil, extra_headers = {})
|
|
53
|
+
res = perform_http_request(path, method: :post, body: body, extra_headers: extra_headers)
|
|
54
|
+
res.body
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def perform_http_request(path, method: :get, body: nil, extra_headers: {})
|
|
51
58
|
uri = get_uri(path)
|
|
59
|
+
http_method = method.to_s.upcase
|
|
52
60
|
|
|
53
|
-
logger.debug "Making request to: #{uri}"
|
|
61
|
+
logger.debug "Making #{http_method} request to: #{uri}"
|
|
54
62
|
|
|
55
63
|
ssl_verify = Oxidized.config.input.http.ssl_verify? ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
56
64
|
|
|
57
|
-
res = make_request(uri, ssl_verify)
|
|
65
|
+
res = make_request(uri, ssl_verify, extra_headers, method: method, body: body)
|
|
58
66
|
|
|
59
67
|
if res.code == '401' && res['www-authenticate']&.include?('Digest')
|
|
60
68
|
uri.user = @username
|
|
61
69
|
uri.password = URI.encode_www_form_component(@password)
|
|
62
70
|
logger.debug "Server requires Digest authentication"
|
|
63
|
-
auth = Net::HTTP::DigestAuth.new.auth_header(uri, res['www-authenticate'], 'GET')
|
|
64
71
|
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
auth = Net::HTTP::DigestAuth.new.auth_header(uri, res['www-authenticate'], http_method)
|
|
73
|
+
res = make_request(uri, ssl_verify, extra_headers.merge('Authorization' => auth),
|
|
74
|
+
method: method, body: body)
|
|
75
|
+
|
|
76
|
+
elsif @username && @password && !authorization_header_present?(extra_headers)
|
|
67
77
|
logger.debug "Falling back to Basic authentication"
|
|
68
|
-
res = make_request(uri, ssl_verify, 'Authorization' => basic_auth_header)
|
|
78
|
+
res = make_request(uri, ssl_verify, extra_headers.merge('Authorization' => basic_auth_header),
|
|
79
|
+
method: method, body: body)
|
|
69
80
|
end
|
|
70
81
|
|
|
71
82
|
logger.debug "Response code: #{res.code}"
|
|
72
|
-
res
|
|
83
|
+
res
|
|
73
84
|
end
|
|
74
85
|
|
|
75
|
-
def make_request(uri, ssl_verify, extra_headers = {})
|
|
86
|
+
def make_request(uri, ssl_verify, extra_headers = {}, method: :get, body: nil)
|
|
76
87
|
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https", verify_mode: ssl_verify) do |http|
|
|
77
|
-
|
|
88
|
+
req_class = if method == :get
|
|
89
|
+
Net::HTTP::Get
|
|
90
|
+
elsif method == :post
|
|
91
|
+
Net::HTTP::Post
|
|
92
|
+
else
|
|
93
|
+
raise Oxidized::OxidizedError, "Unsupported HTTP method: #{method.inspect}. " \
|
|
94
|
+
"Only :get and :post are supported"
|
|
95
|
+
end
|
|
96
|
+
req = req_class.new(uri)
|
|
78
97
|
@headers.merge(extra_headers).each { |header, value| req.add_field(header, value) }
|
|
79
|
-
|
|
98
|
+
req.body = body if body
|
|
99
|
+
|
|
100
|
+
logger.debug "Sending #{method.to_s.upcase} request with headers: #{@headers.merge(extra_headers)}"
|
|
80
101
|
http.request(req)
|
|
81
102
|
end
|
|
82
103
|
end
|
|
83
104
|
|
|
105
|
+
def authorization_header_present?(headers)
|
|
106
|
+
headers.keys.any? { |key| key.to_s.downcase == 'authorization' }
|
|
107
|
+
end
|
|
108
|
+
|
|
84
109
|
def basic_auth_header
|
|
85
110
|
"Basic " + ["#{@username}:#{@password}"].pack('m').delete("\r\n")
|
|
86
111
|
end
|
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
|
data/lib/oxidized/input/scp.rb
CHANGED
|
@@ -1,54 +1,20 @@
|
|
|
1
1
|
module Oxidized
|
|
2
2
|
require 'net/ssh'
|
|
3
|
-
|
|
3
|
+
begin
|
|
4
|
+
require 'net/scp'
|
|
5
|
+
rescue LoadError
|
|
6
|
+
raise OxidizedError, 'net/scp not found: sudo gem install net-scp'
|
|
7
|
+
end
|
|
4
8
|
require 'timeout'
|
|
5
|
-
require_relative '
|
|
9
|
+
require_relative 'sshbase'
|
|
6
10
|
|
|
7
|
-
class SCP <
|
|
11
|
+
class SCP < SSHBase
|
|
8
12
|
RESCUE_FAIL = {
|
|
9
|
-
|
|
10
|
-
Net::SSH::Disconnect,
|
|
11
|
-
Net::SSH::ConnectionTimeout
|
|
12
|
-
],
|
|
13
|
-
warn: [
|
|
14
|
-
Net::SCP::Error,
|
|
15
|
-
Net::SSH::HostKeyUnknown,
|
|
16
|
-
Net::SSH::AuthenticationFailed,
|
|
17
|
-
Timeout::Error
|
|
18
|
-
]
|
|
13
|
+
Net::SCP::Error => :warn
|
|
19
14
|
}.freeze
|
|
20
|
-
include Input::CLI
|
|
21
|
-
|
|
22
|
-
def connect(node) # rubocop:disable Naming/PredicateMethod
|
|
23
|
-
@node = node
|
|
24
|
-
@node.model.cfg['scp'].each { |cb| instance_exec(&cb) }
|
|
25
|
-
@log = File.open(Oxidized::Config::LOG + "/#{@node.ip}-scp", 'w') if Oxidized.config.input.debug?
|
|
26
|
-
@ssh = Net::SSH.start(@node.ip, @node.auth[:username], make_ssh_opts)
|
|
27
|
-
connected?
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def make_ssh_opts
|
|
31
|
-
secure = Oxidized.config.input.scp.secure?
|
|
32
|
-
ssh_opts = {
|
|
33
|
-
number_of_password_prompts: 0,
|
|
34
|
-
verify_host_key: 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
15
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
ssh_logger.level = Oxidized.config.input.debug? ? :debug : :fatal
|
|
45
|
-
ssh_opts[:logger] = ssh_logger
|
|
46
|
-
|
|
47
|
-
ssh_opts
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def connected?
|
|
51
|
-
@ssh && (not @ssh.closed?)
|
|
16
|
+
def self.rescue_fail
|
|
17
|
+
super.merge(RESCUE_FAIL)
|
|
52
18
|
end
|
|
53
19
|
|
|
54
20
|
def cmd(file)
|
|
@@ -57,25 +23,5 @@ module Oxidized
|
|
|
57
23
|
@ssh.scp.download!(file)
|
|
58
24
|
end
|
|
59
25
|
end
|
|
60
|
-
|
|
61
|
-
def send(my_proc)
|
|
62
|
-
my_proc.call
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def output
|
|
66
|
-
""
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
private
|
|
70
|
-
|
|
71
|
-
def disconnect
|
|
72
|
-
Timeout.timeout(@node.timeout) do
|
|
73
|
-
@ssh.close
|
|
74
|
-
end
|
|
75
|
-
rescue Timeout::Error
|
|
76
|
-
logger.debug "#{@node.name} timed out while disconnecting"
|
|
77
|
-
ensure
|
|
78
|
-
@log.close if Oxidized.config.input.debug?
|
|
79
|
-
end
|
|
80
26
|
end
|
|
81
27
|
end
|