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,55 +1,92 @@
1
1
  module Oxidized
2
- class OxidizedFile < Output
3
- require 'fileutils'
2
+ module Output
3
+ # ruby's File class must be accessed with ::File to avoid name conflicts
4
+ class File < Output
5
+ require 'fileutils'
4
6
 
5
- attr_reader :commitref
7
+ attr_reader :commitref
6
8
 
7
- def initialize
8
- super
9
- @cfg = Oxidized.config.output.file
10
- end
9
+ def initialize
10
+ super
11
+ @cfg = Oxidized.config.output.file
12
+ end
11
13
 
12
- def setup
13
- return unless @cfg.empty?
14
+ def setup
15
+ return unless @cfg.empty?
14
16
 
15
- Oxidized.asetus.user.output.file.directory = File.join(Config::Root, 'configs')
16
- Oxidized.asetus.save :user
17
- raise NoConfig, 'no output file config, edit ~/.config/oxidized/config'
18
- end
17
+ Oxidized.asetus.user.output.file.directory = ::File.join(Config::ROOT, 'configs')
18
+ Oxidized.asetus.save :user
19
+ raise NoConfig, "no output file config, edit #{Oxidized::Config.configfile}"
20
+ end
19
21
 
20
- def store(node, outputs, opt = {})
21
- file = File.expand_path @cfg.directory
22
- file = File.join File.dirname(file), opt[:group] if opt[:group]
23
- FileUtils.mkdir_p file
24
- file = File.join file, node
25
- File.write(file, outputs.to_cfg)
26
- @commitref = file
27
- end
22
+ # node: node name (String)
23
+ # outputs: Oxidized::Models::Outputs
24
+ # opts: dict of optional parameters:
25
+ # - group: node group
26
+ # - significant_changes:
27
+ # nil / not set / true -> store as usual
28
+ # false -> do not store
29
+ def store(node, outputs, opt = {})
30
+ return false if opt[:significant_changes] == false
28
31
 
29
- def fetch(node, group)
30
- cfg_dir = File.expand_path @cfg.directory
31
- node_name = node.name
32
-
33
- if group # group is explicitly defined by user
34
- cfg_dir = File.join File.dirname(cfg_dir), group
35
- File.read File.join(cfg_dir, node_name)
36
- elsif File.exist? File.join(cfg_dir, node_name) # node configuration file is stored on base directory
37
- File.read File.join(cfg_dir, node_name)
38
- else
39
- path = Dir.glob(File.join(File.dirname(cfg_dir), '**', node_name)).first # fetch node in all groups
40
- File.read path
41
- end
42
- rescue Errno::ENOENT
43
- nil
44
- end
32
+ file = ::File.expand_path @cfg.directory
33
+ file = ::File.join ::File.dirname(file), opt[:group] if opt[:group]
34
+ FileUtils.mkdir_p file
35
+ file = ::File.join file, node
36
+ ::File.write(file, outputs.to_cfg)
37
+ @commitref = file
38
+ end
45
39
 
46
- def version(_node, _group)
47
- # not supported
48
- []
49
- end
40
+ def fetch(node, group)
41
+ cfg_dir = ::File.expand_path @cfg.directory
42
+ node_name = node.name
43
+
44
+ if group # group is explicitly defined by user
45
+ cfg_dir = ::File.join ::File.dirname(cfg_dir), group
46
+ ::File.read ::File.join(cfg_dir, node_name)
47
+ elsif ::File.exist? ::File.join(cfg_dir, node_name) # node configuration file is stored on base directory
48
+ ::File.read ::File.join(cfg_dir, node_name)
49
+ else
50
+ path = Dir.glob(::File.join(::File.dirname(cfg_dir), '**', node_name)).first # fetch node in all groups
51
+ ::File.read path
52
+ end
53
+ rescue Errno::ENOENT
54
+ nil
55
+ end
50
56
 
