oxidized 0.32.1 → 0.34.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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +45 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  4. data/.github/ISSUE_TEMPLATE/support-request.md +39 -0
  5. data/.github/workflows/publishdocker.yml +35 -16
  6. data/.github/workflows/ruby.yml +4 -2
  7. data/.gitignore +2 -0
  8. data/.rubocop.yml +29 -8
  9. data/.rubocop_todo.yml +1 -60
  10. data/CHANGELOG.md +103 -2
  11. data/CONTRIBUTING.md +20 -10
  12. data/Dockerfile +37 -64
  13. data/README.md +47 -141
  14. data/Rakefile +9 -11
  15. data/docs/Configuration.md +236 -27
  16. data/docs/DeviceSimulation.md +19 -7
  17. data/docs/Docker.md +245 -0
  18. data/docs/Issues.md +27 -1
  19. data/docs/Model-Notes/EatonNetwork.md +18 -0
  20. data/docs/Model-Notes/HPEAruba.md +3 -2
  21. data/docs/ModelUnitTests.md +35 -25
  22. data/docs/Outputs.md +83 -2
  23. data/docs/Release.md +34 -24
  24. data/docs/Supported-OS-Types.md +7 -0
  25. data/docs/Troubleshooting.md +4 -13
  26. data/extra/device2yaml.rb +24 -9
  27. data/extra/rest_client.rb +3 -2
  28. data/extra/syslog.rb +8 -3
  29. data/lib/oxidized/cli.rb +7 -3
  30. data/lib/oxidized/config/vars.rb +22 -14
  31. data/lib/oxidized/config.rb +3 -2
  32. data/lib/oxidized/core.rb +30 -8
  33. data/lib/oxidized/hook/ciscosparkdiff.rb +11 -9
  34. data/lib/oxidized/hook/exec.rb +5 -4
  35. data/lib/oxidized/hook/githubrepo.rb +23 -17
  36. data/lib/oxidized/hook/noophook.rb +2 -2
  37. data/lib/oxidized/hook/slackdiff.rb +9 -8
  38. data/lib/oxidized/hook/xmppdiff.rb +9 -9
  39. data/lib/oxidized/hook.rb +10 -8
  40. data/lib/oxidized/input/cli.rb +8 -3
  41. data/lib/oxidized/input/exec.rb +1 -1
  42. data/lib/oxidized/input/ftp.rb +2 -2
  43. data/lib/oxidized/input/http.rb +6 -6
  44. data/lib/oxidized/input/input.rb +1 -0
  45. data/lib/oxidized/input/scp.rb +2 -2
  46. data/lib/oxidized/input/ssh.rb +21 -14
  47. data/lib/oxidized/input/telnet.rb +3 -3
  48. data/lib/oxidized/input/tftp.rb +1 -1
  49. data/lib/oxidized/job.rb +7 -4
  50. data/lib/oxidized/logger.rb +51 -0
  51. data/lib/oxidized/model/acos.rb +1 -0
  52. data/lib/oxidized/model/aos7.rb +9 -0
  53. data/lib/oxidized/model/aoscx.rb +2 -0
  54. data/lib/oxidized/model/aosw.rb +22 -17
  55. data/lib/oxidized/model/aricentiss.rb +2 -2
  56. data/lib/oxidized/model/asa.rb +3 -3
  57. data/lib/oxidized/model/awplus.rb +13 -10
  58. data/lib/oxidized/model/eatonnetwork.rb +65 -0
  59. data/lib/oxidized/model/edgecos.rb +2 -1
  60. data/lib/oxidized/model/edgeos.rb +7 -6
  61. data/lib/oxidized/model/edgeswitch.rb +3 -1
  62. data/lib/oxidized/model/efos.rb +41 -0
  63. data/lib/oxidized/model/eltex.rb +1 -1
  64. data/lib/oxidized/model/fabricos.rb +1 -1
  65. data/lib/oxidized/model/fastiron.rb +3 -1
  66. data/lib/oxidized/model/firelinuxos.rb +12 -3
  67. data/lib/oxidized/model/fortios.rb +5 -4
  68. data/lib/oxidized/model/gaiaos.rb +4 -4
  69. data/lib/oxidized/model/ingate.rb +47 -0
  70. data/lib/oxidized/model/ios.rb +16 -5
  71. data/lib/oxidized/model/ironware.rb +1 -1
  72. data/lib/oxidized/model/junos.rb +4 -0
  73. data/lib/oxidized/model/linksyssrw.rb +3 -3
  74. data/lib/oxidized/model/mlnxos.rb +14 -7
  75. data/lib/oxidized/model/model.rb +4 -3
  76. data/lib/oxidized/model/netgear.rb +8 -0
  77. data/lib/oxidized/model/nsxdfw.rb +2 -1
  78. data/lib/oxidized/model/nsxfirewall.rb +2 -1
  79. data/lib/oxidized/model/nxos.rb +2 -2
  80. data/lib/oxidized/model/openwrt.rb +6 -6
  81. data/lib/oxidized/model/powerconnect.rb +31 -10
  82. data/lib/oxidized/model/procurve.rb +3 -1
  83. data/lib/oxidized/model/qtech.rb +3 -1
  84. data/lib/oxidized/model/quantaos.rb +8 -6
  85. data/lib/oxidized/model/routeros.rb +3 -2
  86. data/lib/oxidized/model/saos10.rb +38 -0
  87. data/lib/oxidized/model/sixwind.rb +28 -0
  88. data/lib/oxidized/model/sonicos.rb +1 -1
  89. data/lib/oxidized/model/srosmd.rb +1 -1
  90. data/lib/oxidized/model/supermicro.rb +1 -1
  91. data/lib/oxidized/model/timos.rb +1 -1
  92. data/lib/oxidized/model/tmos.rb +1 -0
  93. data/lib/oxidized/model/tnsr.rb +53 -0
  94. data/lib/oxidized/model/trango.rb +3 -1
  95. data/lib/oxidized/model/unifiap.rb +144 -0
  96. data/lib/oxidized/model/vrp.rb +3 -1
  97. data/lib/oxidized/model/xos.rb +3 -1
  98. data/lib/oxidized/model/zhoneolt.rb +3 -1
  99. data/lib/oxidized/model/zynos.rb +3 -3
  100. data/lib/oxidized/node.rb +44 -27
  101. data/lib/oxidized/nodes.rb +8 -4
  102. data/lib/oxidized/output/file.rb +28 -0
  103. data/lib/oxidized/output/git.rb +148 -41
  104. data/lib/oxidized/output/gitcrypt.rb +18 -13
  105. data/lib/oxidized/output/http.rb +5 -4
  106. data/lib/oxidized/output/output.rb +14 -0
  107. data/lib/oxidized/source/http.rb +4 -2
  108. data/lib/oxidized/version.rb +6 -4
  109. data/lib/oxidized/worker.rb +13 -13
  110. data/lib/oxidized.rb +3 -24
  111. data/lib/refinements.rb +2 -0
  112. data/oxidized.gemspec +10 -8
  113. metadata +74 -41
  114. data/examples/podman-compose/Makefile +0 -103
  115. data/examples/podman-compose/README.md +0 -94
  116. data/examples/podman-compose/docker-compose.yml +0 -30
  117. data/examples/podman-compose/gitserver/.gitignore +0 -1
  118. data/examples/podman-compose/gitserver/Dockerfile +0 -14
  119. data/examples/podman-compose/model-simulation/Dockerfile-model +0 -13
  120. data/examples/podman-compose/model-simulation/asternos.sh +0 -36
  121. data/examples/podman-compose/oxidized-config/.gitignore +0 -10
  122. data/examples/podman-compose/oxidized-config/config +0 -46
  123. data/examples/podman-compose/oxidized-config/config_csv-file +0 -46
  124. data/examples/podman-compose/oxidized-config/config_csv-gitserver +0 -56
  125. data/examples/podman-compose/oxidized-config/router.db +0 -1
  126. data/examples/podman-compose/oxidized-ssh/.gitignore +0 -1
  127. data/examples/podman-compose/oxidized-ssh/README.md +0 -14
