panda-motd 0.0.8 → 0.0.10

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e96416cb34d16f565eb89916c6156cd2e293a04f0ba8ffeede5df5e92d73acc3
4
- data.tar.gz: abc7ad56a5b1a0d90dcbf4cd4a7f948bed78232357f2ac7fc352f7388d136d0f
3
+ metadata.gz: 7036e8cca753cf96292eb0c4dd7bb29524852e486265b5de0403089af3f7897d
4
+ data.tar.gz: c9237796bc85552104a451801e202a0bd57e139f4c2bafc2cea9d2275ff92291
5
5
  SHA512:
6
- metadata.gz: 452c47ad204b5dfb044abf370c0f0643c24bcfea8565ca5329ad487717cd116fd1d4c88378985be2d14a4759269cbe363d7b86bfdba31425e673d9f5be4aecef
7
- data.tar.gz: a2209d1666149ac3222c005b79af5d3edb129a79dec9bc12c8c07c906ad26aa384dfc0222378ce3aa4c8da097cb425c33e4de76b67d92a0c2677ead2ba493366
6
+ metadata.gz: 8336450885b5e4f51d1afe19c75a18f3c3eca1928cc1ce9967287933960f0e695c2f6d2908ad3820e0cfcd705ade94388c9aa9657a9a894a0a1ee8d59090df03
7
+ data.tar.gz: f5b94c6be1aa9859735f0aa3eddeba09283c76664930127724ab628f29a82e917fbb1f63494ffa626514e82fd4e33891bbe607c7fa6222d506ec259d384a0e85
@@ -6,7 +6,11 @@ class PandaMOTD
6
6
  if ARGV[0].nil?
7
7
  puts 'You must provide a config file path as an argument to panda-motd.'
8
8
  else
9
- return MOTD.new(ARGV[0])
9
+ MOTD.new(ARGV[0])
10
10
  end
11
11
  end
12
+
13
+ def self.root
14
+ File.expand_path('..', __dir__)
15
+ end
12
16
  end
@@ -0,0 +1,26 @@
1
+ class Component
2
+ attr_reader :name, :errors, :results, :config
3
+
4
+ def initialize(motd, name)
5
+ @name = name
6
+ @motd = motd
7
+ @config = motd.config.component_config(@name)
8
+ @errors = []
9
+ end
10
+
11
+ def process
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def to_s
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def lines_before
20
+ @motd.config.component_config(@name)['lines_before'] || 1
21
+ end
22
+
23
+ def lines_after
24
+ @motd.config.component_config(@name)['lines_after'] || 1
25
+ end
26
+ end
@@ -1,29 +1,21 @@
1
1
  require 'artii'
2
2
  require 'colorize'
3
3
 
4
- class ASCIITextArt
5
- attr_reader :name, :errors, :results
6
-
4
+ class ASCIITextArt < Component
7
5
  def initialize(motd)
8
- @name = 'ascii_text_art'
9
- @motd = motd
10
- @config = motd.config.component_config(@name)
11
- @errors = []
6
+ super(motd, 'ascii_text_art')
12
7
  end
13
8
 
14
9
  def process
15
10
  @text = `#{@config['command']}`
16
-
17
- begin
18
- @art = Artii::Base.new font: @config['font']
19
- @results = @art.asciify(@text)
20
- @results = @results.colorize(@config['color'].to_sym) if @config['color']
21
- rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
22
- @errors << ComponentError.new(self, 'Invalid font name')
23
- end
11
+ @art = Artii::Base.new font: @config['font']
12
+ @results = @art.asciify(@text)
13
+ @results = @results.colorize(@config['color'].to_sym) if @config['color']
14
+ rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
15
+ @errors << ComponentError.new(self, 'Invalid font name')
24
16
  end
25
17
 
26
18
  def to_s
27
- return @results
19
+ @results
28
20
  end
29
21
  end
