panda-motd 0.0.8 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
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.