redfish_client 0.5.4 → 0.6.1

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: 685c1b4535b5934f7683db9bdb964e5961962c9d85fc3d83ef5c97051568d58b
4
- data.tar.gz: f60cc0d54626c4ab2c55f84e2179a507ff95d8732a40d99bb300bde7666f40f2
3
+ metadata.gz: 8af3d1554bfb0aaef0e0c39a93dcff0a651bc4c5c46f10e554bc2d532bc21ad2
4
+ data.tar.gz: f33ec3d32eb011c22d74e7ac54c24c6bfd4d252a1706e31d46e4acc20a799d72
5
5
  SHA512:
6
- metadata.gz: f5b8378d17b32e736e1007c51a3e22a5ad746e816acdb5e586cf91552c07ca5153a5140923a70ae8364c7b7435fb84b9010756157269e2d88d053b692dd055b3
7
- data.tar.gz: 5711720aa89461803460a09d323d043505ee54b6cef50183abddb336364f5145a030c6e0050723c22d85e706476e0a49c5f55a357a613a800c55b0e05be62e3d
6
+ metadata.gz: 9843e66d9cdab43dac189edc0106e31f4e41155b3ec7de7fcbaecd164ce39776f989e0370f8e58a2d5d3de832c9ee9231b8cd7c35a759afff71fd5dc6e0cf1b1
7
+ data.tar.gz: 7dd3ab463ae71d36f0d422ec898a6cfb54a5e818b5a42b36d3888e1c5c4e84e8cebd260d9ec46296b412501467a918f53c3f49b6ef3162967f80317a617c0be2
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redfish_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tadej Borovšak
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-25 00:00:00.000000000 Z
11
+ date: 2025-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0.71'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: '0.71'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: server_sent_events
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -136,40 +142,19 @@ dependencies:
136
142
  - - ">="
137
143
  - !ruby/object:Gem::Version
138
144
  version: '0'
139
- description:
145
+ description:
140
146
  email:
141
147
  - tadej.borovsak@xlab.si
142
148
  executables: []
143
149
  extensions: []
144
150
  extra_rdoc_files: []
145
- files:
146
- - ".codeclimate.yml"
147
- - ".github/workflows/ci.yml"
148
- - ".gitignore"
149
- - ".rspec"
150
- - ".rubocop.yml"
151
- - ".simplecov"
152
- - ".yardopts"
153
- - Gemfile
154
- - README.md
155
- - Rakefile
156
- - bin/console
157
- - bin/setup
158
- - lib/redfish_client.rb
159
- - lib/redfish_client/connector.rb
160
- - lib/redfish_client/event_listener.rb
161
- - lib/redfish_client/nil_hash.rb
162
- - lib/redfish_client/resource.rb
163
- - lib/redfish_client/response.rb
164
- - lib/redfish_client/root.rb
165
- - lib/redfish_client/version.rb
166
- - redfish_client.gemspec
151
+ files: []
167
152
  homepage: https://github.com/xlab-steampunk/redfish-client-ruby
168
153
  licenses:
169
154
  - Apache-2.0
170
155
  metadata:
171
156
  allowed_push_host: https://rubygems.org
172
- post_install_message:
157
+ post_install_message:
173
158
  rdoc_options: []
174
159
  require_paths:
175
160
  - lib
@@ -184,8 +169,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
184
169
  - !ruby/object:Gem::Version
185
170
  version: '0'
186
171
  requirements: []
187
- rubygems_version: 3.2.3
188
- signing_key:
172
+ rubygems_version: 3.1.6
173
+ signing_key:
189
174
  specification_version: 4
190
175
  summary: Simple Redfish client library
191
176
  test_files: []
