rfacter 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 57a9d18faa5cfba3610ad4e136cc0f6b67674f7e
4
+ data.tar.gz: ddc6e4a94f5cb61e2ff6133c46d1eb1bda340eed
5
+ SHA512:
6
+ metadata.gz: e2633c5de708b220c5d8cb5bb39aadb635cce530fe9d7a5d1c228860c82f3f698c54ca4d45f33850fd796914e6a53b69f96f3efaa2908583eb6b3cd2a585c20f
7
+ data.tar.gz: b6ebc55e51bfb161ef8363e3f1381e9b4ce5d37ef21bfc27eee45b101c055c2a279a747bd36049f2b0c59c5752de9fa8426d4af7931c1e7f9646576a6f198244
data/bin/rfacter ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rfacter/cli'
4
+
5
+ RFacter::CLI.run(ARGV)
@@ -0,0 +1,51 @@
1
+ require 'forwardable'
2
+ require 'json'
3
+
4
+ require 'rfacter'
5
+
6
+ require_relative 'config'
7
+ require_relative 'node'
8
+ require_relative 'util/collection'
9
+
10
+ module RFacter::CLI
11
+ extend SingleForwardable
12
+
13
+ delegate([:logger] => :@config)
14
+
15
+ def self.run(argv)
16
+ names = RFacter::Config.configure_from_argv!(argv)
17
+ @config = RFacter::Config.config
18
+
19
+ if @config.nodes.empty?
20
+ @config.nodes['localhost'] = RFacter::Node.new('localhost')
21
+ end
22
+
23
+ logger.info('cli::run') { "Configured nodes: #{@config.nodes.values.map(&:hostname)}" }
24
+
25
+ collection = RFacter::Util::Collection.new
26
+ collection.load_all
27
+
28
+ facts = @config.nodes.values.inject(Hash.new) do |h, node|
29
+ node_facts = if names.empty?
30
+ collection.to_hash(node)
31
+ else
32
+ names.inject(Hash.new) do |n, name|
33
+ n[name] = collection.value(name, node)
34
+ n
35
+ end
36
+ end
37
+
38
+ # TODO: Implement proper per-node Fact caching so that we don't just
39
+ # reset the colleciton on each loop.
40
+ collection.flush
41
+
42
+ h[node.hostname] = node_facts
43
+ h
44
+ end
45
+
46
+
47
+ puts JSON.pretty_generate(facts)
48
+
49
+ exit 0
50
+ end
51
+ end
@@ -0,0 +1,31 @@
1
+ require 'rfacter'
2
+ require_relative '../util/logger'
3
+
4
+ # Class for top-level RFacter configuration
5
+ #
6
+ # Instances of this class hold top-level configuration values and shared
7
+ # service objects such as loggers.
8
+ #
9
+ # @since 0.1.0
10
+ class RFacter::Config::Settings
11
+ # Access the logger instance
12
+ #
13
+ # The object stored here should conform to the interface prresented by
14
+ # the Ruby logger.
15
+ #
16
+ # @return [Logger]
17
+ attr_reader :logger
18
+
19
+ # A list of nodes to operate on
20
+ #
21
+ # @return [Hash{String => RFacter::Node}] A list of URIs identifying nodes along with the
22
+ # schemes to use when contacting them.
23
+ attr_reader :nodes
24
+
25
+ def initialize(**options)
26
+ @logger = RFacter::Util::Logger.new($stderr)
27
+ @logger.level = Logger::WARN
28
+
29
+ @nodes = Hash.new
30
+ end
31
+ end
@@ -0,0 +1,87 @@
1
+ require 'optparse'
2
+ require 'optparse/uri'
3
+ require 'logger'
4
+
5
+ require 'rfacter'
6
+
7
+ require_relative 'config/settings'
8
+ require_relative 'node'
9
+
10
+ # Stores and sets global configuration
11
+ #
12
+ # This module stores a global instance of {RFacter::Config::Settings}
13
+ # and contains methods for initializing the settings instance from
14
+ # various sources.
15
+ #
16
+ # @since 0.1.0
17
+ module RFacter::Config
18
+ # Return global configuration
19
+ #
20
+ # @return [RFacter::Config::Settings]
21
+ def self.config
22
+ @settings ||= RFacter::Config::Settings.new
23
+ end
24
+
25
+ # Set global configuration from an argument vector
26
+ #
27
+ # This method calls {.parse_argv} and uses the results to update the
28
+ # settings instance returned by {.config}.
29
+ #
30
+ # @param argv [Array<String>] A list of strings passed as command line
31
+ # arguments.
32
+ #
33
+ # @return [Array<string>] An array of command line arguments that were
34
+ # not consumed by the parser.
35
+ def self.configure_from_argv!(argv)
36
+ args, _ = parse_argv(argv, self.config)
37
+
38
+ args
39
+ end
40
+
41
+ # Configure a settings instance by parsing an argument vector
42
+ #
43
+ # @param argv [Array<String>] Command line arguments as an array of
44
+ # strings.
45
+ #
46
+ # @param settings [RFacter::Config::Settings, nil] A settings object to
47
+ # configure. A new object will be created if nothing is passed.
48
+ #
49
+ # @return [Array<Array<String>, RFacter::Config::Settings>>] A tuple
50
+ # containing a configured instance of {RFacter::Config::Settings}
51
+ # followed by an array of command line arguments that were not consumed
52
+ # by the parser.
53
+ def self.parse_argv(argv, settings = nil)
54
+ settings ||= RFacter::Config::Settings.new
55
+ parser = OptionParser.new
56
+ args = argv.dup
57
+
58
+ parser.separator("\nOptions\n=======")
59
+
60
+ parser.on('--version', 'Print version number and exit.') do
61
+ puts RFacter::VERSION
62
+ exit 0
63
+ end
64
+
65
+ parser.on('-h', '--help', 'Print this help message.') do
66
+ puts parser.help
67
+ exit 0
68
+ end
69
+
70
+ parser.on('-v', '--verbose', 'Raise log level to INFO.') do
71
+ settings.logger.level = Logger::INFO
72
+ end
73
+
74
+ parser.on('-d', '--debug', 'Raise log level to DEBUG.') do
75
+ settings.logger.level = Logger::DEBUG
76
+ end
77
+
78
+ parser.on('-n', '--node', '=MANDATORY', URI, 'Add a node by URI.') do |uri|
79
+ node = RFacter::Node.new(uri)
80
+ settings.nodes[node.hostname] = node
81
+ end
82
+
83
+ parser.parse!(args)
84
+
85
+ [args, settings]
86
+ end
87
+ end
@@ -0,0 +1,228 @@
1
+ require 'forwardable'
2
+
3
+ require 'rfacter'
4
+ require_relative '../config'
5
+
6
+ # Aggregates provide a mechanism for facts to be resolved in multiple steps.
7
+ #
8
+ # Aggregates are evaluated in two parts: generating individual chunks and then
9
+ # aggregating all chunks together. Each chunk is a block of code that generates
10
+ # a value, and may depend on other chunks when it runs. After all chunks have
11
+ # been evaluated they are passed to the aggregate block as Hash<name, result>.
12
+ # The aggregate block converts the individual chunks into a single value that is
13
+ # returned as the final value of the aggregate.
14
+ #
15
+ # @api public
16
+ # @since 2.0.0
17
+ class RFacter::Core::Aggregate
18
+ require_relative 'directed_graph'
19
+ require_relative 'resolvable'
20
+ require_relative 'suitable'
21
+ require_relative '../util/values'
22
+
23
+ extend Forwardable
24
+
25
+ instance_delegate([:logger] => :@config)
26
+
27
+ include RFacter::Core::Suitable
28
+ include RFacter::Core::Resolvable
29
+
30
+ # @!attribute [r] name
31
+ # @return [Symbol] The name of the aggregate resolution
32
+ attr_reader :name
33
+
34
+ # @!attribute [r] deps
35
+ # @api private
36
+ # @return [Facter::Core::DirectedGraph]
37
+ attr_reader :deps
38
+
39
+ # @!attribute [r] confines
40
+ # @return [Array<Facter::Core::Confine>] An array of confines restricting
41
+ # this to a specific platform
42
+ # @see Facter::Core::Suitable
43
+ attr_reader :confines
44
+
45
+ # @!attribute [r] fact
46
+ # @return [Facter::Util::Fact]
47
+ # @api private
48
+ attr_reader :fact
49
+
50
+ def initialize(name, fact, config: RFacter::Config.config, **options)
51
+ @name = name
52
+ @fact = fact
53
+ @config = config
54
+
55
+ @confines = []
56
+ @chunks = {}
57
+
58
+ @aggregate = nil
59
+ @deps = RFacter::Core::DirectedGraph.new
60
+ end
61
+
62
+ def set_options(options)
63
+ if options[:name]
64
+ @name = options.delete(:name)
65
+ end
66
+
67
+ if options.has_key?(:timeout)
68
+ @timeout = options.delete(:timeout)
69
+ end
70
+
71
+ if options.has_key?(:weight)
72
+ @weight = options.delete(:weight)
73
+ end
74
+
75
+ if not options.keys.empty?
76
+ raise ArgumentError, "Invalid aggregate options #{options.keys.inspect}"
77
+ end
78
+ end
79
+
80
+ def evaluate(&block)
81
+ instance_eval(&block)
82
+ end
83
+
84
+ # Define a new chunk for the given aggregate
85
+ #
86
+ # @api public
87
+ #
88
+ # @example Defining a chunk with no dependencies
89
+ # aggregate.chunk(:mountpoints) do
90
+ # # generate mountpoint information
91
+ # end
92
+ #
93
+ # @example Defining an chunk to add mount options
94
+ # aggregate.chunk(:mount_options, :require => [:mountpoints]) do |mountpoints|
95
+ # # `mountpoints` is the result of the previous chunk
96
+ # # generate mount option information based on the mountpoints
97
+ # end
98
+ #
99
+ # @param name [Symbol] A name unique to this aggregate describing the chunk
100
+ # @param opts [Hash]
101
+ # @option opts [Array<Symbol>, Symbol] require One or more chunks to evaluate
102
+ # and pass to this block.
103
+ # @yield [*Object] Zero or more chunk results
104
+ #
105
+ # @return [void]
106
+ def chunk(name, opts = {}, &block)
107
+ if not block_given?
108
+ raise ArgumentError, "#{self.class.name}#chunk requires a block"
109
+ end
110
+
111
+ deps = Array(opts.delete(:require))
112
+
113
+ if not opts.empty?
114
+ raise ArgumentError, "Unexpected options passed to #{self.class.name}#chunk: #{opts.keys.inspect}"
115
+ end
116
+
117
+ @deps[name] = deps
118
+ @chunks[name] = block
119
+ end
120
+
121
+ # Define how all chunks should be combined
122
+ #
123
+ # @api public
124
+ #
125
+ # @example Merge all chunks
126
+ # aggregate.aggregate do |chunks|
127
+ # final_result = {}
128
+ # chunks.each_value do |chunk|
129
+ # final_result.deep_merge(chunk)
130
+ # end
131
+ # final_result
132
+ # end
133
+ #
134
+ # @example Sum all chunks
135
+ # aggregate.aggregate do |chunks|
136
+ # total = 0
137
+ # chunks.each_value do |chunk|
138
+ # total += chunk
139
+ # end
140
+ # total
141
+ # end
142
+ #
143
+ # @yield [Hash<Symbol, Object>] A hash containing chunk names and
144
+ # chunk values
145
+ #
146
+ # @return [void]
147
+ def aggregate(&block)
148
+ if block_given?
149
+ @aggregate = block
150
+ else
151
+ raise ArgumentError, "#{self.class.name}#aggregate requires a block"
152
+ end
153
+ end
154
+
155
+ def resolution_type
156
+ :aggregate
157
+ end
158
+
159
+ private
160
+
161
+ # Evaluate the results of this aggregate.
162
+ #
163
+ # @see Facter::Core::Resolvable#value
164
+ # @return [Object]
165
+ def resolve_value
166
+ chunk_results = run_chunks()
167
+ aggregate_results(chunk_results)
168
+ end
169
+
170
+ # Order all chunks based on their dependencies and evaluate each one, passing
171
+ # dependent chunks as needed.
172
+ #
173
+ # @return [Hash<Symbol, Object>] A hash containing the chunk that
174
+ # generated value and the related value.
175
+ def run_chunks
176
+ results = {}
177
+ order_chunks.each do |(name, block)|
178
+ input = @deps[name].map { |dep_name| results[dep_name] }
179
+
180
+ output = block.call(*input)
181
+ results[name] = RFacter::Util::Values.deep_freeze(output)
182
+ end
183
+
184
+ results
185
+ end
186
+
187
+ # Process the results of all chunks with the aggregate block and return the
188
+ # results. If no aggregate block has been specified, fall back to deep
189
+ # merging the given data structure
190
+ #
191
+ # @param results [Hash<Symbol, Object>] A hash of chunk names and the output
192
+ # of that chunk.
193
+ # @return [Object]
194
+ def aggregate_results(results)
195
+ if @aggregate
196
+ @aggregate.call(results)
197
+ else
198
+ default_aggregate(results)
199
+ end
200
+ end
201
+
202
+ def default_aggregate(results)
203
+ results.values.inject do |result, current|
204
+ RFacter::Util::Values.deep_merge(result, current)
205
+ end
206
+ rescue RFacter::Util::Values::DeepMergeError => e
207
+ raise ArgumentError, "Could not deep merge all chunks (Original error: " +
208
+ "#{e.message}), ensure that chunks return either an Array or Hash or " +
209
+ "override the aggregate block", e.backtrace
210
+ end
211
+
212
+ # Order chunks based on their dependencies
213
+ #
214
+ # @return [Array<Symbol, Proc>] A list of chunk names and blocks in evaluation order.
215
+ def order_chunks
216
+ if not @deps.acyclic?
217
+ raise DependencyError, "Could not order chunks; found the following dependency cycles: #{@deps.cycles.inspect}"
218
+ end
219
+
220
+ sorted_names = @deps.tsort
221
+
222
+ sorted_names.map do |name|
223
+ [name, @chunks[name]]
224
+ end
225
+ end
226
+
227
+ class DependencyError < StandardError; end
228
+ end
@@ -0,0 +1,48 @@
1
+ require 'set'
2
+ require 'tsort'
3
+
4
+ require 'rfacter'
5
+
6
+ module RFacter
7
+ module Core
8
+ class DirectedGraph < Hash
9
+ include TSort
10
+
11
+ def acyclic?
12
+ cycles.empty?
13
+ end
14
+
15
+ def cycles
16
+ cycles = []
17
+ each_strongly_connected_component do |component|
18
+ cycles << component if component.size > 1
19
+ end
20
+ cycles
21
+ end
22
+
23
+ alias tsort_each_node each_key
24
+
25
+ def tsort_each_child(node)
26
+ fetch(node, []).each do |child|
27
+ yield child
28
+ end
29
+ end
30
+
31
+ def tsort
32
+ missing = Set.new(self.values.flatten) - Set.new(self.keys)
33
+
34
+ if not missing.empty?
35
+ raise MissingVertex, "Cannot sort elements; cannot depend on missing elements #{missing.to_a}"
36
+ end
37
+
38
+ super
39
+
40
+ rescue TSort::Cyclic
41
+ raise CycleError, "Cannot sort elements; found the following cycles: #{cycles.inspect}"
42
+ end
43
+
44
+ class CycleError < StandardError; end
45
+ class MissingVertex < StandardError; end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,97 @@
1
+ require 'timeout'
2
+
3
+ require 'rfacter'
4
+ require_relative '../util/normalization'
5
+
6
+ # The resolvable mixin defines behavior for evaluating and returning fact
7
+ # resolutions.
8
+ #
9
+ # Classes including this mixin should implement at #name method describing
10
+ # the value being resolved and a #resolve_value that actually executes the code
11
+ # to resolve the value.
12
+ module RFacter::Core::Resolvable
13
+
14
+ # The timeout, in seconds, for evaluating this resolution.
15
+ # @return [Integer]
16
+ # @api public
17
+ attr_accessor :timeout
18
+
19
+ # Return the timeout period for resolving a value.
20
+ # (see #timeout)
21
+ # @return [Numeric]
22
+ # @comment requiring 'timeout' stdlib class causes Object#timeout to be
23
+ # defined which delegates to Timeout.timeout. This method may potentially
24
+ # overwrite the #timeout attr_reader on this class, so we define #limit to
25
+ # avoid conflicts.
26
+ def limit
27
+ @timeout || 0
28
+ end
29
+
30
+ ##
31
+ # on_flush accepts a block and executes the block when the resolution's value
32
+ # is flushed. This makes it possible to model a single, expensive system
33
+ # call inside of a Ruby object and then define multiple dynamic facts which
34
+ # resolve by sending messages to the model instance. If one of the dynamic
35
+ # facts is flushed then it can, in turn, flush the data stored in the model
36
+ # instance to keep all of the dynamic facts in sync without making multiple,
37
+ # expensive, system calls.
38
+ #
39
+ # Please see the Solaris zones fact for an example of how this feature may be
40
+ # used.
41
+ #
42
+ # @see Facter::Util::Fact#flush
43
+ # @see Facter::Util::Resolution#flush
44
+ #
45
+ # @api public
46
+ def on_flush(&block)
47
+ @on_flush_block = block
48
+ end
49
+
50
+ ##
51
+ # flush executes the block, if any, stored by the {on_flush} method
52
+ #
53
+ # @see Facter::Util::Fact#flush
54
+ # @see Facter::Util::Resolution#on_flush
55
+ #
56
+ # @api private
57
+ def flush
58
+ @on_flush_block.call if @on_flush_block
59
+ end
60
+
61
+ def value
62
+ result = nil
63
+
64
+ with_timing do
65
+ Timeout.timeout(limit) do
66
+ result = resolve_value
67
+ end
68
+ end
69
+
70
+ RFacter::Util::Normalization.normalize(result)
71
+ rescue Timeout::Error => detail
72
+ logger.log_exception(detail, "Timed out after #{limit} seconds while resolving #{qualified_name}")
73
+ return nil
74
+ rescue RFacter::Util::Normalization::NormalizationError => detail
75
+ logger.log_exception(detail, "Fact resolution #{qualified_name} resolved to an invalid value: #{detail.message}")
76
+ return nil
77
+ rescue => detail
78
+ logger.log_exception(detail, "Could not retrieve #{qualified_name}: #{detail.message}")
79
+ return nil
80
+ end
81
+
82
+ private
83
+
84
+ def with_timing
85
+ starttime = Time.now.to_f
86
+
87
+ yield
88
+
89
+ finishtime = Time.now.to_f
90
+ ms = (finishtime - starttime) * 1000
91
+ #::Facter.show_time "#{qualified_name}: #{"%.2f" % ms}ms"
92
+ end
93
+
94
+ def qualified_name
95
+ "fact='#{@fact.name.to_s}', resolution='#{@name || '<anonymous>'}'"
96
+ end
97
+ end
@@ -0,0 +1,114 @@
1
+ require 'rfacter'
2
+
3
+ # The Suitable mixin provides mechanisms for confining objects to run on
4
+ # certain platforms and determining the run precedence of these objects.
5
+ #
6
+ # Classes that include the Suitable mixin should define a `#confines` method
7
+ # that returns an Array of zero or more Facter::Util::Confine objects.
8
+ module RFacter::Core::Suitable
9
+ require_relative '../util/confine'
10
+
11
+ attr_writer :weight
12
+
13
+ # Sets the weight of this resolution. If multiple suitable resolutions
14
+ # are found, the one with the highest weight will be used. If weight
15
+ # is not given, the number of confines set on a resolution will be
16
+ # used as its weight (so that the most specific resolution is used).
17
+ #
18
+ # @param weight [Integer] the weight of this resolution
19
+ #
20
+ # @return [void]
21
+ #
22
+ # @api public
23
+ def has_weight(weight)
24
+ @weight = weight
25
+ end
26
+
27
+ # Sets the conditions for this resolution to be used. This method accepts
28
+ # multiple forms of arguments to determine suitability.
29
+ #
30
+ # @return [void]
31
+ #
32
+ # @api public
33
+ #
34
+ # @overload confine(confines)
35
+ # Confine a fact to a specific fact value or values. This form takes a
36
+ # hash of fact names and values. Every fact must match the values given for
37
+ # that fact, otherwise this resolution will not be considered suitable. The
38
+ # values given for a fact can be an array, in which case the value of the
39
+ # fact must be in the array for it to match.
40
+ # @param [Hash{String,Symbol=>String,Array<String>}] confines set of facts identified by the hash keys whose
41
+ # fact value must match the argument value.
42
+ # @example Confining to Linux
43
+ # Facter.add(:powerstates) do
44
+ # # This resolution only makes sense on linux systems
45
+ # confine :kernel => "Linux"
46
+ # setcode do
47
+ # File.read('/sys/power/states')
48
+ # end
49
+ # end
50
+ #
51
+ # @overload confine(confines, &block)
52
+ # Confine a fact to a block with the value of a specified fact yielded to
53
+ # the block.
54
+ # @param [String,Symbol] confines the fact name whose value should be
55
+ # yielded to the block
56
+ # @param [Proc] block determines the suitability of the fact. If the block
57
+ # evaluates to `false` or `nil` then the confined fact will not be
58
+ # evaluated.
59
+ # @yield [value] the value of the fact identified by {confines}
60
+ # @example Confine the fact to a host with an ipaddress in a specific
61
+ # subnet
62
+ # confine :ipaddress do |addr|
63
+ # require 'ipaddr'
64
+ # IPAddr.new('192.168.0.0/16').include? addr
65
+ # end
66
+ #
67
+ # @overload confine(&block)
68
+ # Confine a fact to a block. The fact will be evaluated only if the block
69
+ # evaluates to something other than `false` or `nil`.
70
+ # @param [Proc] block determines the suitability of the fact. If the block
71
+ # evaluates to `false` or `nil` then the confined fact will not be
72
+ # evaluated.
73
+ # @example Confine the fact to systems with a specific file.
74
+ # confine { File.exist? '/bin/foo' }
75
+ def confine(confines = nil, &block)
76
+ case confines
77
+ when Hash
78
+ confines.each do |fact, values|
79
+ @confines.push RFacter::Util::Confine.new(fact, *values)
80
+ end
81
+ else
82
+ if block
83
+ if confines
84
+ @confines.push RFacter::Util::Confine.new(confines, &block)
85
+ else
86
+ @confines.push RFacter::Util::Confine.new(&block)
87
+ end
88
+ else
89
+ end
90
+ end
91
+ end
92
+
93
+ # Returns the importance of this resolution. If the weight was not
94
+ # given, the number of confines is used instead (so that a more
95
+ # specific resolution wins over a less specific one).
96
+ #
97
+ # @return [Integer] the weight of this resolution
98
+ #
99
+ # @api private
100
+ def weight
101
+ if @weight
102
+ @weight
103
+ else
104
+ @confines.length
105
+ end
106
+ end
107
+
108
+ # Is this resolution mechanism suitable on the system in question?
109
+ #
110
+ # @api private
111
+ def suitable?
112
+ @confines.all? { |confine| confine.true? }
113
+ end
114
+ end