data/lib/oxidized/node.rb CHANGED
@@ -6,15 +6,18 @@ module Oxidized
6
6
  class ModelNotFound < OxidizedError; end
7
7
 
8
8
  class Node
9
+ include SemanticLogger::Loggable
10
+
9
11
  attr_reader :name, :ip, :model, :input, :output, :group, :auth, :prompt, :vars, :last, :repo
10
12
  attr_accessor :running, :user, :email, :msg, :from, :stats, :retry, :err_type, :err_reason
11
13
  alias running? running
12
14
 
15
+ # opt is a hash with the node parameters given in the source (:name, :group, :ip...)
13
16
  def initialize(opt)
14
- Oxidized.logger.debug 'resolving DNS for %s...' % opt[:name]
17
+ logger.debug 'resolving DNS for %s...' % opt[:name]
15
18
  # remove the prefix if an IP Address is provided with one as IPAddr converts it to a network address.
16
19
  ip_addr, = opt[:ip].to_s.split("/")
17
- Oxidized.logger.debug 'IPADDR %s' % ip_addr.to_s
20
+ logger.debug 'IPADDR %s' % ip_addr.to_s
18
21
  @name = opt[:name]
19
22
  @ip = IPAddr.new(ip_addr).to_s rescue nil
20
23
  @ip ||= Resolv.new.getaddress(@name) if Oxidized.config.resolve_dns?
@@ -37,7 +40,8 @@ module Oxidized
37
40
  end
38
41
 
39
42
  def run
40
- status, config = :fail, nil
43
+ status = :fail
44
+ config = nil
41
45
  @input.each do |input|
42
46
  # don't try input if model is missing config block, we may need strong config to class_name map
43
47
  cfg_name = input.to_s.split('::').last.downcase
