File: train.rb

package info (click to toggle)
ruby-train 3.2.28-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye
  • size: 1,116 kB
  • sloc: ruby: 9,246; sh: 17; makefile: 8
file content (193 lines) | stat: -rw-r--r-- 7,210 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# encoding: utf-8
#
# Author:: Dominik Richter (<dominik.richter@gmail.com>)

require_relative "train/version"
require_relative "train/options"
require_relative "train/plugins"
require_relative "train/errors"
require_relative "train/platforms"
require "addressable/uri"

module Train
  # Create a new transport instance, with the plugin indicated by the
  # given name.
  #
  # @param [String] name of the plugin
  # @param [Array] *args list of arguments for the plugin
  # @return [Transport] instance of the new transport or nil
  def self.create(name, *args)
    cls = load_transport(name)
    cls.new(*args) unless cls.nil?
  end

  # Retrieve the configuration options of a transport plugin.
  #
  # @param [String] name of the plugin
  # @return [Hash] map of default options
  def self.options(name)
    cls = load_transport(name)
    cls.default_options unless cls.nil?
  end

  # Load the transport plugin indicated by name. If the plugin is not
  # yet found in the plugin registry, it will be attempted to load from
  # `train/transports/plugin_name`.
  #
  # @param [String] name of the plugin
  # @return [Train::Transport] the transport plugin
  def self.load_transport(transport_name)
    transport_name = transport_name.to_s
    transport_class = Train::Plugins.registry[transport_name]
    return transport_class unless transport_class.nil?

    # Try to load the transport name from the core transports...
    require "train/transports/" + transport_name
    Train::Plugins.registry[transport_name]
  rescue LoadError => _
    begin
      # If it's not in the core transports, try loading from a train plugin gem.
      gem_name = "train-" + transport_name
      require gem_name
      return Train::Plugins.registry[transport_name]
      # rubocop: disable Lint/HandleExceptions
    rescue LoadError => _
      # rubocop: enable Lint/HandleExceptions
      # Intentionally empty rescue - we're handling it below anyway
    end

    ex = Train::PluginLoadError.new("Can't find train plugin #{transport_name}. Please install it first.")
    ex.transport_name = transport_name
    raise ex
  end

  # Legacy code to unpack a series of items from an incoming Hash
  # Inspec::Config.unpack_train_credentials now handles this in most cases that InSpec needs
  # If you need to unpack a URI, use unpack_target_from_uri
  # TODO: deprecate; can't issue a warning because train doesn't have a logger until the connection is setup (See base_connection.rb)
  def self.target_config(config = nil)
    conf = config.dup
    # Symbolize keys
    conf.keys.each do |key|
      unless key.is_a? Symbol
        conf[key.to_sym] = conf.delete(key)
      end
    end

    group_keys_and_keyfiles(conf) # TODO: move logic into SSH plugin
    return conf if conf[:target].to_s.empty?

    unpack_target_from_uri(conf[:target], conf).merge(conf)
  end

  # Given a string that looks like a URI, unpack connection credentials.
  # The name of the desired transport is always taken from the 'scheme' slot of the URI;
  # the remaining portion of the URI is parsed as if it were an HTTP URL, and then
  # the URL components are stored in the credentials hash.  It is up to the transport
  # to interpret the fields in a sensible way for that transport.
  # New transport authors are encouraged to use transport://credset format (see
  # inspec/inspec/issues/3661) rather than inventing a new field mapping.
  def self.unpack_target_from_uri(uri_string, opts = {}) # rubocop: disable Metrics/AbcSize
    creds = {}
    return creds if uri_string.empty?

    # split up the target's host/scheme configuration
    uri = parse_uri(uri_string)
    unless uri.host.nil? && uri.scheme.nil?
      creds[:backend]  ||= uri.scheme
      creds[:host]     ||= uri.hostname
      creds[:port]     ||= uri.port
      creds[:user]     ||= uri.user
      creds[:path]     ||= uri.path
      creds[:password] ||=
        if opts[:www_form_encoded_password] && !uri.password.nil?
          Addressable::URI.unencode_component(uri.password)
        else
          uri.password
        end
    end

    # ensure path is nil, if its empty; e.g. required to reset defaults for winrm # TODO: move logic into winrm plugin
    creds[:path] = nil if !creds[:path].nil? && creds[:path].to_s.empty?

    # compact! is available in ruby 2.4+
    # TODO: rewrite next line using compact! once we drop support for ruby 2.3
    creds = creds.delete_if { |_, value| value.nil? }

    # return the updated config
    creds
  end

  # Parse a URI. Supports empty URI's with paths, e.g. `mock://`
  #
  # @param string [string] URI string, e.g. `schema://domain.com`
  # @return [Addressable::URI] parsed URI object
  def self.parse_uri(string)
    u = Addressable::URI.parse(string)
    # A use-case we want to catch is parsing empty URIs with a schema
    # e.g. mock://. To do this, we match it manually and fake the hostname
    if u.scheme && (u.host.nil? || u.host.empty?) && u.path.empty?
      case string
      when %r{^([a-z]+)://$}
        string += "dummy"
      when /^([a-z]+):$/
        string += "//dummy"
      end
      u = Addressable::URI.parse(string)
      u.host = nil
    end
    u
  rescue Addressable::URI::InvalidURIError => e
    raise Train::UserError, e
  end
  private_class_method :parse_uri

  # Examine the given credential information, and if all is well,
  # return the transport name.
  # TODO: this actually does no validation of the credential options whatsoever
  def self.validate_backend(credentials, default_transport_name = "local")
    return default_transport_name if credentials.nil?

    transport_name = credentials[:backend]

    # TODO: Determine if it is ever possible (or supported) for transport_name to be 'localhost'
    # TODO: After inspec/inspec/pull/3750 is merged, should be able to remove nil from the list
    if credentials[:sudo] && [nil, "local", "localhost"].include?(transport_name)
      raise Train::UserError, "Sudo is only valid when running against a remote host. "\
        "To run this locally with elevated privileges, run the command with `sudo ...`."
    end

    return transport_name unless transport_name.nil?

    unless credentials[:target].nil?
      # We should not get here, because if target_uri unpacking was successful,
      # it would have set credentials[:backend]
      raise Train::UserError, "Cannot determine backend from target "\
           "configuration #{credentials[:target]}. Valid example: ssh://192.168.0.1"
    end

    unless credentials[:host].nil?
      raise Train::UserError, "Host configured, but no backend was provided. Please "\
           "specify how you want to connect. Valid example: ssh://192.168.0.1"
    end

    credentials[:backend] = default_transport_name
  end

  def self.group_keys_and_keyfiles(conf)
    # in case the user specified a key-file, register it that way
    # we will clear the list of keys and put keys and key_files separately
    keys_mixed = conf[:keys]
    return if keys_mixed.nil?

    conf[:key_files] = []
    conf[:keys] = []
    keys_mixed.each do |key|
      if !key.nil? && File.file?(key)
        conf[:key_files].push(key)
      else
        conf[:keys].push(key)
      end
    end
  end
end