data/.codeclimate.yml DELETED
@@ -1,4 +0,0 @@
1
- plugins:
2
- rubocop:
3
- enabled: true
4
- channel: rubocop-0-54
@@ -1,21 +0,0 @@
1
- name: Ruby
2
-
3
- on:
4
- push:
5
- branches: [ master ]
6
- pull_request:
7
-
8
- jobs:
9
- test:
10
- runs-on: ubuntu-latest
11
- steps:
12
- - name: Clone repo
13
- uses: actions/checkout@v2
14
- - name: Set up Ruby
15
- uses: ruby/setup-ruby@v1
16
- with:
17
- ruby-version: 2.6
18
- - name: Install dependencies
19
- run: bundle install
20
- - name: Run tests
21
- run: bundle exec rake
data/.gitignore DELETED
@@ -1,11 +0,0 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
4
- /coverage/
5
- /doc/
6
- /pkg/
7
- /spec/reports/
8
- /tmp/
9
-
10
- # rspec failure tracking
11
- .rspec_status
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
data/.rubocop.yml DELETED
@@ -1,48 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.3
3
- Exclude:
4
- - redfish_client.gemspec
5
- - Gemfile
6
- - Rakefile
7
- - bin/**
8
-
9
- Layout/MultilineOperationIndentation:
10
- EnforcedStyle: indented
11
-
12
- Layout/MultilineMethodCallIndentation:
13
- EnforcedStyle: indented
14
-
15
- Style/MethodMissing:
16
- Exclude:
17
- - lib/redfish_client/resource.rb
18
-
19
- Style/StringLiterals:
20
- EnforcedStyle: double_quotes
21
-
22
- Style/Documentation:
23
- Enabled: false
24
-
25
- Style/BracesAroundHashParameters:
26
- EnforcedStyle: context_dependent
27
-
28
- Style/TrailingCommaInArguments:
29
- EnforcedStyleForMultiline: comma
30
-
31
- Style/TrailingCommaInArrayLiteral:
32
- EnforcedStyleForMultiline: comma
33
-
34
- Style/TrailingCommaInHashLiteral:
35
- EnforcedStyleForMultiline: comma
36
-
37
- Metrics/AbcSize:
38
- Max: 20
39
-
40
- Metrics/BlockLength:
41
- Exclude:
42
- - spec/**/*.rb
43
-
44
- Metrics/ClassLength:
45
- Max: 500
46
-
47
- Metrics/ParameterLists:
48
- CountKeywordArgs: false
data/.simplecov DELETED
@@ -1,3 +0,0 @@
1
- SimpleCov.start do
2
- add_filter "/spec/"
3
- end
data/.yardopts DELETED
@@ -1,3 +0,0 @@
1
- --markup markdown
2
- -
3
- README.md
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- source "https://rubygems.org"
2
-
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
-
5
- # Specify your gem's dependencies in redfish_client.gemspec
6
- gemspec
data/README.md DELETED
@@ -1,98 +0,0 @@
1
- # Redfish Ruby Client
2
-
3
- [![Build Status](https://travis-ci.org/xlab-si/redfish-client-ruby.svg?branch=master)](https://travis-ci.org/xlab-si/redfish-client-ruby)
4
- [![Maintainability](https://api.codeclimate.com/v1/badges/884ef5e8d68dff90567f/maintainability)](https://codeclimate.com/github/xlab-si/redfish-client-ruby/maintainability)
5
- [![Test Coverage](https://api.codeclimate.com/v1/badges/884ef5e8d68dff90567f/test_coverage)](https://codeclimate.com/github/xlab-si/redfish-client-ruby/test_coverage)
6
- [![Dependency Status](https://beta.gemnasium.com/badges/github.com/xlab-si/redfish_client.svg)](https://beta.gemnasium.com/projects/github.com/xlab-si/redfish_client)
7
- [![security](https://hakiri.io/github/xlab-si/redfish_client/master.svg)](https://hakiri.io/github/xlab-si/redfish_client/master)
8
-
9
-
10
- This repository contains source code for redfish_client gem that can be used
11
- to connect to Redfish services.
12
-
13
-
14
- ## Installation
15
-
16
- Add this line to your application's Gemfile:
17
-
18
- gem "redfish_client"
19
-
20
- And then execute:
21
-
22
- $ bundle
23
-
24
- Or install it yourself as:
25
-
26
- $ gem install redfish_client
27
-
28
-
29
- ## Usage
30
-
31
- Minimal program that uses this gem would look something like this:
32
-
33
- require "redfish_client"
34
-
35
- root = RedfishClient.new("https://localhost:8000",
36
- prefix: "/redfish/v1",
37
- verify: false)
38
- puts root
39
- root.login("username", "password")
40
- puts root.Systems
41
- root.logout
42
-
43
-
44
- ## Handling asynchronous operations
45
-
46
- Redfish service can return a 202 status when we request an execution of a
47
- long-running operation (e.g. updating firmware). We are expected to poll the
48
- monitor for changes until the job terminates.
49
-
50
- Responses in Redfish client have a built-in support for this, so polling the
51
- service is rather painless:
52
-
53
- # Start the async action
54
- response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
55
- field: "target", payload: { ... },
56
- )
57
- # Wait for the termination
58
- response = update_service.wait(response)
59
- # Do something with response
60
-
61
- It is also possible to manually poll the response like this:
62
-
63
- response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
64
- field: "target", payload: { ... },
65
- )
66
- until response.done?
67
- # wait a bit
68
- response = update_service.get(response.monitor)
69
- end
70
-
71
- Response is also safe to (de)serialize, which means that the process that
72
- started the async operation and the process that will wait for it can be
73
- separate:
74
-
75
- response = update_service.Actions["#UpdateService.SimpleUpdate"].post(
76
- field: "target", payload: { ... },
77
- )
78
- send_response_somewhere(response.to_h)
79
-
80
- # Somewhere else
81
- response = Response.from_hash(receive_response_from_somewhere)
82
-
83
-
84
- ## Development
85
-
86
- After checking out the repo, run `bin/setup` to install dependencies. Then,
87
- run `bundle exec rake spec` to run the tests. You can also run `bin/console`
88
- for an interactive prompt that will allow you to experiment.
89
-
90
- To create new release, increment the version number, commit the change, tag
91
- the commit and push tag to the GitHub. Travis CI will pick from there on and
92
- create new release, publishing it on https://rubygems.org.
93
-
94
-
95
- ## Contributing
96
-
97
- Bug reports and pull requests are welcome on GitHub at
98
- https://github.com/xlab-si/redfish_client.
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/bin/console DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "redfish_client"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- require "pry"
10
- Pry.start
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
@@ -1,260 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "base64"
4
- require "excon"
5
- require "json"
6
-
7
- require "redfish_client/nil_hash"
8
- require "redfish_client/response"
9
-
10
- module RedfishClient
11
- # Connector serves as a low-level wrapper around HTTP calls that are used
12
- # to retrieve data from the service API. It abstracts away implementation
13
- # details such as sending the proper headers in request, which do not
14
- # change between resource fetches.
15
- #
16
- # Library users should treat this class as an implementation detail and
17
- # use higer-level {RedfishClient::Resource} instead.
18
- class Connector
19
- # AuthError is raised if the credentials are invalid.
20
- class AuthError < StandardError; end
21
-
22
- # Default headers, as required by Redfish spec
23
- # https://redfish.dmtf.org/schemas/DSP0266_1.4.0.html#request-headers
24
- DEFAULT_HEADERS = {
25
- "Accept" => "application/json",
26
- "OData-Version" => "4.0",
27
- }.freeze
28
-
29
- # Basic and token authentication header names
30
- BASIC_AUTH_HEADER = "Authorization"
31
- TOKEN_AUTH_HEADER = "X-Auth-Token"
32
- LOCATION_HEADER = "Location"
33
-
34
- # Create new connector.
35
- #
36
- # By default, connector performs no caching. If caching is desired,
37
- # Hash should be used as a cache implementation.
38
- #
39
- # It is also possible to pass in custom caching class. Instances of that
40
- # class should respond to the following four methods:
41
- #
42
- # 1. `[](key)` - Used to access cached content and should return
43
- # `nil` if the key has no associated value.
44
- # 2. `[]=(key, value)` - Cache `value` under the `key`
45
- # 3. `clear` - Clear the complete cache.
46
- # 4. `delete(key)` - Invalidate cache entry associated with `key`.
47
- #
48
- # @param url [String] base url of the Redfish service
49
- # @param verify [Boolean] verify SSL certificate of the service
50
- # @param cache [Object] cache backend
51
- def initialize(url, verify: true, cache: nil)
52
- @url = url
53
- @headers = DEFAULT_HEADERS.dup
54
- middlewares = Excon.defaults[:middlewares] +
55
- [Excon::Middleware::RedirectFollower]
56
- @connection = Excon.new(@url,
57
- ssl_verify_peer: verify,
58
- middlewares: middlewares)
59
- @cache = cache || NilHash.new
60
- end
61
-
62
- # Add HTTP headers to the requests made by the connector.
63
- #
64
- # @param headers [Hash<String, String>] headers to be added
65
- def add_headers(headers)
66
- @headers.merge!(headers)
67
- end
68
-
69
- # Remove HTTP headers from requests made by the connector.
70
- #
71
- # Headers that are not currently set are silently ignored and no error is
72
- # raised.
73
- #
74
- # @param headers [List<String>] headers to remove
75
- def remove_headers(headers)
76
- headers.each { |h| @headers.delete(h) }
77
- end
78
-
79
- # Issue requests to the service.
80
- #
81
- # @param mathod [Symbol] HTTP method (:get, :post, :patch or :delete)
82
- # @param path [String] path to the resource, relative to the base
83
- # @param data [Hash] data to be sent over the socket
84
- # @return [Response] response object
85
- def request(method, path, data = nil)
86
- return @cache[path] if method == :get && @cache[path]
87
-
88
- do_request(method, path, data).tap do |r|
89
- @cache[path] = r if method == :get && r.status == 200
90
- end
91
- end
92
-
93
- # Issue GET request to service.
94
- #
95
- # This method will first try to return cached response if available. If
96
- # cache does not contain entry for this request, data will be fetched from
97
- # remote and then cached, but only if the response has an OK (200) status.
98
- #
99
- # @param path [String] path to the resource, relative to the base url
100
- # @return [Response] response object
101
- def get(path)
102
- request(:get, path)
103
- end
104
-
105
- # Issue POST requests to the service.
106
- #
107
- # @param path [String] path to the resource, relative to the base
108
- # @param data [Hash] data to be sent over the socket, JSON encoded
109
- # @return [Response] response object
110
- def post(path, data = nil)
111
- request(:post, path, data)
112
- end
113
-
114
- # Issue PATCH requests to the service.
115
- #
116
- # @param path [String] path to the resource, relative to the base
117
- # @param data [Hash] data to be sent over the socket
118
- # @return [Response] response object
119
- def patch(path, data = nil)
120
- request(:patch, path, data)
121
- end
122
-
123
- # Issue DELETE requests to the service.
124
- #
125
- # @param path [String] path to the resource, relative to the base
126
- # @return [Response] response object
127
- def delete(path)
128
- request(:delete, path)
129
- end
130
-
131
- # Clear the cached responses.
132
- #
133
- # If path is passed as a parameter, only one cache entry gets invalidated,
134
- # else complete cache gets invalidated.
135
- #
136
- # Next GET request will repopulate the cache.
137
- #
138
- # @param path [String] path to invalidate
139
- def reset(path = nil)
140
- path.nil? ? @cache.clear : @cache.delete(path)
141
- end
142
-
143
- # Set authentication-related variables.
144
- #
145
- # Last parameter controls the kind of login connector will perform. If
146
- # session_path is `nil`, basic authentication will be used, otherwise
147
- # connector will use session-based authentication.
148
- #
149
- # Note that actual login is done lazily. If you need to check for
150
- # credential validity, call #{login} method.
151
- #
152
- # @param username [String] API username
153
- # @param password [String] API password
154
- # @param auth_test_path [String] API path to test credential's validity
155
- # @param session_path [String, nil] API session path
156
- def set_auth_info(username, password, auth_test_path, session_path = nil)
157
- @username = username
158
- @password = password
159
- @auth_test_path = auth_test_path
160
- @session_path = session_path
161
- end
162
-
163
- # Authenticate against the service.
164
- #
165
- # Calling this method will try to authenticate against API using
166
- # credentials provided by #{set_auth_info} call.
167
- # If authentication fails, # {AuthError} will be raised.
168
- #
169
- # @raise [AuthError] if credentials are invalid
170
- def login
171
- @session_path ? session_login : basic_login
172
- end
173
-
174
- # Sign out of the service.
175
- def logout
176
- # We bypass request here because we do not want any retries on 401
177
- # when doing logout.
178
- if @session_oid
179
- params = prepare_request_params(:delete, @session_oid)
180
- @connection.request(params)
181
- @session_oid = nil
182
- end
183
- remove_headers([BASIC_AUTH_HEADER, TOKEN_AUTH_HEADER])
184
- end
185
-
186
- private
187
-
188
- def do_request(method, path, data)
189
- params = prepare_request_params(method, path, data)
190
- r = @connection.request(params)
191
- if r.status == 401
192
- login
193
- r = @connection.request(params)
194
- end
195
- Response.new(r.status, downcase_headers(r.data[:headers]), r.data[:body])
196
- end
197
-
198
- def downcase_headers(headers)
199
- headers.each_with_object({}) { |(k, v), obj| obj[k.downcase] = v }
200
- end
201
-
202
- def prepare_request_params(method, path, data = nil)
203
- params = { method: method, path: path }
204
- if data
205
- params[:body] = data.to_json
206
- params[:headers] = @headers.merge("Content-Type" => "application/json")
207
- else
208
- params[:headers] = @headers
209
- end
210
- params
211
- end
212
-
213
- def session_login
214
- # We bypass request here because we do not want any retries on 401
215
- # when doing login.
216
- params = prepare_request_params(:post, @session_path,
217
- "UserName" => @username,
218
- "Password" => @password)
219
- r = @connection.request(params)
220
- raise_invalid_auth_error unless r.status == 201
221
-
222
- body = JSON.parse(r.data[:body])
223
- headers = r.data[:headers]
224
-
225
- add_headers(TOKEN_AUTH_HEADER => headers[TOKEN_AUTH_HEADER])
226
- save_session_oid!(body, headers)
227
- end
228
-
229
- def save_session_oid!(body, headers)
230
- @session_oid = body["@odata.id"] if body.key?("@odata.id")
231
- return if @session_oid
232
-
233
- return unless headers.key?(LOCATION_HEADER)
234
-
235
- location = URI.parse(headers[LOCATION_HEADER])
236
- @session_oid = [location.path, location.query].compact.join("?")
237
- end
238
-
239
- def basic_login
240
- payload = Base64.encode64("#{@username}:#{@password}").strip
241
- add_headers(BASIC_AUTH_HEADER => "Basic #{payload}")
242
- return if auth_valid?
243
-
244
- remove_headers([BASIC_AUTH_HEADER])
245
- raise_invalid_auth_error
246
- end
247
-
248
- def raise_invalid_auth_error
249
- raise AuthError, "Invalid credentials"
250
- end
251
-
252
- def auth_valid?
253
- # We bypass request here because we do not want any retries on 401
254
- # when checking authentication headers.
255
- reset(@auth_test_path) # Do not want to see cached response
256
- params = prepare_request_params(:get, @auth_test_path)
257
- @connection.request(params).status == 200
258
- end
259
- end
260
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "uri"
5
-
6
- module RedfishClient
7
- # EventListener class can be used to stream events from Redfish service. It
8
- # is a thin wrapper around SSE listener that does the dirty work of
9
- # splitting each event into its EventRecords and reporting them as separate
10
- # events.
11
- class EventListener
12
- # Create new EventListener instance.
13
- #
14
- # @param sse_client [ServerSentEvents::Client] SSE client
15
- def initialize(sse_client)
16
- @sse_client = sse_client
17
- end
18
-
19
- # Stream events from redfish service.
20
- #
21
- # Events that this method yields are actually EventRecords, extracted from
22
- # the actual Redfish Event.
23
- def listen
24
- @sse_client.listen do |event|
25
- split_event_into_records(event).each { |r| yield(r) }
26
- end
27
- end
28
-
29
- private
30
-
31
- def split_event_into_records(event)
32
- JSON.parse(event.data).fetch("Events", [])
33
- end
34
- end
35
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RedfishClient
4
- # NilHash imitates the built-in Hash class without storing anything
5
- # permanently.
6
- #
7
- # Main use of this class is as a non-caching connector backend.
8
- class NilHash
9
- # Access hash member.
10
- #
11
- # Since this implementation does not store any data, return value is
12
- # always nil.
13
- #
14
- # @param _key not used
15
- # @return [nil]
16
- def [](_key)
17
- nil
18
- end
19
-
20
- # Set hash member.
21
- #
22
- # This is just a pass-through method, since it always simply returns the
23
- # value without actually storing it.
24
- #
25
- # @param _key not used
26
- # @param value [Object] any value
27
- # @return [Object] value
28
- def []=(_key, value)
29
- value
30
- end
31
-
32
- # Clear the contents of the cache.
33
- #
34
- # Since hash is not storing anything, this is a no-op.
35
- def clear; end
36
-
37
- # Delete entry from hash.
38
- #
39
- # Since hash is not storing anything, this is a no-op.
40
- #
41
- # @param _key not used
42
- def delete(_key) end
43
- end
44
- end
@@ -1,290 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module RedfishClient
6
- # Resource is basic building block of Redfish client and serves as a
7
- # container for the data that is retrieved from the Redfish service.
8
- #
9
- # When we interact with the Redfish service, resource will wrap the data
10
- # retrieved from the service API and offer us dot-notation accessors for
11
- # values stored.
12
- #
13
- # Resource will also load any sub-resource on demand when we access it.
14
- # For example, if we have a root Redfish resource stored in `root`,
15
- # accessing `root.SessionService` will automatically fetch the appropriate
16
- # resource from the API.
17
- #
18
- # In order to reduce the amount of requests being sent to the service,
19
- # resource can also utilise caching connector. If we would like to get
20
- # fresh values from the service, {#refresh} call will flush the cache and
21
- # retrieve fresh data from the remote.
22
- class Resource
23
- # NoODataId error is raised when operation would need OpenData id of the
24
- # resource to accomplish the task a hand.
25
- class NoODataId < StandardError; end
26
-
27
- # NoResource error is raised if the service cannot find requested
28
- # resource.
29
- class NoResource < StandardError; end
30
-
31
- # Timeout error is raised if the async request is not handled in due time.
32
- class Timeout < StandardError; end
33
-
34
- # Headers, returned from the service when resource has been constructed.
35
- #
36
- # @return [Hash] resource headers
37
- attr_reader :headers
38
-
39
- # Raw data that has been used to construct resource by either fetching it
40
- # from the remote API or by being passed-in as a parameter to constructor.
41
- #
42
- # @return [Hash] resource raw data
43
- attr_reader :raw
44
-
45
- # Create new resource.
46
- #
47
- # Resource can be created either by passing in OpenData identifier or
48
- # supplying the content (hash). In the first case, connector will be used
49
- # to fetch the resource data. In the second case, resource only wraps the
50
- # passed-in hash and does no fetching.
51
- #
52
- # @param connector [RedfishClient::Connector] connector that will be used
53
- # to fetch the resources
54
- # @param oid [String] OpenData id of the resource
55
- # @param raw [Hash] raw content to populate resource with
56
- # @raise [NoResource] resource cannot be retrieved from the service
57
- def initialize(connector, oid: nil, raw: nil)
58
- @connector = connector
59
- if oid
60
- initialize_from_service(oid)
61
- else
62
- @raw = raw
63
- end
64
- end
65
-
66
- # Wait for the potentially async operation to terminate
67
- #
68
- # Note that this can be safely called on response from non-async
69
- # operations where the function will return immediately and without making
70
- # any additional requests to the service.
71
- #
72
- # @param response [RedfishClient::Response] response
73
- # @param retries [Integer] number of retries
74
- # @param delay [Integer] number of seconds between retries
75
- # @return [RedfishClient::Response] final response
76
- # @raise [Timeout] if the operation did not terminate in time
77
- def wait(response, retries: 10, delay: 1)
78
- retries.times do |_i|
79
- return response if response.done?
80
-
81
- sleep(delay)
82
- response = get(path: response.monitor)
83
- end
84
- raise Timeout, "Async operation did not terminate in allotted time"
85
- end
86
-
87
- # Access resource content.
88
- #
89
- # This function offers a way of accessing resource data in the same way
90
- # that hash exposes its content.
91
- #
92
- # @param attr [String] key for accessing data
93
- # @return associated value or `nil` if attr is missing
94
- def [](attr)
95
- build_resource(raw[attr])
96
- end
97
-
98
- # Safely access nested resource content.
99
- #
100
- # This function is an equivalent of safe navigation operator that can be
101
- # used with arbitrary keys.
102
- #
103
- # Calling `res.dig("a", "b", "c")` is equivalent to `res.a&.b&.c` and
104
- # `res["a"] && res["a"]["b"] && res["a"]["b"]["c"]`.
105
- # @params keys [Array<Symbol, String>] sequence of keys to access
106
- # @return associated value or `nil` if any key is missing
107
- def dig(*keys)
108
- keys.reduce(self) { |a, k| a.nil? ? nil : a[k] }
109
- end
110
-
111
- # Test if resource contains required key.
112
- #
113
- # @param name [String, Symbol] key name to test
114
- # @return [Boolean] inclusion test result
115
- def key?(name)
116
- raw.key?(name.to_s)
117
- end
118
-
119
- # Convenience access for resource data.
120
- #
121
- # Calling `resource.Value` is exactly the same as `resource["Value"]`.
122
- def method_missing(symbol, *_args, &_block)
123
- self[symbol.to_s]
124
- end
125
-
126
- def respond_to_missing?(symbol, include_private = false)
127
- key?(symbol.to_s) || super
128
- end
129
-
130
- # Pretty-print the wrapped content.
131
- #
132
- # @return [String] JSON-serialized raw data
133
- def to_s
134
- JSON.pretty_generate(raw)
135
- end
136
-
137
- # Issue a requests to the selected endpoint.
138
- #
139
- # By default, request will be sent to the path, stored in `@odata.id`
140
- # field. Source field can be changed by specifying the `field` parameter
141
- # when calling this function. Specifying the `path` argument will bypass
142
- # the field lookup altogether and issue a request directly to the selected
143
- # path.
144
- #
145
- # If the resource has no lookup field, {NoODataId} error will be raised,
146
- # since posting to non-networked resources makes no sense and probably
147
- # indicates bug in library consumer.
148
- #
149
- # @param method [Symbol] HTTP method (:get, :post, :patch or :delete)
150
- # @param field [String, Symbol] path lookup field
151
- # @param path [String] path to post to
152
- # @return [RedfishClient::Response] response
153
- # @raise [NoODataId] resource has no OpenData id
154
- def request(method, field, path, payload = nil)
155
- @connector.request(method, get_path(field, path), payload)
156
- end
157
-
158
- # Issue a GET requests to the selected endpoint.
159
- #
160
- # By default, GET request will be sent to the path, stored in `@odata.id`
161
- # field. Source field can be changed by specifying the `field` parameter
162
- # when calling this function. Specifying the `path` argument will bypass
163
- # the field lookup altogether and issue a GET request directly to the
164
- # selected path.
165
- #
166
- # If the resource has no lookup field, {NoODataId} error will be raised,
167
- # since posting to non-networked resources makes no sense and probably
168
- # indicates bug in library consumer.
169
- #
170
- # @param field [String, Symbol] path lookup field
171
- # @param path [String] path to post to
172
- # @return [RedfishClient::Response] response
173
- # @raise [NoODataId] resource has no OpenData id
174
- def get(field: "@odata.id", path: nil)
175
- request(:get, field, path)
176
- end
177
-
178
- # Issue a POST requests to the selected endpoint.
179
- #
180
- # By default, POST request will be sent to the path, stored in `@odata.id`
181
- # field. Source field can be changed by specifying the `field` parameter
182
- # when calling this function. Specifying the `path` argument will bypass
183
- # the field lookup altogether and POST directly to the requested path.
184
- #
185
- # In order to avoid having to manually serialize data to JSON, this
186
- # function call takes Hash as a payload and encodes it before sending it
187
- # to the endpoint.
188
- #
189
- # If the resource has no lookup field, {NoODataId} error will be raised,
190
- # since posting to non-networked resources makes no sense and probably
191
- # indicates bug in library consumer.
192
- #
193
- # @param field [String, Symbol] path lookup field
194
- # @param path [String] path to post to
195
- # @param payload [Hash<String, >] data to send
196
- # @return [RedfishClient::Response] response
197
- # @raise [NoODataId] resource has no OpenData id
198
- def post(field: "@odata.id", path: nil, payload: nil)
199
- request(:post, field, path, payload)
200
- end
201
-
202
- # Issue a PATCH requests to the selected endpoint.
203
- #
204
- # Works exactly the same as the {post} method, but issued a PATCH request
205
- # to the server.
206
- #
207
- # @param field [String, Symbol] path lookup field
208
- # @param path [String] path to patch
209
- # @param payload [Hash<String, >] data to send
210
- # @return [RedfishClient::Response] response
211
- # @raise [NoODataId] resource has no OpenData id
212
- def patch(field: "@odata.id", path: nil, payload: nil)
213
- request(:patch, field, path, payload)
214
- end
215
-
216
- # Issue a DELETE requests to the endpoint of the resource.
217
- #
218
- # If the resource has no `@odata.id` field, {NoODataId} error will be
219
- # raised, since deleting non-networked resources makes no sense and
220
- # probably indicates bug in library consumer.
221
- #
222
- # @return [RedfishClient::Response] response
223
- # @raise [NoODataId] resource has no OpenData id
224
- def delete(field: "@odata.id", path: nil, payload: nil)
225
- request(:delete, field, path, payload)
226
- end
227
-
228
- # Refresh resource content from the API
229
- #
230
- # Caling this method will ensure that the resource data is in sync with
231
- # the Redfis API, invalidating any caches as necessary.
232
- def refresh
233
- return unless self["@odata.id"]
234
-
235
- # TODO(@tadeboro): raise more sensible exception if resource cannot be
236
- # refreshed.
237
- @connector.reset(self["@odata.id"])
238
- initialize_from_service(self["@odata.id"])
239
- end
240
-
241
- private
242
-
243
- def initialize_from_service(oid)
244
- url, fragment = oid.split("#", 2)
245
- resp = wait(get(path: url))
246
- raise NoResource unless [200, 201].include?(resp.status)
247
-
248
- @raw = get_fragment(JSON.parse(resp.body), fragment)
249
- @raw["@odata.id"] = oid
250
- @headers = resp.headers
251
- end
252
-
253
- def get_fragment(data, fragment)
254
- # data, /my/0/part -> data["my"][0]["part"]
255
- parse_fragment_string(fragment).reduce(data) do |acc, c|
256
- acc[acc.is_a?(Array) ? c.to_i : c]
257
- end
258
- end
259
-
260
- def parse_fragment_string(fragment)
261
- # /my/0/part -> ["my", "0", "part"]
262
- fragment ? fragment.split("/").reject { |i| i == "" } : []
263
- end
264
-
265
- def get_path(field, path)
266
- raise NoODataId if path.nil? && !key?(field)
267
- path || raw[field]
268
- end
269
-
270
- def build_resource(data)
271
- return nil if data.nil?
272
-
273
- case data
274
- when Hash then build_hash_resource(data)
275
- when Array then data.collect { |d| build_resource(d) }
276
- else data
277
- end
278
- end
279
-
280
- def build_hash_resource(data)
281
- if data.key?("@odata.id")
282
- Resource.new(@connector, oid: data["@odata.id"])
283
- else
284
- Resource.new(@connector, raw: data)
285
- end
286
- rescue NoResource
287
- nil
288
- end
289
- end
290
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "uri"
4
-
5
- module RedfishClient
6
- # Response struct.
7
- #
8
- # This struct is returned from the methods that interact with the remote API.
9
- class Response
10
- attr_reader :status
11
- attr_reader :headers
12
- attr_reader :body
13
-
14
- def initialize(status, headers, body)
15
- @status = status
16
- @headers = headers
17
- @body = body
18
- end
19
-
20
- def done?
21
- status != 202
22
- end
23
-
24
- def monitor
25
- return nil if done?
26
-
27
- uri = URI.parse(headers["location"])
28
- [uri.path, uri.query].compact.join("?")
29
- end
30
-
31
- def to_h
32
- { "status" => status, "headers" => headers, "body" => body }
33
- end
34
-
35
- def to_s
36
- "Response[status=#{status}, headers=#{headers}, body='#{body}']"
37
- end
38
-
39
- def self.from_hash(data)
40
- new(*data.values_at("status", "headers", "body"))
41
- end
42
- end
43
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "server_sent_events"
4
-
5
- require "redfish_client/event_listener"
6
- require "redfish_client/resource"
7
-
8
- module RedfishClient
9
- # Root resource represents toplevel entry point into Redfish service data.
10
- # Its main purpose is to provide authentication support for the API.
11
- class Root < Resource
12
- # Find Redfish service object by OData ID field.
13
- #
14
- # @param oid [String] Odata id of the resource
15
- # @return [Resource, nil] new resource or nil if resource cannot be found
16
- def find(oid)
17
- find!(oid)
18
- rescue NoResource
19
- nil
20
- end
21
-
22
- # Find Redfish service object by OData ID field.
23
- #
24
- # @param oid [String] Odata id of the resource
25
- # @return [Resource] new resource
26
- # @raise [NoResource] resource cannot be fetched
27
- def find!(oid)
28
- Resource.new(@connector, oid: oid)
29
- end
30
-
31
- # Return event listener.
32
- #
33
- # If the service does not support SSE, this function will return nil.
34
- #
35
- # @return [EventListener, nil] event listener
36
- def event_listener
37
- address = dig("EventService", "ServerSentEventUri")
38
- return nil if address.nil?
39
-
40
- EventListener.new(ServerSentEvents.create_client(address))
41
- end
42
-
43
- # Authenticate against the service.
44
- #
45
- # Calling this method will select the appropriate method of authentication
46
- # and try to login using provided credentials.
47
- #
48
- # @param username [String] username
49
- # @param password [String] password
50
- # @raise [RedfishClient::AuthenticatedConnector::AuthError] if user
51
- # session could not be authenticated
52
- def login(username, password)
53
- @connector.set_auth_info(
54
- username, password, auth_test_path, session_path
55
- )
56
- @connector.login
57
- end
58
-
59
- # Sign out of the service.
60
- def logout
61
- @connector.logout
62
- end
63
-
64
- private
65
-
66
- def session_path
67
- # We access raw values here on purpose, since calling dig on resource
68
- # instance would try to download the sessions collection, which would
69
- # fail since we are not yet logged in.
70
- raw.dig("Links", "Sessions", "@odata.id")
71
- end
72
-
73
- def auth_test_path
74
- raw.values.find { |v| v["@odata.id"] }["@odata.id"]
75
- end
76
- end
77
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RedfishClient
4
- VERSION = "0.5.4"
5
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "redfish_client/connector"
4
- require "redfish_client/nil_hash"
5
- require "redfish_client/root"
6
- require "redfish_client/version"
7
-
8
- module RedfishClient
9
- # Create new Redfish API client.
10
- #
11
- # @param url [String] base URL of Redfish API
12
- # @param prefix [String] Redfish API prefix
13
- # @param verify [Boolean] verify certificates for https connections
14
- # @param use_cache [Boolean] cache API responses
15
- def self.new(url, prefix: "/redfish/v1", verify: true, use_cache: true)
16
- cache = (use_cache ? Hash : NilHash).new
17
- con = Connector.new(url, verify: verify, cache: cache)
18
- Root.new(con, oid: prefix)
19
- end
20
- end
@@ -1,42 +0,0 @@
1
-
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "redfish_client/version"
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "redfish_client"
8
- spec.version = RedfishClient::VERSION
9
- spec.authors = ["Tadej Borovšak"]
10
- spec.email = ["tadej.borovsak@xlab.si"]
11
-
12
- spec.summary = "Simple Redfish client library"
13
- spec.homepage = "https://github.com/xlab-steampunk/redfish-client-ruby"
14
- spec.license = "Apache-2.0"
15
-
16
- if spec.respond_to?(:metadata)
17
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
- else
19
- raise "RubyGems 2.0 or newer is required to protect against " \
20
- "public gem pushes."
21
- end
22
-
23
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
- f.match(%r{^(test|spec|features)/})
25
- end
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
29
-
30
- spec.required_ruby_version = ">= 2.1"
31
-
32
- spec.add_runtime_dependency "excon", "~> 0.71"
33
- spec.add_runtime_dependency "server_sent_events", "~> 0.1"
34
-
35
- spec.add_development_dependency "rake", ">= 11.0"
36
- spec.add_development_dependency "rspec", ">= 3.7"
37
- spec.add_development_dependency "simplecov"
38
- spec.add_development_dependency "webmock", "~> 3.4"
39
- spec.add_development_dependency "yard"
40
- spec.add_development_dependency "rubocop", "~> 0.54.0"
41
- spec.add_development_dependency "pry"
42
- end