octofacts-updater 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "octofacts_updater"
5
+ cli = OctofactsUpdater::CLI.new(ARGV)
6
+ cli.run
@@ -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