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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: 8d4906b3c8c203b722479456407cb2a14f1cc236e60e096bd819522d6bb5dbac
4
- data.tar.gz: 0aed905391cc7877b3060d4d6f133980afac8e100a9c54be2bd1221160b50417
2
+ SHA1:
3
+ metadata.gz: 5e27df79cdab81d15c44ff575075e876eb975b44
4
+ data.tar.gz: 954bf7eb0ff70fd62b86918dfe1cbd95c84d56bb
5
5
  SHA512:
6
- metadata.gz: 7ae257444243189b4c6000b1151e7f9c3deb0a4a9fa8ce58766ac6c8bfef618d1df747cbe3f913972575116730bf9ceabad49fb1e0226b3c80b361617a943da1
7
- data.tar.gz: bca4470e8c9e2513a794f99af9502b2c5e3dbc4171787bde6f2c66ca3cd4b8ed3fa9478cfdaedbc828b9af6f0025e818b14f2b2823703454aa67efbbb241496e
6
+ metadata.gz: af66d7e99d7396984468e5824b1902b1e595aea9022a27c1c128df98416f386c89f23f1c5415aca4da26045af458e4b6e5b5844111a3488aabb1e15270d79351
7
+ data.tar.gz: 306ec97a05b35bfb2a0418018fbfc1b627c0e69007bef88ac7932b29bb319418637f416ec2c3903de1ddfcd44f29e6abe413944b153ec85fc04669177ffbd4fc
data/bin/panda-motd CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'panda_motd'
3
+ require "panda_motd"
4
4
 
5
5
  motd = PandaMOTD.new_motd
6
6
  puts motd
data/lib/panda_motd.rb CHANGED
@@ -1,16 +1,21 @@
1
- require 'require_all'
2
- require_rel 'panda_motd'
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 'You must provide a config file path as an argument to panda-motd.'
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('..', __dir__)
19
+ File.expand_path("..", __dir__)
15
20
  end
16
21
  end
@@ -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)['lines_before'] || 1
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)['lines_after'] || 1
33
+ @motd.config.component_config(@name)["lines_after"] || 1
25
34
  end
26
35
  end
@@ -1,4 +1,6 @@
1
- require 'colorize'
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
- require 'artii'
2
- require 'colorize'
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, 'ascii_text_art')
8
+ super(motd, "ascii_text_art")
7
9
  end
8
10
 
9
11
  def process
10
- @text = `#{@config['command']}`
11
- @art = Artii::Base.new font: @config['font']
12
+ @text = `#{@config["command"]}`
13
+ @art = Artii::Base.new font: @config["font"]
12
14
  @results = @art.asciify(@text)
13
- @results = @results.colorize(@config['color'].to_sym) if @config['color']
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, 'Invalid font name')
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, 'fail_2_ban')
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['jails'].each do |jail|
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 += " #{name}:\n"
24
- result += " Total bans: #{stats[:total]}\n"
25
- result += " Current bans: #{stats[:current]}\n"
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
- require 'ruby-units'
2
- require 'colorize'
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, 'filesystems')
8
+ super(motd, "filesystems")
7
9
  end
8
10
 
9
11
  def process
10
- @results = parse_filesystem_usage(@config['filesystems'])
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 = 'Filesystems'.ljust(size_w_padding, ' ')
21
- result += "Size Used Free Use%\n"
22
+ result = +"Filesystems".ljust(size_w_padding, " ")
23
+ result << "Size Used Free Use%\n"
22
24
 
23
25
  @results.each do |filesystem|
24
- result += format_filesystem(filesystem, size_w_padding)
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 += " #{filesystem[:pretty_name]}".ljust(size, ' ')
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 += format_metric(filesystem, metric)
43
+ result << format_metric(filesystem, metric)
42
44
  end
45
+
43
46
  percent_used = calc_percent_used(filesystem)
44
- result += format_percent_used(percent_used)
45
- result += "\n"
47
+ result << format_percent_used(percent_used)
48
+ result << "\n"
46
49
 
47
50
  # visual bar representation of use%
