oxidized 0.29.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. checksums.yaml +4 -4
  2. data/.coderabbit.yaml +21 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +45 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  5. data/.github/ISSUE_TEMPLATE/support-request.md +39 -0
  6. data/.github/workflows/codeql.yml +4 -4
  7. data/.github/workflows/publishdocker.yml +108 -13
  8. data/.github/workflows/ruby.yml +3 -12
  9. data/.github/workflows/stale.yml +6 -1
  10. data/.gitignore +2 -0
  11. data/.rubocop.yml +69 -13
  12. data/.rubocop_todo.yml +13 -143
  13. data/CHANGELOG.md +431 -4
  14. data/CONTRIBUTING.md +189 -0
  15. data/Dockerfile +89 -34
  16. data/README.md +59 -188
  17. data/Rakefile +58 -12
  18. data/bin/oxidized +2 -5
  19. data/docs/Configuration.md +382 -149
  20. data/docs/Creating-Models.md +262 -10
  21. data/docs/DeviceSimulation.md +196 -0
  22. data/docs/Docker.md +246 -0
  23. data/docs/Hooks.md +146 -51
  24. data/docs/Inputs.md +237 -0
  25. data/docs/Issues.md +123 -0
  26. data/docs/Model-Notes/APC.md +72 -0
  27. data/docs/Model-Notes/Cumulus.md +5 -0
  28. data/docs/Model-Notes/EatonNetwork.md +18 -0
  29. data/docs/Model-Notes/ExaLink.md +43 -0
  30. data/docs/Model-Notes/FSOS.md +17 -0
  31. data/docs/Model-Notes/Fortinet.md +75 -0
  32. data/docs/Model-Notes/GrandstreamHT8xx.md +8 -0
  33. data/docs/Model-Notes/HPEAruba.md +32 -0
  34. data/docs/Model-Notes/IvantiConnectSecure.md +59 -0
  35. data/docs/Model-Notes/OS6.md +10 -0
  36. data/docs/Model-Notes/README.md +1 -20
  37. data/docs/Model-Notes/RouterOS.md +28 -0
  38. data/docs/Model-Notes/SikluMHTG.md +7 -0
  39. data/docs/Model-Notes/TrueNAS.md +23 -0
  40. data/docs/Model-Notes/XGS4600-Zyxel.md +5 -0
  41. data/docs/ModelUnitTests.md +219 -0
  42. data/docs/Outputs.md +102 -5
  43. data/docs/Release.md +102 -0
  44. data/docs/Ruby-API.md +135 -2
  45. data/docs/Sources.md +34 -0
  46. data/docs/Supported-OS-Types.md +207 -270
  47. data/docs/Troubleshooting.md +43 -1
  48. data/extra/device2yaml.rb +259 -0
  49. data/extra/gitdiff-msteams.sh +32 -5
  50. data/extra/hooks/modelrules.rb +55 -0
  51. data/extra/hooks/modelrulesadvanced.rb +167 -0
  52. data/extra/hooks/srcipmap.rb +54 -0
  53. data/extra/nagios_check_failing_nodes.rb +1 -1
  54. data/extra/oxidized.runit +1 -1
  55. data/extra/rest_client.rb +5 -4
  56. data/extra/syslog.rb +11 -6
  57. data/lib/oxidized/cli/support.rb +152 -0
  58. data/lib/oxidized/cli.rb +21 -6
  59. data/lib/oxidized/config/vars.rb +33 -14
  60. data/lib/oxidized/config.rb +28 -17
  61. data/lib/oxidized/core.rb +50 -8
  62. data/lib/oxidized/hook/ciscosparkdiff.rb +11 -9
  63. data/lib/oxidized/hook/exec.rb +5 -4
  64. data/lib/oxidized/hook/githubrepo.rb +55 -18
  65. data/lib/oxidized/hook/noophook.rb +2 -2
  66. data/lib/oxidized/hook/slackdiff.rb +50 -14
  67. data/lib/oxidized/hook/xmppdiff.rb +9 -9
  68. data/lib/oxidized/hook.rb +68 -16
  69. data/lib/oxidized/input/cli.rb +8 -3
  70. data/lib/oxidized/input/debugtext.rb +40 -0
  71. data/lib/oxidized/input/debugyaml.rb +82 -0
  72. data/lib/oxidized/input/exec.rb +2 -11
  73. data/lib/oxidized/input/ftp.rb +2 -19
  74. data/lib/oxidized/input/http.rb +67 -17
  75. data/lib/oxidized/input/input.rb +35 -14
  76. data/lib/oxidized/input/scp.rb +27 -0
  77. data/lib/oxidized/input/ssh.rb +59 -83
  78. data/lib/oxidized/input/sshbase.rb +102 -0
  79. data/lib/oxidized/input/telnet.rb +20 -19
  80. data/lib/oxidized/input/tftp.rb +8 -8
  81. data/lib/oxidized/job.rb +18 -7
  82. data/lib/oxidized/jobs.rb +11 -1
  83. data/lib/oxidized/logger.rb +51 -0
  84. data/lib/oxidized/manager.rb +18 -17
  85. data/lib/oxidized/model/acos.rb +2 -1
  86. data/lib/oxidized/model/addpack.rb +26 -0
  87. data/lib/oxidized/model/adtran.rb +5 -1
  88. data/lib/oxidized/model/adva.rb +3 -3
  89. data/lib/oxidized/model/airfiber.rb +1 -1
  90. data/lib/oxidized/model/aos7.rb +11 -0
  91. data/lib/oxidized/model/aoscx.rb +37 -29
  92. data/lib/oxidized/model/aosw.rb +39 -29
  93. data/lib/oxidized/model/apc_aos.rb +7 -2
  94. data/lib/oxidized/model/apcaos.rb +39 -0
  95. data/lib/oxidized/model/aricentiss.rb +9 -2
  96. data/lib/oxidized/model/arubainstant.rb +93 -0
  97. data/lib/oxidized/model/asa.rb +11 -10
  98. data/lib/oxidized/model/asternos.rb +22 -0
  99. data/lib/oxidized/model/asyncos.rb +3 -3
  100. data/lib/oxidized/model/audiocodes.rb +2 -2
  101. data/lib/oxidized/model/awplus.rb +15 -12
  102. data/lib/oxidized/model/axos.rb +1 -1
  103. data/lib/oxidized/model/bdcom.rb +1 -0
  104. data/lib/oxidized/model/c4cmts.rb +1 -2
  105. data/lib/oxidized/model/ciscosma.rb +1 -1
  106. data/lib/oxidized/model/ciscosmb.rb +6 -1
  107. data/lib/oxidized/model/cnos.rb +13 -10
  108. data/lib/oxidized/model/comtrol.rb +1 -1
  109. data/lib/oxidized/model/comware.rb +5 -3
  110. data/lib/oxidized/model/cumulus.rb +14 -2
  111. data/lib/oxidized/model/defacto.rb +26 -0
  112. data/lib/oxidized/model/dellx.rb +1 -1
  113. data/lib/oxidized/model/dlink.rb +5 -2
  114. data/lib/oxidized/model/dlinknextgen.rb +55 -0
  115. data/lib/oxidized/model/dnos.rb +3 -0
  116. data/lib/oxidized/model/dslcommands.rb +93 -0
  117. data/lib/oxidized/model/dslsetup.rb +102 -0
  118. data/lib/oxidized/model/eatonnetwork.rb +70 -0
  119. data/lib/oxidized/model/edgecos.rb +3 -1
  120. data/lib/oxidized/model/edgeos.rb +7 -6
  121. data/lib/oxidized/model/edgeswitch.rb +3 -1
  122. data/lib/oxidized/model/efos.rb +41 -0
  123. data/lib/oxidized/model/eltex.rb +4 -2
  124. data/lib/oxidized/model/enterasys800.rb +2 -2
  125. data/lib/oxidized/model/enterprise_sonic.rb +46 -0
  126. data/lib/oxidized/model/eos.rb +3 -1
  127. data/lib/oxidized/model/exalink.rb +36 -0
  128. data/lib/oxidized/model/f5os.rb +17 -0
  129. data/lib/oxidized/model/fabricos.rb +2 -2
  130. data/lib/oxidized/model/fastiron.rb +5 -3
  131. data/lib/oxidized/model/firebrick.rb +2 -2
  132. data/lib/oxidized/model/firelinuxos.rb +13 -6
  133. data/lib/oxidized/model/firewareos.rb +10 -1
  134. data/lib/oxidized/model/fortigate.rb +160 -0
  135. data/lib/oxidized/model/fortios.rb +38 -48
  136. data/lib/oxidized/model/fsos.rb +46 -0
  137. data/lib/oxidized/model/gaiaos.rb +4 -4
  138. data/lib/oxidized/model/garderos.rb +38 -0
  139. data/lib/oxidized/model/grandstreamht8xx.rb +19 -0
  140. data/lib/oxidized/model/h3c.rb +1 -1
  141. data/lib/oxidized/model/ibos.rb +1 -0
  142. data/lib/oxidized/model/ingate.rb +47 -0
  143. data/lib/oxidized/model/ios.rb +57 -30
  144. data/lib/oxidized/model/iosxr.rb +2 -2
  145. data/lib/oxidized/model/ironware.rb +7 -5
  146. data/lib/oxidized/model/ivanti.rb +54 -0
  147. data/lib/oxidized/model/junos.rb +12 -5
  148. data/lib/oxidized/model/kornfeldos.rb +33 -0
  149. data/lib/oxidized/model/lenovonos.rb +2 -0
  150. data/lib/oxidized/model/linksyssrw.rb +3 -3
  151. data/lib/oxidized/model/linuxgeneric.rb +4 -2
  152. data/lib/oxidized/model/macros.rb +60 -0
  153. data/lib/oxidized/model/mimosab11.rb +34 -0
  154. data/lib/oxidized/model/ml66.rb +33 -0
  155. data/lib/oxidized/model/mlnxos.rb +18 -14
  156. data/lib/oxidized/model/model.rb +98 -103
  157. data/lib/oxidized/model/mtrlrfs.rb +1 -0
  158. data/lib/oxidized/model/ndms.rb +6 -0
  159. data/lib/oxidized/model/netgear.rb +14 -3
  160. data/lib/oxidized/model/netonix.rb +1 -1
  161. data/lib/oxidized/model/netscaler.rb +1 -1
  162. data/lib/oxidized/model/nodegrid.rb +1 -1
  163. data/lib/oxidized/model/nsxdfw.rb +31 -0
  164. data/lib/oxidized/model/nsxfirewall.rb +2 -1
  165. data/lib/oxidized/model/nxos.rb +20 -15
  166. data/lib/oxidized/model/ocnos.rb +42 -0
  167. data/lib/oxidized/model/onefinity.rb +18 -0
  168. data/lib/oxidized/model/openbsd.rb +1 -1
  169. data/lib/oxidized/model/opengear.rb +36 -1
  170. data/lib/oxidized/model/openwrt.rb +6 -6
  171. data/lib/oxidized/model/opnsense.rb +9 -1
  172. data/lib/oxidized/model/os6.rb +48 -0
  173. data/lib/oxidized/model/outputs.rb +5 -0
  174. data/lib/oxidized/model/panos.rb +2 -0
  175. data/lib/oxidized/model/perle.rb +41 -0
  176. data/lib/oxidized/model/pfsense.rb +9 -0
  177. data/lib/oxidized/model/powerconnect.rb +32 -11
  178. data/lib/oxidized/model/procurve.rb +6 -2
  179. data/lib/oxidized/model/qtech.rb +3 -1
  180. data/lib/oxidized/model/quantaos.rb +8 -6
  181. data/lib/oxidized/model/rgos.rb +33 -0
  182. data/lib/oxidized/model/riverbed.rb +104 -0
  183. data/lib/oxidized/model/routeros.rb +18 -11
  184. data/lib/oxidized/model/saos.rb +18 -1
  185. data/lib/oxidized/model/saos10.rb +38 -0
  186. data/lib/oxidized/model/siklu.rb +1 -1
  187. data/lib/oxidized/model/siklumhtg.rb +22 -0
  188. data/lib/oxidized/model/sixwind.rb +28 -0
  189. data/lib/oxidized/model/slxos.rb +3 -3
  190. data/lib/oxidized/model/smartbyte.rb +48 -0
  191. data/lib/oxidized/model/sonicos.rb +27 -20
  192. data/lib/oxidized/model/speedtouch.rb +1 -1
  193. data/lib/oxidized/model/sros.rb +3 -3
  194. data/lib/oxidized/model/srosmd.rb +1 -1
  195. data/lib/oxidized/model/supermicro.rb +1 -1
  196. data/lib/oxidized/model/telco.rb +1 -1
  197. data/lib/oxidized/model/tmos.rb +1 -0
  198. data/lib/oxidized/model/tnsr.rb +57 -0
  199. data/lib/oxidized/model/tplink.rb +5 -5
  200. data/lib/oxidized/model/trango.rb +4 -2
  201. data/lib/oxidized/model/truenas.rb +64 -3
  202. data/lib/oxidized/model/ucs.rb +1 -1
  203. data/lib/oxidized/model/unifiap.rb +144 -0
  204. data/lib/oxidized/model/uplinkolt.rb +46 -0
  205. data/lib/oxidized/model/voltaire.rb +1 -1
  206. data/lib/oxidized/model/voss.rb +3 -0
  207. data/lib/oxidized/model/vrp.rb +5 -1
  208. data/lib/oxidized/model/vyatta.rb +9 -3
  209. data/lib/oxidized/model/vyos.rb +37 -0
  210. data/lib/oxidized/model/xos.rb +10 -1
  211. data/lib/oxidized/model/zhoneolt.rb +4 -2
  212. data/lib/oxidized/model/zynos.rb +67 -3
  213. data/lib/oxidized/model/zynosadsl.rb +14 -0
  214. data/lib/oxidized/model/zynoscli.rb +1 -1
  215. data/lib/oxidized/model/zynosgs.rb +2 -0
  216. data/lib/oxidized/model/zynosmgs.rb +32 -0
  217. data/lib/oxidized/node.rb +99 -64
  218. data/lib/oxidized/nodes.rb +40 -19
  219. data/lib/oxidized/output/file.rb +80 -43
  220. data/lib/oxidized/output/git.rb +295 -151
  221. data/lib/oxidized/output/gitcrypt.rb +193 -185
  222. data/lib/oxidized/output/http.rb +63 -51
  223. data/lib/oxidized/output/output.rb +20 -4
  224. data/lib/oxidized/signals.rb +44 -0
  225. data/lib/oxidized/source/csv.rb +49 -49
  226. data/lib/oxidized/source/http.rb +76 -63
  227. data/lib/oxidized/source/jsonfile.rb +68 -0
  228. data/lib/oxidized/source/source.rb +73 -18
  229. data/lib/oxidized/source/sql.rb +71 -59
  230. data/lib/oxidized/version.rb +6 -4
  231. data/lib/oxidized/worker.rb +57 -29
  232. data/lib/oxidized.rb +4 -24
  233. data/lib/refinements.rb +21 -1
  234. data/oxidized.gemspec +45 -21
  235. metadata +248 -41
  236. data/lib/oxidized/model/timos.rb +0 -10
