train-rest 0.2.1 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59770f2304519af69df3caaac4f4ef19f964a0a4700952ff6a3b96834867bf13
4
- data.tar.gz: 39546783f42569561a1f740c094d0f2adb1a31c49c710b5c9736f172a961306f
3
+ metadata.gz: 1550899c2f8aad404793b83b1f7700ece96937efc8b78cb1eb7aebb8d6d2a2a2
4
+ data.tar.gz: b9251734100cc43233504afbe6e8d6fbd11bec1b07a16db7884720e907a164e2
5
5
  SHA512:
6
- metadata.gz: c8b2b55eb1723538a552ecbc7ce452a3545bd8ae3e35a82c263f3bbc16d6bfed161d053a6a711b0e34723c4f332e6d7797a40035d6142fc9957618a10ad6dac6
7
- data.tar.gz: 967e71707994db3086ea1f71e5d987324c405930ecb4473b9ad886ca59b32d15efb2e4c42f4f4075a607d642a11d78d4fe5e4fe6946468eb348735dd24bde514
6
+ metadata.gz: 1630381a0f378a7a4a275202fc19a60f71579b99f6b1ae50229c72b67cce2466e5d8b2f4a5b5c3c23f8c1c5c4ccc4817f768fc36cd65c3c8ac9c1b81609b1994
7
+ data.tar.gz: 02ed2d874ac736d016fc75f24a1c02e8b9209ec5f43cbb9281bf615106a04d53f4d49d26682e84cb7c2b7ab6a949dc2589823a769e9b87aa3c03aa428c50d919
data/README.md CHANGED
@@ -20,7 +20,7 @@ rake install:local
20
20
  | Option | Explanation | Default |
21
21
  | -------------------- | --------------------------------------- | ----------- |
22
22
  | `endpoint` | Endpoint of the REST API | _required_ |
23
- | `validate_ssl` | Check certificate and chain | true |
23
+ | `verify_ssl` | Check certificate and chain | true |
24
24
  | `auth_type` | Authentication type | `anonymous` |
25
25
  | `debug_rest` | Enable debugging of HTTP traffic | false |
26
26
  | `logger` | Alternative logging class | |
@@ -34,7 +34,18 @@ Identifier: `auth_type: :anonymous`
34
34
  No actions for authentication, logging in/out or session handing are made. This
35
35
  assumes a public API.
36
36
 
37
- ### Basic Authentication
37
+ ### Authtype Apikey
38
+
39
+ This will inject a HTTP header `Authorization: Apikey xxxx` with the passed
40
+ API key into requests.
41
+
42
+ Identifier: `auth_type: :authtype_apikey`
43
+
44
+ | Option | Explanation | Default |
45
+ | -------------------- | --------------------------------------- | ----------- |
46
+ | `apikey` | API Key for authentication | _required_ |
47
+
48
+ ### Basic (RFC 2617)
38
49
 
39
50
  Identifier: `auth_type: :basic`
40
51
 
@@ -46,6 +57,29 @@ Identifier: `auth_type: :basic`
46
57
  If you supply a `username` and a `password`, authentication will automatically
47
58
  switch to `basic`.
48
59
 
60
+ ### Bearer (RFC 7650)
61
+
62
+ This will inject a HTTP header `Authorization: Bearer xxxx` with the passed
63
+ token into requests.
64
+
65
+ Identifier: `auth_type: :bearer`
66
+
67
+ | Option | Explanation | Default |
68
+ | -------------------- | --------------------------------------- | ----------- |
69
+ | `token` | Tokenb to pass | _required_ |
70
+
71
+ ### Header-based
72
+
73
+ This will inject an additional HTTP header with the passed value. If no name
74
+ for the header is passed, it will default to `X-API-Key`.
75
+
76
+ Identifier: `auth_type: :header`
77
+
78
+ | Option | Explanation | Default |
79
+ | -------------------- | --------------------------------------- | ----------- |
80
+ | `apikey` | API Key for authentication | _required_ |
81
+ | `header` | Name of the HTTP header to include | `X-API-Key` |
82
+
49
83
  ### Redfish
