octofacts-updater 0.5.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/.version +1 -0
- data/bin/octofacts-updater +6 -0
- data/lib/octofacts_updater.rb +19 -0
- data/lib/octofacts_updater/cli.rb +239 -0
- data/lib/octofacts_updater/fact.rb +145 -0
- data/lib/octofacts_updater/fact_index.rb +164 -0
- data/lib/octofacts_updater/fixture.rb +136 -0
- data/lib/octofacts_updater/plugin.rb +70 -0
- data/lib/octofacts_updater/plugins/ip.rb +38 -0
- data/lib/octofacts_updater/plugins/ssh.rb +23 -0
- data/lib/octofacts_updater/plugins/static.rb +53 -0
- data/lib/octofacts_updater/service/base.rb +35 -0
- data/lib/octofacts_updater/service/enc.rb +41 -0
- data/lib/octofacts_updater/service/github.rb +230 -0
- data/lib/octofacts_updater/service/local_file.rb +36 -0
- data/lib/octofacts_updater/service/puppetdb.rb +42 -0
- data/lib/octofacts_updater/service/ssh.rb +58 -0
- data/lib/octofacts_updater/version.rb +3 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 05efcd40e8410c43eb2fd197165de57b754d0e28
|
4
|
+
data.tar.gz: ad65e6ce8d288593e0b2b03075fcda6f52fbd4cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 507c0eecd6dd8c7c281e6734f1dbdde0731ee2e8cfcedd2bc5f9d7fc68fc7805207d5032d3ffc1d20df0e75154afb3786e0ff6f93d03af9e271592d9d45996ec
|
7
|
+
data.tar.gz: e017a38aa6681a768d06cc085f4dc4480e4ef736d6782fd6f05a9d2b24b391bc5911b9e0d47791f080ea3321c96b3117f44cd864e60ba2108f95c22c675e60f7
|
data/.version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.0
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "octofacts_updater/cli"
|
2
|
+
require "octofacts_updater/fact"
|
3
|
+
require "octofacts_updater/fact_index"
|
4
|
+
require "octofacts_updater/fixture"
|
5
|
+
require "octofacts_updater/plugin"
|
6
|
+
require "octofacts_updater/plugins/ip"
|
7
|
+
require "octofacts_updater/plugins/ssh"
|
8
|
+
require "octofacts_updater/plugins/static"
|
9
|
+
require "octofacts_updater/service/base"
|
10
|
+
require "octofacts_updater/service/enc"
|
11
|
+
require "octofacts_updater/service/github"
|
12
|
+
require "octofacts_updater/service/local_file"
|
13
|
+
require "octofacts_updater/service/puppetdb"
|
14
|
+
require "octofacts_updater/service/ssh"
|
15
|
+
require "octofacts_updater/version"
|
16
|
+
|
17
|
+
module OctofactsUpdater
|
18
|
+
#
|
19
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
# :nocov:
|
2
|
+
require "optparse"
|
3
|
+
|
4
|
+
module OctofactsUpdater
|
5
|
+
class CLI
|
6
|
+
# Constructor.
|
7
|
+
#
|
8
|
+
# argv - The Array with command line arguments.
|
9
|
+
def initialize(argv)
|
10
|
+
@opts = {}
|
11
|
+
OptionParser.new(argv) do |opts|
|
12
|
+
opts.banner = "Usage: octofacts-updater [options]"
|
13
|
+
|
14
|
+
opts.on("-a", "--action <action>", String, "Action to take") do |a|
|
15
|
+
@opts[:action] = a
|
16
|
+
end
|
17
|
+
|
18
|
+
opts.on("-c", "--config <config_file>", String, "Path to configuration file") do |f|
|
19
|
+
raise "Invalid configuration file" unless File.file?(f)
|
20
|
+
@opts[:config] = f
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-H", "--hostname <hostname>", String, "FQDN of the host whose facts are to be gathered") do |h|
|
24
|
+
@opts[:hostname] = h
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-o", "--output-file <filename>", String, "Path to output file to write") do |i|
|
28
|
+
@opts[:output_file] = i
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-l", "--list <host1,host2,...>", Array, "List of hosts to update or index") do |l|
|
32
|
+
@opts[:host_list] = l
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("--[no-]quick", "Quick indexing: Use existing YAML fact fixtures when available") do |q|
|
36
|
+
@opts[:quick] = q
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-p", "--path <directory>", "Path where to read/write host fixtures when working in bulk") do |path|
|
40
|
+
@opts[:path] = path
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("--github", "Push any changes to a branch on GitHub (requires --action=bulk)") do
|
44
|
+
@opts[:github] ||= {}
|
45
|
+
@opts[:github][:enabled] = true
|
46
|
+
end
|
47
|
+
|
48
|
+
opts.on("--datasource <datasource>", "Specify the data source to use when retrieving facts (localfile, puppetdb, ssh)") do |ds|
|
49
|
+
unless %w{localfile puppetdb ssh}.include?(ds)
|
50
|
+
raise ArgumentError, "Invalid datasource #{ds.inspect}. Acceptable values: localfile, puppetdb, ssh."
|
51
|
+
end
|
52
|
+
@opts[:datasource] = ds.to_sym
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on("--config-override <section:key=value>", Array, "Override a portion of the configuration") do |co_array|
|
56
|
+
co_array.each do |co|
|
57
|
+
if co =~ /\A(\w+):(\S+?)=(.+?)\z/
|
58
|
+
@opts[Regexp.last_match(1).to_sym] ||= {}
|
59
|
+
@opts[Regexp.last_match(1).to_sym][Regexp.last_match(2).to_sym] = Regexp.last_match(3)
|
60
|
+
else
|
61
|
+
raise ArgumentError, "Malformed argument: --config-override must be in the format section:key=value"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end.parse!
|
66
|
+
validate_cli
|
67
|
+
end
|
68
|
+
|
69
|
+
def usage
|
70
|
+
puts "Usage: octofacts-updater --action <action> [--config-file /path/to/config.yaml] [other options]"
|
71
|
+
puts ""
|
72
|
+
puts "Available actions:"
|
73
|
+
puts " bulk: Update fixtures and index in bulk"
|
74
|
+
puts " facts: Obtain facts for one node (requires --hostname <hostname>)"
|
75
|
+
puts ""
|
76
|
+
end
|
77
|
+
|
78
|
+
# Run method. Call this to run the octofacts updater with the object that was
|
79
|
+
# previously construcuted.
|
80
|
+
def run
|
81
|
+
unless opts[:action]
|
82
|
+
usage
|
83
|
+
exit 255
|
84
|
+
end
|
85
|
+
|
86
|
+
@config = {}
|
87
|
+
|
88
|
+
if opts[:config]
|
89
|
+
@config = YAML.load_file(opts[:config])
|
90
|
+
substitute_relative_paths!(@config, File.dirname(opts[:config]))
|
91
|
+
load_plugins(@config["plugins"]) if @config.key?("plugins")
|
92
|
+
end
|
93
|
+
|
94
|
+
@config[:options] = {}
|
95
|
+
opts.each do |k, v|
|
96
|
+
if v.is_a?(Hash)
|
97
|
+
@config[k.to_s] ||= {}
|
98
|
+
v.each do |v_key, v_val|
|
99
|
+
@config[k.to_s][v_key.to_s] = v_val
|
100
|
+
@config[k.to_s].delete(v_key.to_s) if v_val.nil?
|
101
|
+
end
|
102
|
+
else
|
103
|
+
@config[:options][k] = v
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
return handle_action_bulk if opts[:action] == "bulk"
|
108
|
+
return handle_action_facts if opts[:action] == "facts"
|
109
|
+
|
110
|
+
usage
|
111
|
+
exit 255
|
112
|
+
end
|
113
|
+
|
114
|
+
def substitute_relative_paths!(object_in, basedir)
|
115
|
+
if object_in.is_a?(Hash)
|
116
|
+
object_in.each { |k, v| object_in[k] = substitute_relative_paths!(v, basedir) }
|
117
|
+
elsif object_in.is_a?(Array)
|
118
|
+
object_in.map! { |v| substitute_relative_paths!(v, basedir) }
|
119
|
+
elsif object_in.is_a?(String)
|
120
|
+
if object_in =~ %r{^\.\.?(/|\z)}
|
121
|
+
object_in = File.expand_path(object_in, basedir)
|
122
|
+
end
|
123
|
+
object_in
|
124
|
+
else
|
125
|
+
object_in
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def handle_action_bulk
|
130
|
+
facts_to_index = @config.fetch("index", {})["indexed_facts"]
|
131
|
+
unless facts_to_index.is_a?(Array)
|
132
|
+
raise ArgumentError, "Must declare index:indexed_facts in configuration to use bulk update"
|
133
|
+
end
|
134
|
+
|
135
|
+
nodes = if opts[:host_list]
|
136
|
+
opts[:host_list]
|
137
|
+
elsif opts[:hostname]
|
138
|
+
[opts[:hostname]]
|
139
|
+
else
|
140
|
+
OctofactsUpdater::FactIndex.load_file(index_file).nodes(true)
|
141
|
+
end
|
142
|
+
if nodes.empty?
|
143
|
+
raise ArgumentError, "Cannot run bulk update with no nodes to check"
|
144
|
+
end
|
145
|
+
|
146
|
+
path = opts[:path] || @config.fetch("index", {})["node_path"]
|
147
|
+
paths = []
|
148
|
+
|
149
|
+
fixtures = nodes.map do |hostname|
|
150
|
+
if opts[:quick] && path && File.file?(File.join(path, "#{hostname}.yaml"))
|
151
|
+
OctofactsUpdater::Fixture.load_file(hostname, File.join(path, "#{hostname}.yaml"))
|
152
|
+
else
|
153
|
+
fixture = OctofactsUpdater::Fixture.make(hostname, @config)
|
154
|
+
if path && File.directory?(path)
|
155
|
+
fixture.write_file(File.join(path, "#{hostname}.yaml"))
|
156
|
+
paths << File.join(path, "#{hostname}.yaml")
|
157
|
+
end
|
158
|
+
fixture
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
index = OctofactsUpdater::FactIndex.load_file(index_file)
|
163
|
+
index.reindex(facts_to_index, fixtures)
|
164
|
+
index.write_file
|
165
|
+
paths << index_file
|
166
|
+
|
167
|
+
if opts[:github] && opts[:github][:enabled]
|
168
|
+
OctofactsUpdater::Service::GitHub.run(config["github"]["base_directory"], paths, @config)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def handle_action_facts
|
173
|
+
unless opts[:hostname]
|
174
|
+
raise ArgumentError, "--hostname <hostname> must be specified to use --action facts"
|
175
|
+
end
|
176
|
+
|
177
|
+
facts_for_one_node
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
attr_reader :config, :opts
|
183
|
+
|
184
|
+
# Determine the facts for one node and print to the console or write to the specified file.
|
185
|
+
def facts_for_one_node
|
186
|
+
fixture = OctofactsUpdater::Fixture.make(opts[:hostname], @config)
|
187
|
+
print_or_write(fixture.to_yaml)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Get the index file from the options or configuration file. Raise error if it does not exist or
|
191
|
+
# was not specified.
|
192
|
+
def index_file
|
193
|
+
@index_file ||= begin
|
194
|
+
if config.fetch("index", {})["file"]
|
195
|
+
return config["index"]["file"] if File.file?(config["index"]["file"])
|
196
|
+
raise Errno::ENOENT, "Index file (#{config['index']['file'].inspect}) does not exist"
|
197
|
+
end
|
198
|
+
raise ArgumentError, "No index file specified on command line (--index-file) or in configuration file"
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Load plugins as per configuration file. Note: all plugins embedded in this gem are automatically
|
203
|
+
# loaded. This is just for user-specified plugins.
|
204
|
+
#
|
205
|
+
# plugins - An Array of file names to load
|
206
|
+
def load_plugins(plugins)
|
207
|
+
unless plugins.is_a?(Array)
|
208
|
+
raise ArgumentError, "load_plugins expects an array, got #{plugins.inspect}"
|
209
|
+
end
|
210
|
+
|
211
|
+
plugins.each do |plugin|
|
212
|
+
plugin_file = plugin.start_with?("/") ? plugin : File.expand_path("../../#{plugin}", File.dirname(__FILE__))
|
213
|
+
unless File.file?(plugin_file)
|
214
|
+
raise Errno::ENOENT, "Failed to find plugin #{plugin.inspect} at #{plugin_file}"
|
215
|
+
end
|
216
|
+
require plugin_file
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Print or write to file depending on whether or not the output file was set.
|
221
|
+
#
|
222
|
+
# data - Data to print or write.
|
223
|
+
def print_or_write(data)
|
224
|
+
if opts[:output_file]
|
225
|
+
File.open(opts[:output_file], "w") { |f| f.write(data) }
|
226
|
+
else
|
227
|
+
puts data
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Validate command line options. Kick out invalid combinations of options immediately.
|
232
|
+
def validate_cli
|
233
|
+
if opts[:path] && !File.directory?(opts[:path])
|
234
|
+
raise Errno::ENOENT, "An existing directory must be specified with -p/--path"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
# :nocov:
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# This class represents a fact, either structured or unstructured.
|
2
|
+
# The fact has a name and a value. The name is a string, and the value
|
3
|
+
# can either be a string/integer/boolean (unstructured) or a hash (structured).
|
4
|
+
# This class also has methods used to deal with structured facts (in particular, allowing
|
5
|
+
# representation of a structure delimited with ::).
|
6
|
+
|
7
|
+
module OctofactsUpdater
|
8
|
+
class Fact
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
# Constructor.
|
12
|
+
#
|
13
|
+
# name - The String naming the fact.
|
14
|
+
# value - The arbitrary object with the value of the fact.
|
15
|
+
def initialize(name, value)
|
16
|
+
@name = name
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get the value of the fact. If the name is specified, this will dig into a structured fact to pull
|
21
|
+
# out the value within the structure.
|
22
|
+
#
|
23
|
+
# name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters)
|
24
|
+
#
|
25
|
+
# Returns the value of the fact.
|
26
|
+
def value(name_in = nil)
|
27
|
+
# Just a normal lookup -- return the value
|
28
|
+
return @value if name_in.nil?
|
29
|
+
|
30
|
+
# Structured lookup returns nil unless the fact is actually structured.
|
31
|
+
return unless @value.is_a?(Hash)
|
32
|
+
|
33
|
+
# Dig into the hash to pull out the desired value.
|
34
|
+
pointer = @value
|
35
|
+
parts = name_in.split("::")
|
36
|
+
last_part = parts.pop
|
37
|
+
|
38
|
+
parts.each do |part|
|
39
|
+
return unless pointer[part].is_a?(Hash)
|
40
|
+
pointer = pointer[part]
|
41
|
+
end
|
42
|
+
|
43
|
+
pointer[last_part]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the value of the fact.
|
47
|
+
#
|
48
|
+
# new_value - An object with the new value for the fact
|
49
|
+
def value=(new_value)
|
50
|
+
set_value(new_value)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Set the value of the fact. If the name is specified, this will dig into a structured fact to set
|
54
|
+
# the value within the structure.
|
55
|
+
#
|
56
|
+
# new_value - An object with the new value for the fact
|
57
|
+
# name_in - An optional String to dig into the structure (formatted with :: indicating hash delimiters)
|
58
|
+
def set_value(new_value, name_in = nil)
|
59
|
+
if name_in.nil?
|
60
|
+
if new_value.is_a?(Proc)
|
61
|
+
return @value = new_value.call(@value)
|
62
|
+
end
|
63
|
+
|
64
|
+
return @value = new_value
|
65
|
+
end
|
66
|
+
|
67
|
+
parts = if name_in.is_a?(String)
|
68
|
+
name_in.split("::")
|
69
|
+
elsif name_in.is_a?(Array)
|
70
|
+
name_in.map do |item|
|
71
|
+
if item.is_a?(String)
|
72
|
+
item
|
73
|
+
elsif item.is_a?(Hash) && item.key?("regexp")
|
74
|
+
Regexp.new(item["regexp"])
|
75
|
+
else
|
76
|
+
raise ArgumentError, "Unable to interpret structure item: #{item.inspect}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
else
|
80
|
+
raise ArgumentError, "Unable to interpret structure: #{name_in.inspect}"
|
81
|
+
end
|
82
|
+
|
83
|
+
set_structured_value(@value, parts, new_value)
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
# Set a value in the data structure of a structured fact. This is intended to be
|
89
|
+
# called recursively.
|
90
|
+
#
|
91
|
+
# subhash - The Hash, part of the fact, being operated upon
|
92
|
+
# parts - The Array to dig in to the hash
|
93
|
+
# value - The value to set the ultimate last part to
|
94
|
+
#
|
95
|
+
# Does not return anything, but modifies 'subhash'
|
96
|
+
def set_structured_value(subhash, parts, value)
|
97
|
+
return if subhash.nil?
|
98
|
+
raise ArgumentError, "Cannot set structured value at #{parts.first.inspect}" unless subhash.is_a?(Hash)
|
99
|
+
raise ArgumentError, "parts must be an Array, got #{parts.inspect}" unless parts.is_a?(Array)
|
100
|
+
|
101
|
+
# At the top level, find all keys that match the first item in the parts.
|
102
|
+
matching_keys = subhash.keys.select do |key|
|
103
|
+
if parts.first.is_a?(String)
|
104
|
+
key == parts.first
|
105
|
+
elsif parts.first.is_a?(Regexp)
|
106
|
+
parts.first.match(key)
|
107
|
+
else
|
108
|
+
# :nocov:
|
109
|
+
# This is a bug - this code should be unreachable because of the checking in `set_value`
|
110
|
+
raise ArgumentError, "part must be a string or regexp, got #{parts.first.inspect}"
|
111
|
+
# :nocov:
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Auto-create a new hash if there is a value, the part is a string, and the key doesn't exist.
|
116
|
+
if parts.first.is_a?(String) && !value.nil? && !subhash.key?(parts.first)
|
117
|
+
subhash[parts.first] = {}
|
118
|
+
matching_keys << parts.first
|
119
|
+
end
|
120
|
+
return unless matching_keys.any?
|
121
|
+
|
122
|
+
# If we are at the end, set the value or delete the key.
|
123
|
+
if parts.size == 1
|
124
|
+
if value.nil?
|
125
|
+
matching_keys.each { |k| subhash.delete(k) }
|
126
|
+
elsif value.is_a?(Proc)
|
127
|
+
matching_keys.each do |k|
|
128
|
+
new_value = value.call(subhash[k])
|
129
|
+
if new_value.nil?
|
130
|
+
subhash.delete(k)
|
131
|
+
else
|
132
|
+
subhash[k] = new_value
|
133
|
+
end
|
134
|
+
end
|
135
|
+
else
|
136
|
+
matching_keys.each { |k| subhash[k] = value }
|
137
|
+
end
|
138
|
+
return
|
139
|
+
end
|
140
|
+
|
141
|
+
# We are not at the end. Recurse down to the next level.
|
142
|
+
matching_keys.each { |k| set_structured_value(subhash[k], parts[1..-1], value) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
# This class represents a fact index, which is ultimately represented by a YAML file of
|
2
|
+
# each index fact, the values seen, and the node(s) containing each value.
|
3
|
+
#
|
4
|
+
# fact_one:
|
5
|
+
# value_one:
|
6
|
+
# - node-1.example.net
|
7
|
+
# - node-2.example.net
|
8
|
+
# value_three:
|
9
|
+
# - node-3.example.net
|
10
|
+
# fact_two:
|
11
|
+
# value_abc:
|
12
|
+
# - node-1.example.net
|
13
|
+
# value_def:
|
14
|
+
# - node-2.example.net
|
15
|
+
# - node-3.example.net
|
16
|
+
|
17
|
+
require "set"
|
18
|
+
require "yaml"
|
19
|
+
|
20
|
+
module OctofactsUpdater
|
21
|
+
class FactIndex
|
22
|
+
# We will create a pseudo-fact that simply lists all of the nodes that were considered
|
23
|
+
# in the index. Define the name of that pseudo-fact here.
|
24
|
+
TOP_LEVEL_NODES_KEY = "_nodes".freeze
|
25
|
+
|
26
|
+
attr_reader :index_data
|
27
|
+
|
28
|
+
# Load an index from the YAML file.
|
29
|
+
#
|
30
|
+
# filename - A String with the file to be loaded.
|
31
|
+
#
|
32
|
+
# Returns a OctofactsUpdater::FactIndex object.
|
33
|
+
def self.load_file(filename)
|
34
|
+
unless File.file?(filename)
|
35
|
+
raise Errno::ENOENT, "load_index cannot load #{filename.inspect}"
|
36
|
+
end
|
37
|
+
|
38
|
+
data = YAML.safe_load(File.read(filename))
|
39
|
+
new(data, filename: filename)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Constructor.
|
43
|
+
#
|
44
|
+
# data - A Hash of existing index data.
|
45
|
+
# filename - Optionally, a String with a file name to write the index to
|
46
|
+
def initialize(data = {}, filename: nil)
|
47
|
+
@index_data = data
|
48
|
+
@filename = filename
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add a fact to the index. If the fact already exists in the index, this will overwrite it.
|
52
|
+
#
|
53
|
+
# fact_name - A String with the name of the fact
|
54
|
+
# fixtures - An Array with fact fixtures (must respond to .facts and .hostname)
|
55
|
+
def add(fact_name, fixtures)
|
56
|
+
@index_data[fact_name] ||= {}
|
57
|
+
fixtures.each do |fixture|
|
58
|
+
fact_value = get_fact(fixture, fact_name)
|
59
|
+
next if fact_value.nil?
|
60
|
+
@index_data[fact_name][fact_value] ||= []
|
61
|
+
@index_data[fact_name][fact_value] << fixture.hostname
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Get a list of all of the nodes in the index. This supports a quick mode (default) where the
|
66
|
+
# TOP_LEVEL_NODES_KEY key is used, and a more detailed mode where this digs through each indexed
|
67
|
+
# fact and value to build a list of nodes.
|
68
|
+
#
|
69
|
+
# quick_mode - Boolean whether to use quick mode (default=true)
|
70
|
+
#
|
71
|
+
# Returns an Array of nodes whose facts are indexed.
|
72
|
+
def nodes(quick_mode = true)
|
73
|
+
if quick_mode && @index_data.key?(TOP_LEVEL_NODES_KEY)
|
74
|
+
return @index_data[TOP_LEVEL_NODES_KEY]
|
75
|
+
end
|
76
|
+
|
77
|
+
seen_hosts = Set.new
|
78
|
+
@index_data.each do |fact_name, fact_values|
|
79
|
+
next if fact_name == TOP_LEVEL_NODES_KEY
|
80
|
+
fact_values.each do |_fact_value, nodes|
|
81
|
+
seen_hosts.merge(nodes)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
seen_hosts.to_a.sort
|
85
|
+
end
|
86
|
+
|
87
|
+
# Rebuild an index with a specified list of facts. This will remove any indexed facts that
|
88
|
+
# are not on the list of facts to use.
|
89
|
+
#
|
90
|
+
# facts_to_index - An Array of Strings with facts to index
|
91
|
+
# fixtures - An Array with fact fixtures (must respond to .facts and .hostname)
|
92
|
+
def reindex(facts_to_index, fixtures)
|
93
|
+
@index_data = {}
|
94
|
+
facts_to_index.each { |fact| add(fact, fixtures) }
|
95
|
+
set_top_level_nodes_fact(fixtures)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Create the top level nodes pseudo-fact.
|
99
|
+
#
|
100
|
+
# fixtures - An Array with fact fixtures (must respond to .hostname)
|
101
|
+
def set_top_level_nodes_fact(fixtures)
|
102
|
+
@index_data[TOP_LEVEL_NODES_KEY] = fixtures.map { |f| f.hostname }.sort
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get YAML representation of the index.
|
106
|
+
# This sorts the hash and any arrays without modifying the object.
|
107
|
+
def to_yaml
|
108
|
+
YAML.dump(recursive_sort(index_data))
|
109
|
+
end
|
110
|
+
|
111
|
+
def recursive_sort(object_in)
|
112
|
+
if object_in.is_a?(Hash)
|
113
|
+
object_out = {}
|
114
|
+
object_in.keys.sort.each { |k| object_out[k] = recursive_sort(object_in[k]) }
|
115
|
+
object_out
|
116
|
+
elsif object_in.is_a?(Array)
|
117
|
+
object_in.sort.map { |v| recursive_sort(v) }
|
118
|
+
else
|
119
|
+
object_in
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Write the fact index out to a YAML file.
|
124
|
+
#
|
125
|
+
# filename - A String with the file to write (defaults to filename from constructor if available)
|
126
|
+
def write_file(filename = nil)
|
127
|
+
filename ||= @filename
|
128
|
+
unless filename.is_a?(String)
|
129
|
+
raise ArgumentError, "Called write_file() for fact_index without a filename"
|
130
|
+
end
|
131
|
+
File.open(filename, "w") { |f| f.write(to_yaml) }
|
132
|
+
end
|
133
|
+
|
134
|
+
private
|
135
|
+
|
136
|
+
# Extract a (possibly) structured fact.
|
137
|
+
#
|
138
|
+
# fixture - Fact fixture, must respond to .facts
|
139
|
+
# fact_name - A String with the name of the fact
|
140
|
+
#
|
141
|
+
# Returns the value of the fact, or nil if fact or structure does not exist.
|
142
|
+
def get_fact(fixture, fact_name)
|
143
|
+
pointer = fixture.facts
|
144
|
+
|
145
|
+
# Get the fact of interest from the fixture, whether structured or not.
|
146
|
+
components = fact_name.split(".")
|
147
|
+
first_component = components.shift
|
148
|
+
return unless pointer.key?(first_component)
|
149
|
+
|
150
|
+
# For simple non-structured facts, just return the value.
|
151
|
+
return pointer[first_component].value if components.empty?
|
152
|
+
|
153
|
+
# Structured facts: dig into the structure.
|
154
|
+
pointer = pointer[first_component].value
|
155
|
+
last_component = components.pop
|
156
|
+
components.each do |part|
|
157
|
+
return unless pointer.key?(part)
|
158
|
+
return unless pointer[part].is_a?(Hash)
|
159
|
+
pointer = pointer[part]
|
160
|
+
end
|
161
|
+
pointer[last_component]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|