@@ -1,20 +1,30 @@
1
+ require 'rugged'
2
+
1
3
  class GithubRepo < Oxidized::Hook
2
4
  def validate_cfg!
3
5
  raise KeyError, 'hook.remote_repo is required' unless cfg.has_key?('remote_repo')
4
6
  end
5
7
 
6
8
  def run_hook(ctx)
9
+ unless ctx.node
10
+ logger.error 'GithubRepo.run_hook: no node provided'
11
+ return
12
+ end
13
+
14
+ unless ctx.node.repo
15
+ logger.error "Oxidized output is not git, can't push to remote"
16
+ return
17
+ end
7
18
  repo = Rugged::Repository.new(ctx.node.repo)
8
19
  creds = credentials(ctx.node)
9
20
  url = remote_repo(ctx.node)
10
21
 
11
22
  if url.nil? || url.empty?
12
- log "No repository defined for #{ctx.node.group}/#{ctx.node.name}", :debug
23
+ logger.error "No repository defined for #{ctx.node.group}/#{ctx.node.name}"
13
24
  return
14
25
  end
15
26
 
16
- log "Pushing local repository(#{repo.path})..."
17
- log "to remote: #{url}"
27
+ logger.info "Pushing local repository(#{repo.path}) to remote: #{url}"
18
28
 