51
- def get_version(_node, _group, _oid)
52
- 'not supported'
57
+ def version(_node, _group)
58
+ # not supported
59
+ []
60
+ end
61
+
62
+ def get_version(_node, _group, _oid)
63
+ 'not supported'
64
+ end
65
+
66
+ def self.node_path(node_name, group_name = nil)
67
+ cfg_dir = ::File.expand_path Oxidized.config.output.file.directory
68
+
69
+ if group_name
70
+ ::File.join ::File.dirname(cfg_dir), group_name, node_name
71
+ else
72
+ ::File.join cfg_dir, node_name
73
+ end
74
+ end
75
+
76
+ def self.clean_obsolete_nodes(active_nodes)
77
+ cfg_dir = ::File.expand_path Oxidized.config.output.file.directory
78
+ dir_base = ::File.dirname(cfg_dir)
79
+ default_dir = ::File.basename(cfg_dir)
80
+
81
+ keep_files = active_nodes.map { |n| node_path(n.name, n.group) }
82
+ active_groups = active_nodes.map(&:group).compact.uniq
83
+
84
+ [default_dir, *active_groups].each do |group|
85
+ Dir.glob(::File.join(dir_base, group, "*")).each do |file|
86
+ ::File.delete(file) if ::File.file?(file) && !keep_files.include?(file)
87
+ end
88
+ end
89
+ end
53
90
  end
54
91
  end
55
92
  end
@@ -1,193 +1,337 @@
1
1
  module Oxidized
2
- class Git < Output
3
- class GitError < OxidizedError; end
4
- begin
5
- require 'rugged'
6
- rescue LoadError
7
- raise OxidizedError, 'rugged not found: sudo gem install rugged'
8
- end
2
+ module Output
3
+ class Git < Output
4
+ using Refinements
9
5
 
10
- attr_reader :commitref
6
+ class GitError < OxidizedError; end
7
+ begin
8
+ require 'rugged'
9
+ rescue LoadError
10
+ raise OxidizedError, 'rugged not found: sudo gem install rugged'
11
+ end
11
12
 
12
- def initialize
13
- super
14
- @cfg = Oxidized.config.output.git
15
- end
13
+ attr_reader :commitref
16
14
 
17
- def setup
18
- if @cfg.empty?
19
- Oxidized.asetus.user.output.git.user = 'Oxidized'
20
- Oxidized.asetus.user.output.git.email = 'o@example.com'
21
- Oxidized.asetus.user.output.git.repo = File.join(Config::Root, 'oxidized.git')
22
- Oxidized.asetus.save :user
23
- raise NoConfig, 'no output git config, edit ~/.config/oxidized/config'
15
+ def initialize
16
+ super
17
+ @cfg = Oxidized.config.output.git
24
18
  end
25
19
 
26
- if @cfg.repo.respond_to?(:each)
27
- @cfg.repo.each do |group, repo|
28
- @cfg.repo["#{group}="] = File.expand_path repo
20
+ def setup
21
+ if @cfg.empty?
22
+ Oxidized.asetus.user.output.git.user = 'Oxidized'
23
+ Oxidized.asetus.user.output.git.email = 'o@example.com'
24
+ Oxidized.asetus.user.output.git.repo = ::File.join(Config::ROOT, 'oxidized.git')
25
+ Oxidized.asetus.save :user
26
+ raise NoConfig, "no output git config, edit #{Oxidized::Config.configfile}"
27
+ end
28
+
29
+ if @cfg.repo.respond_to?(:each)
30
+ @cfg.repo.each do |group, repo|
31
+ @cfg.repo["#{group}="] = ::File.expand_path repo
32
+ end
33
+ else
34
+ @cfg.repo = ::File.expand_path @cfg.repo
29
35
  end
30
- else
31
- @cfg.repo = File.expand_path @cfg.repo
32
36
  end
33
- end
34
37
 