@@ -45,14 +49,16 @@ module Oxidized
45
49
 
46
50
  @model.input = input = input.new
47
51
  if (config = run_input(input))
48
- Oxidized.logger.debug "lib/oxidized/node.rb: #{input.class.name} ran for #{name} successfully"
52
+ logger.debug "#{input.class.name} ran for #{name} successfully"
49
53
  status = :success
50
54
  break
51
55
  else
52
- Oxidized.logger.debug "lib/oxidized/node.rb: #{input.class.name} failed for #{name}"
56
+ logger.debug "#{input.class.name} failed for #{name}"
53
57
  status = :no_connection
54
58
  end
55
59
  end
60
+ logger.error "No suitable input found for #{name}" unless @model.input
61
+
56
62
  @model.input = nil
57
63
  [status, config]
58
64
  end
@@ -75,13 +81,18 @@ module Oxidized
75
81
  level = rescue_fail[resc]
76
82
  resc = " (rescued #{resc})"
77
83
  end
78
- Oxidized.logger.send(level, '%s raised %s%s with msg "%s"' % [ip, err.class, resc, err.message])
84
+ logger.send(level, '%s raised %s%s with msg "%s"' % [ip, err.class, resc, err.message])
79
85
  @err_type = err.class.to_s
80
86
  @err_reason = err.message.to_s
81
87
  false
82
88
  rescue StandardError => e
83
89
  # Send a message in debug mode in case we are not able to create a crashfile
84
- Oxidized.logger.send(:debug, '%s raised %s with msg "%s", creating crashfile' % [ip, e.class, e.message])
90
+ logger.error "#{ip} raised #{e.class} with msg #{e.message}, creating crashfile"
91
+ unless Oxidized.config.crash.directory?
92
+ logger.error "Cannot create crashfile for exception", e
93
+ return false
94
+ end
95
+
85
96
  crashdir = Oxidized.config.crash.directory
86
97
  crashfile = Oxidized.config.crash.hostnames? ? name : ip.to_s
87
98
  FileUtils.mkdir_p(crashdir) unless File.directory?(crashdir)
@@ -92,7 +103,7 @@ module Oxidized
92
103
  fh.puts '-' * 50
93
104
  fh.puts e.backtrace
94
105
  end
95
- Oxidized.logger.error '%s raised %s with msg "%s", %s saved' % [ip, e.class, e.message, crashfile]
106
+ logger.error '%s raised %s with msg "%s", %s saved' % [ip, e.class, e.message, crashfile]
96
107
  @err_type = e.class.to_s
97
108
  @err_reason = e.message.to_s
98
109
  false
@@ -122,14 +133,10 @@ module Oxidized
122
133
  h
123
134
  end
124
135
 
136
+ JobStruct = Struct.new(:start, :end, :status, :time)
125
137
  def last=(job)
126
138
  if job
127
- ostruct = OpenStruct.new
128
- ostruct.start = job.start
129
- ostruct.end = job.end
130
- ostruct.status = job.status
131
- ostruct.time = job.time
132
- @last = ostruct
139
+ @last = JobStruct.new(job.start, job.end, job.status, job.time)
133
140
  else
134
141
  @last = nil
135
142
  end
@@ -160,8 +167,11 @@ module Oxidized
160
167
 
161
168
  def resolve_input(opt)
162
169
  inputs = resolve_key :input, opt, Oxidized.config.input.default
163
- inputs.split(/\s*,\s*/).map do |input|
164
- Oxidized.mgr.add_input(input) || raise(MethodNotFound, "#{input} not found for node #{ip}") unless Oxidized.mgr.input[input]
170
+ inputs.split(',').map do |input|
171
+ input.strip!
172
+ unless Oxidized.mgr.input[input]
173
+ Oxidized.mgr.add_input(input) || raise(MethodNotFound, "#{input} not found for node #{ip}")
174
+ end
165
175
 
166
176
  Oxidized.mgr.input[input]
167
177
  end
@@ -169,7 +179,10 @@ module Oxidized
169
179
 
170
180
  def resolve_output(opt)
171
181
  output = resolve_key :output, opt, Oxidized.config.output.default
172
- Oxidized.mgr.add_output(output) || raise(MethodNotFound, "#{output} not found for node #{ip}") unless Oxidized.mgr.output[output]
182
+ unless Oxidized.mgr.output[output]
183
+ Oxidized.mgr.add_output(output) || raise(MethodNotFound,
184
+ "#{output} not found for node #{ip}")
185
+ end
173
186
 
174
187
  Oxidized.mgr.output[output]
175
188
  end
@@ -177,7 +190,7 @@ module Oxidized
177
190
  def resolve_model(opt)
178
191
  model = resolve_key :model, opt
179
192
  unless Oxidized.mgr.model[model]
180
- Oxidized.logger.debug "lib/oxidized/node.rb: Loading model #{model.inspect}"
193
+ logger.debug "Loading model #{model.inspect}"
181
194
  Oxidized.mgr.add_model(model) || raise(ModelNotFound, "#{model} not found for node #{ip}")