19
29
  if repo.remotes['origin'].nil?
20
30
  repo.remotes.create('origin', url)
@@ -23,28 +33,46 @@ class GithubRepo < Oxidized::Hook
23
33
  end
24
34
  remote = repo.remotes['origin']
25
35
 
26
- fetch_and_merge_remote(repo, creds)
27
-
28
- remote.push([repo.head.name], credentials: creds)
36
+ begin
37
+ fetch_and_merge_remote(repo, creds)
38
+ remote.push([repo.head.name], credentials: creds)
39
+ rescue Rugged::NetworkError => e
40
+ if e.message == 'unsupported URL protocol'
41
+ logger.warn "Rugged does not support the git URL '#{url}'."
42
+ unless Rugged.features.include?(:ssh)
43
+ logger.warn "Note: Rugged isn't installed with ssh support. You may need " \
44
+ '"gem install rugged -- --with-ssh"'
45
+ end
46
+ end
47
+ # re-raise exception for the calling method
48
+ raise
49
+ end
29
50
  end
30
51
 
31
52
  def fetch_and_merge_remote(repo, creds)
32
53
  result = repo.fetch('origin', [repo.head.name], credentials: creds)
33
- log result.inspect, :debug
54
+ logger.debug result.inspect
55
+
56
+ their_branch = remote_branch(repo)
34
57
 