35
- def store(file, outputs, opt = {})
36
- @msg = opt[:msg]
37
- @user = (opt[:user] || @cfg.user)
38
- @email = (opt[:email] || @cfg.email)
39
- @opt = opt
40
- @commitref = nil
41
- repo = @cfg.repo
42
-
43
- outputs.types.each do |type|
44
- type_cfg = ''
45
- type_repo = File.join(File.dirname(repo), type + '.git')
46
- outputs.type(type).each do |output|
47
- (type_cfg << output; next) unless output.name # rubocop:disable Style/Semicolon
48
- type_file = file + '--' + output.name
49
- if @cfg.type_as_directory?
50
- type_file = type + '/' + type_file
51
- type_repo = repo
38
+ # file: node name (String)
39
+ # outputs: Oxidized::Models::Outputs
40
+ # opts: dict of optional parameters:
41
+ # - msg: commit message
42
+ # - email: committer email
43
+ # - user: committer name
44
+ # - group: node group
45
+ # - significant_changes:
46
+ # nil / not set / true: store as usual
47
+ # false: skip general config, only store configs where type != nil
48
+ def store(file, outputs, opt = {})
49
+ @msg = opt[:msg]
50
+ @user = opt[:user] || @cfg.user
51
+ @email = opt[:email] || @cfg.email
52
+ @opt = opt
53
+ @commitref = nil
54
+ repo = @cfg.repo
55
+
56
+ outputs.types.each do |type|
57
+ type_cfg = ''
58
+ type_repo = ::File.join(::File.dirname(repo), type + '.git')
59
+ outputs.type(type).each do |output|
60
+ (type_cfg << output; next) unless output.name # rubocop:disable Style/Semicolon
61
+ type_file = file + '--' + output.name
62
+ if @cfg.type_as_directory?
63
+ type_file = type + '/' + type_file
64
+ type_repo = repo
65
+ end
66
+ update type_repo, type_file, output
52
67
  end
53
- update type_repo, type_file, output
68
+ update type_repo, file, type_cfg
54
69
  end
55
- update type_repo, file, type_cfg
70
+
71
+ update repo, file, outputs.to_cfg unless opt[:significant_changes] == false
56
72
  end
57
73
 
58
- update repo, file, outputs.to_cfg
59
- end
74
+ # Returns the configuration of group/node_name
75
+ #
76
+ # #fetch is called by Nodes#fetch
77
+ # Nodes#fetch creates a new Output object each time, so it not easy
78
+ # to cache the repo index in memory. But as we keep the repo index up
79
+ # to date on disk in #update_repo, we can read it from disk instead of
80
+ # rebuilding it each time.
81
+ def fetch(node, group)
82
+ repo, path = yield_repo_and_path(node, group)
83
+ repo = Rugged::Repository.new repo
84
+ # Read the index from disk
85
+ index = repo.index
60
86
 
61
- def fetch(node, group)
62
- repo, path = yield_repo_and_path(node, group)
63
- repo = Rugged::Repository.new repo
64
- index = repo.index
65
- index.read_tree repo.head.target.tree unless repo.empty?
66
- repo.read(index.get(path)[:oid]).data
67
- rescue StandardError
68
- 'node not found'
69
- end
87
+ repo.read(index.get(path)[:oid]).data
88
+ rescue StandardError
89
+ 'node not found'
90
+ end
70
91
 
71
- # give a hash of all oid revision for the given node, and the date of the commit
72
- def version(node, group)
73
- repo, path = yield_repo_and_path(node, group)
74
-
75
- repo = Rugged::Repository.new repo
76
- walker = Rugged::Walker.new(repo)
77
- walker.sorting(Rugged::SORT_DATE)
78
- walker.push(repo.head.target.oid)
79
- i = -1
80
- tab = []
81
- walker.each do |commit|
82
- # Diabled rubocop because the suggested .empty? does not work here.
83
- next if commit.diff(paths: [path]).size.zero? # rubocop:disable Style/ZeroLengthPredicate
84
-
85
- hash = {}
86
- hash[:date] = commit.time.to_s
87
- hash[:oid] = commit.oid
88
- hash[:author] = commit.author
89
- hash[:message] = commit.message
90
- tab[i += 1] = hash
92
+ # give a hash of all oid revisions for the given node, and the date of
93
+ # the commit.
94
+ #
95
+ # Called by Nodes#version
96
+ def version(node, group)
97
+ repo_path, node_path = yield_repo_and_path(node, group)
98
+ self.class.hash_list(node_path, repo_path)
99
+ rescue StandardError
100
+ 'node not found'
91
101
  end
