train-rest 0.2.1 → 0.4.0

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