35
- unless result[:total_deltas].positive?
36
- log "nothing received after fetch", :debug
58
+ unless their_branch
59
+ logger.debug 'remote branch does not exist yet, nothing to merge'
37
60
  return
38
61
  end
39
62
 
40
- their_branch = repo.branches["origin/master"]
63
+ result = repo.merge_analysis(their_branch.target_id)
41
64
 
42
- log "merging fetched branch #{their_branch.name}", :debug
65
+ if result.include? :up_to_date
66
+ logger.debug 'nothing to merge'
67
+ return
68
+ end
69
+
70
+ logger.debug "merging fetched branch #{their_branch.name}"
43
71
 
44
72
  merge_index = repo.merge_commits(repo.head.target_id, their_branch.target_id)
45
73
 
46
74
  if merge_index.conflicts?
47
- log("Conflicts detected, skipping Rugged::Commit.create", :warn)
75
+ logger.warn "Conflicts detected, skipping Rugged::Commit.create"
48
76
  return
49
77
  end
50
78
 
@@ -58,21 +86,24 @@ class GithubRepo < Oxidized::Hook
58
86
  private
59
87
 
60
88
  def credentials(node)
61
- Proc.new do |_url, username_from_url, _allowed_types| # rubocop:disable Style/Proc
89
+ proc do |_url, username_from_url, _allowed_types|
62
90
  git_user = cfg.has_key?('username') ? cfg.username : (username_from_url || 'git')