50
84
 
51
85
  Identifier: `auth_type: :redfish`
@@ -60,6 +94,9 @@ The Redfish standard is defined in <http://www.dmtf.org/standards/redfish> and
60
94
  this handler does initial login, reuses the received session and logs out when
61
95
  closing the transport cleanly.
62
96
 
97
+ Known vendors which implement RedFish based management for their systems include
98
+ HPE, Dell, IBM, SuperMicro, Lenovo, Huawei and others.
99
+
63
100
  ## Debugging and use in Chef
64
101
 
65
102
  You can activate debugging by setting the `debug_rest` flag to `true'. Please
@@ -77,6 +114,46 @@ train = Train.create('rest', {
77
114
  })
78
115
  ```
79
116
 
117
+ ## Request Methods
118
+
119
+ This transport does not implement the `run_command` method, as there is no
120
+ line-based protocol to execute commands against. Instead, it implements its own
121
+ custom methods which suit REST interfaces. Trying to call this method will
122
+ throw an Exception.
123
+
124
+ ### Generic Request
125
+
126
+ The `request` methods allows to send free-form requests against any defined or
127
+ custom methods.
128
+
129
+ `request(path, method = :get, request_parameters: {}, data: nil, headers: {},
130
+ json_processing: true)`
131
+
132
+ - `path`: The path to request, which will be appended to the `endpoint`
133
+ - `method`: The HTTP method in Ruby Symbol syntax
134
+ - `request_parameters`: A hash of parameters to the `rest-client` request
135
+ method for additional settings
136
+ - `data`: Data for actions like `:post` or `:put`. Not all methods accept
137
+ a data body.
138
+ - `headers`: Additional headers for the request
139
+ - `json_processing`: If the response is a JSON and you want to receive a
140
+ processed Hash/Array instead of text
141
+
142
+ For `request_parameters` and `headers`, there is data mixed in to add
143
+ authenticator responses, JSON processing etc. Please check the
144
+ implementation in `connection.rb` for details.
145
+
146
+ ### Convenience Methods
147
+
148
+ Simplified wrappers are generated for the most common request types:
149
+
150
+ - `delete(path, request_parameters: {}, headers: {}, json_processing: true)`
151
+ - `head(path, request_parameters: {}, headers: {}, json_processing: true)`
152
+ - `get(path, request_parameters: {}, headers: {}, json_processing: true)`
153
+ - `post(path, request_parameters: {}, data: nil, headers: {}, json_processing: true)`
154
+ - `put(path, request_parameters: {}, data: nil, headers: {}, json_processing: true)`
155
+ - `patch(path, request_parameters: {}, data: nil, headers: {}, json_processing: true)`
156
+
80
157
  ## Example use
81
158
 
82
159
  ```ruby
@@ -133,7 +210,7 @@ require 'train-rest'
133
210
  # This will immediately do a login and add headers
134
211
  train = Train.create('rest', {
135
212
  endpoint: 'https://10.20.30.40',
136
- validate_ssl: false,
213
+ verify_ssl: false,
137
214
 
138
215
  auth_type: :redfish,
139
216
  username: 'iloadmin',
@@ -176,5 +253,28 @@ conn.close
176
253
  1. Run against the defiend targets via Chef Target Mode:
177
254
 
178
255
  ```shell