92
- walker.reset
93
- tab
94
- rescue StandardError
95
- 'node not found'
96
- end
97
102
 
98
- # give the blob of a specific revision
99
- def get_version(node, group, oid)
100
- repo, path = yield_repo_and_path(node, group)
101
- repo = Rugged::Repository.new repo
102
- repo.blob_at(oid, path).content
103
- rescue StandardError
104
- 'version not found'
105
- end
103
+ # give the blob of a specific revision
104
+ def get_version(node, group, oid)
105
+ repo, path = yield_repo_and_path(node, group)
106
+ repo = Rugged::Repository.new repo
107
+ repo.blob_at(oid, path).content
108
+ rescue StandardError
109
+ 'version not found'
110
+ end
111
+
112
+ # give a hash with the patch of a diff between 2 revision and the stats (added and deleted lines)
113
+ def get_diff(node, group, oid1, oid2)
114
+ diff_commits = nil
115
+ repo, = yield_repo_and_path(node, group)
116
+ repo = Rugged::Repository.new repo
117
+ commit = repo.lookup(oid1)
118
+
119
+ if oid2
120
+ commit_old = repo.lookup(oid2)
121
+ diff = repo.diff(commit_old, commit)
122
+ diff.each do |patch|
123
+ if /#{node.name}\s+/ =~ patch.to_s.lines.first
124
+ diff_commits = { patch: patch.to_s, stat: patch.stat }
125
+ break
126
+ end
127
+ end
128
+ else
129
+ stat = commit.parents[0].diff(commit).stat
130
+ stat = [stat[1], stat[2]]
131
+ patch = commit.parents[0].diff(commit).patch
132
+ diff_commits = { patch: patch, stat: stat }
133
+ end
134
+
135
+ diff_commits
136
+ rescue StandardError
137
+ 'no diffs'
138
+ end
106
139
 
107
- # give a hash with the patch of a diff between 2 revision and the stats (added and deleted lines)
108
- def get_diff(node, group, oid1, oid2)
109
- diff_commits = nil
110
- repo, = yield_repo_and_path(node, group)
111
- repo = Rugged::Repository.new repo
112
- commit = repo.lookup(oid1)
113
-
114
- if oid2
115
- commit_old = repo.lookup(oid2)
116
- diff = repo.diff(commit_old, commit)
117
- diff.each do |patch|
118
- if /#{node.name}\s+/ =~ patch.to_s.lines.first
119
- diff_commits = { patch: patch.to_s, stat: patch.stat }
140
+ # Return the list of oids for node_path in the repository repo_path
141
+ def self.hash_list(node_path, repo_path)
142
+ update_cache(repo_path)
143
+ @gitcache[repo_path][:nodes][node_path] || []
144
+ end
145
+
146
+ # Update @gitcache, a class instance variable, ensuring persistence
147
+ # by saving the cache independently of object instances
148
+ def self.update_cache(repo_path)
149
+ # initialize our cache as a class instance variable
150
+ @gitcache ||= {}
151
+ # When single_repo == false, we have multiple repositories
152
+ unless @gitcache[repo_path]
153
+ @gitcache[repo_path] = {}
154
+ @gitcache[repo_path][:nodes] = {}
155
+ @gitcache[repo_path][:last_commit] = nil
156
+ end
157
+
158
+ repo = Rugged::Repository.new repo_path
159
+
160
+ walker = Rugged::Walker.new(repo)
161
+ walker.sorting(Rugged::SORT_DATE)
162
+ walker.push(repo.head.target.oid)
163
+
164
+ # We store the commits into a temporary cache. It will be prepended
165
+ # to @gitcache to preserve the order of the commits.
166
+ cache = {}
167
+ walker.each do |commit|
168
+ if commit.oid == @gitcache[repo_path][:last_commit]
169
+ # we have reached the last cached commit, so we're done
120
170
  break
