File: base_connection.rb

package info (click to toggle)
ruby-train 3.13.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,208 kB
  • sloc: ruby: 10,002; sh: 17; makefile: 8
file content (276 lines) | stat: -rw-r--r-- 10,369 bytes parent folder | download | duplicates (2)
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
require_relative "../errors"
require_relative "../extras"
require_relative "../file"
require "fileutils" unless defined?(FileUtils)
require "logger"
require_relative "../audit_log"

class Train::Plugins::Transport
  # A Connection instance can be generated and re-generated, given new
  # connection details such as connection port, hostname, credentials, etc.
  # This object is responsible for carrying out the actions on the remote
  # host such as executing commands, transferring files, etc.
  #
  # @author Fletcher Nichol <fnichol@nichol.ca>
  class BaseConnection
    include Train::Extras

    # Create a new Connection instance.
    #
    # @param options [Hash] connection options
    # @yield [self] yields itself for block-style invocation
    def initialize(options = nil)
      @options = options || {}

      @logger = @options.delete(:logger) || Logger.new($stdout, level: :fatal)
      Train::Platforms::Detect::Specifications::OS.load
      Train::Platforms::Detect::Specifications::Api.load

      # In run_command all options are not accessible as some of them gets deleted in transit.
      # To make the data like hostname, username available to aduit logs dup the options
      @audit_log_data = options.dup || {}
      # For transport other than local all audit log options accessible inside transport_options key
      if !@options.empty? && @options[:transport_options] && @options[:transport_options][:enable_audit_log]
        @audit_log = Train::AuditLog.create(options[:transport_options])
      elsif !@options.empty? && @options[:enable_audit_log]
        @audit_log = Train::AuditLog.create(@options)
      end

      # default caching options
      @cache_enabled = {
        file: true,
        command: false,
        api_call: false,
      }

      @cache = {}
      @cache_enabled.each_key do |type|
        clear_cache(type)
      end
    end

    def with_sudo_pty
      yield
    end

    # Returns cached client if caching enabled. Otherwise returns whatever is
    # given in the block.
    #
    # @example
    #
    #   def demo_client
    #     cached_client(:api_call, :demo_connection) do
    #       DemoClient.new(args)
    #     end
    #   end
    #
    # @param [symbol] type one of [:api_call, :file, :command]
    # @param [symbol] key for your cached connection
    def cached_client(type, key)
      return yield unless cache_enabled?(type)

      @cache[type][key] ||= yield
    end

    def cache_enabled?(type)
      @cache_enabled[type.to_sym]
    end

    # Enable caching types for Train. Currently we support
    # :api_call, :file and :command types
    def enable_cache(type)
      raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)

      @cache_enabled[type.to_sym] = true
    end

    def disable_cache(type)
      raise Train::UnknownCacheType, "#{type} is not a valid cache type" unless @cache_enabled.keys.include?(type.to_sym)

      @cache_enabled[type.to_sym] = false
      clear_cache(type.to_sym)
    end

    # Closes the session connection, if it is still active.
    def close
      # this method may be left unimplemented if that is applicable
    end

    def to_json
      {
        "files" => Hash[@cache[:file].map { |x, y| [x, y.to_json] }],
      }
    end

    def load_json(j)
      require_relative "../transports/mock"
      j["files"].each do |path, jf|
        @cache[:file][path] = Train::Transports::Mock::Connection::File.from_json(jf)
      end
    end

    def force_platform!(name, platform_details = nil)
      plat = Train::Platforms.name(name)
      plat.backend = self
      plat.platform = platform_details unless platform_details.nil?
      plat.find_family_hierarchy
      plat.add_platform_methods
      plat
    end

    def backend_type
      @options[:backend] || "unknown"
    end

    def inspect
      "%s[%s]" % [self.class, backend_type]
    end

    alias direct_platform force_platform!

    # Get information on the operating system which this transport connects to.
    #
    # @return [Platform] system information
    def platform
      @platform ||= Train::Platforms::Detect.scan(self)
    end
    # we need to keep os as a method for backwards compatibility with inspec
    alias os platform

    # This is the main command call for all connections. This will call the private
    # run_command_via_connection on the connection with optional caching
    #
    # This command accepts an optional data handler block. When provided,
    # inbound data will be published vi `data_handler.call(data)`. This can allow
    # callers to receive and render updates from remote command execution.
    #
    # @param [String] the command to run
    # @param [Hash] optional hash of options for this command. The derived connection
    #               class's implementation of run_command_via_connection should receive
    #               and apply these options.
    def run_command(cmd, opts = {}, &data_handler)
      # Some implementations do not accept an opts argument.
      # We cannot update all implementations to accept opts due to them being separate plugins.
      # Therefore here we check the implementation's arity to maintain compatibility.
      @audit_log.info({ type: "cmd", command: "#{cmd}", user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log

      case method(:run_command_via_connection).arity.abs
      when 1
        return run_command_via_connection(cmd, &data_handler) unless cache_enabled?(:command)

        @cache[:command][cmd] ||= run_command_via_connection(cmd, &data_handler)

      when 2
        return run_command_via_connection(cmd, opts, &data_handler) unless cache_enabled?(:command)

        @cache[:command][cmd] ||= run_command_via_connection(cmd, opts, &data_handler)
      else
        raise NotImplementedError, "#{self.class} does not implement run_command_via_connection with arity #{method(:run_command_via_connection).arity}"
      end
    end

    # This is the main file call for all connections. This will call the private
    # file_via_connection on the connection with optional caching
    def file(path, *args)
      @audit_log.info({ type: "file", path: "#{path}", user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log
      return file_via_connection(path, *args) unless cache_enabled?(:file)

      @cache[:file][path] ||= file_via_connection(path, *args)
    end

    # Uploads local files to remote host.
    #
    # @param locals [String, Array<String>] path to local files
    # @param remote [String] path to remote destination
    # @raise [TransportFailed] if the files could not all be uploaded
    #   successfully, which may vary by implementation
    def upload(locals, remote)
      remote_directory = file(remote).directory?

      if locals.is_a?(Array) && !remote_directory
        raise Train::TransportError, "#{self.class} expects remote directory as second upload parameter for multi-file uploads"
      end

      Array(locals).each do |local|
        remote_file = remote_directory ? File.join(remote, File.basename(local)) : remote
        @audit_log.info({ type: "file upload", source: local, destination: remote_file, user: @audit_log_data[:username], hostname: @audit_log_data[:hostname] }) if @audit_log
        logger.debug("Attempting to upload '#{local}' as file #{remote_file}")

        file(remote_file).content = File.read(local)
      end
    end

    # Download remote files or directories to local host.
    #
    # @param remotes [Array<String>] paths to remote files or directories
    # @param local [String] path to local destination. If `local` is an
    #   existing directory, `remote` will be downloaded into the directory
    #   using its original name
    # @raise [TransportFailed] if the files could not all be downloaded
    #   successfully, which may vary by implementation
    def download(remotes, local)
      FileUtils.mkdir_p(File.dirname(local))

      Array(remotes).each do |remote|
        new_content = file(remote).content
        local_file = File.join(local, File.basename(remote))
        logger.debug("Attempting to download '#{remote}' as file #{local_file}")

        File.open(local_file, "w") { |fp| fp.write(new_content) }
      end
    end

    # Builds a LoginCommand which can be used to open an interactive
    # session on the remote host.
    #
    # @return [LoginCommand] array of command line tokens
    def login_command
      raise NotImplementedError, "#{self.class} does not implement #login_command()"
    end

    # Block and return only when the remote host is prepared and ready to
    # execute command and upload files. The semantics and details will
    # vary by implementation, but a round trip through the hosted
    # service is preferred to simply waiting on a socket to become
    # available.
    def wait_until_ready
      # this method may be left unimplemented if that is applicable log
    end

    private

    # Execute a command using this connection.
    #
    # @param command [String] command string to execute
    # @param &data_handler(data) [Proc] proc that is called when data arrives from
    # the connection.  This block is optional. Individual transports will need
    # to explicitly invoke the block in their implementation of run_command_via_connection;
    # if they do not, the block is ignored and will not be used to report data back to the caller.
    #
    # @return [CommandResult] contains the result of running the command
    def run_command_via_connection(_command, &_data_handler)
      raise NotImplementedError, "#{self.class} does not implement #run_command_via_connection()"
    end

    # Interact with files on the target. Read, write, and get metadata
    # from files via the transport.
    #
    # @param [String] path which is being inspected
    # @return [FileCommon] file object that allows for interaction
    def file_via_connection(_path, *_args)
      raise NotImplementedError, "#{self.class} does not implement #file_via_connection(...)"
    end

    def clear_cache(type)
      @cache[type.to_sym] = {}
    end

    # @return [Logger] logger for reporting information
    # @api private
    attr_reader :logger

    # @return [Hash] connection options
    # @api private
    attr_reader :options
  end
end