panda-motd 0.0.3 → 0.0.4

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
2
  SHA256:
3
- metadata.gz: 602cdbb28a1c8eb7b902dd7c1dee4f0caffb0663fcda3d4b505ca9f53b2aebf7
4
- data.tar.gz: 638ca548913a3de96d7534bc22f84aa1fef57e4ee45ad18c4b295de7ce8add42
3
+ metadata.gz: 324ec514bbb141949c82866eed44d830d6f0f768824f42cda912f872f205bdc8
4
+ data.tar.gz: 867433a26a6277120037b51c7f16b0eb6cb32edac085f1f245048c7097148829
5
5
  SHA512:
6
- metadata.gz: ca2a3254437207351125a08175382980cc43905a22b9f84127993e900d9a2e20213b89d79f973fc9505b8ee38423e24b8542e78135f6cb4423f729782d60400f
7
- data.tar.gz: fb2e551bc3bcf911fc9d01aad4af7906aaad435ec19e89026232e34e53ef536d6eae592a6a68e9700b69ac3924d7714e4d22a299e6f75746acbdf951880eaa1e
6
+ metadata.gz: bec0220f18e5b3c5c2f32958ba93cd463efe92ce87dee363b9dd5ee2aec2ab7579b3ce1cc6794dae483f82b8b8ba38d15d974ef77a8bb91b2524fd5ff12a4180
7
+ data.tar.gz: 71e8cba32a1936bdf7304c296364ef2dc2aeb9cce2f4a9fb747db3bd97bee4f4ae8c5c4130ca0c6abc9a719f78ad01f98215b4ede31b72d48736e7c871a36fc2
@@ -0,0 +1,14 @@
1
+ require 'colorize'
2
+
3
+ class ComponentError
4
+ attr_reader :component, :message
5
+
6
+ def initialize(component, message)
7
+ @component = component
8
+ @message = message
9
+ end
10
+
11
+ def to_s
12
+ return "#{@component.name} error: ".red + @message.to_s
13
+ end
14
+ end
@@ -1,18 +1,29 @@
1
1
  require 'artii'
2
2
  require 'colorize'
3
3
 
4
- class AsciiTextArt
4
+ class ASCIITextArt
5
+ attr_reader :name, :errors, :results
6
+
5
7
  def initialize(motd)
8
+ @name = 'ascii_text_art'
6
9
  @motd = motd
7
- @config = motd.config.component_config('ascii_text_art')
10
+ @config = motd.config.component_config(@name)
11
+ @errors = []
8
12
  end
9
13
 
10
14
  def process
11
15
  @text = `hostname`
12
- @art = Artii::Base.new font: @config['font']
16
+
17
+ begin
18
+ @art = Artii::Base.new font: @config['font']
19
+ @results = @art.asciify(@text)
20
+ @results = @results.send(@config['color'].to_sym) if @config['color']
21
+ rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
22
+ @errors << ComponentError.new(self, 'Invalid font name')
23
+ end
13
24
  end
14
25
 
15
26
  def to_s
16
- return @art.asciify(@text).red
27
+ return @results
17
28
  end
18
29
  end