182
195
  end
183
196
  Oxidized.mgr.model[model].new
@@ -200,42 +213,46 @@ module Oxidized
200
213
  end
201
214
 
202
215
  def resolve_key(key, opt, global = nil)
203
- # resolve key: the priority is as follows: node -> group specific model -> group -> model -> global passed -> global
216
+ # resolve key: the priority is as follows:
217
+ # node -> group specific model -> group -> model -> global passed -> global
204
218
  # where node has the highest priority (= if defined, overwrites other values)
205
219
  key_sym = key.to_sym
206
220
  key_str = key.to_s
207
221
  model_name = @model.class.name.to_s.downcase
208
- Oxidized.logger.debug "node.rb: resolving node key '#{key}', with passed global value of '#{global}' and node value '#{opt[key_sym]}'"
222
+ logger.debug "resolving node key '#{key}', with passed global value of '#{global}' " \
223
+ "and node value '#{opt[key_sym]}'"
209
224
 
210
225
  # Node
211
226
  if opt[key_sym]
212
227
  value = opt[key_sym]
213
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from node"
228
+ logger.debug "setting node key '#{key}' to value '#{value}' from node"
214
229
 
215
230
  # Group specific model
216
- elsif Oxidized.config.groups.has_key?(@group) && Oxidized.config.groups[@group].models.has_key?(model_name) && Oxidized.config.groups[@group].models[model_name].has_key?(key_str)
231
+ elsif Oxidized.config.groups.has_key?(@group) &&
232
+ Oxidized.config.groups[@group].models.has_key?(model_name) &&
233
+ Oxidized.config.groups[@group].models[model_name].has_key?(key_str)
217
234
  value = Oxidized.config.groups[@group].models[model_name][key_str]
218
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from model in group"
235
+ logger.debug "setting node key '#{key}' to value '#{value}' from model in group"
219
236
 
220
237
  # Group
221
238
  elsif Oxidized.config.groups.has_key?(@group) && Oxidized.config.groups[@group].has_key?(key_str)
222
239
  value = Oxidized.config.groups[@group][key_str]
223
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from group"
240
+ logger.debug "setting node key '#{key}' to value '#{value}' from group"
224
241
 
225
242
  # Model
226
243
  elsif Oxidized.config.models.has_key?(model_name) && Oxidized.config.models[model_name].has_key?(key_str)
227
244
  value = Oxidized.config.models[model_name][key_str]
228
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from model"
245
+ logger.debug "setting node key '#{key}' to value '#{value}' from model"
229
246
 
230
247
  # Global passed
231
248
  elsif global
232
249
  value = global
233
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from passed global value"
250
+ logger.debug "setting node key '#{key}' to value '#{value}' from passed global value"
234
251
 
235
252
  # Global
236
253
  elsif Oxidized.config.has_key?(key_str)
237
254
  value = Oxidized.config[key_str]
238
- Oxidized.logger.debug "node.rb: setting node key '#{key}' to value '#{value}' from global"
255
+ logger.debug "setting node key '#{key}' to value '#{value}' from global"
239
256
  end
240
257
  value
241
258
  end
@@ -5,6 +5,8 @@ module Oxidized
5
5
  class NodeNotFound < OxidizedError; end
6
6
 
7
7
  class Nodes < Array
8
+ include SemanticLogger::Loggable
9
+
8
10
  attr_accessor :source, :jobs
9
11
  alias put unshift
10
12
  def load(node_want = nil)
@@ -12,7 +14,7 @@ module Oxidized
12
14
  new = []
13
15
  @source = Oxidized.config.source.default
14
16
  Oxidized.mgr.add_source(@source) || raise(MethodNotFound, "cannot load node source '#{@source}', not found")
15
- Oxidized.logger.info "lib/oxidized/nodes.rb: Loading nodes"
17
+ logger.info "Loading nodes"
16
18
  nodes = Oxidized.mgr.source[@source].new.load node_want
17
19
  nodes.each do |node|
18
20
  # we want to load specific node(s), not all of them
@@ -22,13 +24,14 @@ module Oxidized
22
24
  node_obj = Node.new node
23
25
  new.push node_obj
24
26
  rescue ModelNotFound => e
25
- Oxidized.logger.error "node %s raised %s with message '%s'" % [node, e.class, e.message]
27
+ logger.error "node %s raised %s with message '%s'" % [node, e.class, e.message]
26
28
  rescue Resolv::ResolvError => e
27
- Oxidized.logger.error "node %s is not resolvable, raised %s with message '%s'" % [node, e.class, e.message]
29
+ logger.error "node %s is not resolvable, raised %s with message '%s'" % [node, e.class, e.message]
28
30
  end
29
31
  end
30
32
  size.zero? ? replace(new) : update_nodes(new)