63
91
  if cfg.has_key?('password')
64
- log "Authenticating using username and password as '#{git_user}'", :debug
92
+ logger.debug "Authenticating using username and password as '#{git_user}'"
65
93
  Rugged::Credentials::UserPassword.new(username: git_user, password: cfg.password)
66
94
  elsif cfg.has_key?('privatekey')
67
95
  pubkey = cfg.has_key?('publickey') ? cfg.publickey : nil
68
- log "Authenticating using ssh keys as '#{git_user}'", :debug
96
+ logger.debug "Authenticating using ssh keys as '#{git_user}'"
69
97
  rugged_sshkey(git_user: git_user, privkey: cfg.privatekey, pubkey: pubkey)
70
- elsif cfg.has_key?('remote_repo') && cfg.remote_repo.has_key?(node.group) && cfg.remote_repo[node.group].has_key?('privatekey')
98
+ elsif cfg.has_key?('remote_repo') &&
99
+ cfg.remote_repo.respond_to?(:has_key?) &&
100
+ cfg.remote_repo.has_key?(node.group) &&
101
+ cfg.remote_repo[node.group].has_key?('privatekey')
71
102
  pubkey = cfg.remote_repo[node.group].has_key?('publickey') ? cfg.remote_repo[node.group].publickey : nil
72
- log "Authenticating using ssh keys as '#{git_user}' for '#{node.group}/#{node.name}'", :debug
103
+ logger.debug "Authenticating using ssh keys as '#{git_user}' for '#{node.group}/#{node.name}'"
73
104
  rugged_sshkey(git_user: git_user, privkey: cfg.remote_repo[node.group].privatekey, pubkey: pubkey)
74
105
  else
75
- log "Authenticating using ssh agent as '#{git_user}'", :debug
106
+ logger.debug "Authenticating using ssh agent as '#{git_user}'"
76
107
  Rugged::Credentials::SshKeyFromAgent.new(username: git_user)
77
108
  end
78
109
  end
@@ -97,4 +128,10 @@ class GithubRepo < Oxidized::Hook
97
128
  cfg.remote_repo[node.group].url
98
129
  end
99
130
  end
131
+
132
+ # Returns a Rugged::Branch to the remote branch or nil if it doen't exist
133
+ def remote_branch(repo)
134
+ head_branch = repo.branches[repo.head.name]
135
+ repo.branches['origin/' + head_branch.name]
136
+ end
100
137
  end
@@ -1,9 +1,9 @@
1
1
  class NoopHook < Oxidized::Hook
2
2
  def validate_cfg!
3
- log "Validate config"
3
+ logger.info "Validate config"
4
4
  end