121
171
  end
172
+
173
+ commit.diff.each_delta do |delta|
174
+ next unless delta.added? || delta.modified?
175
+
176
+ hash = {}
177
+ # We keep :date for reverse compatibility on oxidized-web <= 0.15.1
178
+ hash[:date] = commit.time.to_s
179
+ # date as a Time instance for more flexibility in oxidized-web
180
+ hash[:time] = commit.time
181
+ hash[:oid] = commit.oid
182
+ hash[:author] = commit.author
183
+ hash[:message] = commit.message
184
+
185
+ filename = delta.new_file[:path]
186
+ if cache[filename]
187
+ cache[filename].append hash
188
+ else
189
+ cache[filename] = [hash]
190
+ end
191
+ end
192
+ end
193
+
194
+ cache.each_pair do |filename, hashlist|
195
+ if @gitcache[repo_path][:nodes][filename]
196
+ # using the splat operator (*) should be OK as hashlist should
197
+ # not be very big when working on deltas
198
+ @gitcache[repo_path][:nodes][filename].prepend(*hashlist)
199
+ else
200
+ @gitcache[repo_path][:nodes][filename] = hashlist
201
+ end
122
202
  end
123
- else
124
- stat = commit.parents[0].diff(commit).stat
125
- stat = [stat[1], stat[2]]
126
- patch = commit.parents[0].diff(commit).patch
127
- diff_commits = { patch: patch, stat: stat }
203
+
204
+ # Store the most recent commit
205
+ @gitcache[repo_path][:last_commit] = repo.head.target.oid
128
206
  end
129
207
 
130
- diff_commits
131
- rescue StandardError
132
- 'no diffs'
133
- end
208
+ # Currently only used in unit tests
209
+ def self.clear_cache
210
+ @gitcache = nil
211
+ end
134
212
 
135
- private
213
+ def self.clean_obsolete_nodes(active_nodes)
214
+ git_config = Oxidized.config.output.git
215
+ repo_path = git_config.repo
136
216
 
137
- def yield_repo_and_path(node, group)
138
- repo, path = node.repo, node.name
217
+ unless git_config.single_repo?
218
+ logger.warn "clean_obsolete_nodes is not implemented for " \
219
+ "multiple git repositories"
220
+ return
221
+ end
139
222
 
140
- path = "#{group}/#{node.name}" if group && @cfg.single_repo?
223
+ if git_config.type_as_directory?
224
+ logger.warn "clean_obsolete_nodes is not implemented for output " \
225
+ "types as a directory within the git repository"
226
+ return
227
+ end
141
228
 
142
- [repo, path]
143
- end
229
+ # The repo might not exist on the first run
230
+ return unless ::File.directory?(repo_path)
144
231
 
145
- def update(repo, file, data)
146
- return if data.empty?
232
+ repo = Rugged::Repository.new repo_path
233
+ return if repo.empty?
147
234
 
148
- if @opt[:group]
149
- if @cfg.single_repo?
150
- file = File.join @opt[:group], file
151
- else
152
- repo = if repo.is_a?(::String)
153
- File.join File.dirname(repo), @opt[:group] + '.git'
154
- else
155
- repo[@opt[:group]]
156
- end
235
+ keep_files = active_nodes.map do |n|
236
+ n.group ? ::File.join(n.group, n.name) : n.name
237
+ end
238
+
239
+ tree = repo.last_commit.tree
240
+ files_to_delete = []
241
+
242
+ tree.walk_blobs do |root, entry|
243
+ file_path = root.empty? ? entry[:name] : ::File.join(root, entry[:name])
244
+ files_to_delete << file_path unless keep_files.include?(file_path)
157
245
  end
