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