oxidized 0.32.1 → 0.33.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug_report.md +42 -0
  3. data/.github/ISSUE_TEMPLATE/feature_request.md +22 -0
  4. data/.github/ISSUE_TEMPLATE/support-request.md +36 -0
  5. data/.github/workflows/publishdocker.yml +35 -16
  6. data/.gitignore +1 -0
  7. data/.rubocop.yml +0 -2
  8. data/.rubocop_todo.yml +5 -31
  9. data/CHANGELOG.md +54 -2
  10. data/CONTRIBUTING.md +10 -7
  11. data/Dockerfile +37 -64
  12. data/README.md +4 -95
  13. data/Rakefile +8 -9
  14. data/docs/Configuration.md +139 -27
  15. data/docs/Docker.md +240 -0
  16. data/docs/Issues.md +17 -0
  17. data/docs/Model-Notes/EatonNetwork.md +18 -0
  18. data/docs/Model-Notes/HPEAruba.md +3 -2
  19. data/docs/Release.md +5 -3
  20. data/docs/Supported-OS-Types.md +3 -0
  21. data/docs/Troubleshooting.md +6 -1
  22. data/extra/rest_client.rb +1 -1
  23. data/lib/oxidized/config/vars.rb +18 -14
  24. data/lib/oxidized/config.rb +3 -1
  25. data/lib/oxidized/core.rb +26 -5
  26. data/lib/oxidized/input/http.rb +1 -1
  27. data/lib/oxidized/model/aos7.rb +3 -0
  28. data/lib/oxidized/model/eatonnetwork.rb +65 -0
  29. data/lib/oxidized/model/fortios.rb +3 -3
  30. data/lib/oxidized/model/ingate.rb +47 -0
  31. data/lib/oxidized/model/ios.rb +1 -0
  32. data/lib/oxidized/model/netgear.rb +6 -0
  33. data/lib/oxidized/model/powerconnect.rb +31 -10
  34. data/lib/oxidized/model/srosmd.rb +1 -1
  35. data/lib/oxidized/model/unifiap.rb +142 -0
  36. data/lib/oxidized/output/git.rb +82 -32
  37. data/lib/oxidized/output/gitcrypt.rb +3 -0
  38. data/lib/oxidized/version.rb +6 -4
  39. data/lib/oxidized/worker.rb +2 -5
  40. data/lib/refinements.rb +2 -0
  41. data/oxidized.gemspec +5 -6
  42. metadata +30 -30
  43. data/examples/podman-compose/Makefile +0 -103
  44. data/examples/podman-compose/README.md +0 -94
  45. data/examples/podman-compose/docker-compose.yml +0 -30
  46. data/examples/podman-compose/gitserver/.gitignore +0 -1
  47. data/examples/podman-compose/gitserver/Dockerfile +0 -14
  48. data/examples/podman-compose/model-simulation/Dockerfile-model +0 -13
  49. data/examples/podman-compose/model-simulation/asternos.sh +0 -36
  50. data/examples/podman-compose/oxidized-config/.gitignore +0 -10
  51. data/examples/podman-compose/oxidized-config/config +0 -46
  52. data/examples/podman-compose/oxidized-config/config_csv-file +0 -46
  53. data/examples/podman-compose/oxidized-config/config_csv-gitserver +0 -56
  54. data/examples/podman-compose/oxidized-config/router.db +0 -1
  55. data/examples/podman-compose/oxidized-ssh/.gitignore +0 -1
  56. data/examples/podman-compose/oxidized-ssh/README.md +0 -14
