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