31
- Oxidized.logger.info "lib/oxidized/nodes.rb: Loaded #{size} nodes"
33
+ Output.clean_obsolete_nodes(self) if node_want.nil?
34
+ logger.info "Loaded #{size} nodes"
32
35
  end
33
36
  end
34
37
 
@@ -74,6 +77,7 @@ module Oxidized
74
77
  def next(node, opt = {})
75
78
  return if running.find_index(node)
76
79
 
80
+ logger.info "Add node #{node} to running jobs"
77
81
  with_lock do
78
82
  n = del node
79
83
  n.user = opt['user']
@@ -19,6 +19,9 @@ module Oxidized
19
19
  raise NoConfig, "no output file config, edit #{Oxidized::Config.configfile}"
20
20
  end
21
21
 
22
+ # node: node name (String)
23
+ # outputs: Oxidized::Models::Outputs
24
+ # opts: hash of node vars
22
25
  def store(node, outputs, opt = {})
23
26
  file = ::File.expand_path @cfg.directory
24
27
  file = ::File.join ::File.dirname(file), opt[:group] if opt[:group]
@@ -53,6 +56,31 @@ module Oxidized
53
56
  def get_version(_node, _group, _oid)
54
57
  'not supported'
55
58
  end
59
+
60
+ def self.node_path(node_name, group_name = nil)
61
+ cfg_dir = ::File.expand_path Oxidized.config.output.file.directory
62
+
63
+ if group_name
64
+ ::File.join ::File.dirname(cfg_dir), group_name, node_name
65
+ else
66
+ ::File.join cfg_dir, node_name
67
+ end
68
+ end
69
+
70
+ def self.clean_obsolete_nodes(active_nodes)
71
+ cfg_dir = ::File.expand_path Oxidized.config.output.file.directory
72
+ dir_base = ::File.dirname(cfg_dir)
73
+ default_dir = ::File.basename(cfg_dir)
74
+
75
+ keep_files = active_nodes.map { |n| node_path(n.name, n.group) }
76
+ active_groups = active_nodes.map(&:group).compact.uniq
77
+
78
+ [default_dir, *active_groups].each do |group|
79
+ Dir.glob(::File.join(dir_base, group, "*")).each do |file|
80
+ ::File.delete(file) if ::File.file?(file) && !keep_files.include?(file)
81
+ end
82
+ end
83
+ end
56
84
  end
57
85
  end
58
86
  end
@@ -21,17 +21,17 @@ module Oxidized
21
21
  if @cfg.empty?
22
22
  Oxidized.asetus.user.output.git.user = 'Oxidized'
23
23
  Oxidized.asetus.user.output.git.email = 'o@example.com'
24
- Oxidized.asetus.user.output.git.repo = File.join(Config::ROOT, 'oxidized.git')
24
+ Oxidized.asetus.user.output.git.repo = ::File.join(Config::ROOT, 'oxidized.git')
25
25
  Oxidized.asetus.save :user
26
26
  raise NoConfig, "no output git config, edit #{Oxidized::Config.configfile}"
27
27
  end
28
28
 
29
29
  if @cfg.repo.respond_to?(:each)
30
30
  @cfg.repo.each do |group, repo|
31
- @cfg.repo["#{group}="] = File.expand_path repo
31
+ @cfg.repo["#{group}="] = ::File.expand_path repo
32
32
  end
33
33
  else
34
- @cfg.repo = File.expand_path @cfg.repo
34
+ @cfg.repo = ::File.expand_path @cfg.repo
35
35
  end
36
36
  end
37
37
 
@@ -45,7 +45,7 @@ module Oxidized
45
45
 
46
46
  outputs.types.each do |type|
47
47
  type_cfg = ''
48
- type_repo = File.join(File.dirname(repo), type + '.git')
48
+ type_repo = ::File.join(::File.dirname(repo), type + '.git')
49
49
  outputs.type(type).each do |output|
50
50
  (type_cfg << output; next) unless output.name # rubocop:disable Style/Semicolon
51
51
  type_file = file + '--' + output.name
@@ -64,8 +64,8 @@ module Oxidized
64
64
  # Returns the configuration of group/node_name
65
65
  #
66
66
  # #fetch is called by Nodes#fetch
67
- # Nodes#fetch creates a new Output object each time, so we cannot
68
- # store the repo index in memory. But as we keep the repo index up
67
+ # Nodes#fetch creates a new Output object each time, so it not easy
68
+ # to cache the repo index in memory. But as we keep the repo index up
69
69
  # to date on disk in #update_repo, we can read it from disk instead of
70
70
  # rebuilding it each time.
71
71
  def fetch(node, group)
@@ -79,29 +79,13 @@ module Oxidized
79
79
  'node not found'
80
80
  end
81
81
 
82
- # give a hash of all oid revision for the given node, and the date of the commit
82
+ # give a hash of all oid revisions for the given node, and the date of
83
+ # the commit.
84
+ #
85
+ # Called by Nodes#version
83
86
  def version(node, group)
