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.
@@ -0,0 +1,137 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'forwardable'
4
+
5
+ require 'rfacter'
6
+ require_relative 'config'
7
+
8
+ require 'train'
9
+ require 'concurrent'
10
+
11
+ # Interface to a local or remote host
12
+ #
13
+ # @note This class should be refacter to provide an abstracted interface to
14
+ # different transport backends like Train, Vagrant, Chloride, etc.
15
+ #
16
+ # @since 0.1.0
17
+ class RFacter::Node
18
+ extend Forwardable
19
+
20
+ instance_delegate([:logger] => :@config)
21
+
22
+ # @return [URI]
23
+ attr_reader :uri
24
+
25
+ # @return [String]
26
+ attr_reader :hostname
27
+ # @return [String]
28
+ attr_reader :scheme
29
+ # @return [Integer, nil]
30
+ attr_reader :port
31
+ # @return [String, nil]
32
+ attr_reader :user
33
+ # @return [String, nil]
34
+ attr_reader :password
35
+ # @return [Hash]
36
+ attr_reader :options
37
+
38
+ attr_reader :transport
39
+
40
+ def initialize(uri, config: RFacter::Config.config, **opts)
41
+ @config = config
42
+
43
+ @uri = unless uri.is_a?(URI)
44
+ URI.parse(uri.to_s)
45
+ else
46
+ uri
47
+ end
48
+
49
+ @hostname = @uri.hostname || @uri.path
50
+ @scheme = if @uri.scheme.nil? && (@hostname == 'localhost')
51
+ 'local'
52
+ elsif @uri.scheme.nil?
53
+ 'ssh'
54
+ else
55
+ @uri.scheme
56
+ end
57
+
58
+ case @scheme
59
+ when 'ssh'
60
+ @port = @uri.port || 22
61
+ @user = @uri.user || 'root'
62
+ when 'winrm'
63
+ @user = @uri.user || 'Administrator'
64
+ end
65
+
66
+ @password = CGI.unescape(@uri.password) unless @uri.password.nil?
67
+ @options = @uri.query.nil? ? Hash.new : CGI.parse(@uri.query)
68
+ @options.update(opts)
69
+
70
+ # TODO: This should be abstracted.
71
+ @transport = Train.create(@scheme,
72
+ host: @hostname,
73
+ user: @user,
74
+ password: @password,
75
+ port: @port,
76
+ logger: logger, **@options)
77
+ end
78
+
79
+ # FIXME: For some reason, Train's connection re-use logic isn't working, so a
80
+ # new connection is being negotiated for each command. File a bug.
81
+ #
82
+ # TODO: Ensure connection use is thread-safe.
83
+ def connection
84
+ @connection ||= @transport.connection
85
+ end
86
+
87
+ # Execute a command on the node asynchronously
88
+ #
89
+ # This method initiates the execution of a command line and returns an
90
+ # object representing the result.
91
+ #
92
+ # @param command [String] The command string to execute.
93
+ #
94
+ # @return [Train::Extras::CommandResult] The result of the command including
95
+ # stdout, stderr and exit code.
96
+ #
97
+ # @todo Add support for setting user accounts and environment variables.
98
+ def execute(command)
99
+ connection.run_command(command)
100
+ end
101
+
102
+ # Determine if an executable exists and return the path
103
+ #
104
+ # @param executable [String] The executable to locate.
105
+ #
106
+ # @return [String] The path to the executable if it exists.
107
+ #
108
+ # @return [nil] Returned when no matching executable can be located.
109
+ #
110
+ # @todo Add support for setting user accounts and environment variables.
111
+ def which(executable)
112
+ # TODO: Abstract away from the Train "os" implementation.
113
+ result = if connection.os.windows?
114
+ connection.run_command("(Get-Command -TotalCount 1 #{executable}).Path")
115
+ else
116
+ connection.run_command("which #{executable}")
117
+ end
118
+
119
+ if (result.exit_status != 0) || (result.stdout.chomp.empty?)
120
+ nil
121
+ else
122
+ result.stdout.chomp
123
+ end
124
+ end
125
+
126
+ # Interact with remote files in a read-only manner
127
+ #
128
+ # This method returns an object that can povide read only access to the stats
129
+ # and content of a particular file path.
130
+ #
131
+ # @param path [String] The file path to interact with.
132
+ #
133
+ # @return [Train::Extras::FileCommon] An object representing the remote file.
134
+ def file(path)
135
+ connection.file(path)
136
+ end
137
+ end
@@ -0,0 +1,166 @@
1
+ require 'forwardable'
2
+
3
+ require 'rfacter'
4
+ require_relative '../config'
5
+ require_relative '../dsl'
6
+ require_relative 'loader'
7
+ require_relative 'fact'
8
+
9
+ # Manage which facts exist and how we access them. Largely just a wrapper
10
+ # around a hash of facts.
11
+ #
12
+ # @api private
13
+ class RFacter::Util::Collection
14
+ # Ensures unqualified namespaces like `Facter` and `Facter::Util` get
15
+ # re-directed to RFacter shims when the loader calls `instance_eval`
16
+ include RFacter::DSL
17
+ extend Forwardable
18
+
19
+ instance_delegate([:logger] => :@config)
20
+
21
+ def initialize(config: RFacter::Config.config, **opts)
22
+ @config = config
23
+ @facts = Hash.new
24
+ @internal_loader = RFacter::Util::Loader.new
25
+ end
26
+
27
+ # Return a fact object by name.
28
+ def [](name, node)
29
+ value(name, node)
30
+ end
31
+
32
+ # Define a new fact or extend an existing fact.
33
+ #
34
+ # @param name [Symbol] The name of the fact to define
35
+ # @param options [Hash] A hash of options to set on the fact
36
+ #
37
+ # @return [Facter::Util::Fact] The fact that was defined
38
+ def define_fact(name, options = {}, &block)
39
+ fact = create_or_return_fact(name, options)
40
+
41
+ if block_given?
42
+ fact.instance_eval(&block)
43
+ end
44
+
45
+ fact
46
+ rescue => e
47
+ logger.log_exception(e, "Unable to add fact #{name}: #{e}")
48
+ end
49
+
50
+ # Add a resolution mechanism for a named fact. This does not distinguish
51
+ # between adding a new fact and adding a new way to resolve a fact.
52
+ #
53
+ # @param name [Symbol] The name of the fact to define
54
+ # @param options [Hash] A hash of options to set on the fact and resolution
55
+ #
56
+ # @return [Facter::Util::Fact] The fact that was defined
57
+ def add(name, options = {}, &block)
58
+ fact = create_or_return_fact(name, options)
59
+
60
+ fact.add(options, &block)
61
+
62
+ return fact
63
+ end
64
+
65
+ include Enumerable
66
+
67
+ # Iterate across all of the facts.
68
+ def each(node)
69
+ load_all
70
+
71
+ RFacter::DSL::COLLECTION.bind(self) do
72
+ RFacter::DSL::NODE.bind(node) do
73
+ @facts.each do |name, fact|
74
+ value = fact.value
75
+ unless value.nil?
76
+ yield name.to_s, value
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ # Return a fact by name.
84
+ def fact(name)
85
+ name = canonicalize(name)
86
+
87
+ # Try to load the fact if necessary
88
+ load(name) unless @facts[name]
89
+
90
+ # Try HARDER
91
+ load_all unless @facts[name]
92
+
93
+ if @facts.empty?
94
+ logger.warnonce("No facts loaded from #{@internal_loader.search_path.join(File::PATH_SEPARATOR)}")
95
+ end
96
+
97
+ @facts[name]
98
+ end
99
+
100
+ # Flush all cached values.
101
+ def flush
102
+ @facts.each { |name, fact| fact.flush }
103
+ end
104
+
105
+ # Return a list of all of the facts.
106
+ def list
107
+ load_all
108
+ return @facts.keys
109
+ end
110
+
111
+ def load(name)
112
+ @internal_loader.load(name, self)
113
+ end
114
+
115
+ # Load all known facts.
116
+ def load_all
117
+ @internal_loader.load_all(self)
118
+ end
119
+
120
+ # Return a hash of all of our facts.
121
+ def to_hash(node)
122
+ @facts.inject({}) do |h, ary|
123
+ resolved_value = RFacter::DSL::COLLECTION.bind(self) do
124
+ RFacter::DSL::NODE.bind(node) do
125
+ ary[1].value
126
+ end
127
+ end
128
+
129
+ # For backwards compatibility, convert the fact name to a string.
130
+ h[ary[0].to_s] = resolved_value unless resolved_value.nil?
131
+
132
+ h
133
+ end
134
+ end
135
+
136
+ def value(name, node)
137
+ RFacter::DSL::COLLECTION.bind(self) do
138
+ RFacter::DSL::NODE.bind(node) do
139
+ if fact = fact(name)
140
+ fact.value
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ private
147
+
148
+ def create_or_return_fact(name, options)
149
+ name = canonicalize(name)
150
+
151
+ fact = @facts[name]
152
+
153
+ if fact.nil?
154
+ fact = RFacter::Util::Fact.new(name, options)
155
+ @facts[name] = fact
156
+ else
157
+ fact.extract_ldapname_option!(options)
158
+ end
159
+
160
+ fact
161
+ end
162
+
163
+ def canonicalize(name)
164
+ name.to_s.downcase.to_sym
165
+ end
166
+ end
@@ -0,0 +1,75 @@
1
+ require 'forwardable'
2
+
3
+ require 'rfacter'
4
+ require_relative '../config'
5
+ require_relative '../dsl'
6
+ require_relative 'values'
7
+
8
+ # A restricting tag for fact resolution mechanisms. The tag must be true
9
+ # for the resolution mechanism to be suitable.
10
+ class RFacter::Util::Confine
11
+ extend Forwardable
12
+
13
+ instance_delegate([:logger] => :@config)
14
+
15
+ attr_accessor :fact, :values
16
+
17
+ include RFacter::Util::Values
18
+
19
+ # Add the restriction. Requires the fact name, an operator, and the value
20
+ # we're comparing to.
21
+ #
22
+ # @param fact [Symbol] Name of the fact
23
+ # @param values [Array] One or more values to match against.
24
+ # They can be any type that provides a === method.
25
+ # @param block [Proc] Alternatively a block can be supplied as a check. The fact
26
+ # value will be passed as the argument to the block. If the block returns
27
+ # true then the fact will be enabled, otherwise it will be disabled.
28
+ def initialize(fact = nil, *values, config: RFacter::Config.config, **options, &block)
29
+ raise ArgumentError, "The fact name must be provided" unless fact or block_given?
30
+ if values.empty? and not block_given?
31
+ raise ArgumentError, "One or more values or a block must be provided"
32
+ end
33
+ @fact = fact
34
+ @values = values
35
+ @config = config
36
+ @block = block
37
+ end
38
+
39
+ def to_s
40
+ return @block.to_s if @block
41
+ return "'%s' '%s'" % [@fact, @values.join(",")]
42
+ end
43
+
44
+ # Evaluate the fact, returning true or false.
45
+ # if we have a block paramter then we only evaluate that instead
46
+ def true?
47
+ if @block and not @fact then
48
+ begin
49
+ return !! @block.call
50
+ rescue StandardError => error
51
+ logger.debug "Confine raised #{error.class} #{error}"
52
+ return false
53
+ end
54
+ end
55
+
56
+ unless fact = RFacter::DSL::Facter[@fact]
57
+ logger.debug "No fact for %s" % @fact
58
+ return false
59
+ end
60
+ value = convert(fact.value)
61
+
62
+ return false if value.nil?
63
+
64
+ if @block then
65
+ begin
66
+ return !! @block.call(value)
67
+ rescue StandardError => error
68
+ logger.debug "Confine raised #{error.class} #{error}"
69
+ return false
70
+ end
71
+ end
72
+
73
+ return @values.any? do |v| convert(v) === value end
74
+ end
75
+ end
@@ -0,0 +1,213 @@
1
+ require 'forwardable'
2
+
3
+ require 'rfacter'
4
+ require_relative '../config'
5
+
6
+ # This class represents a fact. Each fact has a name and multiple
7
+ # {Facter::Util::Resolution resolutions}.
8
+ #
9
+ # Create facts using {Facter.add}
10
+ #
11
+ # @api public
12
+ class RFacter::Util::Fact
13
+ require_relative '../core/aggregate'
14
+ require_relative 'resolution'
15
+
16
+ extend Forwardable
17
+
18
+ instance_delegate([:logger] => :@config)
19
+
20
+ # The name of the fact
21
+ # @return [String]
22
+ attr_accessor :name
23
+
24
+ # @return [String]
25
+ # @deprecated
26
+ attr_accessor :ldapname
27
+
28
+ # Creates a new fact, with no resolution mechanisms. See {Facter.add}
29
+ # for the public API for creating facts.
30
+ # @param name [String] the fact name
31
+ # @param options [Hash] optional parameters
32
+ # @option options [String] :ldapname set the ldapname property on the fact
33
+ #
34
+ # @api private
35
+ def initialize(name, config: RFacter::Config.config, **options)
36
+ @name = name.to_s.downcase.intern
37
+ @config = config
38
+
39
+ extract_ldapname_option!(options)
40
+
41
+ @ldapname ||= @name.to_s
42
+
43
+ @resolves = []
44
+ @searching = false
45
+
46
+ @value = nil
47
+ end
48
+
49
+ # Adds a new {Facter::Util::Resolution resolution}. This requires a
50
+ # block, which will then be evaluated in the context of the new
51
+ # resolution.
52
+ #
53
+ # @param options [Hash] A hash of options to set on the resolution
54
+ #
55
+ # @return [Facter::Util::Resolution]
56
+ #
57
+ # @api private
58
+ def add(options = {}, &block)
59
+ define_resolution(nil, options, &block)
60
+ end
61
+
62
+ # Define a new named resolution or return an existing resolution with
63
+ # the given name.
64
+ #
65
+ # @param resolution_name [String] The name of the resolve to define or look up
66
+ # @param options [Hash] A hash of options to set on the resolution
67
+ # @return [Facter::Util::Resolution]
68
+ #
69
+ # @api public
70
+ def define_resolution(resolution_name, options = {}, &block)
71
+
72
+ resolution_type = options.delete(:type) || :simple
73
+
74
+ resolve = create_or_return_resolution(resolution_name, resolution_type)
75
+
76
+ resolve.set_options(options) unless options.empty?
77
+ resolve.evaluate(&block) if block
78
+
79
+ resolve
80
+ rescue => e
81
+ logger.log_exception(e, "Unable to add resolve #{resolution_name.inspect} for fact #{@name}: #{e.message}")
82
+ end
83
+
84
+ # Retrieve an existing resolution by name
85
+ #
86
+ # @param name [String]
87
+ #
88
+ # @return [Facter::Util::Resolution, nil] The resolution if exists, nil if
89
+ # it doesn't exist or name is nil
90
+ def resolution(name)
91
+ return nil if name.nil?
92
+
93
+ @resolves.find { |resolve| resolve.name == name }
94
+ end
95
+
96
+ # Flushes any cached values.
97
+ #
98
+ # @return [void]
99
+ #
100
+ # @api private
101
+ def flush
102
+ @resolves.each { |r| r.flush }
103
+ @value = nil
104
+ end
105
+
106
+ # Returns the value for this fact. This searches all resolutions by
107
+ # suitability and weight (see {Facter::Util::Resolution}). If no
108
+ # suitable resolution is found, it returns nil.
109
+ #
110
+ # @api public
111
+ def value
112
+ return @value if @value
113
+
114
+ if @resolves.empty?
115
+ logger.debug("No resolves for #{@name}")
116
+ return nil
117
+ end
118
+
119
+ searching do
120
+
121
+ suitable_resolutions = sort_by_weight(find_suitable_resolutions(@resolves))
122
+ @value = find_first_real_value(suitable_resolutions)
123
+
124
+ announce_when_no_suitable_resolution(suitable_resolutions)
125
+ announce_when_no_value_found(@value)
126
+
127
+ @value
128
+ end
129
+ end
130
+
131
+ # @api private
132
+ # @deprecated
133
+ def extract_ldapname_option!(options)
134
+ if options[:ldapname]
135
+ logger.warnonce("ldapname is deprecated and will be removed in a future version")
136
+ self.ldapname = options.delete(:ldapname)
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ # Are we in the midst of a search?
143
+ def searching?
144
+ @searching
145
+ end
146
+
147
+ # Lock our searching process, so we never ge stuck in recursion.
148
+ def searching
149
+ raise RuntimeError, "Caught recursion on #{@name}" if searching?
150
+
151
+ # If we've gotten this far, we're not already searching, so go ahead and do so.
152
+ @searching = true
153
+ begin
154
+ yield
155
+ ensure
156
+ @searching = false
157
+ end
158
+ end
159
+
160
+ def find_suitable_resolutions(resolutions)
161
+ resolutions.find_all{ |resolve| resolve.suitable? }
162
+ end
163
+
164
+ def sort_by_weight(resolutions)
165
+ resolutions.sort { |a, b| b.weight <=> a.weight }
166
+ end
167
+
168
+ def find_first_real_value(resolutions)
169
+ resolutions.each do |resolve|
170
+ value = resolve.value
171
+ if not value.nil?
172
+ return value
173
+ end
174
+ end
175
+ nil
176
+ end
177
+
178
+ def announce_when_no_suitable_resolution(resolutions)
179
+ if resolutions.empty?
180
+ logger.debug("Found no suitable resolves of #{@resolves.length} for #{@name}")
181
+ end
182
+ end
183
+
184
+ def announce_when_no_value_found(value)
185
+ if value.nil?
186
+ logger.debug("value for #{name} is still nil")
187
+ end
188
+ end
189
+
190
+ def create_or_return_resolution(resolution_name, resolution_type)
191
+ resolve = self.resolution(resolution_name)
192
+
193
+ if resolve
194
+ if resolution_type != resolve.resolution_type
195
+ raise ArgumentError, "Cannot return resolution #{resolution_name} with type" +
196
+ " #{resolution_type}; already defined as #{resolve.resolution_type}"
197
+ end
198
+ else
199
+ case resolution_type
200
+ when :simple
201
+ resolve = RFacter::Util::Resolution.new(resolution_name, self)
202
+ when :aggregate
203
+ resolve = RFacter::Core::Aggregate.new(resolution_name, self)
204
+ else
205
+ raise ArgumentError, "Expected resolution type to be one of (:simple, :aggregate) but was #{resolution_type}"
206
+ end
207
+
208
+ @resolves << resolve
209
+ end
210
+
211
+ resolve
212
+ end
213
+ end
@@ -0,0 +1,115 @@
1
+ require 'pathname'
2
+ require 'forwardable'
3
+
4
+ require 'rfacter'
5
+ require_relative '../config'
6
+ require_relative '../dsl'
7
+
8
+ # Load facts on demand.
9
+ #
10
+ # @api private
11
+ class RFacter::Util::Loader
12
+ extend Forwardable
13
+
14
+ instance_delegate([:logger] => :@config)
15
+
16
+ def initialize(config: RFacter::Config.config, **opts)
17
+ @config = config
18
+ @loaded = []
19
+ end
20
+
21
+ # Load all resolutions for a single fact.
22
+ #
23
+ # @api public
24
+ # @param fact [Symbol]
25
+ def load(fact, collection)
26
+ # Now load from the search path
27
+ shortname = fact.to_s.downcase
28
+
29
+ filename = shortname + ".rb"
30
+
31
+ paths = search_path
32
+ unless paths.nil?
33
+ paths.each do |dir|
34
+ # Load individual files
35
+ file = File.join(dir, filename)
36
+
37
+ load_file(file, collection) if File.file?(file)
38
+ end
39
+ end
40
+ end
41
+
42
+ # Load all facts from all directories.
43
+ #
44
+ # @api public
45
+ def load_all(collection)
46
+ return if defined?(@loaded_all)
47
+
48
+ paths = search_path
49
+ unless paths.nil?
50
+ paths.each do |dir|
51
+ # dir is already an absolute path
52
+ Dir.glob(File.join(dir, '*.rb')).each do |path|
53
+ # exclude dirs that end with .rb
54
+ load_file(path, collection) if File.file?(path)
55
+ end
56
+ end
57
+ end
58
+
59
+ @loaded_all = true
60
+ end
61
+
62
+ # List directories to search for fact files.
63
+ #
64
+ # Search paths are gathered from the following sources:
65
+ #
66
+ # 1. A core set of facts from the rfacter/facts directory
67
+ # 2. ENV['RFACTERLIB'] is split and used verbatim
68
+ #
69
+ # A warning will be generated for paths that are not
70
+ # absolute directories.
71
+ #
72
+ # @api public
73
+ # @return [Array<String>]
74
+ def search_path
75
+ search_paths = [File.expand_path('../../facts', __FILE__)]
76
+
77
+ if ENV.include?("RFACTERLIB")
78
+ search_paths += ENV["RFACTERLIB"].split(File::PATH_SEPARATOR)
79
+ end
80
+
81
+ search_paths.delete_if { |path| ! valid_search_path?(path) }
82
+
83
+ search_paths.uniq
84
+ end
85
+
86
+ # Validate that the given path is valid, ie it is an absolute path.
87
+ #
88
+ # @param path [String]
89
+ # @return [Boolean]
90
+ def valid_search_path?(path)
91
+ Pathname.new(path).absolute? && File.directory?(path)
92
+ end
93
+
94
+ # Load a file and record is paths to prevent duplicate loads.
95
+ #
96
+ # @param file [String] The *absolute path* to the file to load
97
+ def load_file(file, collection)
98
+ return if @loaded.include? file
99
+
100
+ # We have to specify Kernel.load, because we have a load method.
101
+ begin
102
+ # Store the file path so we don't try to reload it
103
+ @loaded << file
104
+
105
+ RFacter::DSL::COLLECTION.bind(collection) do
106
+ collection.instance_eval(File.read(file), file)
107
+ end
108
+ rescue Exception => detail
109
+ # Don't store the path if the file can't be loaded
110
+ # in case it's loadable later on.
111
+ @loaded.delete(file)
112
+ logger.log_exception(detail, "Error loading fact #{file}: #{detail.message}")
113
+ end
114
+ end
115
+ end