48
- result += generate_usage_bar(filesystem, size, percent_used)
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 += " [#{('=' * used_ticks).send(pct_color(percent_used))}"\
58
- "#{('=' * (total_ticks - used_ticks)).light_black}]"
59
- result += "\n" unless filesystem == @results.last
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, ' ') + '%').send(pct_color(percent_used))
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
- formatted.rjust(4, ' ') + ' '
102
+
103
+ formatted.rjust(4, " ") + " "
100
104
  end
101
105
 
102
106
  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'
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
- 'bytes'
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
- require 'date'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
2
4
 
3
5
  class LastLogin < Component
4
6
  def initialize(motd)
5
- super(motd, 'last_login')
7
+ super(motd, "last_login")
6
8
  end
7
9
 
10
+ # @see Component#process
8
11
  def process
9
- @users = @config['users']
10
- @results = parse_last_logins(@users)
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
- ' no logins found for user.'
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('%m/%d/%Y %I:%M%p')
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] == 'still logged in'
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
- def parse_last_logins(users)
51
- users.map do |(username, num_logins)|
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
- require 'colorize'
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
2
4
 
3
5
  class ServiceStatus < Component
4
6
  def initialize(motd)
5
- super(motd, 'service_status')
7
+ super(motd, "service_status")
6
8
  end
7
9
 
10
+ # @see Component#process
8
11
  def process
9
- @services = @config['services']
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
- spaces = (' ' * (longest_name_size - name.to_s.length + 1))
21
- status_part = status.to_s.colorize(service_colors[status.to_sym])
22
- " #{name}#{spaces}#{status_part}"
23
- end.join("\n")}
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 = `systemctl is-active #{service[0]}`.strip
44
+ cmd_result = `systemd is-active #{service[0]}`.strip
33
45
  if cmd_result.empty?
34
- @errors << ComponentError.new(self, 'systemctl output was blank.')
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
- require 'date'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
2
4
 
3
5
  class SSLCertificates < Component
4
6
  def initialize(motd)
5
- super(motd, 'ssl_certificates')
7
+ super(motd, "ssl_certificates")
6
8
  end
7
9
 
10
+ # @see Component#process
8
11
  def process
9
- @certs = @config['certs']
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
- return " #{cert}" if cert.is_a? String # print the not found message
23
+ return " #{cert}" if cert.is_a? String # print the not found message
18
24
 
19
- parse_cert(cert)
20
- end.join("\n")}
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('%e %b %Y %H:%M:%S%p')
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['sort_method'] == 'alphabetical'
58
+ if @config["sort_method"] == "alphabetical"
41
59
  @results.sort_by { |c| c[0] }
42
- elsif @config['sort_method'] == 'expiration'
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, 'Unable to find certificate '\
67
- 'expiration date')
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, 'Found expiration date, but unable '\
75
- 'to parse as date')
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: 'valid until',
100
- expiring: 'expiring at',
101
- expired: 'expired at'
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
- require 'sysinfo'
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, 'uptime')
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['prefix'] || 'up'} #{format_uptime}"
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}#{'s' if n != 1}" }
28
- .join(', ')
44
+ .map { |n, word| "#{n} #{word}#{"s" if n != 1}" }
45
+ .join(", ")
29
46
  end
30
47
  end
@@ -1,11 +1,15 @@
1
- require 'yaml'
2
- require 'fileutils'
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, '.config', 'panda-motd.yaml')
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['components'].map { |c, s| c if s['enabled'] }.compact
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['components'][component_name.to_s]
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__), 'panda_motd', 'default_config.yaml'
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['components'] = [] if @config['components'].nil?
67
+ @config["components"] = [] if @config["components"].nil?
51
68
  end
52
69
  end
@@ -1,14 +1,27 @@
1
- require 'sysinfo'
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 = config_path ? Config.new(config_path) : Config.new
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?
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class PandaMOTD
2
4
  #:nodoc:
3
- VERSION ||= '0.0.11'.freeze
5
+ VERSION ||= "0.0.12"
4
6
  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.11
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: 2018-12-31 00:00:00.000000000 Z
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: taylorthurlow8@gmail.com
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.3'
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
- rubygems_version: 3.0.1
251
- signing_key:
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: []