84
- repo, path = yield_repo_and_path(node, group)
85
-
86
- repo = Rugged::Repository.new repo
87
- walker = Rugged::Walker.new(repo)
88
- walker.sorting(Rugged::SORT_DATE)
89
- walker.push(repo.head.target.oid)
90
- i = -1
91
- tab = []
92
- walker.each do |commit|
93
- # Diabled rubocop because the suggested .empty? does not work here.
94
- next if commit.diff(paths: [path]).size.zero? # rubocop:disable Style/ZeroLengthPredicate
95
-
96
- hash = {}
97
- hash[:date] = commit.time.to_s
98
- hash[:oid] = commit.oid
99
- hash[:author] = commit.author
100
- hash[:message] = commit.message
101
- tab[i += 1] = hash
102
- end
103
- walker.reset
104
- tab
87
+ repo_path, node_path = yield_repo_and_path(node, group)
88
+ self.class.hash_list(node_path, repo_path)
105
89
  rescue StandardError
106
90
  'node not found'
107
91
  end
@@ -143,10 +127,139 @@ module Oxidized
143
127
  'no diffs'
144
128
  end
145
129
 
130
+ # Return the list of oids for node_path in the repository repo_path
131
+ def self.hash_list(node_path, repo_path)
132
+ update_cache(repo_path)
133
+ @gitcache[repo_path][:nodes][node_path] || []
134
+ end
135
+
136
+ # Update @gitcache, a class instance variable, ensuring persistence
137
+ # by saving the cache independently of object instances
138
+ def self.update_cache(repo_path)
139
+ # initialize our cache as a class instance variable
140
+ @gitcache ||= {}
141
+ # When single_repo == false, we have multiple repositories
142
+ unless @gitcache[repo_path]
143
+ @gitcache[repo_path] = {}
144
+ @gitcache[repo_path][:nodes] = {}
145
+ @gitcache[repo_path][:last_commit] = nil
146
+ end
147
+
148
+ repo = Rugged::Repository.new repo_path
149
+
150
+ walker = Rugged::Walker.new(repo)
151
+ walker.sorting(Rugged::SORT_DATE)
152
+ walker.push(repo.head.target.oid)
153
+
154
+ # We store the commits into a temporary cache. It will be prepended
155
+ # to @gitcache to preserve the order of the commits.
156
+ cache = {}
157
+ walker.each do |commit|
158
+ if commit.oid == @gitcache[repo_path][:last_commit]
159
+ # we have reached the last cached commit, so we're done
160
+ break
161
+ end
162
+
163
+ commit.diff.each_delta do |delta|
164
+ next unless delta.added? || delta.modified?
165
+
166
+ hash = {}
167
+ # We keep :date for reverse compatibility on oxidized-web <= 0.15.1
168
+ hash[:date] = commit.time.to_s
169
+ # date as a Time instance for more flexibility in oxidized-web
170
+ hash[:time] = commit.time
171
+ hash[:oid] = commit.oid
172
+ hash[:author] = commit.author
173
+ hash[:message] = commit.message
174
+
175
+ filename = delta.new_file[:path]
176
+ if cache[filename]
177
+ cache[filename].append hash
178
+ else
179
+ cache[filename] = [hash]
180
+ end
181
+ end
182
+ end
183
+
184
+ cache.each_pair do |filename, hashlist|
185
+ if @gitcache[repo_path][:nodes][filename]
186
+ # using the splat operator (*) should be OK as hashlist should
187
+ # not be very big when working on deltas
188
+ @gitcache[repo_path][:nodes][filename].prepend(*hashlist)
189
+ else
190
+ @gitcache[repo_path][:nodes][filename] = hashlist
191
+ end
192
+ end
193
+
194
+ # Store the most recent commit
195
+ @gitcache[repo_path][:last_commit] = repo.head.target.oid
196
+ end
197
+
198
+ # Currently only used in unit tests
199
+ def self.clear_cache
200
+ @gitcache = nil
201
+ end
202
+
203
+ def self.clean_obsolete_nodes(active_nodes)
204
+ git_config = Oxidized.config.output.git
205
+ repo_path = git_config.repo
206
+
207
+ unless git_config.single_repo?
208
+ logger.warn "clean_obsolete_nodes is not implemented for " \
209
+ "multiple git repositories"
210
+ return
211
+ end
212
+
213
+ if git_config.type_as_directory?
214
+ logger.warn "clean_obsolete_nodes is not implemented for output " \
215
+ "types as a directory within the git repository"
216
+ return
217
+ end
218
+
219
+ # The repo might not exist on the first run
220
+ return unless ::File.directory?(repo_path)
221
+
222
+ repo = Rugged::Repository.new repo_path
223
+ return if repo.empty?
224
+
225
+ keep_files = active_nodes.map do |n|
226
+ n.group ? ::File.join(n.group, n.name) : n.name
227
+ end
228
+
229
+ tree = repo.last_commit.tree
230
+ files_to_delete = []
231
+
232
+ tree.walk_blobs do |root, entry|
233
+ file_path = root.empty? ? entry[:name] : ::File.join(root, entry[:name])
234
+ files_to_delete << file_path unless keep_files.include?(file_path)
235
+ end
236
+
237
+ return if files_to_delete.empty?
238
+
239
+ logger.info "clean_obsolete_nodes: removing " \
240
+ "#{files_to_delete.size} obsolete configs"
241
+ index = repo.index
242
+
243
+ files_to_delete.each { |file_path| index.remove(file_path) }
244
+
245
+ repo.config['user.name'] = git_config.user
246
+ repo.config['user.email'] = git_config.email
247
+ Rugged::Commit.create(
248
+ repo,
249
+ tree: index.write_tree(repo),
250
+ message: "Removing #{files_to_delete.size} obsolete configs",
251
+ parents: [repo.head.target].compact,
252
+ update_ref: 'HEAD'
253
+ )
254
+
255
+ index.write
256
+ end
257
+
146
258
  private