@@ -1,17 +1,21 @@
1
- module Oxidized::Config::Vars
2
- # convenience method for accessing node, group or global level user variables
3
- def vars(name)
4
- model_name = @node.model.class.name.to_s.downcase
5
- if @node.vars&.has_key?(name)
6
- @node.vars[name]
7
- elsif Oxidized.config.groups.has_key?(@node.group) && Oxidized.config.groups[@node.group].models.has_key(model_name) && Oxidized.config.groups[@node.group].models[model_name].vars.has_key?(name.to_s)
8
- Oxidized.config.groups[@node.group].models[model_name].vars[name.to_s]
9
- elsif Oxidized.config.groups.has_key?(@node.group) && Oxidized.config.groups[@node.group].vars.has_key?(name.to_s)
10
- Oxidized.config.groups[@node.group].vars[name.to_s]
11
- elsif Oxidized.config.models.has_key(model_name) && Oxidized.config.models[model_name].vars.has_key?(name.to_s)
12
- Oxidized.config.models[model_name].vars[name.to_s]
13
- elsif Oxidized.config.vars.has_key?(name.to_s)
14
- Oxidized.config.vars[name.to_s]
1
+ module Oxidized
2
+ class Config
3
+ module Vars
4
+ # convenience method for accessing node, group or global level user variables
5
+ def vars(name)
6
+ model_name = @node.model.class.name.to_s.downcase
7
+ if @node.vars&.has_key?(name)
8
+ @node.vars[name]
9
+ elsif Oxidized.config.groups.has_key?(@node.group) && Oxidized.config.groups[@node.group].models.has_key(model_name) && Oxidized.config.groups[@node.group].models[model_name].vars.has_key?(name.to_s)
10
+ Oxidized.config.groups[@node.group].models[model_name].vars[name.to_s]
11
+ elsif Oxidized.config.groups.has_key?(@node.group) && Oxidized.config.groups[@node.group].vars.has_key?(name.to_s)
12
+ Oxidized.config.groups[@node.group].vars[name.to_s]
13
+ elsif Oxidized.config.models.has_key(model_name) && Oxidized.config.models[model_name].vars.has_key?(name.to_s)
14
+ Oxidized.config.models[model_name].vars[name.to_s]
15
+ elsif Oxidized.config.vars.has_key?(name.to_s)
16
+ Oxidized.config.vars[name.to_s]
17
+ end
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -35,7 +35,6 @@ module Oxidized
35
35
  asetus.default.timeout = 20
36
36
  asetus.default.retries = 3