5
5
 
6
6
  def run_hook(ctx)
7
- log "Run hook with context: #{ctx}"
7
+ logger.info "Run hook with context: #{ctx}"
8
8
  end
9
9
  end
@@ -1,4 +1,6 @@
1
- require 'slack'
1
+ require 'slack_ruby_client'
2
+ require 'uri'
3
+ require 'net/http'
2
4
 
3
5
  # defaults to posting a diff, if messageformat is supplied them a message will be posted too
4
6
  # diff defaults to true
@@ -9,39 +11,73 @@ class SlackDiff < Oxidized::Hook
9
11
  raise KeyError, 'hook.channel is required' unless cfg.has_key?('channel')
10
12
  end
11
13
 
14
+ def slack_upload(client, title, content, channel, proxy)
15
+ logger.info "Posting diff as snippet to #{channel}"
16
+ upload_dest = client.files_getUploadURLExternal(filename: "change",
17
+ length: content.length,
18
+ snippet_type: "diff")
19
+ file_uri = URI.parse(upload_dest[:upload_url])
20
+
21
+ proxy_uri = URI.parse(proxy) if proxy
22
+ proxy_address = proxy_uri ? proxy_uri.host : :ENV
23
+ proxy_port = proxy_uri&.port
24
+ proxy_user = proxy_uri&.user
25
+ proxy_pass = proxy_uri&.password
26
+
27
+ http = Net::HTTP.new(file_uri.host, file_uri.port, proxy_address, proxy_port, proxy_user, proxy_pass)
28
+ http.use_ssl = true
29
+
30
+ request = Net::HTTP::Post.new(file_uri.request_uri, { Host: file_uri.host })
31
+ request.body = content
32
+ response = http.request(request)
33
+
34
+ raise 'Slack file upload failed' unless response.is_a? Net::HTTPSuccess
35
+
36
+ files = [{
37
+ id: upload_dest[:file_id],
38
+ title: title
39
+ }]
40
+ begin
41
+ client.files_completeUploadExternal(channel_id: channel,
42
+ files: files.to_json)
43
+ rescue Slack::Web::Api::Errors::NotInChannel
44
+ logger.info "Not in specified channel, attempting to join"
45
+ client.conversations_join(channel: channel)
46
+ client.files_completeUploadExternal(channel_id: channel,
47
+ files: files.to_json)
48
+ end
49
+ end
50
+
12
51
  def run_hook(ctx)
13
52
  return unless ctx.node
14
53
  return unless ctx.event.to_s == "post_store"
15
54
 
16
- log "Connecting to slack"
55
+ logger.info "Connecting to slack"
17
56
  Slack::Web::Client.configure do |config|
18
57
  config.token = cfg.token
19
58
  config.proxy = cfg.proxy if cfg.has_key?('proxy')
20
59
  end
21
60
  client = Slack::Web::Client.new
22
61
  client.auth_test
23
- log "Connected"
62
+ logger.info "Connected"
24
63
  if cfg.has_key?("diff") ? cfg.diff : true
25
64
  gitoutput = ctx.node.output.new
26
65
  diff = gitoutput.get_diff ctx.node, ctx.node.group, ctx.commitref, nil
27
66
  unless diff == "no diffs"
28
67
  title = "#{ctx.node.name} #{ctx.node.group} #{ctx.node.model.class.name.to_s.downcase}"
29
- log "Posting diff as snippet to #{cfg.channel}"
30
- client.files_upload(channels: cfg.channel, as_user: true,
31
- content: diff[:patch].lines.to_a[4..-1].join,
32
- filetype: "diff",
33
- title: title,
34
- filename: "change")
68
+ content = diff[:patch].lines.to_a[4..-1].join
69
+ slack_upload(client, title, content, cfg.channel, cfg.has_key?('proxy') ? cfg.proxy : nil)
35
70
  end
36
71
  end
37
72
  # message custom formatted - optional
38
73
  if cfg.message?
39
- log cfg.message
40
- msg = cfg.message % { node: ctx.node.name.to_s, group: ctx.node.group.to_s, commitref: ctx.commitref, model: ctx.node.model.class.name.to_s.downcase }
41
- log msg
42
- log "Posting message to #{cfg.channel}"
74
+ logger.info cfg.message
75
+ msg = cfg.message % { node: ctx.node.name.to_s, group: ctx.node.group.to_s, commitref: ctx.commitref,
76
+ model: ctx.node.model.class.name.to_s.downcase }
77
+ logger.info msg
78
+ logger.info "Posting message to #{cfg.channel}"
43
79
  client.chat_postMessage(channel: cfg.channel, text: msg, as_user: true)