@@ -0,0 +1,44 @@
1
+ class Fail2Ban < Component
2
+ def initialize(motd)
3
+ super(motd, 'fail_2_ban')
4
+ end
5
+
6
+ def process
7
+ @results = {
8
+ jails: {}
9
+ }
10
+
11
+ @config['jails'].each do |jail|
12
+ status = jail_status(jail)
13
+ @results[:jails][jail] = {
14
+ total: status[:total],
15
+ current: status[:current]
16
+ }
17
+ end
18
+ end
19
+
20
+ def to_s
21
+ result = "Fail2Ban:\n"
22
+ @results[:jails].each do |name, stats|
23
+ result += " #{name}:\n"
24
+ result += " Total bans: #{stats[:total]}\n"
25
+ result += " Current bans: #{stats[:current]}\n"
26
+ end
27
+
28
+ result.gsub(/\s$/, '')
29
+ end
30
+
31
+ private
32
+
33
+ def jail_status(jail)
34
+ cmd_result = `fail2ban-client status #{jail}`
35
+ if cmd_result =~ /Sorry but the jail '#{jail}' does not exist/
36
+ @errors << ComponentError.new(self, "Invalid jail name '#{jail}'.")
37
+ else
38
+ total = cmd_result.match(/Total banned:\s+([0-9]+)/)[1].to_i
39
+ current = cmd_result.match(/Currently banned:\s+([0-9]+)/)[1].to_i
40
+ end
41
+
42
+ { total: total, current: current }
43
+ end
44
+ end
@@ -1,15 +1,9 @@
1
1
  require 'ruby-units'
2
2
  require 'colorize'
3
3
 
4
- class Filesystems
5
- attr_reader :name, :errors
6
- attr_reader :results
7
-
4
+ class Filesystems < Component
8
5
  def initialize(motd)
9
- @name = 'filesystems'
10
- @motd = motd
11
- @config = motd.config.component_config(@name)
12
- @errors = []
6
+ super(motd, 'filesystems')
13
7
  end
14
8
 
15
9
  def process
@@ -17,69 +11,56 @@ class Filesystems
17
11
  end
18
12
 
19
13
  def to_s
20
- name_col_size = @results.select { |r| r.is_a? Hash }.map { |r| r[:pretty_name].length }.max
21
- name_col_size = 0 if name_col_size.nil?
14
+ name_col_size = @results.select { |r| r.is_a? Hash }
15
+ .map { |r| r[:pretty_name].length }
16
+ .max || 0
22
17
 
23
- name_col_size_with_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
18
+ size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
24
19
 
25
- result = 'Filesystems'.ljust(name_col_size_with_padding, ' ')
20
+ result = 'Filesystems'.ljust(size_w_padding, ' ')
26
21
  result += "Size Used Free Use%\n"
27
22
 
28
23
  @results.each do |filesystem|
