rfacter 0.0.1

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