37
37
  asetus.default.prompt = /^([\w.@-]+[#>]\s?)$/
38
- asetus.default.rest = '127.0.0.1:8888' # or false to disable
39
38
  asetus.default.next_adds_job = false # if true, /next adds job, so device is fetched immmeiately
40
39
  asetus.default.vars = {} # could be 'enable'=>'enablePW'
41
40
  asetus.default.groups = {} # group level configuration
@@ -43,6 +42,9 @@ module Oxidized
43
42
  asetus.default.models = {} # model level configuration
44
43
  asetus.default.pid = File.join(Oxidized::Config::ROOT, 'pid')
45
44
 
45
+ # Extentions
46
+ asetus.default.extensions['oxidized-web'].load = false
47
+
46
48
  asetus.default.crash.directory = File.join(Oxidized::Config::ROOT, 'crashes')
47
49
  asetus.default.crash.hostnames = false
48
50
 
data/lib/oxidized/core.rb CHANGED
@@ -23,15 +23,36 @@ module Oxidized
23
23
  end
24
24
  Signals.register_signal('HUP', reload_proc)
25
25
 
26
- # Initialize REST API and webUI if requested
27
- if Oxidized.config.rest?
26
+ # Load extensions, currently only oxidized-web
27
+ # We have different namespaces for oxidized-web, which needs to be
28
+ # adressed if we need a generic way to load extensions:
29
+ # - gem: oxidized-web
30
+ # - module: Oxidized::API
31
+ # - path: oxidized/web
32
+ # - entrypoint: Oxidized::API::Web.new(nodes, configuration)
33
+
34
+ # Initialize oxidized-web if requested
35
+ if Oxidized.config.has_key? 'rest'
36
+ Oxidized.logger.warn(
37
+ 'configuration: "rest" is deprecated. Migrate to ' \
38
+ '"extensions.oxidized-web" and remove "rest" from the configuration'
39
+ )
40
+ configuration = Oxidized.config.rest
41
+ elsif Oxidized.config.extensions['oxidized-web'].load?
42
+ # This comment stops rubocop complaining about Style/IfUnlessModifier
43
+ configuration = Oxidized.config.extensions['oxidized-web']
44
+ end
45
+
46
+ if configuration
28
47
  begin
29
48
  require 'oxidized/web'
30
49
  rescue LoadError
31
- raise OxidizedError, 'oxidized-web not found: sudo gem install oxidized-web - \
32
- or disable web support by setting "rest: false" in your configuration'
50
+ raise OxidizedError,
51
+ 'oxidized-web not found: install it or disable it by ' \
52
+ 'removing "rest" and "extensions.oxidized-web" from your ' \
53
+ 'configuration'
33
54
  end
34
- @rest = API::Web.new nodes, Oxidized.config.rest
55
+ @rest = API::Web.new nodes, configuration
35
56
  @rest.run
36
57
  end
37
58
  run
@@ -59,7 +59,7 @@ module Oxidized
59
59
 
60
60
  if res.code == '401' && res['www-authenticate']&.include?('Digest')
61
61
  uri.user = @username
62
- uri.password = @password
62
+ uri.password = URI.encode_www_form_component(@password)
63
63
  Oxidized.logger.debug "Server requires Digest authentication"
64
64
  auth = Net::HTTP::DigestAuth.new.auth_header(uri, res['www-authenticate'], 'GET')
65
65
 
@@ -26,6 +26,9 @@ class AOS7 < Oxidized::Model
26
26
  end
27
27
 
28
28
  cmd 'show hardware-info' do |cfg|
29
+ # Remove extra lines occuring when the command runs slow
30
+ cfg.gsub! /^Please wait...\n/, ''
31
+ cfg.gsub! /^\n\n\n/, "\n\n"
29
32
  comment cfg
30
33
  end
31
34
 
@@ -0,0 +1,65 @@
1
+ class EatonNetwork < Oxidized::Model
2
+ using Refinements
3
+ # Eaton Gigabit Network Card M3
4
+
5
+ # -p option is a passphrase used to encrypted parts of the config data, the
6
+ # encrypted data is nondeterministic and changes with each run. Use auth
7
+ # password as the passphrase.
8
+ #
9
+ # See docs/Model-Notes/EatonNetwork.md for more info
10
+ post do
11
+ # Get config in post to allow passing auth password to cmd.
12
+ cfg = cmd "save_configuration -p #{@node.auth[:password]}"
13
+ cfg
14
+ end
15
+
16
+ cmd :all do |cfg|
17
+ # `save_configuration` echos the command back, outputs date time info, with
18
+ # last line is the prompt again.
19
+ json_str = cfg.each_line.select { |line| line.match /^\{/ }.join
20
+ json = JSON.parse(json_str)
21
+
22
+ json['features']['userAndSessionManagement']['data']['settings']['all']['1.0']['local']['1.0']['predefinedAccounts'].each do |n|
23
+ n.delete('attemptLogin')
24
+ n['password'].delete('history')
25
+ end
26
+ json['features']['userAndSessionManagement']['data']['settings']['all']['1.0']['local']['1.0']['createdAccounts'].each do |n|
27
+ n.delete('attemptLogin')
28
+ n['password'].delete('history')
29
+ end
30
+
31
+ cfg = JSON.pretty_generate(json)
32
+ cfg
33
+ end
34
+
35
+ cmd :secret do |cfg|
36
+ # Re-parse json to remove secrets by json path
37
+ json = JSON.parse(cfg)
38
+
39
+ json.delete('passphrase')
40
+ json['features']['rms']['data']['settings'].delete('proxyUsername')
41
+ json['features']['rms']['data']['settings'].delete('proxyPassword')
42
+ json['features']['rms']['data']['settings'].delete('username')
43
+ json['features']['rms']['data']['settings'].delete('password')
44
+ json['features']['rms']['data']['settings'].delete('defaultPassword')
45
+
46
+ json['features']['smtp']['data']['dmeData'].delete('password')
47
+
48
+ json['features']['snmp']['data']['dmeData']['v3']['users'].each do |n|
49
+ n['auth'].delete('password')
50
+ n['priv'].delete('password')
51
+ end
52
+
53
+ json['features']['userAndSessionManagement']['data']['settings']['all']['1.0']['ldap']['1.0']['settings']['connectivity']['bind'].delete('password')
54
+ json['features']['userAndSessionManagement']['data']['settings']['all']['1.0']['radius']['1.0']['settings']['connectivity']['primaryServer'].delete('secret')
55
+ json['features']['userAndSessionManagement']['data']['settings']['all']['1.0']['radius']['1.0']['settings']['connectivity']['secondaryServer'].delete('secret')
56
+
57
+ cfg = JSON.pretty_generate(json)
58
+ cfg
59
+ end
60
+
61
+ cfg :ssh do
62
+ exec true
63
+ pre_logout 'logout'
64
+ end
65
+ end
@@ -3,7 +3,7 @@ class FortiOS < Oxidized::Model
3
3
 
4
4
  comment '# '
5
5
 
6
- prompt /^([-\w.~]+(\s[(\w\-.)]+)?~?\s?[#>$]\s?)$/
6
+ prompt /^(\(\w\) )?([-\w.~]+(\s[(\w\-.)]+)?~?\s?[#>$]\s?)$/
7
7
 
8
8
  # When a post-login-banner is enabled, you have to press "a" to log in
9
9
  expect /^\(Press\s'a'\sto\saccept\):/ do |data, re|
@@ -39,11 +39,11 @@ class FortiOS < Oxidized::Model
39
39
 
40
40
  cmd 'get system status' do |cfg|
41
41
  @vdom_enabled = cfg.match /Virtual domain configuration: (enable|multiple)/
42
- cfg.gsub! /(System time:).*/, '\\1 <stripped>'
42
+ cfg.gsub! /(System time:).*/i, '\\1 <stripped>'
43
43
  cfg.gsub! /(Cluster (?:uptime|state change time):).*/, '\\1 <stripped>'
44
44
  cfg.gsub! /(Current Time\s+:\s+)(.*)/, '\1<stripped>'
45
45
  cfg.gsub! /(Uptime:\s+)(.*)/, '\1<stripped>\3'
46
- cfg.gsub! /(Last reboot:\s+)(.*)/, '\1<stripped>\3'
46
+ cfg.gsub! /(Last reboot:\s+)(.*)/i, '\1<stripped>\3'
47
47
  cfg.gsub! /(Disk Usage\s+:\s+)(.*)/, '\1<stripped>'
48
48
  cfg.gsub! /(^\S+ (?:disk|DB):\s+)(.*)/, '\1<stripped>\3'
49
49
  cfg.gsub! /(VM Registration:\s+)(.*)/, '\1<stripped>\3'
@@ -0,0 +1,47 @@
1
+ class Ingate < Oxidized::Model
2
+ using Refinements
3
+
4
+ cfg_cb = lambda do
5
+ cfg = @m.post(
6
+ @main_url,
7
+ {
8
+ 'page' => 'save',
9
+ 'db.webgui.testmode/1/timelimit' => '30',
10
+ 'db.webgui.testmode/__KEEP_ROWS_ALIVE' => '1',
11
+ 'db.webgui.pending_apply/1/verbosity' => 'always',
12
+ 'db.webgui.pending_apply/__KEEP_ROWS_ALIVE' => '1',
13
+ 'action.admin.download_config_cli' => 'Save config to CLI file',
14
+ 'upload.config_file;filename=type' => 'application/octet-stream',
15
+ 'upload.clicmd_file;filename;type' => 'application/octet-stream',
16
+ 'security' => '',
17
+ 'got_complete_form' => 'yes'
18
+ },
19
+ 'Accept' => 'application/x-config-database'
20
+ )
21
+ cfg.body
22
+ end
23
+
24
+ cmd cfg_cb do |cfg|
25
+ cfg.gsub! /^# Timestamp:.*$/, ''
26
+ cfg
27
+ end
28
+
29
+ cfg :http do
30
+ @secure = true
31
+ @main_page = "/"
32
+ define_singleton_method :login do
33
+ @main_url = URI::HTTP.build host: @node.ip, path: @main_page
34
+ @m.post(
35
+ @main_url,
36
+ {
37
+ 'security_user' => @node.auth[:username],
38
+ 'security_password' => @node.auth[:password],
39
+ 'page' => 'login',
40
+ 'goal' => 'save',
41
+ 'got_complete_form' => 'yes',
42
+ 'security' => ''
43
+ }
44
+ )
45
+ end
46
+ end
47
+ end
@@ -50,6 +50,7 @@ class IOS < Oxidized::Model
50
50
  cfg.gsub! /^( +client \S+ server-key \d) (.*)$/, '\\1 <secret hidden>'
51
51
  cfg.gsub! /^( +domain-password) \S+ ?(.*)/, '\\1 <secret hidden> \\2'
52
52
  cfg.gsub! /^( +pre-shared-key).*/, '\\1 <configuration removed>'
53
+ cfg.gsub! /^(.*server-key(?: \d)?) \S+/, '\\1 <secret hidden>'
53
54
  cfg
54
55
  end
55
56
 
@@ -4,6 +4,12 @@ class Netgear < Oxidized::Model
4
4
  comment '!'
5
5
  prompt /^\(?[\w \-+.]+\)? ?[#>] ?$/
6
6
 
7
+ # Handle pager for "show version" on old Netgear models: #2394
8
+ expect /^--More-- or \(q\)uit$/ do |data, re|
9
+ send ' '
10
+ data.sub re, ''
11
+ end
12
+
7
13
  cmd :secret do |cfg|
8
14
  cfg.gsub!(/password (\S+)/, 'password <hidden>')
9
15
  cfg.gsub!(/encrypted (\S+)/, 'encrypted <hidden>')
@@ -5,13 +5,15 @@ class PowerConnect < Oxidized::Model
5
5
 
6
6
  comment '! '
7
7
 
8
- expect /^\s*--More--\s+.*$/ do |data, re|
8
+ expect /\n\s*--More--\s+.*/ do |data, re| # Also grab the blank line above the --More--
9
9
  send ' '
10
10
  data.sub re, ''
11
11
  end
12
12
 
13
+ # Filter all command output
13
14
  cmd :all do |cfg|
14
- cfg.each_line.to_a[1..-3].join
15
+ cfg.gsub! /\r+/, '' # Remove the CR characters echoed back from the commands
16
+ cfg.cut_tail # Drop the last line which is the next prompt
15
17
  end
16
18
 
17
19
  cmd :secret do |cfg|
@@ -32,7 +34,7 @@ class PowerConnect < Oxidized::Model
32
34
  end
33
35
 
34
36
  cmd 'show running-config' do |cfg|
35
- cfg.sub(/^(sflow \S+ destination owner \S+ timeout )\d+$/, '! \1<timeout>')
37
+ cfg.sub(/^(sflow \S+ destination owner \S+ timeout )\d+$/, '! \1<timeout>') # Remove changing timeout
36
38
  end
37
39
 
38
40
  cfg :telnet, :ssh do
@@ -50,15 +52,21 @@ class PowerConnect < Oxidized::Model
50
52
  end
51
53
  end
52
54
 
53
- post_login "terminal datadump"
54
- post_login "terminal length 0"
55
- pre_logout "logout"
56
- pre_logout "exit"
55
+ post_login do
56
+ cmd "terminal datadump"
57
+ cmd "terminal length 0"
58
+ end
59
+ pre_logout do
60
+ send "exit\r"
61
+ sleep(0.25)
62
+ send "logout\r"
63
+ end
57
64
  end
58
65
 
59
66
  def clean(cfg)
60
67
  out = []
61
- skip_blocks = 0
68
+ len1 = len2 = skip_blocks = 0
69
+
62
70
  cfg.each_line do |line|
63
71
  # If this is a stackable switch we should skip this block of information
64
72
  if line.match(/Up\sTime|Temperature|Power Suppl(ies|y)|Fans/i) && (@stackable == true)
@@ -71,9 +79,22 @@ class PowerConnect < Oxidized::Model
71
79
  skip_blocks -= 1 if /\S/ !~ line
72
80
  next
73
81
  end
74
- out << line.strip
82
+ line = line.strip
83
+ # If the temps were not removed by skipping blocks, then mask them out wih XXX
84
+ # The most recent set of dashes has the spacing we want to match
85
+ if (match = line.match(/^(---+ +)(---+ +)/))
86
+ one, two = match.captures
87
+ len1 = one.length
88
+ len2 = two.length
89
+ end
90
+ # This can only be a temperature, right? ;-)
91
+ if (match = line.match(/^(\d{1,2}) {3,}\d+ (.*)$/))
92
+ one, two = match.captures
93
+ line = one.to_s + (' ' * (len1 - one.length)) + "XXX" + (' ' * (len2 - 3)) + two.to_s
94
+ end
95
+ out << line
75
96
  end
76
- out = out.reject { |line| line[/Up\sTime/] }
97
+ out = out.reject { |line| line[/Up\sTime/] } # Filter out Up Time
77
98
  out = comment out.join "\n"
78
99
  out << "\n"
79
100
  end
@@ -26,7 +26,7 @@ class SROSMD < Oxidized::Model
26
26
  #
27
27
  # Strip uptime.
28
28
  #
29
- cfg.sub! /^System Up Time.*\n/, ''
29
+ cfg.gsub! /^System Up Time.*\n/, ''
30
30
  comment cfg
31
31
  end
32
32
 
@@ -0,0 +1,142 @@
1
+ class Unifiap < Oxidized::Model
2
+ using Refinements
3
+
4
+ # Ubiquiti Unifi AP circa 6.x
5
+ # Should also work for unfi switches, and airOS, maybe they could be combined.
6
+ # Since it relies on exec channels, because the interactive session wouldn't
7
+ # capture all of the system.cfg output, you can't use telnet with this model.
8
+
9
+ # Sometimes there's a handy info command that summarizes some device attributes,
10
+ # but it doesn't seem to be available in exec mode. So we try to build up a similar
11
+ # list by extracting tidbits from various places. AirOS doesn't have some of these
12
+ # files, so we # may have to fall back on other commands, or locations.
13
+
14
+ # First get the board model
15
+ cmd 'head -4 /etc/board.info' do |cfg|
16
+ @model = Regexp.last_match(1) if cfg =~ /board\.name=(\S+)/i
17
+ ""
18
+ end
19
+
20
+ # and version
21
+ cmd 'cat /etc/version' do |cfg|
22
+ @version = Regexp.last_match(1) if cfg =~ /(\S+)$/i
23
+ ""
24
+ end
25
+
26
+ # Now the Mac address
27
+ cmd 'ifconfig eth0' do |cfg|
28
+ @mac = Regexp.last_match(1) if cfg =~ /eth0\s+Link encap:Ethernet\s+HWaddr\s+(\w+:\w+:\w+:\w+:\w+:\w+)/i
29
+ ""
30
+ end
31
+
32
+ # Next see if we can get our IP and host name out of /etc/hosts
33
+ cmd 'cat /etc/hosts' do |cfg|
34
+ cfg = cfg.split("\n").reject { |line| line[/^\s*(127|0000:0000:0000:0000:0000:0000:0000:0001|0:0:0:0:0:0:0:1|::1)/] }
35
+ cfg.select do |line|
36
+ if (match = line.match(/(\d+\.\d+\.\d+\.\d+)\s+(\S+)/))
37
+ @ip, @hostname = match.captures
38
+ end
39
+ end
40
+ ""
41
+ end
42
+
43
+ # We check here to see if we succeeded with /etc/hosts. If not, then we try again with ifconfig, and /tmp/system.cfg
44
+ cmd do
45
+ unless @ip
46
+ cmd 'ifconfig br0' do |cfg|
47
+ @ip = Regexp.last_match(1) if cfg =~ /inet addr:\s*(\d+\.\d+\.\d+\.\d+)/i
48
+ end
49
+
50
+ unless @ip
51
+ cmd 'ifconfig eth0' do |cfg|
52
+ @ip = Regexp.last_match(1) if cfg =~ /inet addr:\s*(\d+\.\d+\.\d+\.\d+)/i
53
+ end
54
+ end
55
+ end
56
+
57
+ unless @hostname
58
+ cmd 'cat /tmp/system.cfg' do |cfg|
59
+ @hostname = Regexp.last_match(1) if cfg =~ /resolv.host.1.name=(\S+)/i
60
+ end
61
+ end
62
+ ""
63
+ end
64
+
65
+ # Check if ntpclient is running
66
+ cmd 'ps wwww' do |cfg|
67
+ @ntpserver = Regexp.last_match(1) if cfg =~ /bin\/ntpclient.+-h\s*(\S+)/i
68
+ ""
69
+ end
70
+
71
+ # If it's a Unifi device it may have NTP health indication
72
+ # If there are other places that Ubiquiti puts these status files, add them here.
73
+ cmd '[ -e /tmp/run/ntp.ready ] || [ -e /var/run/ntp.ready ] && echo "File(s) exist(s)" || echo "No such file"' do |cfg|
74
+ if cfg =~ /No such file/i
75
+ if @ntpserver
76
+ # Ok, now lets try getting the skew from the output of ntpclient
77
+ cmd "ntpclient -d -n -c 2 -i0 -h #{@ntpserver}" do |cfg|
78
+ @skew = ntpskew(cfg)
79
+ end
80
+ @sync = !@skew.nil? && @skew.to_f.abs < 1e6 ? "Synchronized" : "FAIL"
81
+ end
82
+ else
83
+ @ntpserver = true
84
+ @sync = "Synchronized"
85
+ end
86
+ ""
87
+ end
88
+
89
+ # Now we can display it all as a banner
90
+ cmd do
91
+ out = []
92
+ out << "*************************"
93
+ out << "Model: #{@model}"
94
+ out << "Version: #{@version}"
95
+ out << "MAC Address: #{@mac}"
96
+ out << "IP Address: #{@ip}"
97
+ out << "Hostname: #{@hostname}"
98
+ out << "NTP: #{@sync}" if @ntpserver
99
+ out << "*************************"
100
+ comment out.join("\n") + "\n"
101
+ end
102
+
103
+ # Followed by the board info
104
+ cmd 'cat /etc/board.info' do |cfg|
105
+ cfg = "#\n# Board Info:\n#\n" + cfg
106
+ comment cfg
107
+ end
108
+
109
+ # Lastly the system config
110
+ cmd 'cat /tmp/system.cfg' do |cfg|
111
+ cfg = "#\n# System Config:\n#\n" + cfg
112
+ cfg + "\n"
113
+ end
114
+
115
+ cmd :secret do |cfg|
116
+ cfg.gsub! /^((?:users|snmp\.(?:user|community))\.\d+\.password)=.+/, "# \\1=<hidden>"
117
+ cfg
118
+ end
119
+
120
+ cfg :ssh do
121
+ exec true # Don't run shell, run each command in exec channel
122
+ end
123
+
124
+ # NTPskew: Return the skew in micro seconds from the ntpclient output
125
+ def ntpskew(cfg)
126
+ index = skew = nil
127
+
128
+ cfg.each_line do |line|
129
+ # Look for the header just before the stats line, and find which number is skew
130
+ if line.match(/^\s*[a-z]+\s+[a-z]+\s+[a-z]+\s+[a-z]+/i)
131
+ words = line.split
132
+ index = words.map(&:downcase).index("skew")
133
+ end
134
+ # Now look for the single stats line and grab the skew
135
+ if !index.nil? && line.match(/^\s*[\d.]+\s+[\d.]+\s+[\d.]+\s+[\d.]+/)
136
+ numbers = line.split
137
+ skew = numbers[index]
138
+ end
139
+ end
140
+ skew
141
+ end
142
+ end
@@ -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,6 +127,79 @@ 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
+
146
203
  private
147
204
 
148
205
  def yield_repo_and_path(node, group)
@@ -181,14 +238,7 @@ module Oxidized
181
238
  end
182
239
  end
183
240
 
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.
241
+ # Uploads data into file in the repository repo
192
242
  #
193
243
  # update_repo caches the index on disk. An index is usually used in a
194
244
  # working directory and not in a bare repository, which confuses users.
@@ -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