chef-licensing 0.4.44 → 1.0.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 +4 -4
- data/chef-licensing.gemspec +6 -3
- data/lib/chef-licensing/config.rb +9 -1
- data/lib/chef-licensing/context.rb +36 -1
- data/lib/chef-licensing/exceptions/invalid_file_format_version.rb +10 -0
- data/lib/chef-licensing/exceptions/license_file_corrupted.rb +9 -0
- data/lib/chef-licensing/exceptions/unsupported_content_type.rb +9 -0
- data/lib/chef-licensing/license.rb +5 -1
- data/lib/chef-licensing/license_key_fetcher/chef_licensing_interactions.yaml +85 -234
- data/lib/chef-licensing/license_key_fetcher/file.rb +71 -14
- data/lib/chef-licensing/license_key_fetcher/license_file/base.rb +63 -0
- data/lib/chef-licensing/license_key_fetcher/license_file/v3.rb +12 -0
- data/lib/chef-licensing/license_key_fetcher/license_file/v4.rb +27 -0
- data/lib/chef-licensing/license_key_fetcher.rb +45 -20
- data/lib/chef-licensing/list_license_keys.rb +9 -3
- data/lib/chef-licensing/restful_client/base.rb +62 -26
- data/lib/chef-licensing/restful_client/middleware/content_type_validator.rb +24 -0
- data/lib/chef-licensing/restful_client/middleware/exceptions_handler.rb +1 -3
- data/lib/chef-licensing/restful_client/v1.rb +0 -2
- data/lib/chef-licensing/tui_engine/tui_actions.rb +11 -88
- data/lib/chef-licensing/version.rb +1 -1
- data/lib/chef-licensing.rb +5 -0
- metadata +16 -13
- data/LICENSE +0 -1
- data/lib/chef-licensing/exceptions/license_generation_failed.rb +0 -9
- data/lib/chef-licensing/exceptions/license_generation_rejected.rb +0 -7
- data/lib/chef-licensing/license_key_generator.rb +0 -47
@@ -5,6 +5,10 @@ require "date"
|
|
5
5
|
require "fileutils" unless defined?(FileUtils)
|
6
6
|
require_relative "../license_key_fetcher"
|
7
7
|
require_relative "../config"
|
8
|
+
require_relative "../exceptions/license_file_corrupted"
|
9
|
+
require_relative "license_file/v4"
|
10
|
+
require_relative "license_file/v3"
|
11
|
+
require_relative "../exceptions/invalid_file_format_version"
|
8
12
|
|
9
13
|
module ChefLicensing
|
10
14
|
class LicenseKeyFetcher
|
@@ -63,6 +67,15 @@ module ChefLicensing
|
|
63
67
|
@active_trial_status
|
64
68
|
end
|
65
69
|
|
70
|
+
def user_has_active_trial_or_free_license?
|
71
|
+
read_license_key_file
|
72
|
+
return false unless contents&.key?(:licenses)
|
73
|
+
|
74
|
+
all_license_keys = contents[:licenses].collect { |license| license[:license_key] }
|
75
|
+
license_obj = ChefLicensing.client(license_keys: all_license_keys)
|
76
|
+
(%w{trial free}.include? license_obj.license_type&.downcase) && license_obj.active?
|
77
|
+
end
|
78
|
+
|
66
79
|
def fetch_allowed_license_types_for_addition
|
67
80
|
license_types = %i{free trial commercial}
|
68
81
|
existing_license_types = fetch_license_types
|
@@ -118,15 +131,15 @@ module ChefLicensing
|
|
118
131
|
|
119
132
|
@contents = load_license_file(license_key_file_path)
|
120
133
|
|
134
|
+
# Two possible cases:
|
135
|
+
# 1. If contents is nil, load basic license data with the latest structure.
|
136
|
+
# 2. If contents is not nil, but the license server URL in contents is different from the system's,
|
137
|
+
# update the license server URL in contents and licenses.yaml file.
|
121
138
|
if @contents.nil?
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
else
|
127
|
-
if license_server_url_from_system && license_server_url_from_system != @contents[:license_server_url]
|
128
|
-
@contents[:license_server_url] = license_server_url_from_system
|
129
|
-
end
|
139
|
+
url = license_server_url_from_system || license_server_url_from_config
|
140
|
+
load_basic_license_data_to_contents(url, [])
|
141
|
+
elsif @contents && license_server_url_from_system && license_server_url_from_system != @contents[:license_server_url]
|
142
|
+
@contents[:license_server_url] = license_server_url_from_system
|
130
143
|
end
|
131
144
|
|
132
145
|
# Ensure the license server URL is returned to the caller in all cases
|
@@ -207,11 +220,27 @@ module ChefLicensing
|
|
207
220
|
|
208
221
|
# only checking for major version for file format for breaking changes
|
209
222
|
@contents ||= YAML.load(::File.read(path))
|
223
|
+
|
224
|
+
# raise error if the file_format_version key is missing
|
225
|
+
raise LicenseFileCorrupted.new("Unrecognized license file; :file_format_version missing.") unless @contents.key?(:file_format_version)
|
226
|
+
|
227
|
+
# Three possible cases after loading the license file contents:
|
228
|
+
# 1. If the file format version is the same as the current version (latest), verify the structure and return the contents.
|
229
|
+
# 2. If the file format version is different but supported, migrate the contents to the current version and return them.
|
230
|
+
# 3. If the file format version is different and not supported, raise an error.
|
210
231
|
if major_version(@contents[:file_format_version]) == major_version(LICENSE_FILE_FORMAT_VERSION)
|
232
|
+
current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
|
233
|
+
# we ignore any additional keys in the license file during verification
|
234
|
+
raise LicenseFileCorrupted.new("Invalid data found in the license file.") unless current_version_class_name.send(:verify_structure, @contents)
|
235
|
+
|
236
|
+
@contents
|
237
|
+
elsif license_file_class_exists?(@contents[:file_format_version])
|
238
|
+
@contents = migrate_license_file_content_to_current_version(@contents)
|
239
|
+
write_license_file(path) # update the license file contents to the latest version
|
211
240
|
@contents
|
212
241
|
else
|
213
242
|
logger.debug "License File version #{@contents[:file_format_version]} not supported."
|
214
|
-
raise
|
243
|
+
raise ChefLicensing::InvalidFileFormatVersion.new("Unable to read licenses. License File version #{@contents[:file_format_version]} not supported.")
|
215
244
|
end
|
216
245
|
end
|
217
246
|
|
@@ -244,11 +273,7 @@ module ChefLicensing
|
|
244
273
|
|
245
274
|
logger.debug "Loading license data to contents"
|
246
275
|
if @contents.nil? || @contents.empty? # this case is likely to happen only during testing
|
247
|
-
@
|
248
|
-
file_format_version: LICENSE_FILE_FORMAT_VERSION,
|
249
|
-
license_server_url: @license_server_url,
|
250
|
-
licenses: [license_data],
|
251
|
-
}
|
276
|
+
load_basic_license_data_to_contents(@license_server_url, [license_data])
|
252
277
|
elsif @contents[:licenses].nil?
|
253
278
|
@contents[:licenses] = [license_data]
|
254
279
|
elsif fetch_license_keys(@contents[:licenses])&.include?(license_data[:license_key])
|
@@ -270,6 +295,38 @@ module ChefLicensing
|
|
270
295
|
logger.debug "#{e.backtrace.join("\n\t")}"
|
271
296
|
e
|
272
297
|
end
|
298
|
+
|
299
|
+
# Returns the license file class for the given version.
|
300
|
+
def get_license_file_class(version)
|
301
|
+
Object.const_get("ChefLicensing::LicenseFile::V#{major_version(version)}")
|
302
|
+
end
|
303
|
+
|
304
|
+
# Returns true if the license file class for the given version exists.
|
305
|
+
def license_file_class_exists?(version)
|
306
|
+
Object.const_defined?("ChefLicensing::LicenseFile::V#{major_version(version)}")
|
307
|
+
end
|
308
|
+
|
309
|
+
# Loads the basic license data to contents in the current version's structure.
|
310
|
+
def load_basic_license_data_to_contents(url, license_data = [])
|
311
|
+
current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
|
312
|
+
@contents = current_version_class_name.send(:load_primary_structure)
|
313
|
+
@contents[:file_format_version] = LICENSE_FILE_FORMAT_VERSION
|
314
|
+
@contents[:license_server_url] = url || ""
|
315
|
+
@contents[:licenses] = license_data
|
316
|
+
end
|
317
|
+
|
318
|
+
# Migrates the license file content to the current version and returns the migrated contents.
|
319
|
+
def migrate_license_file_content_to_current_version(contents)
|
320
|
+
logger.warn "License File version #{contents[:file_format_version]} is deprecated."
|
321
|
+
logger.warn "Automatically migrating license file to version #{LICENSE_FILE_FORMAT_VERSION}."
|
322
|
+
given_version_class_name = get_license_file_class(contents[:file_format_version])
|
323
|
+
# we ignore any additional keys in the license file during verification
|
324
|
+
raise LicenseFileCorrupted.new("Invalid data found in the license file.") unless given_version_class_name.send(:verify_structure, contents)
|
325
|
+
|
326
|
+
current_version_class_name = get_license_file_class(LICENSE_FILE_FORMAT_VERSION)
|
327
|
+
contents = current_version_class_name.send(:migrate_structure, contents, major_version(contents[:file_format_version]))
|
328
|
+
contents
|
329
|
+
end
|
273
330
|
end
|
274
331
|
end
|
275
332
|
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module ChefLicensing
|
2
|
+
module LicenseFile
|
3
|
+
class Base
|
4
|
+
EXPECTED_STRUCTURE = {
|
5
|
+
file_format_version: "0.0.0",
|
6
|
+
licenses: [
|
7
|
+
{
|
8
|
+
license_key: String,
|
9
|
+
license_type: Symbol,
|
10
|
+
update_time: String,
|
11
|
+
},
|
12
|
+
],
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
# @param [Hash] data: The data to verify
|
16
|
+
# @param [Hash] expected_structure: The structure to verify against
|
17
|
+
# @return [Boolean] true if the data matches the expected structure, false otherwise
|
18
|
+
# @note This method ignores extra keys in the data that are not in the expected structure
|
19
|
+
def self.verify_structure(data, expected_structure = self::EXPECTED_STRUCTURE)
|
20
|
+
return false unless data.is_a?(Hash)
|
21
|
+
|
22
|
+
expected_structure.each do |key, value|
|
23
|
+
return false unless data.key?(key)
|
24
|
+
|
25
|
+
if value.is_a?(Hash)
|
26
|
+
return false unless verify_structure(data[key], value)
|
27
|
+
elsif value.is_a?(Array)
|
28
|
+
return false unless data[key].is_a?(Array)
|
29
|
+
|
30
|
+
data[key].each do |item|
|
31
|
+
return false unless verify_structure(item, value[0])
|
32
|
+
end
|
33
|
+
elsif value.is_a?(Class)
|
34
|
+
return false unless data[key].is_a?(value)
|
35
|
+
else
|
36
|
+
return false unless data[key] == value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Hash] The primary structure of the license file, without nested structures
|
44
|
+
def self.load_primary_structure
|
45
|
+
expected_structure_dup = self::EXPECTED_STRUCTURE.dup
|
46
|
+
expected_structure_dup[:licenses] = []
|
47
|
+
expected_structure_dup
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Hash] The complete structure of the license file, including nested structures
|
51
|
+
def self.load_structure
|
52
|
+
self::EXPECTED_STRUCTURE
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Hash] contents: The contents of the license file
|
56
|
+
# @param [Integer] version: The version of the license file
|
57
|
+
# @return [Hash] The contents of the license file after migration
|
58
|
+
def self.migrate_structure(contents, version)
|
59
|
+
raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
|
3
|
+
module ChefLicensing
|
4
|
+
module LicenseFile
|
5
|
+
class V3 < Base
|
6
|
+
LICENSE_FILE_FORMAT_VERSION = "3.0.0".freeze
|
7
|
+
EXPECTED_STRUCTURE = EXPECTED_STRUCTURE.merge({
|
8
|
+
file_format_version: V3::LICENSE_FILE_FORMAT_VERSION,
|
9
|
+
}).freeze
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "base"
|
2
|
+
require_relative "../../config"
|
3
|
+
|
4
|
+
module ChefLicensing
|
5
|
+
module LicenseFile
|
6
|
+
class V4 < Base
|
7
|
+
LICENSE_FILE_FORMAT_VERSION = "4.0.0".freeze
|
8
|
+
|
9
|
+
EXPECTED_STRUCTURE = EXPECTED_STRUCTURE.merge({
|
10
|
+
file_format_version: V4::LICENSE_FILE_FORMAT_VERSION,
|
11
|
+
license_server_url: String,
|
12
|
+
}).freeze
|
13
|
+
|
14
|
+
# @param [Hash] contents: The contents of the license file
|
15
|
+
# @param [Integer] version: The version of the license file
|
16
|
+
# @return [Hash] The contents of the license file after migration
|
17
|
+
def self.migrate_structure(contents, version)
|
18
|
+
# Backwards compatibility for version 3 license files
|
19
|
+
if version == 3
|
20
|
+
contents[:license_server_url] = ChefLicensing::Config.license_server_url || ""
|
21
|
+
contents[:file_format_version] = V4::LICENSE_FILE_FORMAT_VERSION
|
22
|
+
end
|
23
|
+
contents
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -71,7 +71,9 @@ module ChefLicensing
|
|
71
71
|
|
72
72
|
unless @license_keys.empty?
|
73
73
|
# Licenses expiration check
|
74
|
-
|
74
|
+
# Client API possible errors will be handled in software entitlement check call (made after this)
|
75
|
+
# client_api_call_error is set to true when there is an error in licenses_active? call
|
76
|
+
if licenses_active? || client_api_call_error
|
75
77
|
return @license_keys
|
76
78
|
else
|
77
79
|
# Prompts if the keys are expired or expiring
|
@@ -83,9 +85,9 @@ module ChefLicensing
|
|
83
85
|
end
|
84
86
|
end
|
85
87
|
|
86
|
-
#
|
87
|
-
|
88
|
-
|
88
|
+
# Expired trial licenses and exhausted free licenses will be blocked
|
89
|
+
# Not blocking commercial licenses
|
90
|
+
if license && ((!license.expired? && !license.exhausted?) || (license.license_type.downcase == "commercial"))
|
89
91
|
return @license_keys
|
90
92
|
end
|
91
93
|
|
@@ -116,7 +118,8 @@ module ChefLicensing
|
|
116
118
|
# Return keys if license keys are active and not expired or expiring
|
117
119
|
# Return keys if there is any error in /client API call, and do not block the flow.
|
118
120
|
# Client API possible errors will be handled in software entitlement check call (made after this)
|
119
|
-
|
121
|
+
# client_api_call_error is set to true when there is an error in licenses_active? call
|
122
|
+
return @license_keys if (!@license_keys.empty? && licenses_active? && ChefLicensing::Context.license.license_type.downcase == "commercial") || client_api_call_error
|
120
123
|
|
121
124
|
# Lowest priority is to interactively prompt if we have a TTY
|
122
125
|
if config[:output].isatty
|
@@ -128,13 +131,19 @@ module ChefLicensing
|
|
128
131
|
# If license type is not selected using TUI, assign it using API call to fetch type.
|
129
132
|
prompt_fetcher.license_type ||= get_license_type(new_keys.first)
|
130
133
|
persist_and_concat(new_keys, prompt_fetcher.license_type)
|
131
|
-
|
134
|
+
license ||= ChefLicensing::Context.license
|
135
|
+
# Expired trial licenses and exhausted free licenses will be blocked
|
136
|
+
# Not blocking commercial licenses
|
137
|
+
if (!license&.expired? && !license&.exhausted?) || (license&.license_type&.downcase == "commercial")
|
138
|
+
return license_keys
|
139
|
+
end
|
132
140
|
end
|
133
141
|
end
|
134
142
|
|
135
|
-
#
|
136
|
-
|
137
|
-
|
143
|
+
# Expired trial licenses and exhausted free licenses will be blocked
|
144
|
+
# Not blocking commercial licenses
|
145
|
+
license ||= ChefLicensing::Context.license
|
146
|
+
if new_keys.empty? && license && ((!license.expired? && !license.exhausted?) || (license.license_type.downcase == "commercial"))
|
138
147
|
return @license_keys
|
139
148
|
end
|
140
149
|
|
@@ -195,6 +204,7 @@ module ChefLicensing
|
|
195
204
|
extra_info[:license_type] = license.license_type.capitalize
|
196
205
|
extra_info[:number_of_days_in_expiration] = license.number_of_days_in_expiration
|
197
206
|
extra_info[:license_expiration_date] = Date.parse(license.expiration_date).strftime("%a, %d %b %Y")
|
207
|
+
extra_info[:is_commercial] = license.license_type.downcase == "commercial"
|
198
208
|
end
|
199
209
|
|
200
210
|
unless info.empty? # ability to add info hash through arguments
|
@@ -212,6 +222,9 @@ module ChefLicensing
|
|
212
222
|
# This call returns a license based on client logic
|
213
223
|
# This API call is only made when multiple license keys are present or if client call was never done
|
214
224
|
self.license = ChefLicensing.client(license_keys: @license_keys) if !license || @license_keys.count > 1
|
225
|
+
|
226
|
+
# Cache license context
|
227
|
+
ChefLicensing::Context.license = license
|
215
228
|
# Intentional lag of 2 seconds when license is expiring or expired
|
216
229
|
sleep 2 if license.expiring_or_expired?
|
217
230
|
spinner.success # Stop the spinner
|
@@ -227,7 +240,13 @@ module ChefLicensing
|
|
227
240
|
config[:start_interaction] = :prompt_license_about_to_expire
|
228
241
|
prompt_fetcher.config = config
|
229
242
|
false
|
243
|
+
elsif license.exhausted? && (license.license_type.downcase == "commercial" || license.license_type.downcase == "free")
|
244
|
+
config[:start_interaction] = :prompt_license_exhausted
|
245
|
+
prompt_fetcher.config = config
|
246
|
+
false
|
230
247
|
else
|
248
|
+
# If license is not expired or expiring, return true. But if the license is not commercial, warn the user.
|
249
|
+
config[:start_interaction] = :warn_non_commercial_license unless license.license_type.downcase == "commercial"
|
231
250
|
true
|
232
251
|
end
|
233
252
|
rescue ChefLicensing::ClientError => e
|
@@ -271,8 +290,8 @@ module ChefLicensing
|
|
271
290
|
end
|
272
291
|
|
273
292
|
def get_license_type(license_key)
|
274
|
-
|
275
|
-
|
293
|
+
license_obj = ChefLicensing.client(license_keys: [license_key])
|
294
|
+
license_obj.license_type.downcase.to_sym
|
276
295
|
end
|
277
296
|
|
278
297
|
def license_restricted?(license_type)
|
@@ -283,7 +302,7 @@ module ChefLicensing
|
|
283
302
|
def prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
|
284
303
|
logger.debug "License Key fetcher - prompting license addition restriction"
|
285
304
|
# For trial license
|
286
|
-
# TODO for
|
305
|
+
# TODO for Free Tier License
|
287
306
|
config[:start_interaction] = :prompt_license_addition_restriction
|
288
307
|
prompt_fetcher.config = config
|
289
308
|
# Existing license keys are needed to show details of existing license of license type which is restricted.
|
@@ -294,23 +313,29 @@ module ChefLicensing
|
|
294
313
|
def unrestricted_license_added?(new_keys, license_type)
|
295
314
|
if license_restricted?(license_type)
|
296
315
|
# Existing license keys of same license type are fetched to compare if old license key or a new one is added.
|
297
|
-
# However, if user is trying to add
|
316
|
+
# However, if user is trying to add Free Tier License, and user has active trial license, we fetch the trial license key
|
298
317
|
if license_type == :free && file_fetcher.user_has_active_trial_license?
|
299
318
|
existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(:trial)
|
300
|
-
|
319
|
+
elsif file_fetcher.user_has_active_trial_or_free_license?
|
320
|
+
# Handling license addition restriction scenarios only if the current license is an active license
|
301
321
|
existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(license_type)
|
302
322
|
end
|
323
|
+
|
303
324
|
# Only prompt when a new trial license is added
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
325
|
+
if existing_license_keys_in_file
|
326
|
+
unless existing_license_keys_in_file.last == new_keys.first
|
327
|
+
# prompt the message that this addition of license is restricted.
|
328
|
+
prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
|
329
|
+
return false
|
330
|
+
end
|
308
331
|
end
|
309
|
-
|
332
|
+
# license addition should be restricted but it is not because the key is same as that of one user is trying to add
|
333
|
+
# license addition should be restricted but it is not because the license is expired and warning wont be handled by this restriction
|
334
|
+
true
|
310
335
|
else
|
311
336
|
persist_and_concat(new_keys, license_type)
|
312
337
|
true
|
313
338
|
end
|
314
339
|
end
|
315
340
|
end
|
316
|
-
end
|
341
|
+
end
|
@@ -28,8 +28,11 @@ module ChefLicensing
|
|
28
28
|
|
29
29
|
licenses_metadata.each do |license|
|
30
30
|
puts_bold "License Key : #{license.id}"
|
31
|
+
# Note: The license type is returned as "free" for Free Tier Licenses from the server.
|
32
|
+
# This is capitalized to "Free Tier" for display purposes as recommended by the product team.
|
33
|
+
license_type = license.license_type == "free" ? "Free Tier" : license.license_type.capitalize
|
31
34
|
output.puts <<~LICENSE
|
32
|
-
Type : #{
|
35
|
+
Type : #{license_type}
|
33
36
|
Status : #{license.status}
|
34
37
|
Expiration Date : #{license.expiration_date}
|
35
38
|
|
@@ -57,7 +60,10 @@ module ChefLicensing
|
|
57
60
|
def display_overview
|
58
61
|
output.puts "------------------------------------------------------------"
|
59
62
|
licenses_metadata.each do |license|
|
60
|
-
#
|
63
|
+
# Note: The license type is returned as "free" for Free Tier Licenses from the server.
|
64
|
+
# This is capitalized to "Free Tier" for display purposes as recommended by the product team.
|
65
|
+
license_type = license.license_type == "free" ? "Free Tier" : license.license_type.capitalize
|
66
|
+
# Sets the validity text for a Free Tier License as "Unlimited" and displays the number of days for others.
|
61
67
|
validity = if license.license_type == "free"
|
62
68
|
"Unlimited"
|
63
69
|
else
|
@@ -72,7 +78,7 @@ module ChefLicensing
|
|
72
78
|
#{pastel.bold("License Details")}
|
73
79
|
Asset Name : #{license.limits.first.software}
|
74
80
|
License ID : #{license.id}
|
75
|
-
Type : #{
|
81
|
+
Type : #{license_type}
|
76
82
|
Status : #{license.status.capitalize}
|
77
83
|
Validity : #{validity}
|
78
84
|
No. Of Units : #{num_of_units} #{unit_measure.capitalize.pluralize(num_of_units)}
|
@@ -7,6 +7,7 @@ require_relative "../exceptions/restful_client_connection_error"
|
|
7
7
|
require_relative "../exceptions/missing_api_credentials_error"
|
8
8
|
require_relative "../config"
|
9
9
|
require_relative "middleware/exceptions_handler"
|
10
|
+
require_relative "middleware/content_type_validator"
|
10
11
|
|
11
12
|
module ChefLicensing
|
12
13
|
module RestfulClient
|
@@ -22,6 +23,7 @@ module ChefLicensing
|
|
22
23
|
}.freeze
|
23
24
|
|
24
25
|
CURRENT_ENDPOINT_VERSION = 2
|
26
|
+
REQUEST_LIMIT = 5
|
25
27
|
|
26
28
|
def initialize
|
27
29
|
raise MissingAPICredentialsError, "Missing credential in config: Set in block chef_license_server or use environment variable CHEF_LICENSE_SERVER or pass through argument --chef-license-server" if ChefLicensing::Config.license_server_url.nil?
|
@@ -33,14 +35,6 @@ module ChefLicensing
|
|
33
35
|
invoke_get_api(self.class::END_POINTS[:VALIDATE], { licenseId: license, version: CURRENT_ENDPOINT_VERSION })
|
34
36
|
end
|
35
37
|
|
36
|
-
def generate_trial_license(payload)
|
37
|
-
invoke_post_api(self.class::END_POINTS[:GENERATE_TRIAL_LICENSE], payload)
|
38
|
-
end
|
39
|
-
|
40
|
-
def generate_free_license(payload)
|
41
|
-
invoke_post_api(self.class::END_POINTS[:GENERATE_FREE_LICENSE], payload)
|
42
|
-
end
|
43
|
-
|
44
38
|
def feature_by_name(payload)
|
45
39
|
invoke_post_api(self.class::END_POINTS[:FEATURE_BY_NAME], payload)
|
46
40
|
end
|
@@ -75,53 +69,82 @@ module ChefLicensing
|
|
75
69
|
|
76
70
|
# a common method to handle the get API calls
|
77
71
|
def invoke_get_api(endpoint, params = {})
|
78
|
-
|
79
|
-
|
80
|
-
end
|
72
|
+
response = invoke_api(ChefLicensing::Config.license_server_url.split(","), endpoint, :get, nil, params)
|
73
|
+
response.body
|
81
74
|
end
|
82
75
|
|
83
76
|
# a common method to handle the post API calls
|
84
77
|
def invoke_post_api(endpoint, payload, headers = {})
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
78
|
+
response = invoke_api(ChefLicensing::Config.license_server_url.split(","), endpoint, :post, payload, nil, headers)
|
79
|
+
raise RestfulClientError, format_error_from(response) unless response.success?
|
80
|
+
|
81
|
+
response.body
|
82
|
+
end
|
83
|
+
|
84
|
+
def invoke_api(urls, endpoint, http_method, payload = nil, params = {}, headers = {})
|
85
|
+
handle_connection = http_method == :get ? method(:handle_get_connection) : method(:handle_post_connection)
|
86
|
+
response = nil
|
87
|
+
attempted_urls = []
|
88
|
+
|
89
|
+
logger.warn "Only the first #{REQUEST_LIMIT} urls will be tried." if urls.size > REQUEST_LIMIT
|
90
|
+
urls.each_with_index do |url, i|
|
91
|
+
url = url.strip
|
92
|
+
attempted_urls << url
|
93
|
+
break if i == REQUEST_LIMIT - 1
|
94
|
+
|
95
|
+
logger.debug "Trying to connect to #{url}"
|
96
|
+
handle_connection.call(url) do |connection|
|
97
|
+
response = connection.send(http_method, endpoint) do |request|
|
98
|
+
request.body = payload.to_json if payload
|
99
|
+
request.params = params if params
|
100
|
+
request.headers = headers if headers
|
101
|
+
end
|
89
102
|
end
|
90
|
-
|
91
|
-
|
92
|
-
|
103
|
+
# At this point, we have a successful connection
|
104
|
+
# Update the value of license server url in config
|
105
|
+
ChefLicensing::Config.license_server_url = url
|
106
|
+
logger.debug "Connection succeeded to #{url}"
|
107
|
+
break response
|
108
|
+
rescue RestfulClientConnectionError
|
109
|
+
logger.warn "Connection failed to #{url}"
|
110
|
+
rescue URI::InvalidURIError
|
111
|
+
logger.warn "Invalid URI #{url}"
|
93
112
|
end
|
113
|
+
|
114
|
+
raise_restful_client_conn_error(attempted_urls) if response.nil?
|
115
|
+
response
|
94
116
|
end
|
95
117
|
|
96
|
-
def handle_get_connection
|
118
|
+
def handle_get_connection(url = nil)
|
97
119
|
# handle faraday errors
|
98
|
-
yield get_connection
|
120
|
+
yield get_connection(url)
|
99
121
|
rescue Faraday::ClientError => e
|
100
122
|
logger.debug "Restful Client Error #{e.message}"
|
101
123
|
raise RestfulClientError, e.message
|
102
124
|
end
|
103
125
|
|
104
|
-
def handle_post_connection
|
126
|
+
def handle_post_connection(url = nil)
|
105
127
|
# handle faraday errors
|
106
|
-
yield post_connection
|
128
|
+
yield post_connection(url)
|
107
129
|
rescue Faraday::ClientError => e
|
108
130
|
logger.debug "Restful Client Error #{e.message}"
|
109
131
|
raise RestfulClientError, e.message
|
110
132
|
end
|
111
133
|
|
112
|
-
def get_connection
|
134
|
+
def get_connection(url = nil)
|
113
135
|
store = ::ActiveSupport::Cache.lookup_store(:file_store, Dir.tmpdir)
|
114
|
-
Faraday.new(url:
|
136
|
+
Faraday.new(url: url) do |config|
|
115
137
|
config.request :json
|
116
138
|
config.response :json, parser_options: { object_class: OpenStruct }
|
117
139
|
config.use Faraday::HttpCache, shared_cache: false, logger: logger, store: store
|
118
140
|
config.use Middleware::ExceptionsHandler
|
141
|
+
config.use Middleware::ContentTypeValidator
|
119
142
|
config.adapter Faraday.default_adapter
|
120
143
|
end
|
121
144
|
end
|
122
145
|
|
123
|
-
def post_connection
|
124
|
-
Faraday.new(url:
|
146
|
+
def post_connection(url = nil)
|
147
|
+
Faraday.new(url: url) do |config|
|
125
148
|
config.request :json
|
126
149
|
config.response :json, parser_options: { object_class: OpenStruct }
|
127
150
|
config.use Middleware::ExceptionsHandler
|
@@ -134,6 +157,19 @@ module ChefLicensing
|
|
134
157
|
|
135
158
|
error_details
|
136
159
|
end
|
160
|
+
|
161
|
+
def raise_restful_client_conn_error(urls)
|
162
|
+
error_message = <<~EOM
|
163
|
+
Unable to connect to the licensing server. #{ChefLicensing::Config.chef_product_name} requires server communication to operate.
|
164
|
+
The following URL(s) were tried:\n#{
|
165
|
+
urls.each_with_index.map do |url, index|
|
166
|
+
"#{index + 1}. #{url}"
|
167
|
+
end.join("\n")
|
168
|
+
}
|
169
|
+
EOM
|
170
|
+
|
171
|
+
raise RestfulClientConnectionError, error_message
|
172
|
+
end
|
137
173
|
end
|
138
174
|
end
|
139
175
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "faraday/middleware"
|
2
|
+
require_relative "../../../chef-licensing/exceptions/unsupported_content_type"
|
3
|
+
|
4
|
+
module Middleware
|
5
|
+
class ContentTypeValidator < Faraday::Middleware
|
6
|
+
def call(env)
|
7
|
+
@app.call(env).on_complete do |response_env|
|
8
|
+
content_type = response_env[:response_headers]["content-type"]
|
9
|
+
body = response_env[:body]
|
10
|
+
# trim the body to 1000 characters to avoid printing a huge string in the error message
|
11
|
+
body = body[0..1000] if body.is_a?(String)
|
12
|
+
raise ChefLicensing::UnsupportedContentType, error_message(content_type, body) unless content_type == "application/json"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def error_message(content_type, body = nil)
|
17
|
+
<<~EOM
|
18
|
+
Expected 'application/json' content-type, but received '#{content_type}' from the licensing server.
|
19
|
+
Snippet of body: `#{body}`
|
20
|
+
Possible causes: Check for firewall restrictions, ensure proper server response, or seek support assistance.
|
21
|
+
EOM
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -8,9 +8,7 @@ module Middleware
|
|
8
8
|
def call(env)
|
9
9
|
@app.call(env)
|
10
10
|
rescue Faraday::ConnectionFailed => e
|
11
|
-
|
12
|
-
error_message = "Unable to connect to the licensing server at #{ChefLicensing::Config.license_server_url}.\nPlease check if the server is reachable and try again. #{ChefLicensing::Config.chef_product_name} requires server communication to operate."
|
13
|
-
raise ChefLicensing::RestfulClientConnectionError, error_message
|
11
|
+
raise ChefLicensing::RestfulClientConnectionError, e.message
|
14
12
|
end
|
15
13
|
end
|
16
14
|
end
|