panda-motd 0.0.3 → 0.0.4

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
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