179
- chef-client --local-mode --target 10.0.0.1 --runlist 'recipe[my-cookbook:setup]'
256
+ chef-client --local-mode --target 10.0.0.1 --runlist 'recipe[my-cookbook::setup]'
180
257
  ```
258
+
259
+ ## Use with Prerecorded API responses
260
+
261
+ For testing during and after development, not all APIs can be used to verify your solution against.
262
+ The VCR gem offers the possibility to hook into web requests and intercept them to play back canned
263
+ responses.
264
+
265
+ Please read the documentation of the VCR gem on how to record your API and the concepts like
266
+ "cassettes", "libraries" and matchers.
267
+
268
+ The following options are available in train-rest for this:
269
+
270
+ | Option | Explanation | Default |
271
+ | -------------------- | --------------------------------------- | ------------ |
272
+ | `vcr_cassette` | Name of the response file | nil |
273
+ | `vcr_library` | Directory to search responses in | `vcr` |
274
+ | `vcr_match_on` | Elements to match request by | `method uri` |
275
+ | `vcr_record` | Recording mode | `none` |
276
+ | `vcr_hook_into` | Base library for intercepting | `webmock` |
277
+
278
+ VCR will only be required as a Gem and activated, if you supply a cassette name.
279
+
280
+ You can use all these settings in your Chef Target Mode `credentials` file as well.
@@ -1,4 +1,4 @@
1
- require_relative "../auth_handler.rb"
1
+ require_relative "../auth_handler"
2
2
 
3
3
  module TrainPlugins
4
4
  module Rest
@@ -0,0 +1,24 @@
1
+ require "base64" unless defined?(Base64)
2
+
3
+ require_relative "../auth_handler"
4
+
5
+ module TrainPlugins
6
+ module Rest
7
+ # Authentication via "Apikey" type in Authorization headers.
8
+ #
9
+ # Could not find a norm for this, just references in e.g. ElasticSearch & Cloud Conformity APIs.
10
+ class AuthtypeApikey < AuthHandler
11
+ def check_options
12
+ raise ArgumentError.new("Need :apikey for `Authorization: Apikey` authentication") unless options[:apikey]
13
+ end
14
+
15
+ def auth_parameters
16
+ {
17
+ headers: {
18
+ "Authorization" => format("Apikey %s", options[:apikey]),
19
+ },
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,18 +1,23 @@
1
- require_relative "../auth_handler.rb"
1
+ require "base64" unless defined?(Base64)
2
+
3
+ require_relative "../auth_handler"
2
4
 
3
5
  module TrainPlugins
4
6
  module Rest
5
- # Authentication via HTTP Basic Authentication
7
+ # Authentication via Basic Authentication.
8
+ #
9
+ # @see https://www.rfc-editor.org/rfc/rfc2617#section-4.1
6
10
  class Basic < AuthHandler
7
11
  def check_options
8
- raise ArgumentError.new("Need username for Basic authentication") unless options[:username]
9
- raise ArgumentError.new("Need password for Basic authentication") unless options[:password]
12
+ raise ArgumentError.new("Need :username for Basic authentication") unless options[:username]
13
+ raise ArgumentError.new("Need :password for Basic authentication") unless options[:password]
10
14
  end
11
15
 
12
16
  def auth_parameters
13
17
  {
14
- user: options[:username],
15
- password: options[:password],
18
+ headers: {
19
+ "Authorization" => format("Basic %s", Base64.encode64(options[:username] + ":" + options[:password]).chomp),
20
+ },
16
21
  }
17
22
  end
18
23
  end
@@ -0,0 +1,24 @@
1
+ require "base64" unless defined?(Base64)
2
+
3
+ require_relative "../auth_handler"
4
+
5
+ module TrainPlugins
6
+ module Rest
7
+ # Authentication via Bearer Authentication.
8
+ #
9
+ # @see https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
10
+ class Bearer < AuthHandler
11
+ def check_options
12
+ raise ArgumentError.new("Need :token for Bearer authentication") unless options[:token]
13
+ end
14
+
15
+ def auth_parameters
16
+ {
17
+ headers: {
18
+ "Authorization" => format("Bearer %s", options[:token]),
19
+ },
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../auth_handler"
2
+
3
+ module TrainPlugins
4
+ module Rest
5
+ # Authentication via additional Header.
6
+ #
7
+ # Header name defaults to "X-API-Key"
8
+ class Header < AuthHandler
9
+ def check_options
10
+ raise ArgumentError.new("Need :apikey for Header-based authentication") unless options[:apikey]
11
+
12
+ options[:header] = "X-API-Key" unless options[:header]
13
+ end
14
+
15
+ def auth_parameters
16
+ {
17
+ headers: {
18
+ options[:header] => options[:apikey],
19
+ },
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,4 +1,4 @@
1
- require_relative "../auth_handler.rb"
1
+ require_relative "../auth_handler"
2
2
 
3
3
  module TrainPlugins
4
4
  module Rest
@@ -13,7 +13,16 @@ module TrainPlugins
13
13
  #
14
14
  # @return [String]
15
15
  def self.name
16
- self.to_s.split("::").last.downcase
16
+ class_name = self.to_s.split("::").last
17
+
18
+ convert_to_snake_case(class_name)
19
+ end
20
+
21
+ # List authentication handlers
22
+ #
23
+ # @return [Array] Classes derived from `AuthHandler`
24
+ def self.descendants
25
+ ObjectSpace.each_object(Class).select { |klass| klass < self }
17
26
  end
18
27
 
19
28
  # Store authenticator options and trigger validation
@@ -29,10 +38,18 @@ module TrainPlugins
29
38
  # @raise [ArgumentError] if options are not as needed
30
39
  def check_options; end
31
40
 
32
- # Handle Login
41
+ # Handle login
33
42
  def login; end
34
43
 
35
- # Handle Logout
44
+ # Handle session renewal
45
+ def renew_session; end
46
+
47
+ # Return if session renewal needs to happen soon
48
+ #
49
+ # @return [Boolean]
50
+ def renewal_needed?; end
51
+
52
+ # Handle logout
36
53
  def logout; end
37
54
 
38
55
  # Headers added to the rest-client call
@@ -50,11 +67,21 @@ module TrainPlugins
50
67
  { headers: auth_headers }
51
68
  end
52
69
 
53
- # List authentication handlers
54
- #
55
- # @return [Array] Classes derived from `AuthHandler`
56
- def self.descendants
57
- ObjectSpace.each_object(Class).select { |klass| klass < self }
70
+ class << self
71
+ private
72
+
73
+ # Convert a class name to snake case.
74
+ #
75
+ # @param [String] Class name
76
+ # @return [String]
77
+ # @see https://github.com/chef/chef/blob/main/lib/chef/mixin/convert_to_class_name.rb
78
+ def convert_to_snake_case(str)
79
+ str = str.dup
80
+ str.gsub!(/[A-Z]/) { |s| "_" + s }
81
+ str.downcase!
82
+ str.sub!(/^\_/, "")
83
+ str
84
+ end
58
85
  end
59
86
  end
60
87
  end
@@ -1,9 +1,9 @@
1
- require "json"
2
- require "ostruct"
3
- require "uri"
1
+ require "json" unless defined?(JSON)
2
+ require "ostruct" unless defined?(OpenStruct)
3
+ require "uri" unless defined?(URI)
4
4
 
5
- require "rest-client"
6
- require "train"
5
+ require "rest-client" unless defined?(RestClient)
6
+ require "train" unless defined?(Train)
7
7
 
8
8
  module TrainPlugins
9
9
  module Rest
@@ -19,9 +19,35 @@ module TrainPlugins
19
19
  # Accept string (CLI) and boolean (API) options
20
20
  options[:verify_ssl] = options[:verify_ssl].to_s == "false" ? false : true
21
21
 
22
+ setup_vcr
23
+
22
24
  connect
23
25
  end
24
26
 
27
+ def setup_vcr
28
+ return unless options[:vcr_cassette]
29
+
30
+ require "vcr" unless defined?(VCR)
31
+
32
+ # TODO: Starts from "/" :(
33
+ library = options[:vcr_library]
34
+ match_on = options[:vcr_match_on].split.map(&:to_sym)
35
+
36
+ VCR.configure do |config|
37
+ config.cassette_library_dir = library
38
+ config.hook_into options[:vcr_hook_into]
39
+ config.default_cassette_options = {
40
+ record: options[:vcr_record].to_sym,
41
+ match_requests_on: match_on,
42
+ }
43
+ end
44
+
45
+ VCR.insert_cassette options[:vcr_cassette]
46
+ rescue LoadError
47
+ logger.fatal "Install the vcr gem to use HTTP(S) playback capability"
48
+ raise
49
+ end
50
+
25
51
  def connect
26
52
  login if auth_handlers.include? auth_type
27
53
  end
@@ -36,10 +62,12 @@ module TrainPlugins
36
62
  components.to_s
37
63
  end
38
64
 
65
+ # Allow overwriting to refine the type of REST API
66
+ attr_writer :detected_os
67
+
39
68
  def inventory
40
- # Faking it for Chef Target Mode only
41
69
  OpenStruct.new({
42
- name: "rest",
70
+ name: @detected_os || "rest",
43
71
  release: TrainPlugins::Rest::VERSION,
44
72
  family_hierarchy: %w{rest api},
45
73
  family: "api",
@@ -58,27 +86,34 @@ module TrainPlugins
58
86
 
59
87
  # User-faced API
60
88
 
89
+ # Allow (programmatically) setting additional headers apart from global transport configuration
90
+ attr_accessor :override_headers
91
+
61
92
  %i{get post put patch delete head}.each do |method|
62
- define_method(method) do |path, *parameters|
63
- # TODO: "warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call"
64
- request(path, method, *parameters)
93
+ define_method(method) do |path, **keywords|
94
+ request(path, method, **keywords)
65
95
  end
66
96
  end
67
97
 
68
98
  def request(path, method = :get, request_parameters: {}, data: nil, headers: {}, json_processing: true)
99
+ auth_handler.renew_session if auth_handler.renewal_needed?
100
+
69
101
  parameters = global_parameters.merge(request_parameters)
70
102
 
71
103
  parameters[:method] = method
72
104
  parameters[:url] = full_url(path)
73
105
 
74
106
  if json_processing
107
+ parameters[:headers]["Accept"] = "application/json"
75
108
  parameters[:headers]["Content-Type"] = "application/json"
76
- parameters[:payload] = JSON.generate(data)
109
+ parameters[:payload] = JSON.generate(data) unless data.nil?
77
110
  else
78
111
  parameters[:payload] = data
79
112
  end
80
113
 
81
- parameters[:headers].merge! headers
114
+ # Merge override headers + request specific headers
115
+ parameters[:headers].merge!(override_headers || {})
116
+ parameters[:headers].merge!(headers)
82
117
  parameters.compact!
83
118
 
84
119
  logger.info format("[REST] => %s", parameters.to_s) if options[:debug_rest]
@@ -88,6 +123,27 @@ module TrainPlugins
88
123
  transform_response(response, json_processing)
89
124
  end
90
125
 
126
+ # Allow switching generic handlers for an API-specific one.
127
+ #
128
+ # New handler needs to be loaded prior and be derived from TrainPlugins::REST::AuthHandler.
129
+ def switch_auth_handler(new_handler)
130
+ return if active_auth_handler == new_handler
131
+
132
+ logout
133
+
134
+ options[:auth_type] = new_handler.to_sym
135
+ @auth_handler = nil
136
+
137
+ login
138
+ end
139
+
140
+ # Return active auth handler.
141
+ #
142
+ # @return [Symbol]
143
+ def active_auth_handler
144
+ options[:auth_type]
145
+ end
146
+
91
147
  # Auth Handlers-faced API
92
148
 
93
149
  def auth_parameters
@@ -146,14 +202,14 @@ module TrainPlugins
146
202
  end
147
203
 
148
204
  def login
149
- logger.info format("REST Login via %s authentication handler", auth_type.to_s) if auth_type != :anonymous
205
+ logger.info format("REST Login via %s authentication handler", auth_type.to_s) unless %i{anonymous basic}.include? auth_type
150
206
 
151
207
  auth_handler.options = options
152
208
  auth_handler.login
153
209
  end
154
210
 
155
211
  def logout
156
- logger.info format("REST Logout via %s authentication handler", auth_type.to_s) if auth_type != :anonymous
212
+ logger.info format("REST Logout via %s authentication handler", auth_type.to_s) unless %i{anonymous basic}.include? auth_type
157
213
 
158
214
  auth_handler.logout
159
215
  end
@@ -1,3 +1,5 @@
1
+ require "rubygems" unless defined?(Gem)
2
+
1
3
  require "train-rest/connection"
2
4
 
3
5
  module TrainPlugins
@@ -8,7 +10,7 @@ module TrainPlugins
8
10
  option :endpoint, required: true
9
11
  option :verify_ssl, default: true
10
12
  option :proxy, default: nil
11
- option :headers, default: nil
13
+ option :headers, default: {}
12
14
  option :timeout, default: 120
13
15
 
14
16
  option :auth_type, default: :anonymous
@@ -16,9 +18,32 @@ module TrainPlugins
16
18
  option :password, default: nil
17
19
  option :debug_rest, default: false
18
20
 
21
+ option :vcr_cassette, default: nil
22
+ option :vcr_library, default: "vcr"
23
+ option :vcr_match_on, default: "method uri"
24
+ option :vcr_record, default: "none"
25
+ option :vcr_hook_into, default: "webmock"
26
+
19
27
  def connection(_instance_opts = nil)
28
+ dependency_checks
29
+
20
30
  @connection ||= TrainPlugins::Rest::Connection.new(@options)
21
31
  end
32
+
33
+ private
34
+
35
+ def dependency_checks
36
+ return unless @options[:vcr_cassette]
37
+
38
+ raise Gem::LoadError.new("Install VCR Gem for API playback capability") unless gem_installed?("vcr")
39
+
40
+ stubber = @options[:vcr_hook_into]
41
+ raise Gem::LoadError.new("Install #{stubber} Gem for API playback capability") unless gem_installed?(stubber)
42
+ end
43
+
44
+ def gem_installed?(name)
45
+ Gem::Specification.find_all_by_name(name).any?
46
+ end
22
47
  end
23
48
  end
24
49
  end
@@ -1,5 +1,5 @@
1
1
  module TrainPlugins
2
2
  module Rest
3
- VERSION = "0.2.1".freeze
3
+ VERSION = "0.4.0".freeze
4
4
  end
5
5
  end
data/lib/train-rest.rb CHANGED
@@ -7,5 +7,8 @@ require "train-rest/transport"
7
7
  require "train-rest/connection"
8
8
 
9
9
  require "train-rest/auth_handler/anonymous"
10
+ require "train-rest/auth_handler/authtype-apikey"
11
+ require "train-rest/auth_handler/header"
10
12
  require "train-rest/auth_handler/basic"
13
+ require "train-rest/auth_handler/bearer"
11
14
  require "train-rest/auth_handler/redfish"
data/train-rest.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require "train-rest/version"
4
4
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: train-rest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Heinen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-24 00:00:00.000000000 Z
11
+ date: 2021-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: train
@@ -120,7 +120,10 @@ files:
120
120
  - lib/train-rest.rb
121
121
  - lib/train-rest/auth_handler.rb
122
122
  - lib/train-rest/auth_handler/anonymous.rb
123
+ - lib/train-rest/auth_handler/authtype-apikey.rb
123
124
  - lib/train-rest/auth_handler/basic.rb
125
+ - lib/train-rest/auth_handler/bearer.rb
126
+ - lib/train-rest/auth_handler/header.rb
124
127
  - lib/train-rest/auth_handler/redfish.rb
125
128
  - lib/train-rest/connection.rb
126
129
  - lib/train-rest/transport.rb