panda-motd 0.0.11 → 0.0.12
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 +5 -5
- data/bin/panda-motd +1 -1
- data/lib/panda_motd.rb +9 -4
- data/lib/panda_motd/component.rb +11 -2
- data/lib/panda_motd/component_error.rb +4 -1
- data/lib/panda_motd/components/ascii_text_art.rb +9 -7
- data/lib/panda_motd/components/fail_2_ban.rb +11 -9
- data/lib/panda_motd/components/filesystems.rb +33 -29
- data/lib/panda_motd/components/last_login.rb +43 -13
- data/lib/panda_motd/components/service_status.rb +35 -10
- data/lib/panda_motd/components/ssl_certificates.rb +64 -20
- data/lib/panda_motd/components/uptime.rb +22 -5
- data/lib/panda_motd/config.rb +25 -8
- data/lib/panda_motd/motd.rb +15 -2
- data/lib/panda_motd/version.rb +3 -1
- metadata +51 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e27df79cdab81d15c44ff575075e876eb975b44
|
4
|
+
data.tar.gz: 954bf7eb0ff70fd62b86918dfe1cbd95c84d56bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af66d7e99d7396984468e5824b1902b1e595aea9022a27c1c128df98416f386c89f23f1c5415aca4da26045af458e4b6e5b5844111a3488aabb1e15270d79351
|
7
|
+
data.tar.gz: 306ec97a05b35bfb2a0418018fbfc1b627c0e69007bef88ac7932b29bb319418637f416ec2c3903de1ddfcd44f29e6abe413944b153ec85fc04669177ffbd4fc
|
data/bin/panda-motd
CHANGED
data/lib/panda_motd.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "require_all"
|
4
|
+
require_rel "panda_motd"
|
3
5
|
|
4
6
|
class PandaMOTD
|
7
|
+
# Creates a new MOTD instance, assuming a config file has been passed as an
|
8
|
+
# argument to the command.
|
5
9
|
def self.new_motd
|
6
10
|
if ARGV[0].nil?
|
7
|
-
puts
|
11
|
+
puts "You must provide a config file path as an argument to panda-motd."
|
8
12
|
else
|
9
13
|
MOTD.new(ARGV[0])
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
17
|
+
# Gets the root path of the gem.
|
13
18
|
def self.root
|
14
|
-
File.expand_path(
|
19
|
+
File.expand_path("..", __dir__)
|
15
20
|
end
|
16
21
|
end
|
data/lib/panda_motd/component.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Component
|
2
4
|
attr_reader :name, :errors, :results, :config
|
3
5
|
|
@@ -8,19 +10,26 @@ class Component
|
|
8
10
|
@errors = []
|
9
11
|
end
|
10
12
|
|
13
|
+
# Evaluates the component so that it has some meaningful output when it comes
|
14
|
+
# time to print the MOTD.
|
11
15
|
def process
|
12
16
|
raise NotImplementedError
|
13
17
|
end
|
14
18
|
|
19
|
+
# Gives the output of a component as a string.
|
15
20
|
def to_s
|
16
21
|
raise NotImplementedError
|
17
22
|
end
|
18
23
|
|
24
|
+
# The number of lines to print before the component in the context of the
|
25
|
+
# entire MOTD. 1 by default, if not configured.
|
19
26
|
def lines_before
|
20
|
-
@motd.config.component_config(@name)[
|
27
|
+
@motd.config.component_config(@name)["lines_before"] || 1
|
21
28
|
end
|
22
29
|
|
30
|
+
# The number of lines to print after the component in the context of the
|
31
|
+
# entire MOTD. 1 by default, if not configured.
|
23
32
|
def lines_after
|
24
|
-
@motd.config.component_config(@name)[
|
33
|
+
@motd.config.component_config(@name)["lines_after"] || 1
|
25
34
|
end
|
26
35
|
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
2
4
|
|
3
5
|
class ComponentError
|
4
6
|
attr_reader :component, :message
|
@@ -8,6 +10,7 @@ class ComponentError
|
|
8
10
|
@message = message
|
9
11
|
end
|
10
12
|
|
13
|
+
# Gets a printable error string in red.
|
11
14
|
def to_s
|
12
15
|
return "#{@component.name} error: ".red + @message.to_s
|
13
16
|
end
|
@@ -1,18 +1,20 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "artii"
|
4
|
+
require "colorize"
|
3
5
|
|
4
6
|
class ASCIITextArt < Component
|
5
7
|
def initialize(motd)
|
6
|
-
super(motd,
|
8
|
+
super(motd, "ascii_text_art")
|
7
9
|
end
|
8
10
|
|
9
11
|
def process
|
10
|
-
@text = `#{@config[
|
11
|
-
@art = Artii::Base.new font: @config[
|
12
|
+
@text = `#{@config["command"]}`
|
13
|
+
@art = Artii::Base.new font: @config["font"]
|
12
14
|
@results = @art.asciify(@text)
|
13
|
-
@results = @results.colorize(@config[
|
15
|
+
@results = @results.colorize(@config["color"].to_sym) if @config["color"]
|
14
16
|
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
15
|
-
@errors << ComponentError.new(self,
|
17
|
+
@errors << ComponentError.new(self, "Invalid font name")
|
16
18
|
end
|
17
19
|
|
18
20
|
def to_s
|
@@ -1,31 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Fail2Ban < Component
|
2
4
|
def initialize(motd)
|
3
|
-
super(motd,
|
5
|
+
super(motd, "fail_2_ban")
|
4
6
|
end
|
5
7
|
|
6
8
|
def process
|
7
9
|
@results = {
|
8
|
-
jails: {}
|
10
|
+
jails: {},
|
9
11
|
}
|
10
12
|
|
11
|
-
@config[
|
13
|
+
@config["jails"].each do |jail|
|
12
14
|
status = jail_status(jail)
|
13
15
|
@results[:jails][jail] = {
|
14
16
|
total: status[:total],
|
15
|
-
current: status[:current]
|
17
|
+
current: status[:current],
|
16
18
|
}
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def to_s
|
21
|
-
result = "Fail2Ban:\n"
|
23
|
+
result = +"Fail2Ban:\n"
|
22
24
|
@results[:jails].each do |name, stats|
|
23
|
-
result
|
24
|
-
result
|
25
|
-
result
|
25
|
+
result << " #{name}:\n"
|
26
|
+
result << " Total bans: #{stats[:total]}\n"
|
27
|
+
result << " Current bans: #{stats[:current]}\n"
|
26
28
|
end
|
27
29
|
|
28
|
-
result.gsub(/\s$/,
|
30
|
+
result.gsub!(/\s$/, "")
|
29
31
|
end
|
30
32
|
|
31
33
|
private
|
@@ -1,13 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby-units"
|
4
|
+
require "colorize"
|
3
5
|
|
4
6
|
class Filesystems < Component
|
5
7
|
def initialize(motd)
|
6
|
-
super(motd,
|
8
|
+
super(motd, "filesystems")
|
7
9
|
end
|
8
10
|
|
9
11
|
def process
|
10
|
-
@results = parse_filesystem_usage(@config[
|
12
|
+
@results = parse_filesystem_usage(@config["filesystems"])
|
11
13
|
end
|
12
14
|
|
13
15
|
def to_s
|
@@ -17,11 +19,11 @@ class Filesystems < Component
|
|
17
19
|
|
18
20
|
size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
|
19
21
|
|
20
|
-
result =
|
21
|
-
result
|
22
|
+
result = +"Filesystems".ljust(size_w_padding, " ")
|
23
|
+
result << "Size Used Free Use%\n"
|
22
24
|
|
23
25
|
@results.each do |filesystem|
|
24
|
-
result
|
26
|
+
result << format_filesystem(filesystem, size_w_padding)
|
25
27
|
end
|
26
28
|
|
27
29
|
result
|
@@ -33,30 +35,31 @@ class Filesystems < Component
|
|
33
35
|
return " #{filesystem}\n" if filesystem.is_a? String # handle fs not found
|
34
36
|
|
35
37
|
# filesystem name
|
36
|
-
result =
|
37
|
-
result
|
38
|
+
result = +""
|
39
|
+
result << " #{filesystem[:pretty_name]}".ljust(size, " ")
|
38
40
|
|
39
41
|
# statistics (size, used, free, use%)
|
40
42
|
[:size, :used, :avail].each do |metric|
|
41
|
-
result
|
43
|
+
result << format_metric(filesystem, metric)
|
42
44
|
end
|
45
|
+
|
43
46
|
percent_used = calc_percent_used(filesystem)
|
44
|
-
result
|
45
|
-
result
|
47
|
+
result << format_percent_used(percent_used)
|
48
|
+
result << "\n"
|
46
49
|
|
47
50
|
# visual bar representation of use%
|
48
|
-
result
|
51
|
+
result << generate_usage_bar(filesystem, size, percent_used)
|
49
52
|
|
50
53
|
result
|
51
54
|
end
|
52
55
|
|
53
56
|
def generate_usage_bar(filesystem, size, percent_used)
|
54
|
-
result =
|
57
|
+
result = +""
|
55
58
|
total_ticks = size + 18
|
56
59
|
used_ticks = (total_ticks * (percent_used.to_f / 100)).round
|
57
|
-
result
|
58
|
-
"#{(
|
59
|
-
result
|
60
|
+
result << " [#{("=" * used_ticks).send(pct_color(percent_used))}" \
|
61
|
+
"#{("=" * (total_ticks - used_ticks)).light_black}]"
|
62
|
+
result << "\n" unless filesystem == @results.last
|
60
63
|
result
|
61
64
|
end
|
62
65
|
|
@@ -74,7 +77,7 @@ class Filesystems < Component
|
|
74
77
|
end
|
75
78
|
|
76
79
|
def format_percent_used(percent_used)
|
77
|
-
(percent_used.to_s.rjust(3,
|
80
|
+
(percent_used.to_s.rjust(3, " ") + "%").send(pct_color(percent_used))
|
78
81
|
end
|
79
82
|
|
80
83
|
def calc_metric(value)
|
@@ -96,20 +99,21 @@ class Filesystems < Component
|
|
96
99
|
whole_number_length = value.scalar.floor.to_s.length
|
97
100
|
round_amount = whole_number_length > 1 ? 0 : 1
|
98
101
|
formatted = value.scalar.round(round_amount).to_s + value.units[0].upcase
|
99
|
-
|
102
|
+
|
103
|
+
formatted.rjust(4, " ") + " "
|
100
104
|
end
|
101
105
|
|
102
106
|
def calc_units(value)
|
103
|
-
if value > 10**12
|
104
|
-
|
105
|
-
elsif value > 10**9
|
106
|
-
|
107
|
-
elsif value > 10**6
|
108
|
-
|
109
|
-
elsif value > 10**3
|
110
|
-
|
107
|
+
if value > 10 ** 12
|
108
|
+
"terabytes"
|
109
|
+
elsif value > 10 ** 9
|
110
|
+
"gigabytes"
|
111
|
+
elsif value > 10 ** 6
|
112
|
+
"megabytes"
|
113
|
+
elsif value > 10 ** 3
|
114
|
+
"kilobytes"
|
111
115
|
else
|
112
|
-
|
116
|
+
"bytes"
|
113
117
|
end
|
114
118
|
end
|
115
119
|
|
@@ -126,7 +130,7 @@ class Filesystems < Component
|
|
126
130
|
filesystem_name: filesystem_name,
|
127
131
|
size: size.to_i * 1024,
|
128
132
|
used: used.to_i * 1024,
|
129
|
-
avail: avail.to_i * 1024
|
133
|
+
avail: avail.to_i * 1024,
|
130
134
|
}
|
131
135
|
end
|
132
136
|
end
|
@@ -1,15 +1,19 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
2
4
|
|
3
5
|
class LastLogin < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "last_login")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@users = @config[
|
10
|
-
@results = parse_last_logins
|
12
|
+
@users = @config["users"]
|
13
|
+
@results = parse_last_logins
|
11
14
|
end
|
12
15
|
|
16
|
+
# @see Component#to_s
|
13
17
|
def to_s
|
14
18
|
<<~HEREDOC
|
15
19
|
Last Login:
|
@@ -19,9 +23,17 @@ class LastLogin < Component
|
|
19
23
|
|
20
24
|
private
|
21
25
|
|
26
|
+
# Takes the list of processed results and generates a complete printable
|
27
|
+
# component string.
|
28
|
+
#
|
29
|
+
# @param user [String] the username to generate a string for
|
30
|
+
# @param logins [Array<Hash>] an array of hashes with username keys and hash
|
31
|
+
# values containing the login data (see {#hashify_login})
|
32
|
+
#
|
33
|
+
# @return [String] the printable string for a single user
|
22
34
|
def parse_result(user, logins)
|
23
35
|
logins_part = if logins.empty?
|
24
|
-
|
36
|
+
" no logins found for user."
|
25
37
|
else
|
26
38
|
longest_size = logins.map { |l| l[:location].length }.max
|
27
39
|
logins.map { |l| parse_login(l, longest_size) }.join("\n")
|
@@ -32,11 +44,18 @@ class LastLogin < Component
|
|
32
44
|
HEREDOC
|
33
45
|
end
|
34
46
|
|
47
|
+
# Takes login data and converts it to a heplful printable string.
|
48
|
+
#
|
49
|
+
# @param login [Hash] the login data, see {#hashify_login}
|
50
|
+
# @param longest_size [Integer] the longest string length to help with string
|
51
|
+
# formatting
|
52
|
+
#
|
53
|
+
# @return [String] the formatted string for printing
|
35
54
|
def parse_login(login, longest_size)
|
36
|
-
location = login[:location].ljust(longest_size,
|
37
|
-
start = login[:time_start].strftime(
|
55
|
+
location = login[:location].ljust(longest_size, " ")
|
56
|
+
start = login[:time_start].strftime("%m/%d/%Y %I:%M%p")
|
38
57
|
finish = if login[:time_end].is_a? String # not a date
|
39
|
-
if login[:time_end] ==
|
58
|
+
if login[:time_end] == "still logged in"
|
40
59
|
login[:time_end].green
|
41
60
|
else
|
42
61
|
login[:time_end].yellow
|
@@ -47,8 +66,12 @@ class LastLogin < Component
|
|
47
66
|
" from #{location} at #{start} (#{finish})"
|
48
67
|
end
|
49
68
|
|
50
|
-
|
51
|
-
|
69
|
+
# Takes a list of configured usernames and grabs login data from the system.
|
70
|
+
#
|
71
|
+
# @return [Hash{Symbol => Hash}] a hash with username keys and hash values
|
72
|
+
# containing the login data (see {#hashify_login})
|
73
|
+
def parse_last_logins
|
74
|
+
@users.map do |(username, num_logins)|
|
52
75
|
cmd_result = `last --time-format=iso #{username}`
|
53
76
|
logins = cmd_result.lines
|
54
77
|
.select { |entry| entry.start_with?(username) }
|
@@ -58,6 +81,13 @@ class LastLogin < Component
|
|
58
81
|
end.to_h
|
59
82
|
end
|
60
83
|
|
84
|
+
# A hash representation of a single login.
|
85
|
+
#
|
86
|
+
# @param login [String] the raw string result from the call to the system
|
87
|
+
# containing various bits of login information
|
88
|
+
# @param username [String] the username of the logged user
|
89
|
+
#
|
90
|
+
# @return [Hash] the parsed login entry data, with symbol keys
|
61
91
|
def hashify_login(login, username)
|
62
92
|
re = login.chomp.split(/(?:\s{2,})|(?<=\d)(?:\s-\s)/)
|
63
93
|
date = re[4].scan(/\d{4}-\d{2}-[\dT:]+-\d{4}/)
|
@@ -65,10 +95,10 @@ class LastLogin < Component
|
|
65
95
|
time_end = date.any? ? Time.parse(re[4]) : re[4]
|
66
96
|
|
67
97
|
{
|
68
|
-
username: username,
|
69
|
-
location: re[2],
|
98
|
+
username: username, # string username
|
99
|
+
location: re[2], # string login location, an IP address
|
70
100
|
time_start: Time.parse(re[3]),
|
71
|
-
time_end: time_end
|
101
|
+
time_end: time_end, # Time or string, could be "still logged in"
|
72
102
|
}
|
73
103
|
end
|
74
104
|
end
|
@@ -1,15 +1,20 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
2
4
|
|
3
5
|
class ServiceStatus < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "service_status")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@services = @config[
|
12
|
+
@services = @config["services"]
|
10
13
|
@results = parse_services(@services)
|
11
14
|
end
|
12
15
|
|
16
|
+
# Gets a printable string to be printed in the MOTD. If there are no services
|
17
|
+
# found in the result, it prints a warning message.
|
13
18
|
def to_s
|
14
19
|
return "Services:\n No matching services found." unless @results.any?
|
15
20
|
|
@@ -17,29 +22,49 @@ class ServiceStatus < Component
|
|
17
22
|
result = <<~HEREDOC
|
18
23
|
Services:
|
19
24
|
#{@results.map do |(name, status)|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
25
|
+
spaces = (" " * (longest_name_size - name.to_s.length + 1))
|
26
|
+
status_part = status.to_s.colorize(service_colors[status.to_sym])
|
27
|
+
" #{name}#{spaces}#{status_part}"
|
28
|
+
end.join("\n")}
|
24
29
|
HEREDOC
|
25
30
|
|
26
|
-
result.gsub(/\s$/,
|
31
|
+
result.gsub(/\s$/, "")
|
27
32
|
end
|
28
33
|
|
29
34
|
private
|
30
35
|
|
36
|
+
# Runs a `systemd` command to determine the state of a service. If the state
|
37
|
+
# of the service was unable to be determined, an error will be added to the
|
38
|
+
# component.
|
39
|
+
#
|
40
|
+
# @param service [String] the name of the systemd service
|
41
|
+
#
|
42
|
+
# @return [String] the state of the systemd service
|
31
43
|
def parse_service(service)
|
32
|
-
cmd_result = `
|
44
|
+
cmd_result = `systemd is-active #{service[0]}`.strip
|
33
45
|
if cmd_result.empty?
|
34
|
-
@errors << ComponentError.new(self,
|
46
|
+
@errors << ComponentError.new(self, "systemctl output was blank.")
|
35
47
|
end
|
36
48
|
cmd_result
|
37
49
|
end
|
38
50
|
|
51
|
+
# Takes a list of services from a configuration file, and turns them into a
|
52
|
+
# hash with the service states as values.
|
53
|
+
#
|
54
|
+
# @param services [Array] a two-element array where the first element is the
|
55
|
+
# name of the systemd service, and the second is the pretty name that
|
56
|
+
# represents it.
|
57
|
+
#
|
58
|
+
# @return [Hash]
|
59
|
+
# * `key`: The symbolized name of the systemd service
|
60
|
+
# * `value`: The symbolized service state
|
39
61
|
def parse_services(services)
|
40
62
|
services.map { |s| [s[1].to_sym, parse_service(s).to_sym] }.to_h
|
41
63
|
end
|
42
64
|
|
65
|
+
# A hash of mappings between a service state and a color which represents it.
|
66
|
+
# The hash has a default value of red in order to handle unexpected service
|
67
|
+
# status strings returned by `systemctl`.
|
43
68
|
def service_colors
|
44
69
|
colors = Hash.new(:red)
|
45
70
|
colors[:active] = :green
|
@@ -1,51 +1,77 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
2
4
|
|
3
5
|
class SSLCertificates < Component
|
4
6
|
def initialize(motd)
|
5
|
-
super(motd,
|
7
|
+
super(motd, "ssl_certificates")
|
6
8
|
end
|
7
9
|
|
10
|
+
# @see Component#process
|
8
11
|
def process
|
9
|
-
@certs = @config[
|
12
|
+
@certs = @config["certs"]
|
10
13
|
@results = cert_dates(@certs)
|
11
14
|
end
|
12
15
|
|
16
|
+
# Prints the list of SSL certificates with their statuses. If a certificate
|
17
|
+
# is not found at the configured location, a message will be printed which
|
18
|
+
# explains this.
|
13
19
|
def to_s
|
14
20
|
<<~HEREDOC
|
15
21
|
SSL Certificates:
|
16
22
|
#{sorted_results.map do |cert|
|
17
|
-
|
23
|
+
return " #{cert}" if cert.is_a? String # print the not found message
|
18
24
|
|
19
|
-
|
20
|
-
|
25
|
+
parse_cert(cert)
|
26
|
+
end.join("\n")}
|
21
27
|
HEREDOC
|
22
28
|
end
|
23
29
|
|
24
30
|
private
|
25
31
|
|
32
|
+
# Takes an entry from `@results` and formats it in a way that is conducive
|
33
|
+
# to being printed in the context of the MOTD.
|
34
|
+
#
|
35
|
+
# @param cert [Array] a two-element array in the same format as the return
|
36
|
+
# value of {#parse_result}
|
26
37
|
def parse_cert(cert)
|
27
|
-
name_portion = cert[0].ljust(longest_cert_name_length + 6,
|
38
|
+
name_portion = cert[0].ljust(longest_cert_name_length + 6, " ")
|
28
39
|
status_sym = cert_status(cert[1])
|
29
40
|
status = cert_status_strings[status_sym].to_s
|
30
41
|
colorized_status = status.colorize(cert_status_colors[status_sym])
|
31
|
-
date_portion = cert[1].strftime(
|
42
|
+
date_portion = cert[1].strftime("%e %b %Y %H:%M:%S%p")
|
32
43
|
" #{name_portion} #{colorized_status} #{date_portion}"
|
33
44
|
end
|
34
45
|
|
46
|
+
# Determines the length of the longest SSL certificate name for use in
|
47
|
+
# formatting the output of the component.
|
48
|
+
#
|
49
|
+
# @return [Integer] the length of the longest certificate name
|
35
50
|
def longest_cert_name_length
|
36
51
|
@results.map { |r| r[0].length }.max
|
37
52
|
end
|
38
53
|
|
54
|
+
# Takes the results array and sorts it according to the configured sort
|
55
|
+
# method. If the option is not set or is set improperly, it will default to
|
56
|
+
# alphabetical.
|
39
57
|
def sorted_results
|
40
|
-
if @config[
|
58
|
+
if @config["sort_method"] == "alphabetical"
|
41
59
|
@results.sort_by { |c| c[0] }
|
42
|
-
elsif @config[
|
60
|
+
elsif @config["sort_method"] == "expiration"
|
43
61
|
@results.sort_by { |c| c[1] }
|
44
62
|
else # default to alphabetical
|
45
63
|
@results.sort_by { |c| c[0] }
|
46
64
|
end
|
47
65
|
end
|
48
66
|
|
67
|
+
# Takes a list of certificates and compiles a list of results for each
|
68
|
+
# certificate. If a certificate was not found, a notice will be returned
|
69
|
+
# instead.
|
70
|
+
#
|
71
|
+
# @return [Array] An array of parsed results. If there was an error, the
|
72
|
+
# element will be just a string. If it was successful, the element will be
|
73
|
+
# another two-element array in the same format as the return value of
|
74
|
+
# {#parse_result}.
|
49
75
|
def cert_dates(certs)
|
50
76
|
certs.map do |name, path|
|
51
77
|
if File.exist?(path)
|
@@ -56,26 +82,41 @@ class SSLCertificates < Component
|
|
56
82
|
end.compact # remove nil entries, will have nil if error ocurred
|
57
83
|
end
|
58
84
|
|
85
|
+
# Uses `openssl` to obtain and parse and expiration date for the certificate.
|
86
|
+
#
|
87
|
+
# @param name [String] the name of the SSL certificate
|
88
|
+
# @param path [String] the file path to the SSL certificate
|
89
|
+
#
|
90
|
+
# @return [Array] A pair where the first element is the configured name of
|
91
|
+
# the SSL certificate, and the second element is the expiration date of
|
92
|
+
# the certificate.
|
59
93
|
def parse_result(name, path)
|
60
94
|
cmd_result = `openssl x509 -in #{path} -dates`
|
61
95
|
# 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/
|
96
|
+
exp = /notAfter=([A-Za-z]+) +(\d+) +([\d:]+) +(\d{4}) +([A-Za-z]+)\n/
|
63
97
|
parsed = cmd_result.match(exp)
|
64
98
|
|
65
99
|
if parsed.nil?
|
66
|
-
@errors << ComponentError.new(self,
|
67
|
-
|
100
|
+
@errors << ComponentError.new(self, "Unable to find certificate " \
|
101
|
+
"expiration date")
|
68
102
|
nil
|
69
103
|
else
|
70
|
-
expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(
|
104
|
+
expiry_date = Time.parse([1, 2, 4, 3, 5].map { |n| parsed[n] }.join(" "))
|
71
105
|
[name, expiry_date]
|
72
106
|
end
|
73
107
|
rescue ArgumentError
|
74
|
-
@errors << ComponentError.new(self,
|
75
|
-
|
108
|
+
@errors << ComponentError.new(self, "Found expiration date, but unable " \
|
109
|
+
"to parse as date")
|
76
110
|
[name, Time.now]
|
77
111
|
end
|
78
112
|
|
113
|
+
# Maps an expiration date to a symbol representing the expiration status of
|
114
|
+
# an SSL certificate.
|
115
|
+
#
|
116
|
+
# @param expiry_date [Time] the time at which the certificate expires
|
117
|
+
#
|
118
|
+
# @return [Symbol] A symbol representing the expiration status of the
|
119
|
+
# certificate. Valid values are `:expiring`, `:expired`, and `:valid`.
|
79
120
|
def cert_status(expiry_date)
|
80
121
|
if (Time.now...Time.now + 30).cover? expiry_date # ... range excludes last
|
81
122
|
:expiring
|
@@ -86,19 +127,22 @@ class SSLCertificates < Component
|
|
86
127
|
end
|
87
128
|
end
|
88
129
|
|
130
|
+
# Maps a certificate expiration status to a color that represents it.
|
89
131
|
def cert_status_colors
|
90
132
|
{
|
91
133
|
valid: :green,
|
92
134
|
expiring: :yellow,
|
93
|
-
expired: :red
|
135
|
+
expired: :red,
|
94
136
|
}
|
95
137
|
end
|
96
138
|
|
139
|
+
# Maps a certificate expiration status to a string which can be prefixed to
|
140
|
+
# the expiration date, to aid in explaining when the certificate expires.
|
97
141
|
def cert_status_strings
|
98
142
|
{
|
99
|
-
valid:
|
100
|
-
expiring:
|
101
|
-
expired:
|
143
|
+
valid: "valid until",
|
144
|
+
expiring: "expiring at",
|
145
|
+
expired: "expired at",
|
102
146
|
}
|
103
147
|
end
|
104
148
|
end
|
@@ -1,12 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sysinfo"
|
2
4
|
|
3
5
|
class Uptime < Component
|
4
6
|
attr_reader :days, :hours, :minutes
|
5
7
|
|
6
8
|
def initialize(motd)
|
7
|
-
super(motd,
|
9
|
+
super(motd, "uptime")
|
8
10
|
end
|
9
11
|
|
12
|
+
# Calculates the number of days, hours, and minutes based on the current
|
13
|
+
# uptime value.
|
14
|
+
#
|
15
|
+
# @see Component#process
|
10
16
|
def process
|
11
17
|
uptime = SysInfo.new.uptime
|
12
18
|
|
@@ -15,16 +21,27 @@ class Uptime < Component
|
|
15
21
|
@minutes = ((uptime - @days * 24 - hours) * 60).floor
|
16
22
|
end
|
17
23
|
|
24
|
+
# Gets a printable uptime string with a prefix. The prefix can be configured,
|
25
|
+
# and defaults to "up".
|
18
26
|
def to_s
|
19
|
-
"#{@config[
|
27
|
+
"#{@config["prefix"] || "up"} #{format_uptime}"
|
20
28
|
end
|
21
29
|
|
22
30
|
private
|
23
31
|
|
32
|
+
# Formats the uptime values in such a way that it is easier to read. If any
|
33
|
+
# of the measurements are zero, that part is omitted. Words are properly
|
34
|
+
# pluralized.
|
35
|
+
#
|
36
|
+
# Examples:
|
37
|
+
#
|
38
|
+
# `3d 20h 55m` becomes `3 days, 20 hours, 55 minutes`
|
39
|
+
#
|
40
|
+
# `3d 0h 55m` becomes `3 days, 55 minutes`
|
24
41
|
def format_uptime
|
25
42
|
[@days, @hours, @minutes].zip(%w[day hour minute])
|
26
43
|
.reject { |n, _word| n.zero? }
|
27
|
-
.map { |n, word| "#{n} #{word}#{
|
28
|
-
.join(
|
44
|
+
.map { |n, word| "#{n} #{word}#{"s" if n != 1}" }
|
45
|
+
.join(", ")
|
29
46
|
end
|
30
47
|
end
|
data/lib/panda_motd/config.rb
CHANGED
@@ -1,11 +1,15 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
require "fileutils"
|
3
5
|
|
4
6
|
class Config
|
5
7
|
attr_reader :file_path
|
6
8
|
|
9
|
+
# @param file_path [String] The file path to look for the configuration file.
|
10
|
+
# If not provided, the default file path will be used.
|
7
11
|
def initialize(file_path = nil)
|
8
|
-
@file_path = file_path || File.join(Dir.home,
|
12
|
+
@file_path = file_path || File.join(Dir.home, ".config", "panda-motd.yaml")
|
9
13
|
unless File.exist?(@file_path)
|
10
14
|
create_config(@file_path)
|
11
15
|
puts "panda-motd created a default config file at: #{@file_path}"
|
@@ -13,17 +17,23 @@ class Config
|
|
13
17
|
load_config(@file_path)
|
14
18
|
end
|
15
19
|
|
20
|
+
# A list of enabled components' class constants.
|
16
21
|
def components_enabled
|
17
22
|
# iterate config hash and grab names of enabled components
|
18
|
-
enabled = @config[
|
23
|
+
enabled = @config["components"].map { |c, s| c if s["enabled"] }.compact
|
19
24
|
# get the class constant
|
20
25
|
enabled.map { |e| Config.component_classes[e.to_sym] }
|
21
26
|
end
|
22
27
|
|
28
|
+
# Gets the configuration for a component.
|
29
|
+
#
|
30
|
+
# @param component_name [String] the name of the component to fetch the
|
31
|
+
# configuration for
|
23
32
|
def component_config(component_name)
|
24
|
-
@config[
|
33
|
+
@config["components"][component_name.to_s]
|
25
34
|
end
|
26
35
|
|
36
|
+
# The mapping of component string names to class constants.
|
27
37
|
def self.component_classes
|
28
38
|
{
|
29
39
|
ascii_text_art: ASCIITextArt,
|
@@ -32,21 +42,28 @@ class Config
|
|
32
42
|
ssl_certificates: SSLCertificates,
|
33
43
|
filesystems: Filesystems,
|
34
44
|
last_login: LastLogin,
|
35
|
-
fail_2_ban: Fail2Ban
|
45
|
+
fail_2_ban: Fail2Ban,
|
36
46
|
}
|
37
47
|
end
|
38
48
|
|
39
49
|
private
|
40
50
|
|
51
|
+
# Creates a configuration file at a given file path, from the default
|
52
|
+
# configuration file.
|
53
|
+
#
|
54
|
+
# @param file_path [String] the file path at which to create the config
|
41
55
|
def create_config(file_path)
|
42
56
|
default_config_path = File.join(
|
43
|
-
File.dirname(__dir__),
|
57
|
+
File.dirname(__dir__), "panda_motd", "default_config.yaml"
|
44
58
|
)
|
45
59
|
FileUtils.cp(default_config_path, file_path)
|
46
60
|
end
|
47
61
|
|
62
|
+
# Loads a configuration file.
|
63
|
+
#
|
64
|
+
# @param file_path [String] the file path of the config to load
|
48
65
|
def load_config(file_path)
|
49
66
|
@config = YAML.safe_load(File.read(file_path))
|
50
|
-
@config[
|
67
|
+
@config["components"] = [] if @config["components"].nil?
|
51
68
|
end
|
52
69
|
end
|
data/lib/panda_motd/motd.rb
CHANGED
@@ -1,14 +1,27 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sysinfo"
|
2
4
|
|
3
5
|
class MOTD
|
4
6
|
attr_reader :config, :components
|
5
7
|
|
8
|
+
# Creates an MOTD by parsing the provided config file, and processing each
|
9
|
+
# component.
|
10
|
+
#
|
11
|
+
# @param config_path [String] The path to the configuration file. If not
|
12
|
+
# provided, the default config path will be used.
|
13
|
+
# @param process [Boolean] whether or not to actually process and evaluate
|
14
|
+
# the printable results of each component
|
6
15
|
def initialize(config_path = nil, process = true)
|
7
|
-
@config =
|
16
|
+
@config = Config.new(config_path)
|
8
17
|
@components = @config.components_enabled.map { |ce| ce.new(self) }
|
9
18
|
@components.each(&:process) if process
|
10
19
|
end
|
11
20
|
|
21
|
+
# Takes each component on the MOTD and joins them together in a printable
|
22
|
+
# format. It inserts two newlines in between each component, ensuring that
|
23
|
+
# there is one empty line between each. If a component has any errors, the
|
24
|
+
# error will be printed in a clean way.
|
12
25
|
def to_s
|
13
26
|
@components.map do |c|
|
14
27
|
if c.errors.any?
|
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.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taylor Thurlow
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: artii
|
@@ -150,6 +150,20 @@ dependencies:
|
|
150
150
|
- - ">="
|
151
151
|
- !ruby/object:Gem::Version
|
152
152
|
version: '0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0'
|
153
167
|
- !ruby/object:Gem::Dependency
|
154
168
|
name: rspec
|
155
169
|
requirement: !ruby/object:Gem::Requirement
|
@@ -192,6 +206,20 @@ dependencies:
|
|
192
206
|
- - ">="
|
193
207
|
- !ruby/object:Gem::Version
|
194
208
|
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: rufo
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - ">="
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - ">="
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '0'
|
195
223
|
- !ruby/object:Gem::Dependency
|
196
224
|
name: simplecov
|
197
225
|
requirement: !ruby/object:Gem::Requirement
|
@@ -206,8 +234,22 @@ dependencies:
|
|
206
234
|
- - ">="
|
207
235
|
- !ruby/object:Gem::Version
|
208
236
|
version: '0'
|
237
|
+
- !ruby/object:Gem::Dependency
|
238
|
+
name: solargraph
|
239
|
+
requirement: !ruby/object:Gem::Requirement
|
240
|
+
requirements:
|
241
|
+
- - ">="
|
242
|
+
- !ruby/object:Gem::Version
|
243
|
+
version: '0'
|
244
|
+
type: :development
|
245
|
+
prerelease: false
|
246
|
+
version_requirements: !ruby/object:Gem::Requirement
|
247
|
+
requirements:
|
248
|
+
- - ">="
|
249
|
+
- !ruby/object:Gem::Version
|
250
|
+
version: '0'
|
209
251
|
description: Enhance your MOTD with useful at-a-glance information.
|
210
|
-
email:
|
252
|
+
email: taylorthurlow@me.com
|
211
253
|
executables:
|
212
254
|
- panda-motd
|
213
255
|
extensions: []
|
@@ -232,7 +274,7 @@ homepage: https://github.com/taylorthurlow/panda-motd
|
|
232
274
|
licenses:
|
233
275
|
- MIT
|
234
276
|
metadata: {}
|
235
|
-
post_install_message:
|
277
|
+
post_install_message:
|
236
278
|
rdoc_options: []
|
237
279
|
require_paths:
|
238
280
|
- lib
|
@@ -240,15 +282,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
240
282
|
requirements:
|
241
283
|
- - ">="
|
242
284
|
- !ruby/object:Gem::Version
|
243
|
-
version: '2.
|
285
|
+
version: '2.4'
|
244
286
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
245
287
|
requirements:
|
246
288
|
- - ">="
|
247
289
|
- !ruby/object:Gem::Version
|
248
290
|
version: '0'
|
249
291
|
requirements: []
|
250
|
-
|
251
|
-
|
292
|
+
rubyforge_project:
|
293
|
+
rubygems_version: 2.6.14.4
|
294
|
+
signing_key:
|
252
295
|
specification_version: 4
|
253
296
|
summary: Make your MOTD prettier, and more useful.
|
254
297
|
test_files: []
|