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 +4 -4
- data/lib/panda_motd.rb +5 -1
- data/lib/panda_motd/component.rb +26 -0
- data/lib/panda_motd/components/ascii_text_art.rb +8 -16
- data/lib/panda_motd/components/fail_2_ban.rb +44 -0
- data/lib/panda_motd/components/filesystems.rb +86 -62
- data/lib/panda_motd/components/last_login.rb +52 -48
- data/lib/panda_motd/components/service_status.rb +13 -14
- data/lib/panda_motd/components/ssl_certificates.rb +53 -31
- data/lib/panda_motd/components/uptime.rb +3 -7
- data/lib/panda_motd/config.rb +16 -11
- data/lib/panda_motd/default_config.yaml +21 -1
- data/lib/panda_motd/motd.rb +3 -3
- data/lib/panda_motd/version.rb +1 -1
- metadata +62 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7036e8cca753cf96292eb0c4dd7bb29524852e486265b5de0403089af3f7897d
|
4
|
+
data.tar.gz: c9237796bc85552104a451801e202a0bd57e139f4c2bafc2cea9d2275ff92291
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8336450885b5e4f51d1afe19c75a18f3c3eca1928cc1ce9967287933960f0e695c2f6d2908ad3820e0cfcd705ade94388c9aa9657a9a894a0a1ee8d59090df03
|
7
|
+
data.tar.gz: f5b94c6be1aa9859735f0aa3eddeba09283c76664930127724ab628f29a82e917fbb1f63494ffa626514e82fd4e33891bbe607c7fa6222d506ec259d384a0e85
|
data/lib/panda_motd.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
-
|
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 }
|
21
|
-
|
14
|
+
name_col_size = @results.select { |r| r.is_a? Hash }
|
15
|
+
.map { |r| r[:pretty_name].length }
|
16
|
+
.max || 0
|
22
17
|
|
23
|
-
|
18
|
+
size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
|
24
19
|
|
25
|
-
result = 'Filesystems'.ljust(
|
20
|
+
result = 'Filesystems'.ljust(size_w_padding, ' ')
|
26
21
|
result += "Size Used Free Use%\n"
|
27
22
|
|
28
23
|
@results.each do |filesystem|
|
29
|
-
|
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
|
-
|
27
|
+
result
|
78
28
|
end
|
79
29
|
|
80
30
|
private
|
81
31
|
|
82
|
-
def
|
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
|
-
|
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
|
-
<<~
|
14
|
+
<<~HEREDOC
|
20
15
|
Last Login:
|
21
|
-
#{@results.map
|
22
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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 { |
|
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
|
-
|
44
|
+
{
|
46
45
|
active: :green,
|
47
46
|
inactive: :red
|
48
47
|
}
|
49
48
|
end
|
50
49
|
|
51
50
|
def valid_responses
|
52
|
-
|
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
|
-
|
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
|
-
#{
|
16
|
+
#{sorted_results.map do |cert|
|
23
17
|
return " #{cert}" if cert.is_a? String # print the not found message
|
24
18
|
|
25
|
-
|
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
|
-
|
50
|
+
certs.map do |name, path|
|
38
51
|
if File.exist?(path)
|
39
|
-
|
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 (
|
80
|
+
if (Time.now...Time.now + 30).cover? expiry_date # ... range excludes last
|
59
81
|
:expiring
|
60
|
-
elsif
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
19
|
+
"#{@config['prefix'] || 'up'} #{format_uptime}"
|
24
20
|
end
|
25
21
|
|
26
22
|
private
|
data/lib/panda_motd/config.rb
CHANGED
@@ -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
|
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
|
-
#
|
16
|
-
|
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
|
-
|
20
|
+
enabled.map { |e| Config.component_classes[e.to_sym] }
|
19
21
|
end
|
20
22
|
|
21
23
|
def component_config(component_name)
|
22
|
-
|
24
|
+
@config['components'][component_name.to_s]
|
23
25
|
end
|
24
26
|
|
25
|
-
|
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(
|
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
|
-
#
|
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
|
data/lib/panda_motd/motd.rb
CHANGED
@@ -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
|
-
|
13
|
+
@components.map do |c|
|
14
14
|
if c.errors.any?
|
15
15
|
c.errors.map(&:to_s).join("\n")
|
16
16
|
else
|
data/lib/panda_motd/version.rb
CHANGED
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.
|
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-
|
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:
|
84
|
+
name: factory_bot
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
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: '
|
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: '
|
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: '
|
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: '
|
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: '
|
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: '
|
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: '
|
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
|
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
|
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: '
|
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: '
|
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
|
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
|
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.
|
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.
|