246
+
247
+ return if files_to_delete.empty?
248
+
249
+ logger.info "clean_obsolete_nodes: removing " \
250
+ "#{files_to_delete.size} obsolete configs"
251
+ index = repo.index
252
+
253
+ files_to_delete.each { |file_path| index.remove(file_path) }
254
+
255
+ repo.config['user.name'] = git_config.user
256
+ repo.config['user.email'] = git_config.email
257
+ Rugged::Commit.create(
258
+ repo,
259
+ tree: index.write_tree(repo),
260
+ message: "Removing #{files_to_delete.size} obsolete configs",
261
+ parents: [repo.head.target].compact,
262
+ update_ref: 'HEAD'
263
+ )
264
+
265
+ index.write
158
266
  end
159
267
 
160
- begin
161
- repo = Rugged::Repository.new repo
162
- update_repo repo, file, data
163
- rescue Rugged::OSError, Rugged::RepositoryError => e
268
+ private
269
+
270
+ def yield_repo_and_path(node, group)
271
+ repo = node.repo
272
+ path = node.name
273
+
274
+ path = "#{group}/#{node.name}" if group && !group.empty? && @cfg.single_repo?
275
+
276
+ [repo, path]
277
+ end
278
+
279
+ def update(repo, file, data)
280
+ return if data.empty?
281
+
282
+ if @opt[:group]
283
+ if @cfg.single_repo?
284
+ file = ::File.join @opt[:group], file
285
+ else
286
+ repo = if repo.is_a?(::String)
287
+ ::File.join ::File.dirname(repo), @opt[:group] + '.git'
288
+ else
289
+ repo[@opt[:group]]
290
+ end
291
+ end
292
+ end
293
+
164
294
  begin
165
- Rugged::Repository.init_at repo, :bare
166
- rescue StandardError => create_error
167
- raise GitError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' was while trying to create git repo"
295
+ repo = Rugged::Repository.new repo
296
+ update_repo repo, file, data
297
+ rescue Rugged::OSError, Rugged::RepositoryError => e
298
+ begin
299
+ Rugged::Repository.init_at repo, :bare
300
+ rescue StandardError => create_error
301
+ raise GitError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' " \
302
+ "was while trying to create git repo"
303
+ end
304
+ retry
168
305
  end
169
- retry
170
306
  end
171
- end
172
307
 
173
- def update_repo(repo, file, data)
174
- oid_old = repo.blob_at(repo.head.target_id, file) rescue nil
175
- return false if oid_old && (oid_old.content.b == data.b)
308
+ # Uploads data into file in the repository repo
309
+ #
310
+ # update_repo caches the index on disk. An index is usually used in a
311
+ # working directory and not in a bare repository, which confuses users.
312
+ # The alternative would be to rebuild the index each time, which a little
313
+ # time consuming. Caching the index in memory is difficult because a new
314
+ # Output object is created each time #store is called.
315
+ def update_repo(repo, file, data) # rubocop:disable Naming/PredicateMethod
316
+ oid_old = repo.blob_at(repo.head.target_id, file) rescue nil
317
+ return false if oid_old && (oid_old.content.b == data.b)
176
318
 
177
- oid = repo.write data, :blob
178
- index = repo.index
179
- index.add path: file, oid: oid, mode: 0o100644
319
+ oid = repo.write data, :blob
320
+ # Read the index from disk
321
+ index = repo.index
322
+ index.add path: file, oid: oid, mode: 0o100644
180
323
 
181
- repo.config['user.name'] = @user
182
- repo.config['user.email'] = @email
183
- @commitref = Rugged::Commit.create(repo,
184
- tree: index.write_tree(repo),
185
- message: @msg,
186
- parents: repo.empty? ? [] : [repo.head.target].compact,
187
- update_ref: 'HEAD')
324
+ repo.config['user.name'] = @user
325
+ repo.config['user.email'] = @email
326
+ @commitref = Rugged::Commit.create(repo,
327
+ tree: index.write_tree(repo),
328
+ message: @msg,
329
+ parents: repo.empty? ? [] : [repo.head.target].compact,
330
+ update_ref: 'HEAD')
188
331
 
189
- index.write
190
- true
332
+ index.write
333
+ true
334
+ end
191
335
  end
192
336
  end
193
337
  end