44
80
  end
45
- log "Finished"
81
+ logger.info "Finished"
46
82
  end
47
83
  end
@@ -5,35 +5,35 @@ class XMPPDiff < Oxidized::Hook
5
5
  def connect
6
6
  @client = Jabber::Client.new(Jabber::JID.new(cfg.jid))
7
7
 
8
- log "Connecting to XMPP"
8
+ logger.info "Connecting to XMPP"
9
9
  begin
10
10
  Timeout.timeout(15) do
11
11
  begin
12
12
  @client.connect
13
13
  rescue StandardError => e
14
- log "Failed to connect to XMPP: #{e}"
14
+ logger.info "Failed to connect to XMPP: #{e}"
15
15
  end
16
16
  sleep 1
17
17
 
18
- log "Authenticating to XMPP"
18
+ logger.info "Authenticating to XMPP"
19
19
  @client.auth(cfg.password)
20
20
  sleep 1
21
21
 
22
- log "Connected to XMPP"
22
+ logger.info "Connected to XMPP"
23
23
 
24
24
  @muc = Jabber::MUC::SimpleMUCClient.new(@client)
25
25
  @muc.join(cfg.channel + "/" + cfg.nick)
26
26
 
27
- log "Joined #{cfg.channel}"
27
+ logger.info "Joined #{cfg.channel}"
28
28
  end
29
29
  rescue Timeout::Error
30
- log "timed out"
30
+ logger.info "timed out"
31
31
  @client = nil
32
32
  @muc = nil
33
33
  end
34
34
 
35
35
  @client.on_exception do
36
- log "XMPP connection aborted, reconnecting"
36
+ logger.info "XMPP connection aborted, reconnecting"
37
37
  @client = nil
38
38
  @muc = nil
39
39
  connect
@@ -66,14 +66,14 @@ class XMPPDiff < Oxidized::Hook
66
66
  # Maybe connecting failed, so only proceed if we actually joined the MUC
67
67
  unless @muc.nil?
68
68
  title = "#{ctx.node.name} #{ctx.node.group} #{ctx.node.model.class.name.to_s.downcase}"
69
- log "Posting diff as snippet to #{cfg.channel}"
69
+ logger.info "Posting diff as snippet to #{cfg.channel}"
70
70
 
71
71
  @muc.say(title + "\n\n" + diff[:patch].lines.to_a[4..-1].join)
72
72
  end
73
73
  end
74
74
  end
75
75
  rescue Timeout::Error
76
- log "timed out"
76
+ logger.info "timed out"
77
77
  end
78
78
  end
79
79
  end
data/lib/oxidized/hook.rb CHANGED
@@ -1,9 +1,13 @@
1
1
  module Oxidized
2
2
  class HookManager
3
+ include SemanticLogger::Loggable
4
+
3
5
  class << self
4
6
  def from_config(cfg)
5
7
  mgr = new
6
8
  cfg.hooks.each do |name, h_cfg|
9
+ raise("Please specify an hook type in the configuration") unless h_cfg.type?
10
+
7
11
  h_cfg.events.each do |event|
8
12
  mgr.register event.to_sym, name, h_cfg.type, h_cfg
9
13
  end
@@ -12,18 +16,24 @@ module Oxidized
12
16
  end
13
17
  end
14
18
 
15
- # HookContext is passed to each hook. It can contain anything related to the
16
- # event in question. At least it contains the event name
17
- class HookContext < OpenStruct; end
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
+ )
18
27
 
19
28
  # RegisteredHook is a container for a Hook instance
20
29
  RegisteredHook = Struct.new(:name, :hook)
21
30
 
