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.
Files changed (87) 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 +13 -2
  6. data/.rubocop_todo.yml +21 -2
  7. data/CHANGELOG.md +50 -3
  8. data/README.md +2 -3
  9. data/docs/Configuration.md +30 -1
  10. data/docs/Creating-Models.md +128 -13
  11. data/docs/Docker.md +2 -1
  12. data/docs/Inputs.md +29 -0
  13. data/docs/Model-Notes/APC.md +72 -0
  14. data/docs/Model-Notes/ExaLink.md +43 -0
  15. data/docs/Model-Notes/Fortinet.md +75 -0
  16. data/docs/Model-Notes/IvantiConnectSecure.md +59 -0
  17. data/docs/Model-Notes/TrueNAS.md +19 -0
  18. data/docs/ModelUnitTests.md +23 -0
  19. data/docs/Outputs.md +18 -4
  20. data/docs/Release.md +1 -1
  21. data/docs/Ruby-API.md +86 -5
  22. data/docs/Supported-OS-Types.md +20 -9
  23. data/docs/Troubleshooting.md +1 -1
  24. data/extra/device2yaml.rb +2 -3
  25. data/extra/hooks/modelrules.rb +55 -0
  26. data/extra/hooks/modelrulesadvanced.rb +168 -0
  27. data/extra/hooks/srcipmap.rb +54 -0
  28. data/lib/oxidized/hook/githubrepo.rb +2 -1
  29. data/lib/oxidized/hook.rb +56 -8
  30. data/lib/oxidized/input/exec.rb +0 -4
  31. data/lib/oxidized/input/ftp.rb +0 -13
  32. data/lib/oxidized/input/http.rb +38 -13
  33. data/lib/oxidized/input/input.rb +33 -13
  34. data/lib/oxidized/input/scp.rb +10 -64
  35. data/lib/oxidized/input/ssh.rb +10 -60
  36. data/lib/oxidized/input/sshbase.rb +107 -0
  37. data/lib/oxidized/input/telnet.rb +0 -4
  38. data/lib/oxidized/input/tftp.rb +7 -3
  39. data/lib/oxidized/model/aoscx.rb +5 -3
  40. data/lib/oxidized/model/aosw.rb +10 -11
  41. data/lib/oxidized/model/apc_aos.rb +4 -0
  42. data/lib/oxidized/model/apcaos.rb +39 -0
  43. data/lib/oxidized/model/arubainstant.rb +11 -20
  44. data/lib/oxidized/model/asa.rb +7 -7
  45. data/lib/oxidized/model/comware.rb +3 -1
  46. data/lib/oxidized/model/defacto.rb +26 -0
  47. data/lib/oxidized/model/dslcommands.rb +93 -0
  48. data/lib/oxidized/model/dslsetup.rb +102 -0
  49. data/lib/oxidized/model/efos.rb +5 -5
  50. data/lib/oxidized/model/exalink.rb +36 -0
  51. data/lib/oxidized/model/fastiron.rb +2 -2
  52. data/lib/oxidized/model/firelinuxos.rb +1 -3
  53. data/lib/oxidized/model/fortigate.rb +160 -0
  54. data/lib/oxidized/model/fortios.rb +28 -69
  55. data/lib/oxidized/model/fsos.rb +1 -3
  56. data/lib/oxidized/model/h3c.rb +1 -1
  57. data/lib/oxidized/model/ios.rb +21 -15
  58. data/lib/oxidized/model/ironware.rb +5 -3
  59. data/lib/oxidized/model/ivanti.rb +54 -0
  60. data/lib/oxidized/model/macros.rb +60 -0
  61. data/lib/oxidized/model/mlnxos.rb +11 -7
  62. data/lib/oxidized/model/model.rb +28 -126
  63. data/lib/oxidized/model/ndms.rb +6 -0
  64. data/lib/oxidized/model/netgear.rb +5 -3
  65. data/lib/oxidized/model/nxos.rb +2 -2
  66. data/lib/oxidized/model/outputs.rb +5 -0
  67. data/lib/oxidized/model/perle.rb +14 -8
  68. data/lib/oxidized/model/smartbyte.rb +48 -0
  69. data/lib/oxidized/model/truenas.rb +10 -1
  70. data/lib/oxidized/model/voss.rb +3 -0
  71. data/lib/oxidized/model/vyos.rb +3 -1
  72. data/lib/oxidized/node.rb +25 -23
  73. data/lib/oxidized/nodes.rb +2 -0
  74. data/lib/oxidized/output/file.rb +7 -1
  75. data/lib/oxidized/output/git.rb +11 -1
  76. data/lib/oxidized/output/gitcrypt.rb +1 -1
  77. data/lib/oxidized/output/http.rb +12 -3
  78. data/lib/oxidized/source/csv.rb +5 -0
  79. data/lib/oxidized/source/jsonfile.rb +5 -0
  80. data/lib/oxidized/source/sql.rb +5 -0
  81. data/lib/oxidized/version.rb +2 -2
  82. data/lib/oxidized/worker.rb +36 -15
  83. data/lib/refinements.rb +18 -0
  84. data/oxidized.gemspec +28 -24
  85. metadata +98 -55
  86. data/docs/Model-Notes/APC_AOS.md +0 -65
  87. 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
- 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
@@ -14,11 +14,14 @@ module Oxidized
14
14
  end
15
15
  end
16
16
 
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)
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 ctx
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
 
@@ -1,9 +1,5 @@
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
5
  @log = File.open(Oxidized::Config::LOG + "/#{@node.ip}-exec", "w") if Oxidized.config.input.debug?
@@ -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) }
@@ -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
- res = make_request(uri, ssl_verify, 'Authorization' => auth)
66
- elsif @username && @password
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.body
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
- req = Net::HTTP::Get.new(uri)
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
- logger.debug "Sending request with headers: #{@headers.merge(extra_headers)}"
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
@@ -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
@@ -1,54 +1,20 @@
1
1
  module Oxidized
2
2
  require 'net/ssh'
3
- require 'net/scp'
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 'cli'
9
+ require_relative 'sshbase'
6
10
 
7
- class SCP < Input
11
+ class SCP < SSHBase
8
12
  RESCUE_FAIL = {
9
- debug: [
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
- # Use our logger for Net::SSH
43
- ssh_logger = SemanticLogger[Net::SSH]
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