147
259
 
148
260
  def yield_repo_and_path(node, group)
149
- repo, path = node.repo, node.name
261
+ repo = node.repo
262
+ path = node.name
150
263
 
151
264
  path = "#{group}/#{node.name}" if group && !group.empty? && @cfg.single_repo?
152
265
 
@@ -158,10 +271,10 @@ module Oxidized
158
271
 
159
272
  if @opt[:group]
160
273
  if @cfg.single_repo?
161
- file = File.join @opt[:group], file
274
+ file = ::File.join @opt[:group], file
162
275
  else
163
276
  repo = if repo.is_a?(::String)
164
- File.join File.dirname(repo), @opt[:group] + '.git'
277
+ ::File.join ::File.dirname(repo), @opt[:group] + '.git'
165
278
  else
166
279
  repo[@opt[:group]]
167
280
  end
@@ -175,27 +288,21 @@ module Oxidized
175
288
  begin
176
289
  Rugged::Repository.init_at repo, :bare
177
290
  rescue StandardError => create_error
178
- raise GitError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' was while trying to create git repo"
291
+ raise GitError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' " \
292
+ "was while trying to create git repo"
179
293
  end
180
294
  retry
181
295
  end
182
296
  end
183
297
 
184
- # Uploads data into file in the repo
185
- #
186
- # @param [String] file: the file to save the configuration to
187
- # @param [String] data: the configuration to save
188
- # @param [Rugged::Repository] repo: the git repository to use
189
- #
190
- # If Oxidized.config.output.git.single_repo = false (which is the default),
191
- # there will one repository for each group.
298
+ # Uploads data into file in the repository repo
192
299
  #
193
300
  # update_repo caches the index on disk. An index is usually used in a
194
301
  # working directory and not in a bare repository, which confuses users.
195
302
  # The alternative would be to rebuild the index each time, which a little
196
303
  # time consuming. Caching the index in memory is difficult because a new
197
304
  # Output object is created each time #store is called.
198
- def update_repo(repo, file, data)
305
+ def update_repo(repo, file, data) # rubocop:disable Naming/PredicateMethod
199
306
  oid_old = repo.blob_at(repo.head.target_id, file) rescue nil
200
307
  return false if oid_old && (oid_old.content.b == data.b)
201
308
 
@@ -25,17 +25,17 @@ module Oxidized
25
25
  if @cfg.empty?
26
26
  Oxidized.asetus.user.output.gitcrypt.user = 'Oxidized'
27
27
  Oxidized.asetus.user.output.gitcrypt.email = 'o@example.com'
28
- Oxidized.asetus.user.output.gitcrypt.repo = File.join(Config::ROOT, 'oxidized.git')
28
+ Oxidized.asetus.user.output.gitcrypt.repo = ::File.join(Config::ROOT, 'oxidized.git')
29
29
  Oxidized.asetus.save :user
30
30
  raise NoConfig, "no output git config, edit #{Oxidized::Config.configfile}"
31
31
  end
32
32
 
33
33
  if @cfg.repo.respond_to?(:each)
34
34
  @cfg.repo.each do |group, repo|
35
- @cfg.repo["#{group}="] = File.expand_path repo
35
+ @cfg.repo["#{group}="] = ::File.expand_path repo
36
36
  end
37
37
  else
38
- @cfg.repo = File.expand_path @cfg.repo
38
+ @cfg.repo = ::File.expand_path @cfg.repo
39
39
  end
40
40
  end
41
41
 
@@ -45,7 +45,7 @@ module Oxidized
45
45
  @cfg.users.each do |user|
46
46
  system("#{@gitcrypt_adduser} #{user}")
47
47
  end
48
- File.write(".gitattributes", "* filter=git-crypt diff=git-crypt\n.gitattributes !filter !diff")
48
+ ::File.write(".gitattributes", "* filter=git-crypt diff=git-crypt\n.gitattributes !filter !diff")
49
49
  repo.add(".gitattributes")