22
- Events = %i[
31
+ EVENTS = %i[
23
32
  node_success
24
33
  node_fail
25
34
  post_store
26
35
  nodes_done
36
+ source_node_transform
27
37
  ].freeze
28
38
  attr_reader :registered_hooks
29
39
 
@@ -32,9 +42,9 @@ module Oxidized
32
42
  end
33
43
 
34
44
  def register(event, name, hook_type, cfg)
35
- unless Events.include? event
45
+ unless EVENTS.include? event
36
46
  raise ArgumentError,
37
- "unknown event #{event}, available: #{Events.join ','}"
47
+ "unknown event #{event}, available: #{EVENTS.join ','}"
38
48
  end
39
49
 
40
50
  Oxidized.mgr.add_hook(hook_type) || raise("cannot load hook '#{hook_type}', not found")
@@ -47,24 +57,70 @@ module Oxidized
47
57
  hook.cfg = cfg
48
58
 
49
59
  @registered_hooks[event] << RegisteredHook.new(name, hook)
50
- Oxidized.logger.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
60
+ logger.debug "Hook #{name.inspect} registered #{hook.class} for event #{event.inspect}"
51
61
  end
52
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.
53
106
  def handle(event, ctx_params = {})
54
- ctx = HookContext.new ctx_params
55
- ctx.event = event
107
+ ctx = HookContext.new(event: event, **ctx_params)
56
108
 
57
109
  @registered_hooks[event].each do |r_hook|
58
- r_hook.hook.run_hook ctx
110
+ r_hook.hook.run_hook(ctx)
59
111
  rescue StandardError => e
60
- Oxidized.logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
61
- "(#{e.inspect}) for event #{event.inspect}"
112
+ logger.error "Hook #{r_hook.name} (#{r_hook.hook}) failed " \
113
+ "(#{e.inspect}) for event #{event.inspect}"
62
114
  end
115
+
116
+ nil
63
117
  end
64
118
  end
65
119
 
66
120
  # Hook abstract base class
67
121
  class Hook
122
+ include SemanticLogger::Loggable
123
+
68
124
  attr_reader :cfg
69
125
 
70
126
  def cfg=(cfg)
@@ -75,9 +131,5 @@ module Oxidized
75
131
  def run_hook(_ctx)
76
132
  raise NotImplementedError
77
133
  end
78
-
79
- def log(msg, level = :info)
80
- Oxidized.logger.send(level, "#{self.class.name}: #{msg}")
81
- end
82
134
  end
83
135
  end
@@ -20,15 +20,16 @@ module Oxidized
20
20
  end
21
21
 
22
22
  def connect_cli
23
- Oxidized.logger.debug "lib/oxidized/input/cli.rb: Running post_login commands at #{node.name}"
23
+ logger.debug "Running post_login commands at #{node.name}"
24
24
  @post_login.each do |command, block|
25
- Oxidized.logger.debug "lib/oxidized/input/cli.rb: Running post_login command: #{command.inspect}, block: #{block.inspect} at #{node.name}"
25
+ logger.debug "Running post_login command: #{command.inspect}, " \
26
+ "block: #{block.inspect} at #{node.name}"
26
27
  block ? block.call : (cmd command)
27
28
  end
28
29
  end
29
30
 
30
31
  def disconnect_cli
31
- Oxidized.logger.debug "lib/oxidized/input/cli.rb Running pre_logout commands at #{node.name}"
32
+ logger.debug "Running pre_logout commands at #{node.name}"
32
33
  @pre_logout.each { |command, block| block ? block.call : (cmd command, nil) }
33
34
  end
34
35
 
@@ -52,6 +53,10 @@ module Oxidized
52
53
  @password || (@password = regex)
53
54
  end
54
55
 
56
+ def newline(newline_str = "\n")
57
+ @newline || (@newline = newline_str)
58
+ end
59
+
55
60
  def login
56
61
  match_re = [@node.prompt]
57
62
  match_re << @username if @username
@@ -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,17 +1,12 @@
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
 
13
8
  def cmd(cmd_str)
14
- Oxidized.logger.debug "EXEC: #{cmd_str} @ #{@node.name}"
9
+ logger.debug "EXEC: #{cmd_str} @ #{@node.name}"
15
10
  # I'd really like to do popen3 with separate arguments, but that would
16
11
  # require refactoring cmd to take parameters
17
12
  %x(#{cmd_str})
@@ -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
- RescueFail = {
8
- debug: [
9
- # Net::SSH::Disconnect,
10
- ],
11
- warn: [
12
- # RuntimeError,
13
- # Net::SSH::AuthenticationFailed,
14
- ]
15
- }.freeze
16
- include Input::CLI
17
-
18
- def connect(node)
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]
@@ -30,7 +16,7 @@ module Oxidized
30
16
  end
31
17
 
32
18
  def cmd(file)
33
- Oxidized.logger.debug "FTP: #{file} @ #{@node.name}"
19
+ logger.debug "FTP: #{file} @ #{@node.name}"
34
20
  @ftp.getbinaryfile file, nil
35
21
  end
36
22
 
@@ -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