motoko 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|