rfacter 0.0.1

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