50
50
  repo.commit("Initial commit: crypt all config files")
51
51
  end
@@ -73,7 +73,7 @@ module Oxidized
73
73
 
74
74
  outputs.types.each do |type|
75
75
  type_cfg = ''
76
- type_repo = File.join(File.dirname(repo), type + '.git')
76
+ type_repo = ::File.join(::File.dirname(repo), type + '.git')
77
77
  outputs.type(type).each do |output|
78
78
  (type_cfg << output; next) unless output.name # rubocop:disable Style/Semicolon
79
79
  type_file = file + '--' + output.name
@@ -95,9 +95,9 @@ module Oxidized
95
95
  unlock repo
96
96
  index = repo.index
97
97
  # Empty repo ?
98
- raise 'Empty git repo' if File.exist?(index.path)
98
+ raise 'Empty git repo' if ::File.exist?(index.path)
99
99
 
100
- File.read path
100
+ ::File.read path
101
101
  lock repo
102
102
  rescue StandardError
103
103
  'node not found'
@@ -114,7 +114,10 @@ module Oxidized
114
114
  tab = []
115
115
  walker.each do |commit|
116
116
  hash = {}
117
+ # We keep :date for reverse compatibility on oxidized-web <= 0.15.1
117
118
  hash[:date] = commit.date.to_s
119
+ # date as a Time instance for more flexibility in oxidized-web
120
+ hash[:time] = commit.date
118
121
  hash[:oid] = commit.objectish
119
122
  hash[:author] = commit.author
120
123
  hash[:message] = commit.message
@@ -173,7 +176,8 @@ module Oxidized
173
176
  private
174
177
 
175
178
  def yield_repo_and_path(node, group)
176
- repo, path = node.repo, node.name
179
+ repo = node.repo
180
+ path = node.name
177
181
 
178
182
  path = "#{group}/#{node.name}" if group && @cfg.single_repo?
179
183
 
@@ -185,10 +189,10 @@ module Oxidized
185
189
 
186
190
  if @opt[:group]
187
191
  if @cfg.single_repo?
188
- file = File.join @opt[:group], file
192
+ file = ::File.join @opt[:group], file
189
193
  else
190
194
  repo = if repo.is_a?(::String)
191
- File.join File.dirname(repo), @opt[:group] + '.git'
195
+ ::File.join ::File.dirname(repo), @opt[:group] + '.git'
192
196
  else
193
197
  repo[@opt[:group]]
194
198
  end
@@ -198,12 +202,13 @@ module Oxidized
198
202
  begin
199
203
  update_repo repo, file, data, @msg, @user, @email
200
204
  rescue Git::GitExecuteError, ArgumentError => e
201
- Oxidized.logger.debug "open_error #{e} #{file}"
205
+ logger.debug "open_error #{e} #{file}"
202
206
  begin
203
207
  grepo = Git.init repo
204
208
  crypt_init grepo
205
209
  rescue StandardError => create_error
206
- raise GitCryptError, "first '#{e.message}' was raised while opening git repo, then '#{create_error.message}' was while trying to create git repo"
210
+ raise GitCryptError, "first '#{e.message}' was raised while opening git repo, then " \
211
+ "'#{create_error.message}' was while trying to create git repo"
207
212
  end
208
213
  retry
209
214
  end
@@ -215,7 +220,7 @@ module Oxidized
215
220
  grepo.config('user.email', email)
216
221
  grepo.chdir do
217
222
  unlock grepo
218
- File.write(file, data)
223
+ ::File.write(file, data)
219
224
  grepo.add(file)
220
225
  if grepo.status[file].nil? || !grepo.status[file].type.nil?
221
226
  grepo.commit(msg)
@@ -34,14 +34,14 @@ module Oxidized
34
34
 
35
35
  case response.code.to_i
36
36
  when 200 || 201
37
- Oxidized.logger.info "Configuration http backup complete for #{node}"
37
+ logger.info "Configuration http backup complete for #{node}"
38
38
  p [:success]
39
39
  when (400..499)
40
- Oxidized.logger.info "Configuration http backup for #{node} failed status: #{response.body}"
40
+ logger.info "Configuration http backup for #{node} failed status: #{response.body}"
41
41
  p [:bad_request]
42
42
  when (500..599)
43
43
  p [:server_problems]
44
- Oxidized.logger.info "Configuration http backup for #{node} failed status: #{response.body}"
44
+ logger.info "Configuration http backup for #{node} failed status: #{response.body}"
45
45
  end
46
46
  end
47
47
 
@@ -55,7 +55,8 @@ module Oxidized
55
55
  'group' => opt[:group],
56
56
  'node' => node,
57
57
  'config' => outputs.to_cfg
58
- # actually we need to also iterate outputs, for other types like in gitlab. But most people don't use 'type' functionality.
58
+ # actually we need to also iterate outputs, for other types like in gitlab.
59
+ # But most people don't use 'type' functionality.
59
60
  )
60
61
  end
61
62
  end