29
- # handle filesystem not found
30
- if filesystem.is_a? String
31
- result += " #{filesystem}\n"
32
- next
33
- end
34
-
35
- result += " #{filesystem[:pretty_name]}".ljust(name_col_size_with_padding, ' ')
36
-
37
- [:size, :used, :avail].each do |metric|
38
- units = if filesystem[metric] > 10**12
39
- 'terabytes'
40
- elsif filesystem[metric] > 10**9
41
- 'gigabytes'
42
- elsif filesystem[metric] > 10**6
43
- 'megabytes'
44
- elsif filesystem[metric] > 10**3
45
- 'kilobytes'
46
- else
47
- 'bytes'
48
- end
49
-
50
- value = Unit.new("#{filesystem[metric]} bytes").convert_to(units)
51
-
52
- # we have 4 characters of space to include the number, a potential decimal point, and
53
- # the unit character at the end. if the whole number component is 3+ digits long then
54
- # we omit the decimal point and just display the whole number component. if the whole
55
- # number component is 2 digits long, we can't afford to use a decimal point, so we still
56
- # only display the whole number component. if the whole number component is 1 digit long,
57
- # we use the single whole number component digit, a decimal point, a single fractional
58
- # digit, and the unit character.
59
- whole_number_length = value.scalar.floor.to_s.length
60
- round_amount = whole_number_length > 1 ? 0 : 1
61
- formatted_value = value.scalar.round(round_amount).to_s + units[0].upcase
62
- result += formatted_value.rjust(4, ' ') + ' '
63
- end
64
-
65
- percentage_used = ((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
66
- result += (percentage_used.to_s.rjust(3, ' ') + '%').send(percentage_color(percentage_used))
67
-
68
- result += "\n"
69
-
70
- total_ticks = name_col_size_with_padding + 18
71
- used_ticks = (total_ticks * (percentage_used.to_f / 100)).round
72
-
73
- result += " [#{('=' * used_ticks).send(percentage_color(percentage_used))}#{('=' * (total_ticks - used_ticks)).light_black}]"
74
- result += "\n" unless filesystem == @results.last
24
+ result += format_filesystem(filesystem, size_w_padding)
75
25
  end
76
26
 
77
- return result
27
+ result
78
28
  end
79
29
 
80
30
  private
81
31
 
82
- def percentage_color(percentage)
32
+ def format_filesystem(filesystem, size)
33
+ return " #{filesystem}\n" if filesystem.is_a? String # handle fs not found
34
+
35
+ # filesystem name
36
+ result = ''
37
+ result += " #{filesystem[:pretty_name]}".ljust(size, ' ')
38
+
39
+ # statistics (size, used, free, use%)
40
+ [:size, :used, :avail].each do |metric|
41
+ result += format_metric(filesystem, metric)
42
+ end
43
+ percent_used = calc_percent_used(filesystem)
44
+ result += format_percent_used(percent_used)
45
+ result += "\n"
46
+
47
+ # visual bar representation of use%
48
+ result += generate_usage_bar(filesystem, size, percent_used)
49
+
50
+ result
51
+ end
52
+
53
+ def generate_usage_bar(filesystem, size, percent_used)
54
+ result = ''
55
+ total_ticks = size + 18
56
+ used_ticks = (total_ticks * (percent_used.to_f / 100)).round
57
+ result += " [#{('=' * used_ticks).send(pct_color(percent_used))}"\
58
+ "#{('=' * (total_ticks - used_ticks)).light_black}]"
59
+ result += "\n" unless filesystem == @results.last
60
+ result
61
+ end
62
+
63
+ def pct_color(percentage)
83
64
  case percentage
84
65
  when 0..75 then :green
85
66
  when 76..95 then :yellow
@@ -88,9 +69,52 @@ class Filesystems
88
69
  end
89
70
  end
90
71
 
72
+ def calc_percent_used(filesystem)
73
+ ((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
74
+ end
75
+
76
+ def format_percent_used(percent_used)
77
+ (percent_used.to_s.rjust(3, ' ') + '%').send(pct_color(percent_used))
78
+ end
79
+
80
+ def calc_metric(value)
81
+ Unit.new("#{value} bytes").convert_to(calc_units(value))
82
+ end
83
+
84
+ def format_metric(filesystem, metric)
85
+ # we have 4 characters of space to include the number, a potential
86
+ # decimal point, and the unit character at the end. if the whole number
87
+ # component is 3+ digits long then we omit the decimal point and just
88
+ # display the whole number component. if the whole number component is
89
+ # 2 digits long, we can't afford to use a decimal point, so we still
90
+ # only display the whole number component. if the whole number
91
+ # component is 1 digit long, we use the single whole number component
92
+ # digit, a decimal point, a single fractional digit, and the unit
93
+ # character.
94
+
95
+ value = calc_metric(filesystem[metric])
96
+ whole_number_length = value.scalar.floor.to_s.length
97
+ round_amount = whole_number_length > 1 ? 0 : 1
98
+ formatted = value.scalar.round(round_amount).to_s + value.units[0].upcase
99
+ formatted.rjust(4, ' ') + ' '
100
+ end
101
+
102
+ def calc_units(value)
103
+ if value > 10**12
104
+ 'terabytes'
105
+ elsif value > 10**9
106
+ 'gigabytes'
107
+ elsif value > 10**6
108
+ 'megabytes'
109
+ elsif value > 10**3
110
+ 'kilobytes'
111
+ else
112
+ 'bytes'
113
+ end
114
+ end
115
+
91
116
  def parse_filesystem_usage(filesystems)
92
- entries = `BLOCKSIZE=1024 df --output=source,size,used,avail`.lines
93
- .drop(1)
117
+ entries = `BLOCKSIZE=1024 df --output=source,size,used,avail`.lines.drop(1)
94
118
 
95
119
  filesystems.map do |filesystem, pretty_name|
96
120
  matching_entry = entries.map(&:split).find { |e| e.first == filesystem }
@@ -1,13 +1,8 @@
1
1
  require 'date'
2
2
 
3
- class LastLogin
4
- attr_reader :name, :errors, :results
5
-
3
+ class LastLogin < Component
6
4
  def initialize(motd)
7
- @name = 'last_login'
8
- @motd = motd
9
- @config = motd.config.component_config(@name)
10
- @errors = []
5
+ super(motd, 'last_login')
11
6
  end
12
7
 
13
8
  def process
@@ -16,55 +11,64 @@ class LastLogin
16
11
  end
17
12
 
18
13
  def to_s
19
- <<~LAST
14
+ <<~HEREDOC
20
15
  Last Login:
21
- #{@results.map do |user, logins|
22
- logins_part =
23
- if logins.empty?
24
- ' no logins found for user.'
25
- else
26
- longest_location_size = logins.map { |l| l[:location].length }.max
27
- logins.map do |login|
28
- location_part = login[:location].ljust(longest_location_size, ' ')
29
- start_part = login[:time_start].strftime('%m/%d/%Y %I:%M%p')
30
- end_part = if login[:time_end].is_a? String # still logged in text
31
- login[:time_end].green
32
- else
33
- "#{((login[:time_end] - login[:time_start]) * 24 * 60).to_i} minutes"
34
- end
35
- " from #{location_part} at #{start_part} (#{end_part})"
36
- end.join("\n")
37
- end
38
- <<~USER
39
- #{user}:
40
- #{logins_part}
41
- USER
42
- end.join("\n")}
43
- LAST
16
+ #{@results.map { |u, l| parse_result(u, l) }.join("\n")}
17
+ HEREDOC
44
18
  end
45
19
 
46
20
  private
47
21
 
22
+ def parse_result(user, logins)
23
+ logins_part = if logins.empty?
24
+ ' no logins found for user.'
25
+ else
26
+ longest_size = logins.map { |l| l[:location].length }.max
27
+ logins.map { |l| parse_login(l, longest_size) }.join("\n")
28
+ end
29
+ <<~HEREDOC
30
+ #{user}:
31
+ #{logins_part}
32
+ HEREDOC
33
+ end
34
+
35
+ def parse_login(login, longest_size)
36
+ location = login[:location].ljust(longest_size, ' ')
37
+ start = login[:time_start].strftime('%m/%d/%Y %I:%M%p')
38
+ finish = if login[:time_end].is_a? String # not a date
39
+ if login[:time_end] == 'still logged in'
40
+ login[:time_end].green
41
+ else
42
+ login[:time_end].yellow
43
+ end
44
+ else
45
+ "#{((login[:time_end] - login[:time_start]) / 60).to_i} minutes"
46
+ end
47
+ " from #{location} at #{start} (#{finish})"
48
+ end
49
+
48
50
  def parse_last_logins(users)
49
51
  users.map do |(username, num_logins)|
50
- user_logins =
51
- `last --time-format=iso #{username}`
52
- .lines
53
- .select { |entry| entry.start_with?(username) }
54
- .take(num_logins)
55
- .map do |entry|
56
- data = entry.chomp.split(/(?:\s{2,})|(?:\s-\s)/)
57
- time_end = data[4] == 'still logged in' ? data[4] : DateTime.parse(data[4])
52
+ cmd_result = `last --time-format=iso #{username}`
53
+ logins = cmd_result.lines
54
+ .select { |entry| entry.start_with?(username) }
55
+ .take(num_logins)
58
56
 
59
- {
60
- username: username,
61
- location: data[2],
62
- time_start: DateTime.parse(data[3]),
63
- time_end: time_end
64
- }
65
- end
66
-
67
- [username.to_sym, user_logins]
57
+ [username.to_sym, logins.map! { |l| hashify_login(l, username) }]
68
58
  end.to_h
69
59
  end
60
+
61
+ def hashify_login(login, username)
62
+ re = login.chomp.split(/(?:\s{2,})|(?<=\d)(?:\s-\s)/)
63
+ date = re[4].scan(/\d{4}-\d{2}-[\dT:]+-\d{4}/)
64
+
65
+ time_end = date.any? ? Time.parse(re[4]) : re[4]
66
+
67
+ {
68
+ username: username,
69
+ location: re[2],
70
+ time_start: Time.parse(re[3]),
71
+ time_end: time_end
72
+ }
73
+ end
70
74
  end
@@ -1,14 +1,8 @@
1
1
  require 'colorize'
2
2
 
3
- class ServiceStatus
4
- attr_reader :name, :errors
5
- attr_reader :results
6
-
3
+ class ServiceStatus < Component
7
4
  def initialize(motd)
8
- @name = 'service_status'
9
- @motd = motd
10
- @config = motd.config.component_config(@name)
11
- @errors = []
5
+ super(motd, 'service_status')
12
6
  end
13
7
 
14
8
  def process
@@ -18,8 +12,9 @@ class ServiceStatus
18
12
 
19
13
  def to_s
20
14
  return "Services:\n No matching services found." unless @results.any?
15
+
21
16
  longest_name_size = @results.keys.map { |k| k.to_s.length }.max
22
- <<~HEREDOC
17
+ result = <<~HEREDOC
23
18
  Services:
24
19
  #{@results.map do |(name, status)|
25
20
  name_part = name.to_s.ljust(longest_name_size, ' ') + ':'
@@ -27,28 +22,32 @@ class ServiceStatus
27
22
  " #{name_part} #{status_part}"
28
23
  end.join("\n")}
29
24
  HEREDOC
25
+
26
+ result.gsub(/\s$/, '')
30
27
  end
31
28
 
32
29
  private
33
30
 
34
31
  def parse_service(service)
35
32
  cmd_result = `systemctl is-active #{service[0]}`.strip
36
- @errors << ComponentError.new(self, 'Unable to parse systemctl output') unless valid_responses.include? cmd_result
37
- return cmd_result
33
+ unless valid_responses.include? cmd_result
34
+ @errors << ComponentError.new(self, 'Unable to parse systemctl output')
35
+ end
36
+ cmd_result
38
37
  end
39
38
 
40
39
  def parse_services(services)
41
- services.map { |service| [service[1].to_sym, parse_service(service).to_sym] }.to_h
40
+ services.map { |s| [s[1].to_sym, parse_service(s).to_sym] }.to_h
42
41
  end
43
42
 
44
43
  def service_colors
45
- return {
44
+ {
46
45
  active: :green,
47
46
  inactive: :red
48
47
  }
49
48
  end
50
49
 
51
50
  def valid_responses
52
- return ['active', 'inactive']
51
+ ['active', 'inactive']
53
52
  end
54
53
  end
@@ -1,13 +1,8 @@
1
1
  require 'date'
2
2
 
3
- class SSLCertificates
4
- attr_reader :name, :errors, :results
5
-
3
+ class SSLCertificates < Component
6
4
  def initialize(motd)
7
- @name = 'ssl_certificates'
8
- @motd = motd
9
- @config = motd.config.component_config(@name)
10
- @errors = []
5
+ super(motd, 'ssl_certificates')
11
6
  end
12
7
 
13
8
  def process
@@ -16,48 +11,75 @@ class SSLCertificates
16
11
  end
17
12
 
18
13
  def to_s
19
- longest_name_size = @results.map { |r| r[0].length }.max
20
14
  <<~HEREDOC
21
15
  SSL Certificates:
22
- #{@results.map do |cert|
16
+ #{sorted_results.map do |cert|
23
17
  return " #{cert}" if cert.is_a? String # print the not found message
24
18
 
25
- name_portion = cert[0].ljust(longest_name_size + 6, ' ')
26
- status = cert_status(cert[1])
27
- status = cert_status_strings[status].to_s.colorize(cert_status_colors[status])
28
- date_portion = cert[1].strftime('%e %b %Y %H:%M:%S%p')
29
- " #{name_portion} #{status} #{date_portion}"
19
+ parse_cert(cert)
30
20
  end.join("\n")}
31
21
  HEREDOC
32
22
  end
33
23
 
34
24
  private
35
25
 
26
+ def parse_cert(cert)
27
+ name_portion = cert[0].ljust(longest_cert_name_length + 6, ' ')
28
+ status_sym = cert_status(cert[1])
29
+ status = cert_status_strings[status_sym].to_s
30
+ colorized_status = status.colorize(cert_status_colors[status_sym])
31
+ date_portion = cert[1].strftime('%e %b %Y %H:%M:%S%p')
32
+ " #{name_portion} #{colorized_status} #{date_portion}"
33
+ end
34
+
35
+ def longest_cert_name_length
36
+ @results.map { |r| r[0].length }.max
37
+ end
38
+
39
+ def sorted_results
40
+ if @config['sort_method'] == 'alphabetical'
41
+ @results.sort_by { |c| c[0] }
42
+ elsif @config['sort_method'] == 'expiration'
43
+ @results.sort_by { |c| c[1] }
44
+ else # default to alphabetical
45
+ @results.sort_by { |c| c[0] }
46
+ end
47
+ end
48
+
36
49
  def cert_dates(certs)
37
- return certs.map do |name, path|
50
+ certs.map do |name, path|
38
51
  if File.exist?(path)
39
- cmd_result = `openssl x509 -in #{path} -dates`
40
- parsed = cmd_result.match(/notAfter=([\w\s:]+)\n/)
41
- if parsed.nil?
42
- @errors << ComponentError.new(self, 'Unable to find certificate expiration date')
43
- else
44
- begin
45
- expiry_date = DateTime.parse(parsed[1])
46
- [name, expiry_date]
47
- rescue ArgumentError
48
- @errors << ComponentError.new(self, 'Found expiration date, but unable to parse as date')
49
- end
50
- end
52
+ parse_result(name, path)
51
53
  else
52
54
  "Certificate #{name} not found at path: #{path}"
53
55
  end
56
+ end.compact # remove nil entries, will have nil if error ocurred
57
+ end
58
+
59
+ def parse_result(name, path)
60
+ cmd_result = `openssl x509 -in #{path} -dates`
61
+ # match indices: 1 - month, 2 - day, 3 - time, 4 - year, 5 - zone
62
+ exp = /notAfter=([A-Za-z]+) (\d+) ([\d:]+) (\d{4}) ([A-Za-z]+)\n/
63
+ parsed = cmd_result.match(exp)
64
+
65
+ if parsed.nil?
66
+ @errors << ComponentError.new(self, 'Unable to find certificate '\
67
+ 'expiration date')
68
+ nil
69
+ else
70
+ expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(' '))
71
+ [name, expiry_date]
54
72
  end
73
+ rescue ArgumentError
74
+ @errors << ComponentError.new(self, 'Found expiration date, but unable '\
75
+ 'to parse as date')
76
+ [name, Time.now]
55
77
  end
56
78
 
57
79
  def cert_status(expiry_date)
58
- if (DateTime.now...DateTime.now + 30).cover? expiry_date # ... range excludes end
80
+ if (Time.now...Time.now + 30).cover? expiry_date # ... range excludes last
59
81
  :expiring
60
- elsif DateTime.now >= expiry_date
82
+ elsif Time.now >= expiry_date
61
83
  :expired
62
84
  else
63
85
  :valid
@@ -65,7 +87,7 @@ class SSLCertificates
65
87
  end
66
88
 
67
89
  def cert_status_colors
68
- return {
90
+ {
69
91
  valid: :green,
70
92
  expiring: :yellow,
71
93
  expired: :red
@@ -73,7 +95,7 @@ class SSLCertificates
73
95
  end
74
96
 
75
97
  def cert_status_strings
76
- return {
98
+ {
77
99
  valid: 'valid until',
78
100
  expiring: 'expiring at',
79
101
  expired: 'expired at'
@@ -1,14 +1,10 @@
1
1
  require 'sysinfo'
2
2
 
3
- class Uptime
4
- attr_reader :name, :errors
3
+ class Uptime < Component
5
4
  attr_reader :days, :hours, :minutes
6
5
 
7
6
  def initialize(motd)
8
- @name = 'uptime'
9
- @motd = motd
10
- @config = motd.config.component_config(@name)
11
- @errors = []
7
+ super(motd, 'uptime')
12
8
  end
13
9
 
14
10
  def process
@@ -20,7 +16,7 @@ class Uptime
20
16
  end
21
17
 
22
18
  def to_s
23
- return "#{@config['prefix'] || 'up'} #{format_uptime}"
19
+ "#{@config['prefix'] || 'up'} #{format_uptime}"
24
20
  end
25
21
 
26
22
  private
@@ -2,8 +2,10 @@ require 'yaml'
2
2
  require 'fileutils'
3
3
 
4
4
  class Config
5
+ attr_reader :file_path
6
+
5
7
  def initialize(file_path = nil)
6
- @file_path = file_path.nil? ? File.join(Dir.home, '.config', 'panda-motd.yaml') : file_path
8
+ @file_path = file_path || File.join(Dir.home, '.config', 'panda-motd.yaml')
7
9
  unless File.exist?(@file_path)
8
10
  create_config(@file_path)
9
11
  puts "panda-motd created a default config file at: #{@file_path}"
@@ -12,31 +14,34 @@ class Config
12
14
  end
13
15
 
14
16
  def components_enabled
15
- # first iterate through the config hash and grab all names of enabled components
16
- enabled_list = @config['components'].map { |component, setting| component if setting['enabled'] }.compact
17
+ # iterate config hash and grab names of enabled components
18
+ enabled = @config['components'].map { |c, s| c if s['enabled'] }.compact
17
19
  # get the class constant
18
- return enabled_list.map { |e| component_classes[e.to_sym] }
20
+ enabled.map { |e| Config.component_classes[e.to_sym] }
19
21
  end
20
22
 
21
23
  def component_config(component_name)
22
- return @config['components'][component_name.to_s]
24
+ @config['components'][component_name.to_s]
23
25
  end
24
26
 
25
- private
26
-
27
- def component_classes
28
- return {
27
+ def self.component_classes
28
+ {
29
29
  ascii_text_art: ASCIITextArt,
30
30
  service_status: ServiceStatus,
31
31
  uptime: Uptime,
32
32
  ssl_certificates: SSLCertificates,
33
33
  filesystems: Filesystems,
34
- last_login: LastLogin
34
+ last_login: LastLogin,
35
+ fail_2_ban: Fail2Ban
35
36
  }
36
37
  end
37
38
 
39
+ private
40
+
38
41
  def create_config(file_path)
39
- default_config_path = File.join(File.dirname(__dir__), 'panda_motd', 'default_config.yaml')
42
+ default_config_path = File.join(
43
+ File.dirname(__dir__), 'panda_motd', 'default_config.yaml'
44
+ )
40
45
  FileUtils.cp(default_config_path, file_path)
41
46
  end
42
47
 
@@ -62,6 +62,10 @@ components:
62
62
  # Displays the validity and expiration dates of SSL certificates.
63
63
  #
64
64
  # Settings
65
+ # sort_method: The method used to sort the list of certificates. If no
66
+ # method is specified, the list will be sorted alphabetically. Valid
67
+ # values are 'alphabetical' and 'expiration'. The latter will place
68
+ # the certificates which are most near expiration at the top.
65
69
  # certs: Pairs following the format "PrettyName: absolute_cert_file_path".
66
70
  # The absolute_cert_file_path is the absolute file path of the SSL
67
71
  # certificate. The pretty name is the name that is used in the MOTD to
@@ -70,8 +74,9 @@ components:
70
74
 
71
75
  # ssl_certificates:
72
76
  # enabled: true
77
+ # sort_method: alphabetical
73
78
  # certs:
74
- # taylorjthurlow.com: /etc/letsencrypt/live/taylorjthurlow.com/cert.pem
79
+ # thurlow.io: /etc/letsencrypt/live/thurlow.io/cert.pem
75
80
 
76
81
  #####
77
82
  # Filesystems
@@ -90,6 +95,21 @@ components:
90
95
  # /dev/sda1: Ubuntu
91
96
  # /dev/sdc1: Data
92
97
 
98
+ #####
99
+ # Fail2Ban
100
+ # Displays fail2ban jail statistics.
101
+ #
102
+ # Settings
103
+ # jails: A list of fail2ban jails to obtain statistics from. The name of the
104
+ # jail is the same name used in the `fail2ban-client status jailname`. You
105
+ # will get the total banned and currently banned numbers for each jail.
106
+ #####
107
+
108
+ # fail_2_ban:
109
+ # enabled: true
110
+ # jails:
111
+ # - sshd
112
+ # - anotherjail
93
113
 
94
114
  #####
95
115
  # Last Login
@@ -3,14 +3,14 @@ require 'sysinfo'
3
3
  class MOTD
4
4
  attr_reader :config, :components
5
5
 
6
- def initialize(config_path = nil)
6
+ def initialize(config_path = nil, process = true)
7
7
  @config = config_path ? Config.new(config_path) : Config.new
8
8
  @components = @config.components_enabled.map { |ce| ce.new(self) }
9
- @components.each(&:process)
9
+ @components.each(&:process) if process
10
10
  end
11
11
 
12
12
  def to_s
13
- return @components.map do |c|
13
+ @components.map do |c|
14
14
  if c.errors.any?
15
15
  c.errors.map(&:to_s).join("\n")
16
16
  else
@@ -1,4 +1,4 @@
1
1
  class PandaMOTD
2
2
  #:nodoc:
3
- VERSION ||= '0.0.8'.freeze
3
+ VERSION ||= '0.0.10'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda-motd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taylor Thurlow
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-28 00:00:00.000000000 Z
11
+ date: 2018-11-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: artii
@@ -81,103 +81,131 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0.8'
83
83
  - !ruby/object:Gem::Dependency
84
- name: byebug
84
+ name: factory_bot
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '10.0'
89
+ version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: '10.0'
96
+ version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: guard
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - "~>"
101
+ - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: '2.14'
103
+ version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - "~>"
108
+ - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: '2.14'
110
+ version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: guard-rspec
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - "~>"
115
+ - - ">="
116
116
  - !ruby/object:Gem::Version
117
- version: '4.7'
117
+ version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - "~>"
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: pry
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
123
151
  - !ruby/object:Gem::Version
124
- version: '4.7'
152
+ version: '0'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: rspec
127
155
  requirement: !ruby/object:Gem::Requirement
128
156
  requirements:
129
- - - "~>"
157
+ - - ">="
130
158
  - !ruby/object:Gem::Version
131
- version: '3.7'
159
+ version: '0'
132
160
  type: :development
133
161
  prerelease: false
134
162
  version_requirements: !ruby/object:Gem::Requirement
135
163
  requirements:
136
- - - "~>"
164
+ - - ">="
137
165
  - !ruby/object:Gem::Version
138
- version: '3.7'
166
+ version: '0'
139
167
  - !ruby/object:Gem::Dependency
140
168
  name: rubocop
141
169
  requirement: !ruby/object:Gem::Requirement
142
170
  requirements:
143
- - - "~>"
171
+ - - ">="
144
172
  - !ruby/object:Gem::Version
145
- version: '0.51'
173
+ version: '0'
146
174
  type: :development
147
175
  prerelease: false
148
176
  version_requirements: !ruby/object:Gem::Requirement
149
177
  requirements:
150
- - - "~>"
178
+ - - ">="
151
179
  - !ruby/object:Gem::Version
152
- version: '0.51'
180
+ version: '0'
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: rubocop-rspec
155
183
  requirement: !ruby/object:Gem::Requirement
156
184
  requirements:
157
- - - "~>"
185
+ - - ">="
158
186
  - !ruby/object:Gem::Version
159
- version: '1.25'
187
+ version: '0'
160
188
  type: :development
161
189
  prerelease: false
162
190
  version_requirements: !ruby/object:Gem::Requirement
163
191
  requirements:
164
- - - "~>"
192
+ - - ">="
165
193
  - !ruby/object:Gem::Version
166
- version: '1.25'
194
+ version: '0'
167
195
  - !ruby/object:Gem::Dependency
168
196
  name: simplecov
169
197
  requirement: !ruby/object:Gem::Requirement
170
198
  requirements:
171
- - - "~>"
199
+ - - ">="
172
200
  - !ruby/object:Gem::Version
173
- version: '0.12'
201
+ version: '0'
174
202
  type: :development
175
203
  prerelease: false
176
204
  version_requirements: !ruby/object:Gem::Requirement
177
205
  requirements:
178
- - - "~>"
206
+ - - ">="
179
207
  - !ruby/object:Gem::Version
180
- version: '0.12'
208
+ version: '0'
181
209
  description: Enhance your MOTD with useful at-a-glance information.
182
210
  email: taylorthurlow8@gmail.com
183
211
  executables:
@@ -187,8 +215,10 @@ extra_rdoc_files: []
187
215
  files:
188
216
  - bin/panda-motd
189
217
  - lib/panda_motd.rb
218
+ - lib/panda_motd/component.rb
190
219
  - lib/panda_motd/component_error.rb
191
220
  - lib/panda_motd/components/ascii_text_art.rb
221
+ - lib/panda_motd/components/fail_2_ban.rb
192
222
  - lib/panda_motd/components/filesystems.rb
193
223
  - lib/panda_motd/components/last_login.rb
194
224
  - lib/panda_motd/components/service_status.rb
@@ -218,7 +248,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
218
248
  version: '0'
219
249
  requirements: []
220
250
  rubyforge_project:
221
- rubygems_version: 2.7.6
251
+ rubygems_version: 2.7.8
222
252
  signing_key:
223
253
  specification_version: 4
224
254
  summary: Make your MOTD prettier, and more useful.