motoko 1.0.0
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 +7 -0
- data/.github/workflows/ci.yaml +41 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.simplecov +10 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/inventory +46 -0
- data/exe/pdb-inventory +58 -0
- data/lib/motoko.rb +28 -0
- data/lib/motoko/config.rb +78 -0
- data/lib/motoko/formatter.rb +82 -0
- data/lib/motoko/formatters/base_formatter.rb +20 -0
- data/lib/motoko/formatters/boolean.rb +11 -0
- data/lib/motoko/formatters/datetime.rb +15 -0
- data/lib/motoko/formatters/datetime_ago.rb +21 -0
- data/lib/motoko/formatters/ellipsis.rb +22 -0
- data/lib/motoko/formatters/timestamp.rb +15 -0
- data/lib/motoko/formatters/timestamp_ago.rb +21 -0
- data/lib/motoko/node.rb +34 -0
- data/lib/motoko/option_parser.rb +80 -0
- data/lib/motoko/resolvers/base_resolver.rb +44 -0
- data/lib/motoko/resolvers/cpu.rb +11 -0
- data/lib/motoko/resolvers/fact.rb +19 -0
- data/lib/motoko/resolvers/identity.rb +11 -0
- data/lib/motoko/resolvers/os.rb +11 -0
- data/lib/motoko/resolvers/reboot_required.rb +20 -0
- data/lib/motoko/utils/puppet_db.rb +50 -0
- data/lib/motoko/utils/snake_to_camel.rb +11 -0
- data/lib/motoko/utils/time_ago.rb +29 -0
- data/lib/motoko/version.rb +5 -0
- data/motoko.gemspec +34 -0
- metadata +129 -0
data/exe/inventory
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Or use Puppet's Ruby: #!/opt/puppetlabs/puppet/bin/ruby
|
5
|
+
#
|
6
|
+
# Call-seq: ./inventory.rb -F form_factor=Server -v
|
7
|
+
|
8
|
+
require 'motoko'
|
9
|
+
require 'mcollective'
|
10
|
+
|
11
|
+
include MCollective::RPC # rubocop:disable Style/MixinUsage
|
12
|
+
|
13
|
+
formatter = Motoko::Formatter.new
|
14
|
+
|
15
|
+
options = rpcoptions do |parser, local_options|
|
16
|
+
parser.banner = "usage: #{File.basename(__FILE__)} [options]"
|
17
|
+
|
18
|
+
Motoko::OptionParser.add_inventory_options(parser, formatter)
|
19
|
+
|
20
|
+
parser.on('--[no-]stats', 'Display statistics') do |v|
|
21
|
+
local_options[:stats] = v
|
22
|
+
end
|
23
|
+
|
24
|
+
Motoko::OptionParser.add_shortcut_options(parser, formatter, local_options)
|
25
|
+
end
|
26
|
+
|
27
|
+
options[:stats] = true if options[:stats].nil?
|
28
|
+
|
29
|
+
util = rpcclient('rpcutil', options: options)
|
30
|
+
util.progress = false
|
31
|
+
|
32
|
+
(options[:with_class] || []).compact.each do |klass|
|
33
|
+
util.class_filter(klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
(options[:with_fact] || []).compact.each do |fact|
|
37
|
+
util.fact_filter(fact)
|
38
|
+
end
|
39
|
+
|
40
|
+
util.inventory do |_, resp|
|
41
|
+
formatter.nodes << Motoko::Node::Choria.new(resp)
|
42
|
+
end
|
43
|
+
|
44
|
+
puts formatter.to_s
|
45
|
+
|
46
|
+
printrpcstats if options[:stats]
|
data/exe/pdb-inventory
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'mcollective'
|
5
|
+
require 'motoko'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
filters = []
|
9
|
+
|
10
|
+
oparser = Motoko::OptionParser.new
|
11
|
+
|
12
|
+
options = {}
|
13
|
+
|
14
|
+
oparser.parse do |parser|
|
15
|
+
parser.separator ''
|
16
|
+
parser.separator 'Host Filters'
|
17
|
+
|
18
|
+
parser.on('-C', '--wc', '--with-class CLASS', 'Match hosts with a certain config management class') do |with_class|
|
19
|
+
filters << Motoko::Utils::PuppetDB.class_filter(with_class)
|
20
|
+
end
|
21
|
+
|
22
|
+
parser.on('-F', '--wf', '--with-fact fact=val', 'Match hosts with a certain fact') do |with_fact|
|
23
|
+
filters << Motoko::Utils::PuppetDB.fact_filter(with_fact)
|
24
|
+
end
|
25
|
+
|
26
|
+
parser.on('-I', '--wi', '--with-identity IDENT', 'Match hosts with a certain configured identity') do |with_ident|
|
27
|
+
filters << Motoko::Utils::PuppetDB.identity_filter(with_ident)
|
28
|
+
end
|
29
|
+
|
30
|
+
Motoko::OptionParser.add_shortcut_options(parser, oparser.formatter, options)
|
31
|
+
end
|
32
|
+
|
33
|
+
(options[:with_class] || []).compact.each do |klass|
|
34
|
+
filters << Motoko::Utils::PuppetDB.class_filter(klass)
|
35
|
+
end
|
36
|
+
|
37
|
+
(options[:with_fact] || []).compact.each do |fact|
|
38
|
+
filters << Motoko::Utils::PuppetDB.fact_filter(fact)
|
39
|
+
end
|
40
|
+
|
41
|
+
config = MCollective::Config.instance
|
42
|
+
config.loadconfig(MCollective::Util.config_file_for_user)
|
43
|
+
|
44
|
+
client = MCollective::Util::Choria.new
|
45
|
+
|
46
|
+
response = client.pql_query("facts[certname, name, value] { #{filters.map { |f| "(#{f})" }.join(' and ')} }")
|
47
|
+
|
48
|
+
nodes = Hash.new { |hash, value| hash[value] = {} }
|
49
|
+
|
50
|
+
response.each do |fact|
|
51
|
+
nodes[fact['certname']][fact['name']] = fact['value']
|
52
|
+
end
|
53
|
+
|
54
|
+
nodes.each do |sender, facts|
|
55
|
+
oparser.formatter.nodes << Motoko::Node.new(sender, facts)
|
56
|
+
end
|
57
|
+
|
58
|
+
puts oparser.formatter.to_s
|
data/lib/motoko.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'motoko/config'
|
4
|
+
require 'motoko/formatter'
|
5
|
+
require 'motoko/formatters/base_formatter'
|
6
|
+
require 'motoko/formatters/boolean'
|
7
|
+
require 'motoko/formatters/datetime'
|
8
|
+
require 'motoko/formatters/datetime_ago'
|
9
|
+
require 'motoko/formatters/ellipsis'
|
10
|
+
require 'motoko/formatters/timestamp'
|
11
|
+
require 'motoko/formatters/timestamp_ago'
|
12
|
+
require 'motoko/node'
|
13
|
+
require 'motoko/option_parser'
|
14
|
+
require 'motoko/resolvers/base_resolver'
|
15
|
+
require 'motoko/resolvers/cpu'
|
16
|
+
require 'motoko/resolvers/fact'
|
17
|
+
require 'motoko/resolvers/identity'
|
18
|
+
require 'motoko/resolvers/os'
|
19
|
+
require 'motoko/resolvers/reboot_required'
|
20
|
+
require 'motoko/utils/puppet_db'
|
21
|
+
require 'motoko/utils/snake_to_camel'
|
22
|
+
require 'motoko/utils/time_ago'
|
23
|
+
require 'motoko/version'
|
24
|
+
|
25
|
+
module Motoko
|
26
|
+
class Error < StandardError; end
|
27
|
+
# Your code goes here...
|
28
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Motoko
|
6
|
+
class Config
|
7
|
+
attr_accessor :columns, :sort_by, :columns_spec, :shortcuts
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@columns = %w[host customer role]
|
11
|
+
@sort_by = %w[customer host]
|
12
|
+
@shortcuts = Hash.new { {} }
|
13
|
+
@columns_spec = Hash.new { {} }.merge(default_columns_spec)
|
14
|
+
|
15
|
+
load_system_config
|
16
|
+
load_user_config
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_system_config
|
20
|
+
[
|
21
|
+
'/usr/local/etc/motoko',
|
22
|
+
'/etc/motoko',
|
23
|
+
].each do |d|
|
24
|
+
if File.directory?(d)
|
25
|
+
load_config(d)
|
26
|
+
break
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_user_config
|
32
|
+
d = File.expand_path('~/.config/motoko')
|
33
|
+
load_config(d) if File.directory?(d)
|
34
|
+
end
|
35
|
+
|
36
|
+
def load_config(directory)
|
37
|
+
load_classes(directory)
|
38
|
+
|
39
|
+
filename = File.join(directory, 'config.yaml')
|
40
|
+
|
41
|
+
return unless File.readable?(filename)
|
42
|
+
|
43
|
+
config = YAML.safe_load(File.read(filename))
|
44
|
+
|
45
|
+
@columns = config.delete('columns') if config.key?('columns')
|
46
|
+
@sort_by = config.delete('sort_by') if config.key?('sort_by')
|
47
|
+
|
48
|
+
@shortcuts.merge!(config.delete('shortcuts')) if config.key?('shortcuts')
|
49
|
+
@columns_spec.merge!(config.delete('columns_spec')) if config.key?('columns_spec')
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_classes(directory)
|
53
|
+
Dir["#{directory}/formatters/*.rb", "#{directory}/resolvers/*.rb"].sort.each do |file|
|
54
|
+
require file
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_columns_spec
|
59
|
+
YAML.safe_load(<<~COLUMNS_SPEC)
|
60
|
+
---
|
61
|
+
host:
|
62
|
+
resolver: identity
|
63
|
+
customer:
|
64
|
+
formatter: ellipsis
|
65
|
+
max_length: 20
|
66
|
+
cpu:
|
67
|
+
resolver: cpu
|
68
|
+
os:
|
69
|
+
resolver: os
|
70
|
+
human_name: Operating System
|
71
|
+
reboot_required:
|
72
|
+
resolver: reboot_required
|
73
|
+
formatter: boolean
|
74
|
+
human_name: R
|
75
|
+
COLUMNS_SPEC
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'skittlize'
|
4
|
+
require 'terminal-table'
|
5
|
+
|
6
|
+
require 'motoko/utils/snake_to_camel'
|
7
|
+
|
8
|
+
module Motoko
|
9
|
+
class Formatter
|
10
|
+
attr_reader :options, :columns_spec, :shortcuts
|
11
|
+
attr_accessor :columns, :nodes, :mono, :wide, :count, :sort_by
|
12
|
+
|
13
|
+
include Motoko::Utils::SnakeToCamel
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
config = Config.new
|
17
|
+
@nodes = []
|
18
|
+
@columns = config.columns
|
19
|
+
@mono = false
|
20
|
+
@wide = false
|
21
|
+
@count = false
|
22
|
+
@sort_by = config.sort_by
|
23
|
+
@columns_spec = config.columns_spec
|
24
|
+
@shortcuts = config.shortcuts
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
return '' if nodes.empty?
|
29
|
+
|
30
|
+
@rows = nil
|
31
|
+
@column_resolvers = nil
|
32
|
+
|
33
|
+
columns.uniq!
|
34
|
+
|
35
|
+
table = ::Terminal::Table.new headings: headings, rows: data
|
36
|
+
column_resolvers.each_with_index do |column, idx|
|
37
|
+
table.align_column(idx, column.align) if column.align
|
38
|
+
end
|
39
|
+
table.to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def column_resolvers
|
43
|
+
@column_resolvers ||= columns.map do |column|
|
44
|
+
klass = columns_spec[column].delete('resolver') || 'Fact'
|
45
|
+
|
46
|
+
Object.const_get("Motoko::Resolvers::#{snake_to_camel_case(klass)}").new(column, columns_spec[column])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def data
|
51
|
+
return @data if @data
|
52
|
+
|
53
|
+
@data = sorted_nodes.map! do |node|
|
54
|
+
column_resolvers.map do |column|
|
55
|
+
column.value(node)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
@data.skittlize!(split: "\n", join: ', ') unless mono
|
60
|
+
|
61
|
+
@data
|
62
|
+
end
|
63
|
+
|
64
|
+
def headings
|
65
|
+
column_resolvers.each_with_index.map do |column, idx|
|
66
|
+
name = column.human_name
|
67
|
+
if count
|
68
|
+
different_values = data.map { |line| line[idx] }.uniq.compact.count
|
69
|
+
name += " (#{different_values})" if different_values > 1
|
70
|
+
end
|
71
|
+
{
|
72
|
+
value: mono ? name : "\e[1m#{name}\e[0m",
|
73
|
+
alignment: :center,
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def sorted_nodes
|
79
|
+
nodes.sort_by { |a| sort_by.map { |c| a.fact(c) || '' } + [a.identity] }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motoko
|
4
|
+
module Formatters
|
5
|
+
class BaseFormatter
|
6
|
+
def initialize(options = {}) end
|
7
|
+
|
8
|
+
def format(value)
|
9
|
+
case value
|
10
|
+
when Array
|
11
|
+
value.join("\n")
|
12
|
+
when Hash
|
13
|
+
value.keys.join("\n")
|
14
|
+
else
|
15
|
+
value.to_s
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Motoko
|
6
|
+
module Formatters
|
7
|
+
class Datetime < BaseFormatter
|
8
|
+
def format(value)
|
9
|
+
DateTime.parse(value).to_time.getlocal.to_s
|
10
|
+
rescue ArgumentError, TypeError
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'motoko/utils/time_ago'
|
4
|
+
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module Motoko
|
8
|
+
module Formatters
|
9
|
+
class DatetimeAgo < BaseFormatter
|
10
|
+
include Motoko::Utils::TimeAgo
|
11
|
+
|
12
|
+
def format(value)
|
13
|
+
return nil unless value
|
14
|
+
|
15
|
+
seconds_to_human(Time.now - DateTime.parse(value).to_time)
|
16
|
+
rescue ArgumentError
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motoko
|
4
|
+
module Formatters
|
5
|
+
class Ellipsis < BaseFormatter
|
6
|
+
attr_accessor :max_length
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
super
|
10
|
+
@max_length = options.delete('max_length') || 20
|
11
|
+
end
|
12
|
+
|
13
|
+
def format(value)
|
14
|
+
return nil unless value
|
15
|
+
|
16
|
+
res = value.dup
|
17
|
+
res[(max_length - 1)..-1] = '…' if res.length > max_length
|
18
|
+
res
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'motoko/utils/time_ago'
|
4
|
+
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
module Motoko
|
8
|
+
module Formatters
|
9
|
+
class TimestampAgo < BaseFormatter
|
10
|
+
include Motoko::Utils::TimeAgo
|
11
|
+
|
12
|
+
def format(value)
|
13
|
+
return nil unless value
|
14
|
+
|
15
|
+
seconds_to_human(Time.now - Time.at(Integer(value)))
|
16
|
+
rescue ArgumentError
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/motoko/node.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Motoko
|
4
|
+
class Node
|
5
|
+
attr_reader :identity
|
6
|
+
|
7
|
+
def initialize(identity, facts)
|
8
|
+
@identity = identity
|
9
|
+
@facts = facts
|
10
|
+
end
|
11
|
+
|
12
|
+
def fact(name)
|
13
|
+
result = @facts
|
14
|
+
components = name.to_s.split('.')
|
15
|
+
while (component = components.shift)
|
16
|
+
case result
|
17
|
+
when Hash
|
18
|
+
result = result[component]
|
19
|
+
when Array
|
20
|
+
result = result[Integer(component)]
|
21
|
+
when NilClass
|
22
|
+
return nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
class Choria < Motoko::Node
|
29
|
+
def initialize(node)
|
30
|
+
super(node[:sender], node[:data][:facts])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|