panda-motd 0.0.6 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/bin/panda-motd +1 -1
- data/lib/panda_motd.rb +13 -4
- data/lib/panda_motd/component.rb +35 -0
- data/lib/panda_motd/component_error.rb +4 -1
- data/lib/panda_motd/components/ascii_text_art.rb +12 -18
- data/lib/panda_motd/components/fail_2_ban.rb +46 -0
- data/lib/panda_motd/components/filesystems.rb +110 -94
- data/lib/panda_motd/components/last_login.rb +83 -49
- data/lib/panda_motd/components/service_status.rb +53 -45
- data/lib/panda_motd/components/ssl_certificates.rb +110 -47
- data/lib/panda_motd/components/uptime.rb +31 -14
- data/lib/panda_motd/config.rb +36 -14
- data/lib/panda_motd/default_config.yaml +24 -2
- data/lib/panda_motd/motd.rb +18 -5
- data/lib/panda_motd/version.rb +3 -1
- metadata +110 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5e27df79cdab81d15c44ff575075e876eb975b44
|
4
|
+
data.tar.gz: 954bf7eb0ff70fd62b86918dfe1cbd95c84d56bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af66d7e99d7396984468e5824b1902b1e595aea9022a27c1c128df98416f386c89f23f1c5415aca4da26045af458e4b6e5b5844111a3488aabb1e15270d79351
|
7
|
+
data.tar.gz: 306ec97a05b35bfb2a0418018fbfc1b627c0e69007bef88ac7932b29bb319418637f416ec2c3903de1ddfcd44f29e6abe413944b153ec85fc04669177ffbd4fc
|
data/bin/panda-motd
CHANGED
data/lib/panda_motd.rb
CHANGED
@@ -1,12 +1,21 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "require_all"
|
4
|
+
require_rel "panda_motd"
|
3
5
|
|
4
6
|
class PandaMOTD
|
7
|
+
# Creates a new MOTD instance, assuming a config file has been passed as an
|
8
|
+
# argument to the command.
|
5
9
|
def self.new_motd
|
6
10
|
if ARGV[0].nil?
|
7
|
-
puts
|
11
|
+
puts "You must provide a config file path as an argument to panda-motd."
|
8
12
|
else
|
9
|
-
|
13
|
+
MOTD.new(ARGV[0])
|
10
14
|
end
|
11
15
|
end
|
16
|
+
|
17
|
+
# Gets the root path of the gem.
|
18
|
+
def self.root
|
19
|
+
File.expand_path("..", __dir__)
|
20
|
+
end
|
12
21
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Component
|
4
|
+
attr_reader :name, :errors, :results, :config
|
5
|
+
|
6
|
+
def initialize(motd, name)
|
7
|
+
@name = name
|
8
|
+
@motd = motd
|
9
|
+
@config = motd.config.component_config(@name)
|
10
|
+
@errors = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Evaluates the component so that it has some meaningful output when it comes
|
14
|
+
# time to print the MOTD.
|
15
|
+
def process
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gives the output of a component as a string.
|
20
|
+
def to_s
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
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.
|
26
|
+
def lines_before
|
27
|
+
@motd.config.component_config(@name)["lines_before"] || 1
|
28
|
+
end
|
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.
|
32
|
+
def lines_after
|
33
|
+
@motd.config.component_config(@name)["lines_after"] || 1
|
34
|
+
end
|
35
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
2
4
|
|
3
5
|
class ComponentError
|
4
6
|
attr_reader :component, :message
|
@@ -8,6 +10,7 @@ class ComponentError
|
|
8
10
|
@message = message
|
9
11
|
end
|
10
12
|
|
13
|
+
# Gets a printable error string in red.
|
11
14
|
def to_s
|
12
15
|
return "#{@component.name} error: ".red + @message.to_s
|
13
16
|
end
|
@@ -1,29 +1,23 @@
|
|
1
|
-
|
2
|
-
require 'colorize'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
3
|
+
require "artii"
|
4
|
+
require "colorize"
|
6
5
|
|
6
|
+
class ASCIITextArt < Component
|
7
7
|
def initialize(motd)
|
8
|
-
|
9
|
-
@motd = motd
|
10
|
-
@config = motd.config.component_config(@name)
|
11
|
-
@errors = []
|
8
|
+
super(motd, "ascii_text_art")
|
12
9
|
end
|
13
10
|
|
14
11
|
def process
|
15
|
-
@text = `
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
22
|
-
@errors << ComponentError.new(self, 'Invalid font name')
|
23
|
-
end
|
12
|
+
@text = `#{@config["command"]}`
|
13
|
+
@art = Artii::Base.new font: @config["font"]
|
14
|
+
@results = @art.asciify(@text)
|
15
|
+
@results = @results.colorize(@config["color"].to_sym) if @config["color"]
|
16
|
+
rescue Errno::EISDIR # Artii doesn't handle invalid font names very well
|
17
|
+
@errors << ComponentError.new(self, "Invalid font name")
|
24
18
|
end
|
25
19
|
|
26
20
|
def to_s
|
27
|
-
|
21
|
+
@results
|
28
22
|
end
|
29
23
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Fail2Ban < Component
|
4
|
+
def initialize(motd)
|
5
|
+
super(motd, "fail_2_ban")
|
6
|
+
end
|
7
|
+
|
8
|
+
def process
|
9
|
+
@results = {
|
10
|
+
jails: {},
|
11
|
+
}
|
12
|
+
|
13
|
+
@config["jails"].each do |jail|
|
14
|
+
status = jail_status(jail)
|
15
|
+
@results[:jails][jail] = {
|
16
|
+
total: status[:total],
|
17
|
+
current: status[:current],
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
result = +"Fail2Ban:\n"
|
24
|
+
@results[:jails].each do |name, stats|
|
25
|
+
result << " #{name}:\n"
|
26
|
+
result << " Total bans: #{stats[:total]}\n"
|
27
|
+
result << " Current bans: #{stats[:current]}\n"
|
28
|
+
end
|
29
|
+
|
30
|
+
result.gsub!(/\s$/, "")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def jail_status(jail)
|
36
|
+
cmd_result = `fail2ban-client status #{jail}`
|
37
|
+
if cmd_result =~ /Sorry but the jail '#{jail}' does not exist/
|
38
|
+
@errors << ComponentError.new(self, "Invalid jail name '#{jail}'.")
|
39
|
+
else
|
40
|
+
total = cmd_result.match(/Total banned:\s+([0-9]+)/)[1].to_i
|
41
|
+
current = cmd_result.match(/Currently banned:\s+([0-9]+)/)[1].to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
{ total: total, current: current }
|
45
|
+
end
|
46
|
+
end
|
@@ -1,121 +1,137 @@
|
|
1
|
-
|
2
|
-
require 'colorize'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
attr_reader :results
|
3
|
+
require "ruby-units"
|
4
|
+
require "colorize"
|
7
5
|
|
6
|
+
class Filesystems < Component
|
8
7
|
def initialize(motd)
|
9
|
-
|
10
|
-
@motd = motd
|
11
|
-
@config = motd.config.component_config(@name)
|
12
|
-
@errors = []
|
8
|
+
super(motd, "filesystems")
|
13
9
|
end
|
14
10
|
|
15
11
|
def process
|
16
|
-
@results = parse_filesystem_usage(@config[
|
12
|
+
@results = parse_filesystem_usage(@config["filesystems"])
|
17
13
|
end
|
18
14
|
|
19
15
|
def to_s
|
20
|
-
name_col_size = @results.select { |r| r.is_a? Hash }
|
21
|
-
|
16
|
+
name_col_size = @results.select { |r| r.is_a? Hash }
|
17
|
+
.map { |r| r[:pretty_name].length }
|
18
|
+
.max || 0
|
22
19
|
|
23
|
-
|
20
|
+
size_w_padding = (name_col_size + 6) > 13 ? (name_col_size + 6) : 13
|
24
21
|
|
25
|
-
result =
|
26
|
-
result
|
22
|
+
result = +"Filesystems".ljust(size_w_padding, " ")
|
23
|
+
result << "Size Used Free Use%\n"
|
27
24
|
|
28
25
|
@results.each do |filesystem|
|
29
|
-
|
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}]"
|
74
|
-
result += "\n" unless filesystem == @results.last
|
26
|
+
result << format_filesystem(filesystem, size_w_padding)
|
75
27
|
end
|
76
28
|
|
77
|
-
|
29
|
+
result
|
78
30
|
end
|
79
31
|
|
80
32
|
private
|
81
33
|
|
82
|
-
def
|
83
|
-
return
|
84
|
-
|
85
|
-
|
86
|
-
|
34
|
+
def format_filesystem(filesystem, size)
|
35
|
+
return " #{filesystem}\n" if filesystem.is_a? String # handle fs not found
|
36
|
+
|
37
|
+
# filesystem name
|
38
|
+
result = +""
|
39
|
+
result << " #{filesystem[:pretty_name]}".ljust(size, " ")
|
40
|
+
|
41
|
+
# statistics (size, used, free, use%)
|
42
|
+
[:size, :used, :avail].each do |metric|
|
43
|
+
result << format_metric(filesystem, metric)
|
44
|
+
end
|
45
|
+
|
46
|
+
percent_used = calc_percent_used(filesystem)
|
47
|
+
result << format_percent_used(percent_used)
|
48
|
+
result << "\n"
|
49
|
+
|
50
|
+
# visual bar representation of use%
|
51
|
+
result << generate_usage_bar(filesystem, size, percent_used)
|
52
|
+
|
53
|
+
result
|
87
54
|
end
|
88
55
|
|
89
|
-
def
|
90
|
-
|
56
|
+
def generate_usage_bar(filesystem, size, percent_used)
|
57
|
+
result = +""
|
58
|
+
total_ticks = size + 18
|
59
|
+
used_ticks = (total_ticks * (percent_used.to_f / 100)).round
|
60
|
+
result << " [#{("=" * used_ticks).send(pct_color(percent_used))}" \
|
61
|
+
"#{("=" * (total_ticks - used_ticks)).light_black}]"
|
62
|
+
result << "\n" unless filesystem == @results.last
|
63
|
+
result
|
91
64
|
end
|
92
65
|
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
size_index = find_header_id_by_text(header, 'blocks')
|
100
|
-
used_index = find_header_id_by_text(header, 'used')
|
101
|
-
avail_index = find_header_id_by_text(header, 'avail')
|
102
|
-
|
103
|
-
results = filesystems.map do |filesystem, name|
|
104
|
-
matching_entry = entries.find { |e| e.split[name_index] == filesystem }
|
105
|
-
|
106
|
-
if matching_entry
|
107
|
-
{
|
108
|
-
pretty_name: name,
|
109
|
-
filesystem_name: matching_entry.split[name_index],
|
110
|
-
size: matching_entry.split[size_index].to_i * 1024,
|
111
|
-
used: matching_entry.split[used_index].to_i * 1024,
|
112
|
-
avail: matching_entry.split[avail_index].to_i * 1024
|
113
|
-
}
|
114
|
-
else
|
115
|
-
"#{filesystem} was not found"
|
116
|
-
end
|
66
|
+
def pct_color(percentage)
|
67
|
+
case percentage
|
68
|
+
when 0..75 then :green
|
69
|
+
when 76..95 then :yellow
|
70
|
+
when 96..100 then :red
|
71
|
+
else :white
|
117
72
|
end
|
73
|
+
end
|
118
74
|
|
119
|
-
|
75
|
+
def calc_percent_used(filesystem)
|
76
|
+
((filesystem[:used].to_f / filesystem[:size].to_f) * 100).round
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_percent_used(percent_used)
|
80
|
+
(percent_used.to_s.rjust(3, " ") + "%").send(pct_color(percent_used))
|
81
|
+
end
|
82
|
+
|
83
|
+
def calc_metric(value)
|
84
|
+
Unit.new("#{value} bytes").convert_to(calc_units(value))
|
85
|
+
end
|
86
|
+
|
87
|
+
def format_metric(filesystem, metric)
|
88
|
+
# we have 4 characters of space to include the number, a potential
|
89
|
+
# decimal point, and the unit character at the end. if the whole number
|
90
|
+
# component is 3+ digits long then we omit the decimal point and just
|
91
|
+
# display the whole number component. if the whole number component is
|
92
|
+
# 2 digits long, we can't afford to use a decimal point, so we still
|
93
|
+
# only display the whole number component. if the whole number
|
94
|
+
# component is 1 digit long, we use the single whole number component
|
95
|
+
# digit, a decimal point, a single fractional digit, and the unit
|
96
|
+
# character.
|
97
|
+
|
98
|
+
value = calc_metric(filesystem[metric])
|
99
|
+
whole_number_length = value.scalar.floor.to_s.length
|
100
|
+
round_amount = whole_number_length > 1 ? 0 : 1
|
101
|
+
formatted = value.scalar.round(round_amount).to_s + value.units[0].upcase
|
102
|
+
|
103
|
+
formatted.rjust(4, " ") + " "
|
104
|
+
end
|
105
|
+
|
106
|
+
def calc_units(value)
|
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"
|
115
|
+
else
|
116
|
+
"bytes"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_filesystem_usage(filesystems)
|
121
|
+
entries = `BLOCKSIZE=1024 df --output=source,size,used,avail`.lines.drop(1)
|
122
|
+
|
123
|
+
filesystems.map do |filesystem, pretty_name|
|
124
|
+
matching_entry = entries.map(&:split).find { |e| e.first == filesystem }
|
125
|
+
next "#{filesystem} was not found" unless matching_entry
|
126
|
+
|
127
|
+
filesystem_name, size, used, avail = matching_entry
|
128
|
+
{
|
129
|
+
pretty_name: pretty_name,
|
130
|
+
filesystem_name: filesystem_name,
|
131
|
+
size: size.to_i * 1024,
|
132
|
+
used: used.to_i * 1024,
|
133
|
+
avail: avail.to_i * 1024,
|
134
|
+
}
|
135
|
+
end
|
120
136
|
end
|
121
137
|
end
|
@@ -1,70 +1,104 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
attr_reader :name, :errors, :results
|
3
|
+
require "date"
|
5
4
|
|
5
|
+
class LastLogin < Component
|
6
6
|
def initialize(motd)
|
7
|
-
|
8
|
-
@motd = motd
|
9
|
-
@config = motd.config.component_config(@name)
|
10
|
-
@errors = []
|
7
|
+
super(motd, "last_login")
|
11
8
|
end
|
12
9
|
|
10
|
+
# @see Component#process
|
13
11
|
def process
|
14
|
-
@users = @config[
|
15
|
-
@results = parse_last_logins
|
12
|
+
@users = @config["users"]
|
13
|
+
@results = parse_last_logins
|
16
14
|
end
|
17
15
|
|
16
|
+
# @see Component#to_s
|
18
17
|
def to_s
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
logins.each do |login|
|
25
|
-
location_part = login[:location].ljust(location_string_size, ' ')
|
26
|
-
start_part = login[:time_start].strftime('%m/%d/%Y %I:%M%p')
|
27
|
-
|
28
|
-
end_part = if login[:time_end].is_a? String # still logged in text
|
29
|
-
login[:time_end].green
|
30
|
-
else
|
31
|
-
"#{((login[:time_end] - login[:time_start]) * 24 * 60).to_i} minutes"
|
32
|
-
end
|
18
|
+
<<~HEREDOC
|
19
|
+
Last Login:
|
20
|
+
#{@results.map { |u, l| parse_result(u, l) }.join("\n")}
|
21
|
+
HEREDOC
|
22
|
+
end
|
33
23
|
|
34
|
-
|
35
|
-
end
|
36
|
-
result += ' no logins found for user.' if logins.empty?
|
37
|
-
end
|
24
|
+
private
|
38
25
|
|
39
|
-
|
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
|
34
|
+
def parse_result(user, logins)
|
35
|
+
logins_part = if logins.empty?
|
36
|
+
" no logins found for user."
|
37
|
+
else
|
38
|
+
longest_size = logins.map { |l| l[:location].length }.max
|
39
|
+
logins.map { |l| parse_login(l, longest_size) }.join("\n")
|
40
|
+
end
|
41
|
+
<<~HEREDOC
|
42
|
+
#{user}:
|
43
|
+
#{logins_part}
|
44
|
+
HEREDOC
|
40
45
|
end
|
41
46
|
|
42
|
-
|
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
|
54
|
+
def parse_login(login, longest_size)
|
55
|
+
location = login[:location].ljust(longest_size, " ")
|
56
|
+
start = login[:time_start].strftime("%m/%d/%Y %I:%M%p")
|
57
|
+
finish = if login[:time_end].is_a? String # not a date
|
58
|
+
if login[:time_end] == "still logged in"
|
59
|
+
login[:time_end].green
|
60
|
+
else
|
61
|
+
login[:time_end].yellow
|
62
|
+
end
|
63
|
+
else
|
64
|
+
"#{((login[:time_end] - login[:time_start]) / 60).to_i} minutes"
|
65
|
+
end
|
66
|
+
" from #{location} at #{start} (#{finish})"
|
67
|
+
end
|
43
68
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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)|
|
48
75
|
cmd_result = `last --time-format=iso #{username}`
|
49
|
-
cmd_result.
|
50
|
-
|
51
|
-
|
76
|
+
logins = cmd_result.lines
|
77
|
+
.select { |entry| entry.start_with?(username) }
|
78
|
+
.take(num_logins)
|
52
79
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
username: username,
|
57
|
-
location: data[2],
|
58
|
-
time_start: DateTime.parse(data[3]),
|
59
|
-
time_end: time_end
|
60
|
-
}
|
80
|
+
[username.to_sym, logins.map! { |l| hashify_login(l, username) }]
|
81
|
+
end.to_h
|
82
|
+
end
|
61
83
|
|
62
|
-
|
63
|
-
|
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
|
91
|
+
def hashify_login(login, username)
|
92
|
+
re = login.chomp.split(/(?:\s{2,})|(?<=\d)(?:\s-\s)/)
|
93
|
+
date = re[4].scan(/\d{4}-\d{2}-[\dT:]+-\d{4}/)
|
64
94
|
|
65
|
-
|
66
|
-
end
|
95
|
+
time_end = date.any? ? Time.parse(re[4]) : re[4]
|
67
96
|
|
68
|
-
|
97
|
+
{
|
98
|
+
username: username, # string username
|
99
|
+
location: re[2], # string login location, an IP address
|
100
|
+
time_start: Time.parse(re[3]),
|
101
|
+
time_end: time_end, # Time or string, could be "still logged in"
|
102
|
+
}
|
69
103
|
end
|
70
104
|
end
|