panda-motd 0.0.11 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- 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: []