@@ -0,0 +1,120 @@
1
+ require 'ruby-units'
2
+ require 'colorize'
3
+
4
+ class Filesystems
5
+ attr_reader :name, :errors
6
+ attr_reader :results
7
+
8
+ def initialize(motd)
9
+ @name = 'filesystems'
10
+ @motd = motd
11
+ @config = motd.config.component_config(@name)
12
+ @errors = []
13
+ end
14
+
15
+ def process
16
+ @results = parse_filesystem_usage(@config['filesystems'])
17
+ end
18
+
19
+ def to_s
20
+ name_col_size = @results.select { |r| r.is_a? Hash }.map { |r| r[:pretty_name].length }.max
21
+ name_col_size = 0 if name_col_size.nil?
22
+
23
+ name_col_size_with_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
24
+
25
+ result = 'Filesystems'.ljust(name_col_size_with_padding, ' ')
26
+ result += "Size Used Free Use%\n"
27
+
28
+ @results.each do |filesystem|
29
+ # handle filesystem not found
30
+ if filesystem.is_a? String
31
+ result += " #{filesystem}\n"
32
+ next
33
+ end
34
+
35
+ result += " #{filesystem[:pretty_name]}".ljust(name_col_size_with_padding, ' ')
36
+
37
+ [:size, :used, :avail].each do |metric|
38
+ units = if filesystem[metric] > 10**12
39
+ 'terabytes'
40
+ elsif filesystem[metric] > 10**9
41
+ 'gigabytes'
42
+ elsif filesystem[metric] > 10**6
43
+ 'megabytes'
44
+ elsif filesystem[metric] > 10**3
45
+ 'kilobytes'
46
+ else
47
+ 'bytes'
48
+ end
49
+
50
+ value = Unit.new("#{filesystem[metric]} bytes").convert_to(units)
51
+
52
+ # we have 4 characters of space to include the number, a potential decimal point, and
53
+ # the unit character at the end. if the whole number component is 3+ digits long then
54
+ # we omit the decimal point and just display the whole number component. if the whole
55
+ # number component is 2 digits long, we can't afford to use a decimal point, so we still
56
+ # only display the whole number component. if the whole number component is 1 digit long,
57
+ # we use the single whole number component digit, a decimal point, a single fractional
58
+ # digit, and the unit character.
59
+ whole_number_length = value.scalar.floor.to_s.length
60
+ round_amount = whole_number_length > 1 ? 0 : 1
61
+ formatted_value = value.scalar.round(round_amount).to_s + units[0].upcase
62
+ result += formatted_value.rjust(4, ' ') + ' '
63
+ end
64
+
65
+ percentage_used = ((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
66
+ result += (percentage_used.to_s.rjust(3, ' ') + '%').send(percentage_color(percentage_used))
67
+
68
+ result += "\n"
69
+
70
+ total_ticks = name_col_size_with_padding + 18
71
+ used_ticks = (total_ticks * (percentage_used.to_f / 100)).round
72
+
73
+ result += " [#{('=' * used_ticks).send(percentage_color(percentage_used))}#{('=' * (total_ticks - used_ticks)).light_black}]\n"
74
+ end
75
+
76
+ return result
77
+ end
78
+
79
+ private
80
+
81
+ def percentage_color(percentage)
82
+ return :green if (0..75).cover? percentage
83
+ return :yellow if (75..95).cover? percentage
84
+ return :red if (95..100).cover? percentage
85
+ return :white
86
+ end
87
+
88
+ def find_header_id_by_text(header_array, text)
89
+ return header_array.each_index.select { |i| header_array[i].downcase.include? text }.first
90
+ end
91
+
92
+ def parse_filesystem_usage(filesystems)
93
+ command_result = `BLOCKSIZE=1024 df`.split("\n")
94
+ header = command_result[0].split
95
+ entries = command_result[1..command_result.count]
96
+
97
+ name_index = find_header_id_by_text(header, 'filesystem')
98
+ size_index = find_header_id_by_text(header, 'blocks')
99
+ used_index = find_header_id_by_text(header, 'used')
100
+ avail_index = find_header_id_by_text(header, 'avail')
101
+
102
+ results = filesystems.map do |filesystem, name|
103
+ matching_entry = entries.find { |e| e.split[name_index] == filesystem }
104
+
105
+ if matching_entry
106
+ {
107
+ pretty_name: name,
108
+ filesystem_name: matching_entry.split[name_index],
109
+ size: matching_entry.split[size_index].to_i * 1024,
110
+ used: matching_entry.split[used_index].to_i * 1024,
111
+ avail: matching_entry.split[avail_index].to_i * 1024
112
+ }
113
+ else
114
+ "#{filesystem} was not found"
115
+ end
116
+ end
117
+
118
+ return results
119
+ end
120
+ end
@@ -1,31 +1,30 @@
1
1
  require 'colorize'
2
2
 
3
3
  class ServiceStatus
4
+ attr_reader :name, :errors
4
5
  attr_reader :results
5
6
 
6
7
  def initialize(motd)
8
+ @name = 'service_status'
7
9
  @motd = motd
8
- @config = motd.config.component_config('service_status')
10
+ @config = motd.config.component_config(@name)
11
+ @errors = []
9
12
  end
10
13
 
11
14
  def process
12
15
  @services = @config['services']
13
- @results = {}
14
-
15
- parse_services
16
+ @results = parse_services(@services)
16
17
  end
17
18
 
18
19
  def to_s
19
20
  if @results.any?
20
21
  result = "Services:\n"
21
22
  longest_name_size = @results.keys.map { |k| k.to_s.length }.max + 1 # add 1 for the ':' at the end
22
- @results.each do |name, status|
23
- name_part = if name.to_s.length > longest_name_size
24
- name.to_s.slice(1..longest_name_size)
25
- else
26
- (name.to_s + ':').ljust(longest_name_size, ' ')
27
- end
28
- result += " #{name_part} #{status}\n"
23
+ @results.each_with_index do |(name, status), i|
24
+ name_part = (name.to_s + ':').ljust(longest_name_size, ' ')
25
+ status_part = status.to_s.send(service_colors[status])
26
+ result += " #{name_part} #{status_part}"
27
+ result += "\n" unless i == @results.count - 1 # don't print newline for last entry
29
28
  end
30
29
 
31
30
  return result
@@ -36,62 +35,30 @@ class ServiceStatus
36
35
 
37
36
  private
38
37
 
39
- def parse_services
40
- case Gem::Platform.local.os
41
- when 'darwin'
42
- @results = parse_services_macos(@services)
43
- when 'linux'
44
- @results = parse_services_linux(@services)
45
- end
46
-
47
- return self
48
- end
49
-
50
- def parse_services_macos(services)
38
+ def parse_services(services)
51
39
  results = {}
52
40
 
53
- # valid statuses are started, stopped, error, and unknown
54
- `brew services list`.split("\n").each do |line|
55
- parsed_name = line.split[0]
56
- parsed_status = line.split[1]
41
+ cmd_result = `systemctl | grep '\.service'`
57
42
 
58
- matching_service = services.find { |service, _name| service == parsed_name }
59
-
60
- if matching_service
61
- results[parsed_name.to_sym] = parsed_status.send(macos_service_colors[parsed_status.to_sym])
62
- end
43
+ if cmd_result.empty?
44
+ @errors << ComponentError.new(self, 'Unable to parse systemctl output')
63
45
  end
64
46
 
65
- return results
66
- end
67
-
68
- def parse_services_linux(services)
69
- results = {}
70
-
71
- `systemctl | grep '\.service'`.split("\n").each do |line|
47
+ cmd_result.split("\n").each do |line|
72
48
  parsed_name = line.split[0].gsub('.service', '')
73
49
  parsed_status = line.split[3]
74
50
 
75
51
  matching_service = services.find { |service, _name| service == parsed_name }
76
52
 
77
53
  if matching_service
78
- results[parsed_name.to_sym] = parsed_status.send(linux_service_colors[parsed_status.to_sym])
54
+ results[parsed_name.to_sym] = parsed_status.to_sym
79
55
  end
80
56
  end
81
57
 
82
58
  return results
83
59
  end
84
60
 
85
- def macos_service_colors
86
- return {
87
- started: :green,
88
- stopped: :white,
89
- error: :red,
90
- unknown: :yellow
91
- }
92
- end
93
-
94
- def linux_service_colors
61
+ def service_colors
95
62
  return {
96
63
  running: :green,
97
64
  exited: :white,
@@ -0,0 +1,85 @@
1
+ require 'date'
2
+
3
+ class SSLCertificates
4
+ attr_reader :name, :errors, :results
5
+
6
+ def initialize(motd)
7
+ @name = 'ssl_certificates'
8
+ @motd = motd
9
+ @config = motd.config.component_config(@name)
10
+ @errors = []
11
+ end
12
+
13
+ def process
14
+ @certs = @config['certs']
15
+ @results = cert_dates(@certs)
16
+ end
17
+
18
+ def to_s
19
+ result = "SSL Certificates:\n"
20
+ longest_name_size = @results.map { |r| r[0].length }.max
21
+
22
+ @results.each_with_index do |cert, i|
23
+ if cert.is_a? String # print the not found message
24
+ result += " #{cert}"
25
+ else
26
+ name_portion = " #{cert[0]}".ljust(longest_name_size + 6, ' ')
27
+
28
+ status = cert_status(cert[1])
29
+
30
+ date_portion = "#{cert_status_strings[status]} ".send(cert_status_colors[status]) + cert[1].strftime('%e %b %Y %H:%M:%S%p').to_s
31
+ result += name_portion + date_portion
32
+ end
33
+
34
+ result += "\n" unless i == @results.count - 1 # don't print newline for last entry
35
+ end
36
+
37
+ return result
38
+ end
39
+
40
+ private
41
+
42
+ def cert_dates(certs)
43
+ return certs.map do |name, path|
44
+ if File.exist?(path)
45
+ cmd_result = `openssl x509 -in #{path} -dates`
46
+ parsed = cmd_result.match(/notAfter=([\w\s:]+)\n/)
47
+ if parsed.nil?
48
+ @errors << ComponentError.new(self, 'Unable to find certificate expiration date')
49
+ else
50
+ begin
51
+ expiry_date = DateTime.parse(parsed[1])
52
+ [name, expiry_date]
53
+ rescue ArgumentError
54
+ @errors << ComponentError.new(self, 'Found expiration date, but unable to parse as date')
55
+ end
56
+ end
57
+ else
58
+ "Certificate #{name} not found at path: #{path}"
59
+ end
60
+ end
61
+ end
62
+
63
+ def cert_status(expiry_date)
64
+ status = :valid
65
+ status = :expiring if (DateTime.now...DateTime.now + 30).cover? expiry_date # ... range excludes end
66
+ status = :expired if DateTime.now >= expiry_date
67
+ return status
68
+ end
69
+
70
+ def cert_status_colors
71
+ return {
72
+ valid: :green,
73
+ expiring: :yellow,
74
+ expired: :red
75
+ }
76
+ end
77
+
78
+ def cert_status_strings
79
+ return {
80
+ valid: 'valid until',
81
+ expiring: 'expiring at',
82
+ expired: 'expired at'
83
+ }
84
+ end
85
+ end
@@ -1,11 +1,14 @@
1
1
  require 'sysinfo'
2
2
 
3
3
  class Uptime
4
+ attr_reader :name, :errors
4
5
  attr_reader :days, :hours, :minutes
5
6
 
6
7
  def initialize(motd)
8
+ @name = 'uptime'
7
9
  @motd = motd
8
- @config = motd.config.component_config('ascii_text_art')
10
+ @config = motd.config.component_config(@name)
11
+ @errors = []
9
12
  end
10
13
 
11
14
  def process
@@ -22,6 +25,6 @@ class Uptime
22
25
  result += "#{@days} day#{'s' if @days != 1}, " unless @days.zero?
23
26
  result += "#{@hours} hour#{'s' if @hours != 1}, " unless @hours.zero? && @days.zero?
24
27
  result += "#{@minutes} minute#{'s' if @minutes != 1}"
25
- return "uptime: #{result}"
28
+ return "#{@config['prefix'] || 'up'} #{result}"
26
29
  end
27
30
  end
@@ -3,7 +3,7 @@ require 'fileutils'
3
3
 
4
4
  class Config
5
5
  def initialize(file_path = nil)
6
- @file_path = File.join(Dir.home, '.config', 'panda-motd.yaml') if file_path.nil?
6
+ @file_path = file_path.nil? ? File.join(Dir.home, '.config', 'panda-motd.yaml') : file_path
7
7
  create_config(@file_path) unless File.exist?(@file_path)
8
8
  load_config(@file_path)
9
9
  end
@@ -11,8 +11,8 @@ class Config
11
11
  def components_enabled
12
12
  # first iterate through the config hash and grab all names of enabled components
13
13
  enabled_list = @config['components'].map { |component, setting| component if setting['enabled'] }.compact
14
- # convert each snake_case name to an upper-camel-case name which represents a class, and get that class constant
15
- return enabled_list.map { |e| Object.const_get(e.split('_').map(&:capitalize).join) }
14
+ # get the class constant
15
+ return enabled_list.map { |e| component_classes[e.to_sym] }
16
16
  end
17
17
 
18
18
  def component_config(component_name)
@@ -21,6 +21,16 @@ class Config
21
21
 
22
22
  private
23
23
 
24
+ def component_classes
25
+ return {
26
+ ascii_text_art: ASCIITextArt,
27
+ service_status: ServiceStatus,
28
+ uptime: Uptime,
29
+ ssl_certificates: SSLCertificates,
30
+ filesystems: Filesystems
31
+ }
32
+ end
33
+
24
34
  def create_config(file_path)
25
35
  default_config_path = File.join(File.dirname(__dir__), 'panda_motd', 'default_config.yaml')
26
36
  FileUtils.cp(default_config_path, file_path)
@@ -1,23 +1,89 @@
1
+ # The components section contains an ordered list of components you wish to
2
+ # print in the MOTD. They will be printed in the order they are defined in
3
+ # this file. Duplicate components will override previous definitions. Every
4
+ # component has an "enabled" setting which will turn the component on or off.
5
+
1
6
  components:
2
- ascii_text_art:
3
- enabled: true
4
- font: slant
5
- color: red
6
-
7
- service_status:
8
- enabled: true
9
- services:
10
- chunkwm: chunkwm
11
- skhd: skhd
12
- elasticsearch@5.6: elasticsearch
13
- mysql: MySQL
14
-
15
- uptime:
16
- enabled: true
17
-
18
- # disk_info:
7
+ #####
8
+ # ASCII Text Art
9
+ # Generates ASCII pictures of strings.
10
+ #
11
+ # Settings
12
+ # font: The figlet font to render the text with. All supported fonts
13
+ # can be found at http://www.figlet.org/fontdb.cgi
14
+ # color: The color of the resulting text art. Supports the standard base-8
15
+ # colors: black, red, green, yellow, blue, magenta, cyan, and white. You
16
+ # can also use 'default' for your default terminal color. Additionally,
17
+ # you can prefix any of the base colors with 'light_' to get the lighter
18
+ # version.
19
+ #####
20
+
21
+ # ascii_text_art:
22
+ # enabled: true
23
+ # font: slant
24
+ # color: red
25
+
26
+ #####
27
+ # Service Status
28
+ # Displays the current state of services running on the system. Currently
29
+ # only supports systemd.
30
+ #
31
+ # Settings
32
+ # services: Pairs following the format "service_name: Pretty Name". The
33
+ # service_name is the name of the systemd service to look for, NOT
34
+ # including the '.service' suffix. The pretty name is the name that is
35
+ # used in the MOTD to represent that service.
36
+ #####
37
+
38
+ # service_status:
39
+ # enabled: true
40
+ # services:
41
+ # chunkwm: chunkwm
42
+ # skhd: skhd
43
+ # elasticsearch@5.6: elasticsearch
44
+ # mysql: MySQL
45
+
46
+ #####
47
+ # Uptime
48
+ # Displays the current uptime of the machine.
49
+ #
50
+ # Settings
51
+ # prefix: The text to prepend to the uptime string.
52
+ #####
53
+
54
+ # uptime:
55
+ # enabled: true
56
+ # prefix: up
57
+
58
+ #####
59
+ # SSL Certificates
60
+ # Displays the validity and expiration dates of SSL certificates.
61
+ #
62
+ # Settings
63
+ # certs: Pairs following the format "PrettyName: absolute_cert_file_path".
64
+ # The absolute_cert_file_path is the absolute file path of the SSL
65
+ # certificate. The pretty name is the name that is used in the MOTD to
66
+ # represent that certificate, typically a domain name.
67
+ #####
68
+
69
+ # ssl_certificates:
70
+ # enabled: true
71
+ # certs:
72
+ # taylorjthurlow.com: /etc/letsencrypt/live/taylorjthurlow.com/cert.pem
73
+
74
+ #####
75
+ # Filesystems
76
+ # Displays filesystem usage information.
77
+ #
78
+ # Settings
79
+ # filesystems: Pairs following the format "filesystem_name: Pretty Name".
80
+ # The filesystem_name is the name of the filesystem listed when using the
81
+ # `df` command line tool. The pretty name is the name that is used in the
82
+ # MOTD to represent that filesystem.
83
+ #####
84
+
85
+ # filesystems:
19
86
  # enabled: true
20
- # disks:
21
- # /dev/disk3s1: Macintosh HD
22
- # /dev/disk0s4: Windows
23
- # /dev/disk2s2: Games
87
+ # filesystems:
88
+ # /dev/sda1: Ubuntu
89
+ # /dev/sdc1: Data
@@ -3,18 +3,19 @@ require 'sysinfo'
3
3
  class MOTD
4
4
  attr_reader :config, :components
5
5
 
6
- def initialize
7
- @config = Config.new
8
-
9
- @components = []
10
- @config.components_enabled.each do |component_class|
11
- @components << component_class.new(self)
12
- end
13
-
6
+ def initialize(config_path = nil)
7
+ @config = config_path ? Config.new(config_path) : Config.new
8
+ @components = @config.components_enabled.map { |ce| ce.new(self) }
14
9
  @components.each(&:process)
15
10
  end
16
11
 
17
12
  def to_s
18
- return @components.map(&:to_s).join("\n\n")
13
+ return @components.map do |c|
14
+ if c.errors.any?
15
+ c.errors.map(&:to_s).join("\n")
16
+ else
17
+ c.to_s
18
+ end
19
+ end.join("\n\n")
19
20
  end
20
21
  end
@@ -1,4 +1,4 @@
1
1
  class PandaMOTD
2
2
  #:nodoc:
3
- VERSION ||= '0.0.3'.freeze
3
+ VERSION ||= '0.0.4'.freeze
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda-motd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taylor Thurlow
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-05 00:00:00.000000000 Z
11
+ date: 2018-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: artii
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby-units
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: sysinfo
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -173,9 +187,11 @@ extra_rdoc_files: []
173
187
  files:
174
188
  - bin/panda-motd
175
189
  - lib/panda_motd.rb
176
- - lib/panda_motd/component.rb
190
+ - lib/panda_motd/component_error.rb
177
191
  - lib/panda_motd/components/ascii_text_art.rb
192
+ - lib/panda_motd/components/filesystems.rb
178
193
  - lib/panda_motd/components/service_status.rb
194
+ - lib/panda_motd/components/ssl_certificates.rb
179
195
  - lib/panda_motd/components/uptime.rb
180
196
  - lib/panda_motd/config.rb
181
197
  - lib/panda_motd/default_config.yaml
File without changes