chef-licensing 0.4.43
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 +7 -0
- data/LICENSE +1 -0
- data/chef-licensing.gemspec +35 -0
- data/lib/chef-licensing/api/client.rb +39 -0
- data/lib/chef-licensing/api/describe.rb +62 -0
- data/lib/chef-licensing/api/license_feature_entitlement.rb +55 -0
- data/lib/chef-licensing/api/license_software_entitlement.rb +53 -0
- data/lib/chef-licensing/api/list_licenses.rb +30 -0
- data/lib/chef-licensing/api/parser/client.rb +100 -0
- data/lib/chef-licensing/api/parser/describe.rb +118 -0
- data/lib/chef-licensing/cli_flags/mixlib_cli.rb +28 -0
- data/lib/chef-licensing/cli_flags/thor.rb +21 -0
- data/lib/chef-licensing/config.rb +44 -0
- data/lib/chef-licensing/config_fetcher/arg_fetcher.rb +38 -0
- data/lib/chef-licensing/config_fetcher/env_fetcher.rb +21 -0
- data/lib/chef-licensing/context.rb +98 -0
- data/lib/chef-licensing/exceptions/client_error.rb +9 -0
- data/lib/chef-licensing/exceptions/describe_error.rb +9 -0
- data/lib/chef-licensing/exceptions/error.rb +4 -0
- data/lib/chef-licensing/exceptions/feature_not_entitled.rb +9 -0
- data/lib/chef-licensing/exceptions/invalid_license.rb +10 -0
- data/lib/chef-licensing/exceptions/license_generation_failed.rb +9 -0
- data/lib/chef-licensing/exceptions/license_generation_rejected.rb +7 -0
- data/lib/chef-licensing/exceptions/list_licenses_error.rb +12 -0
- data/lib/chef-licensing/exceptions/missing_api_credentials_error.rb +7 -0
- data/lib/chef-licensing/exceptions/restful_client_connection_error.rb +9 -0
- data/lib/chef-licensing/exceptions/restful_client_error.rb +9 -0
- data/lib/chef-licensing/exceptions/software_not_entitled.rb +9 -0
- data/lib/chef-licensing/license.rb +151 -0
- data/lib/chef-licensing/license_key_fetcher/base.rb +28 -0
- data/lib/chef-licensing/license_key_fetcher/chef_licensing_interactions.yaml +534 -0
- data/lib/chef-licensing/license_key_fetcher/file.rb +275 -0
- data/lib/chef-licensing/license_key_fetcher/prompt.rb +43 -0
- data/lib/chef-licensing/license_key_fetcher.rb +314 -0
- data/lib/chef-licensing/license_key_generator.rb +47 -0
- data/lib/chef-licensing/license_key_validator.rb +24 -0
- data/lib/chef-licensing/licensing_service/local.rb +29 -0
- data/lib/chef-licensing/list_license_keys.rb +142 -0
- data/lib/chef-licensing/restful_client/base.rb +139 -0
- data/lib/chef-licensing/restful_client/middleware/exceptions_handler.rb +16 -0
- data/lib/chef-licensing/restful_client/v1.rb +17 -0
- data/lib/chef-licensing/tui_engine/tui_actions.rb +238 -0
- data/lib/chef-licensing/tui_engine/tui_engine.rb +174 -0
- data/lib/chef-licensing/tui_engine/tui_engine_state.rb +62 -0
- data/lib/chef-licensing/tui_engine/tui_exceptions.rb +17 -0
- data/lib/chef-licensing/tui_engine/tui_interaction.rb +17 -0
- data/lib/chef-licensing/tui_engine/tui_prompt.rb +117 -0
- data/lib/chef-licensing/tui_engine.rb +2 -0
- data/lib/chef-licensing/version.rb +3 -0
- data/lib/chef-licensing.rb +70 -0
- metadata +191 -0
@@ -0,0 +1,275 @@
|
|
1
|
+
require "chef-config/windows"
|
2
|
+
require "chef-config/path_helper"
|
3
|
+
require "yaml"
|
4
|
+
require "date"
|
5
|
+
require "fileutils" unless defined?(FileUtils)
|
6
|
+
require_relative "../license_key_fetcher"
|
7
|
+
require_relative "../config"
|
8
|
+
|
9
|
+
module ChefLicensing
|
10
|
+
class LicenseKeyFetcher
|
11
|
+
|
12
|
+
# Represents a fethced license ID recorded on disk
|
13
|
+
class File
|
14
|
+
LICENSE_KEY_FILE = "licenses.yaml".freeze
|
15
|
+
LICENSE_FILE_FORMAT_VERSION = "4.0.0".freeze
|
16
|
+
|
17
|
+
# License types list
|
18
|
+
LICENSE_TYPES = {
|
19
|
+
free: :free,
|
20
|
+
trial: :trial,
|
21
|
+
commercial: :commercial,
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
attr_reader :logger, :contents, :location
|
25
|
+
attr_accessor :local_dir # Optional local path to use to seek
|
26
|
+
|
27
|
+
def initialize(opts)
|
28
|
+
@opts = opts
|
29
|
+
@logger = ChefLicensing::Config.logger
|
30
|
+
@contents_ivar = nil
|
31
|
+
@location = nil
|
32
|
+
|
33
|
+
@opts[:dir] ||= LicenseKeyFetcher::File.default_file_location
|
34
|
+
@local_dir = @opts[:dir]
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch
|
38
|
+
read_license_key_file
|
39
|
+
contents&.key?(:licenses) ? fetch_license_keys(contents[:licenses]) : []
|
40
|
+
end
|
41
|
+
|
42
|
+
def fetch_license_keys(licenses)
|
43
|
+
licenses.collect { |x| x[:license_key] }
|
44
|
+
end
|
45
|
+
|
46
|
+
def fetch_license_types
|
47
|
+
read_license_key_file
|
48
|
+
|
49
|
+
if contents.nil? || contents[:licenses].nil?
|
50
|
+
[]
|
51
|
+
else
|
52
|
+
contents[:licenses].collect { |x| x[:license_type] }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def user_has_active_trial_license?
|
57
|
+
@active_trial_status = false
|
58
|
+
read_license_key_file
|
59
|
+
|
60
|
+
if contents&.key?(:licenses)
|
61
|
+
@active_trial_status = contents[:licenses].any? { |license| license[:license_type] == :trial && ChefLicensing.client(license_keys: [license[:license_key]]).active? }
|
62
|
+
end
|
63
|
+
@active_trial_status
|
64
|
+
end
|
65
|
+
|
66
|
+
def fetch_allowed_license_types_for_addition
|
67
|
+
license_types = %i{free trial commercial}
|
68
|
+
existing_license_types = fetch_license_types
|
69
|
+
|
70
|
+
license_types -= [:trial] if existing_license_types.include? :trial
|
71
|
+
license_types -= [:free] if existing_license_types.include?(:free) || user_has_active_trial_license?
|
72
|
+
license_types.uniq
|
73
|
+
end
|
74
|
+
|
75
|
+
def fetch_license_keys_based_on_type(license_type)
|
76
|
+
read_license_key_file
|
77
|
+
if contents.nil?
|
78
|
+
[]
|
79
|
+
else
|
80
|
+
contents[:licenses].collect do |x|
|
81
|
+
x[:license_key] if x[:license_type] == license_type
|
82
|
+
end.compact
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Writes a license_id file to disk in the location specified,
|
87
|
+
# with the content given.
|
88
|
+
# @return Array of Errors
|
89
|
+
def persist(license_key, license_type = nil)
|
90
|
+
raise LicenseKeyNotPersistedError.new("License type #{license_type} is not a valid license type.") unless LICENSE_TYPES[license_type.to_sym]
|
91
|
+
|
92
|
+
license_data = {
|
93
|
+
license_key: license_key,
|
94
|
+
license_type: LICENSE_TYPES[license_type.to_sym],
|
95
|
+
update_time: DateTime.now.to_s,
|
96
|
+
}
|
97
|
+
|
98
|
+
dir = @opts[:dir]
|
99
|
+
license_key_file_path = "#{dir}/#{LICENSE_KEY_FILE}"
|
100
|
+
create_license_directory_if_not_exist(dir, license_key_file_path)
|
101
|
+
|
102
|
+
@contents = load_license_file(license_key_file_path)
|
103
|
+
|
104
|
+
load_license_data_to_contents(license_data)
|
105
|
+
write_license_file(license_key_file_path)
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true if a license_key file exists.
|
110
|
+
def persisted?
|
111
|
+
!!seek
|
112
|
+
end
|
113
|
+
|
114
|
+
def fetch_or_persist_url(license_server_url_from_config, license_server_url_from_system = nil)
|
115
|
+
dir = @opts[:dir]
|
116
|
+
license_key_file_path = "#{dir}/#{LICENSE_KEY_FILE}"
|
117
|
+
create_license_directory_if_not_exist(dir, license_key_file_path)
|
118
|
+
|
119
|
+
@contents = load_license_file(license_key_file_path)
|
120
|
+
|
121
|
+
if @contents.nil?
|
122
|
+
@contents = {
|
123
|
+
file_format_version: LICENSE_FILE_FORMAT_VERSION,
|
124
|
+
license_server_url: license_server_url_from_system || license_server_url_from_config,
|
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
|
130
|
+
end
|
131
|
+
|
132
|
+
# Ensure the license server URL is returned to the caller in all cases
|
133
|
+
# (even if it's not persisted to the licenses.yaml file on the disk)
|
134
|
+
begin
|
135
|
+
write_license_file(license_key_file_path)
|
136
|
+
rescue StandardError => e
|
137
|
+
handle_error(e)
|
138
|
+
ensure
|
139
|
+
@license_server_url = @contents[:license_server_url]
|
140
|
+
end
|
141
|
+
logger.debug "License server URL: #{@license_server_url}"
|
142
|
+
@license_server_url
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.default_file_location
|
146
|
+
ChefConfig::PathHelper.home(".chef")
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.fetch_license_keys_based_on_type(license_type, opts = {})
|
150
|
+
new(opts).fetch_license_keys_based_on_type(license_type)
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.user_has_active_trial_license?(opts = {})
|
154
|
+
new(opts).user_has_active_trial_license?
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.fetch_or_persist_url(license_server_url_from_config, license_server_url_from_system = nil, opts = {})
|
158
|
+
new(opts).fetch_or_persist_url(license_server_url_from_config, license_server_url_from_system)
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
attr_accessor :license_server_url
|
164
|
+
|
165
|
+
# Look for an *existing* license_id file in several locations.
|
166
|
+
def seek
|
167
|
+
return location if location
|
168
|
+
|
169
|
+
on_windows = ChefConfig.windows?
|
170
|
+
candidates = []
|
171
|
+
|
172
|
+
# Include the user home directory ~/.chef
|
173
|
+
candidates << "#{self.class.default_file_location}/#{LICENSE_KEY_FILE}"
|
174
|
+
candidates << "/etc/chef/#{LICENSE_KEY_FILE}" unless on_windows
|
175
|
+
|
176
|
+
# Include software installation dirs for bespoke downloads.
|
177
|
+
# TODO: unlikely these would be writable if decision changes.
|
178
|
+
[
|
179
|
+
# TODO - get a complete list
|
180
|
+
"chef-workstation",
|
181
|
+
"inspec",
|
182
|
+
].each do |inst_dir|
|
183
|
+
if on_windows
|
184
|
+
candidates << "C:/opscode/#{inst_dir}/#{LICENSE_KEY_FILE}"
|
185
|
+
else
|
186
|
+
candidates << "/opt/#{inst_dir}/#{LICENSE_KEY_FILE}"
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Include local directory if provided. Not usual, but useful for testing.
|
191
|
+
candidates << "#{local_dir}/#{LICENSE_KEY_FILE}" if local_dir
|
192
|
+
|
193
|
+
# Only picks up the first detected license file out of list of candidates
|
194
|
+
@location = candidates.detect { |c| ::File.exist?(c) }
|
195
|
+
end
|
196
|
+
|
197
|
+
def working_directory
|
198
|
+
(ChefConfig.windows? ? ENV["CD"] : ENV["PWD"]) || Dir.pwd
|
199
|
+
end
|
200
|
+
|
201
|
+
def read_license_key_file
|
202
|
+
return contents if contents
|
203
|
+
|
204
|
+
logger.debug "Reading license file from #{seek}"
|
205
|
+
path = seek
|
206
|
+
return nil unless path
|
207
|
+
|
208
|
+
# only checking for major version for file format for breaking changes
|
209
|
+
@contents ||= YAML.load(::File.read(path))
|
210
|
+
if major_version(@contents[:file_format_version]) == major_version(LICENSE_FILE_FORMAT_VERSION)
|
211
|
+
@contents
|
212
|
+
else
|
213
|
+
logger.debug "License File version #{@contents[:file_format_version]} not supported."
|
214
|
+
raise LicenseKeyNotFetchedError.new("License File version #{@contents[:file_format_version]} not supported.")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def major_version(version)
|
219
|
+
Gem::Version.new(version).segments[0]
|
220
|
+
end
|
221
|
+
|
222
|
+
def create_license_directory_if_not_exist(dir, license_key_file_path)
|
223
|
+
return if ::File.exist?(license_key_file_path)
|
224
|
+
|
225
|
+
logger.debug "Creating directory for license_key file at #{dir}"
|
226
|
+
msg = "Could not create directory for license_key file #{dir}"
|
227
|
+
FileUtils.mkdir_p(dir)
|
228
|
+
rescue StandardError => e
|
229
|
+
handle_error(e, msg)
|
230
|
+
end
|
231
|
+
|
232
|
+
def load_license_file(license_key_file_path)
|
233
|
+
return unless ::File.exist?(license_key_file_path)
|
234
|
+
|
235
|
+
logger.debug "Reading license_key file at #{license_key_file_path}"
|
236
|
+
msg = "Could not read license key file #{license_key_file_path}"
|
237
|
+
YAML.load_file(license_key_file_path)
|
238
|
+
rescue StandardError => e
|
239
|
+
handle_error(e, msg)
|
240
|
+
end
|
241
|
+
|
242
|
+
def load_license_data_to_contents(license_data)
|
243
|
+
return unless license_data
|
244
|
+
|
245
|
+
logger.debug "Loading license data to contents"
|
246
|
+
if @contents.nil? || @contents.empty? # this case is likely to happen only during testing
|
247
|
+
@contents = {
|
248
|
+
file_format_version: LICENSE_FILE_FORMAT_VERSION,
|
249
|
+
license_server_url: @license_server_url,
|
250
|
+
licenses: [license_data],
|
251
|
+
}
|
252
|
+
elsif @contents[:licenses].nil?
|
253
|
+
@contents[:licenses] = [license_data]
|
254
|
+
elsif fetch_license_keys(@contents[:licenses])&.include?(license_data[:license_key])
|
255
|
+
nil
|
256
|
+
else
|
257
|
+
@contents[:licenses] << license_data
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def write_license_file(license_key_file_path)
|
262
|
+
logger.debug "Writing license_key file at #{license_key_file_path}"
|
263
|
+
msg = "Could not write telemetry license_key file #{license_key_file_path}"
|
264
|
+
::File.write(license_key_file_path, YAML.dump(@contents))
|
265
|
+
rescue StandardError => e
|
266
|
+
handle_error(e, msg)
|
267
|
+
end
|
268
|
+
|
269
|
+
def handle_error(e, message = nil)
|
270
|
+
logger.debug "#{e.backtrace.join("\n\t")}"
|
271
|
+
e
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative "../tui_engine"
|
2
|
+
|
3
|
+
module ChefLicensing
|
4
|
+
class LicenseKeyFetcher
|
5
|
+
class Prompt
|
6
|
+
attr_accessor :config, :tui_engine, :license_type
|
7
|
+
|
8
|
+
def initialize(config = {})
|
9
|
+
@config = config
|
10
|
+
initialize_tui_engine
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
# Here info is a hash of { interaction_id: response }
|
15
|
+
info = tui_engine.run_interaction(config[:start_interaction])
|
16
|
+
|
17
|
+
# The interaction_id ask_for_license_id holds the license key
|
18
|
+
# TODO: Do we move this to tui_engine?
|
19
|
+
if info[:fetch_license_id].nil?
|
20
|
+
[]
|
21
|
+
else
|
22
|
+
self.license_type = info[:license_type]
|
23
|
+
[info[:fetch_license_id]]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def append_info_to_tui_engine(extra_info_hash)
|
28
|
+
tui_engine.append_info_to_input(extra_info_hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def initialize_tui_engine
|
34
|
+
# use the default interaction file if interaction_file is nil
|
35
|
+
if config[:interaction_file].nil?
|
36
|
+
interaction_file_path = ::File.join(::File.dirname(__FILE__), "chef_licensing_interactions.yaml")
|
37
|
+
@config.store(:interaction_file, interaction_file_path)
|
38
|
+
end
|
39
|
+
@tui_engine = ChefLicensing::TUIEngine.new(@config)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,314 @@
|
|
1
|
+
require "chef-config/path_helper"
|
2
|
+
require "chef-config/windows"
|
3
|
+
|
4
|
+
require_relative "config"
|
5
|
+
require_relative "context"
|
6
|
+
require_relative "config_fetcher/arg_fetcher"
|
7
|
+
require_relative "config_fetcher/env_fetcher"
|
8
|
+
require_relative "license_key_fetcher/base"
|
9
|
+
require_relative "license_key_fetcher/file"
|
10
|
+
require_relative "license_key_fetcher/prompt"
|
11
|
+
require_relative "../chef-licensing"
|
12
|
+
require "tty-spinner"
|
13
|
+
require_relative "exceptions/invalid_license"
|
14
|
+
require_relative "exceptions/error"
|
15
|
+
require_relative "exceptions/client_error"
|
16
|
+
|
17
|
+
# LicenseKeyFetcher allows us to inspect obtain the license Key from the user in a variety of ways.
|
18
|
+
module ChefLicensing
|
19
|
+
class LicenseKeyFetcher
|
20
|
+
class LicenseKeyNotFetchedError < RuntimeError
|
21
|
+
end
|
22
|
+
|
23
|
+
class LicenseKeyNotPersistedError < RuntimeError
|
24
|
+
end
|
25
|
+
|
26
|
+
class LicenseKeyAddNotAllowed < Error
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :config, :license_keys, :arg_fetcher, :env_fetcher, :file_fetcher, :prompt_fetcher, :logger
|
30
|
+
attr_accessor :client_api_call_error
|
31
|
+
|
32
|
+
def initialize(opts = {})
|
33
|
+
@config = opts
|
34
|
+
@logger = ChefLicensing::Config.logger
|
35
|
+
@config[:output] = ChefLicensing::Config.output
|
36
|
+
config[:logger] = logger
|
37
|
+
config[:dir] = opts[:dir]
|
38
|
+
|
39
|
+
# While using on-prem licensing service, @license_keys are fetched from API
|
40
|
+
logger.debug "License Key fetcher - fetching license keys depending upon the context (either API or file)"
|
41
|
+
# While using global licensing service, @license_keys are fetched from file
|
42
|
+
@license_keys = ChefLicensing::Context.license_keys(opts) || []
|
43
|
+
|
44
|
+
argv = opts[:argv] || ARGV
|
45
|
+
env = opts[:env] || ENV
|
46
|
+
|
47
|
+
# The various things that have a say in fetching the license Key.
|
48
|
+
@arg_fetcher = ChefLicensing::ArgFetcher.new(argv)
|
49
|
+
@env_fetcher = ChefLicensing::EnvFetcher.new(env)
|
50
|
+
@file_fetcher = LicenseKeyFetcher::File.new(config)
|
51
|
+
@prompt_fetcher = LicenseKeyFetcher::Prompt.new(config)
|
52
|
+
@license = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
#
|
56
|
+
# Methods for obtaining consent from the user.
|
57
|
+
#
|
58
|
+
def fetch_and_persist
|
59
|
+
if ChefLicensing::Context.local_licensing_service?
|
60
|
+
perform_on_prem_operations
|
61
|
+
else
|
62
|
+
perform_global_operations
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def perform_on_prem_operations
|
67
|
+
# While using on-prem licensing service no option to add/generate license is enabled
|
68
|
+
|
69
|
+
new_keys = fetch_license_key_from_arg
|
70
|
+
raise LicenseKeyAddNotAllowed.new("'--chef-license-key <value>' option is not supported with airgapped environment. You cannot add license from airgapped environment.") unless new_keys.empty?
|
71
|
+
|
72
|
+
unless @license_keys.empty?
|
73
|
+
# Licenses expiration check
|
74
|
+
if licenses_active?
|
75
|
+
return @license_keys
|
76
|
+
else
|
77
|
+
# Prompts if the keys are expired or expiring
|
78
|
+
if config[:output].isatty
|
79
|
+
append_extra_info_to_tui_engine # will add extra dynamic values in tui flows
|
80
|
+
logger.debug "License Key fetcher - detected TTY, prompting..."
|
81
|
+
prompt_fetcher.fetch
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Scenario: When a user is prompted for license expiry and license is not yet renewed
|
87
|
+
if %i{prompt_license_about_to_expire prompt_license_expired_local_mode}.include?(config[:start_interaction])
|
88
|
+
# Not blocking any license type in case of expiry
|
89
|
+
return @license_keys
|
90
|
+
end
|
91
|
+
|
92
|
+
# Otherwise nothing was able to fetch a license. Throw an exception.
|
93
|
+
logger.debug "License Key fetcher - no license Key able to be fetched."
|
94
|
+
raise LicenseKeyNotFetchedError.new("Unable to obtain a License Key.")
|
95
|
+
end
|
96
|
+
|
97
|
+
def perform_global_operations
|
98
|
+
logger.debug "License Key fetcher examining CLI arg checks"
|
99
|
+
new_keys = fetch_license_key_from_arg
|
100
|
+
license_type = validate_and_fetch_license_type(new_keys)
|
101
|
+
if license_type && !unrestricted_license_added?(new_keys, license_type)
|
102
|
+
# break the flow after the prompt if there is a restriction in adding license
|
103
|
+
# and return the license keys persisted in the file or @license_keys if any
|
104
|
+
return license_keys
|
105
|
+
end
|
106
|
+
|
107
|
+
logger.debug "License Key fetcher examining ENV checks"
|
108
|
+
new_keys = fetch_license_key_from_env
|
109
|
+
license_type = validate_and_fetch_license_type(new_keys)
|
110
|
+
if license_type && !unrestricted_license_added?(new_keys, license_type)
|
111
|
+
# break the flow after the prompt if there is a restriction in adding license
|
112
|
+
# and return the license keys persisted in the file or @license_keys if any
|
113
|
+
return license_keys
|
114
|
+
end
|
115
|
+
|
116
|
+
# Return keys if license keys are active and not expired or expiring
|
117
|
+
# Return keys if there is any error in /client API call, and do not block the flow.
|
118
|
+
# Client API possible errors will be handled in software entitlement check call (made after this)
|
119
|
+
return @license_keys if (!@license_keys.empty? && licenses_active?) || client_api_call_error
|
120
|
+
|
121
|
+
# Lowest priority is to interactively prompt if we have a TTY
|
122
|
+
if config[:output].isatty
|
123
|
+
append_extra_info_to_tui_engine # will add extra dynamic values in tui flows
|
124
|
+
logger.debug "License Key fetcher - detected TTY, prompting..."
|
125
|
+
new_keys = prompt_fetcher.fetch
|
126
|
+
|
127
|
+
unless new_keys.empty?
|
128
|
+
# If license type is not selected using TUI, assign it using API call to fetch type.
|
129
|
+
prompt_fetcher.license_type ||= get_license_type(new_keys.first)
|
130
|
+
persist_and_concat(new_keys, prompt_fetcher.license_type)
|
131
|
+
return license_keys
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Scenario: When a user is prompted for license expiry and license is not yet renewed
|
136
|
+
if new_keys.empty? && %i{prompt_license_about_to_expire prompt_license_expired}.include?(config[:start_interaction])
|
137
|
+
# Not blocking any license type in case of expiry
|
138
|
+
return @license_keys
|
139
|
+
end
|
140
|
+
|
141
|
+
# Otherwise nothing was able to fetch a license. Throw an exception.
|
142
|
+
logger.debug "License Key fetcher - no license Key able to be fetched."
|
143
|
+
raise LicenseKeyNotFetchedError.new("Unable to obtain a License Key.")
|
144
|
+
end
|
145
|
+
|
146
|
+
def add_license
|
147
|
+
logger.debug "License Key fetcher - add license flow, starting..."
|
148
|
+
if ChefLicensing::Context.local_licensing_service?
|
149
|
+
raise LicenseKeyAddNotAllowed.new("'inspec license add' command is not supported with airgapped environment. You cannot generate license from airgapped environment.")
|
150
|
+
else
|
151
|
+
config = {}
|
152
|
+
config[:start_interaction] = :add_license_all
|
153
|
+
prompt_fetcher.config = config
|
154
|
+
append_extra_info_to_tui_engine
|
155
|
+
new_keys = prompt_fetcher.fetch
|
156
|
+
unless new_keys.empty?
|
157
|
+
prompt_fetcher.license_type ||= get_license_type(new_keys.first)
|
158
|
+
persist_and_concat(new_keys, prompt_fetcher.license_type)
|
159
|
+
license_keys
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Note: Fetching from arg and env as well, to be able to fetch license when disk is non-writable
|
165
|
+
def fetch
|
166
|
+
# While using on-prem licensing service, @license_keys have been fetched from API
|
167
|
+
# While using global licensing service, @license_keys have been fetched from file
|
168
|
+
(fetch_license_key_from_arg << fetch_license_key_from_env << @license_keys).flatten.uniq
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.fetch_and_persist(opts = {})
|
172
|
+
new(opts).fetch_and_persist
|
173
|
+
end
|
174
|
+
|
175
|
+
def self.fetch(opts = {})
|
176
|
+
new(opts).fetch
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.add_license(opts = {})
|
180
|
+
new(opts).add_license
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
attr_accessor :license
|
186
|
+
|
187
|
+
def append_extra_info_to_tui_engine(info = {})
|
188
|
+
extra_info = {}
|
189
|
+
|
190
|
+
# default values
|
191
|
+
extra_info[:chef_product_name] = ChefLicensing::Config.chef_product_name&.capitalize
|
192
|
+
if license
|
193
|
+
extra_info[:license_type] = license.license_type.capitalize
|
194
|
+
extra_info[:number_of_days_in_expiration] = license.number_of_days_in_expiration
|
195
|
+
extra_info[:license_expiration_date] = Date.parse(license.expiration_date).strftime("%a, %d %b %Y")
|
196
|
+
end
|
197
|
+
|
198
|
+
unless info.empty? # ability to add info hash through arguments
|
199
|
+
info.each do |key, value|
|
200
|
+
extra_info[key] = value
|
201
|
+
end
|
202
|
+
end
|
203
|
+
prompt_fetcher.append_info_to_tui_engine(extra_info) unless extra_info.empty?
|
204
|
+
end
|
205
|
+
|
206
|
+
def licenses_active?
|
207
|
+
logger.debug "License Key fetcher - checking if licenses are active"
|
208
|
+
spinner = TTY::Spinner.new(":spinner [Running] License validation in progress...", format: :dots, clear: true, output: config[:output])
|
209
|
+
spinner.auto_spin # Start the spinner
|
210
|
+
# This call returns a license based on client logic
|
211
|
+
# This API call is only made when multiple license keys are present or if client call was never done
|
212
|
+
self.license = ChefLicensing.client(license_keys: @license_keys) if !license || @license_keys.count > 1
|
213
|
+
# Intentional lag of 2 seconds when license is expiring or expired
|
214
|
+
sleep 2 if license.expiring_or_expired?
|
215
|
+
spinner.success # Stop the spinner
|
216
|
+
if license.expired? || license.have_grace?
|
217
|
+
if ChefLicensing::Context.local_licensing_service?
|
218
|
+
config[:start_interaction] = :prompt_license_expired_local_mode
|
219
|
+
else
|
220
|
+
config[:start_interaction] = :prompt_license_expired
|
221
|
+
end
|
222
|
+
prompt_fetcher.config = config
|
223
|
+
false
|
224
|
+
elsif license.about_to_expire?
|
225
|
+
config[:start_interaction] = :prompt_license_about_to_expire
|
226
|
+
prompt_fetcher.config = config
|
227
|
+
false
|
228
|
+
else
|
229
|
+
true
|
230
|
+
end
|
231
|
+
rescue ChefLicensing::ClientError => e
|
232
|
+
spinner.success
|
233
|
+
logger.debug "Error in License Expiration Check using Client API #{e.message}"
|
234
|
+
self.client_api_call_error = true
|
235
|
+
false
|
236
|
+
end
|
237
|
+
|
238
|
+
def validate_and_fetch_license_type(new_keys)
|
239
|
+
unless new_keys.empty?
|
240
|
+
is_valid = validate_license_key(new_keys.first)
|
241
|
+
return get_license_type(new_keys.first) if is_valid
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def persist_and_concat(new_keys, license_type)
|
246
|
+
file_fetcher.persist(new_keys.first, license_type)
|
247
|
+
@license_keys.concat(new_keys)
|
248
|
+
end
|
249
|
+
|
250
|
+
def fetch_license_key_from_arg
|
251
|
+
new_key = @arg_fetcher.fetch_value("--chef-license-key")
|
252
|
+
validate_license_key_format(new_key)
|
253
|
+
end
|
254
|
+
|
255
|
+
def fetch_license_key_from_env
|
256
|
+
new_key = @env_fetcher.fetch_value("CHEF_LICENSE_KEY")
|
257
|
+
validate_license_key_format(new_key)
|
258
|
+
end
|
259
|
+
|
260
|
+
def validate_license_key_format(license_key)
|
261
|
+
return [] if license_key.nil?
|
262
|
+
|
263
|
+
license_key = ChefLicensing::LicenseKeyFetcher::Base.verify_and_extract_license(license_key)
|
264
|
+
[license_key]
|
265
|
+
end
|
266
|
+
|
267
|
+
def validate_license_key(license_key)
|
268
|
+
ChefLicensing::LicenseKeyValidator.validate!(license_key)
|
269
|
+
end
|
270
|
+
|
271
|
+
def get_license_type(license_key)
|
272
|
+
self.license = ChefLicensing.client(license_keys: [license_key])
|
273
|
+
license.license_type.downcase.to_sym
|
274
|
+
end
|
275
|
+
|
276
|
+
def license_restricted?(license_type)
|
277
|
+
allowed_license_types = file_fetcher.fetch_allowed_license_types_for_addition
|
278
|
+
!(allowed_license_types.include? license_type)
|
279
|
+
end
|
280
|
+
|
281
|
+
def prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
|
282
|
+
logger.debug "License Key fetcher - prompting license addition restriction"
|
283
|
+
# For trial license
|
284
|
+
# TODO for free license
|
285
|
+
config[:start_interaction] = :prompt_license_addition_restriction
|
286
|
+
prompt_fetcher.config = config
|
287
|
+
# Existing license keys are needed to show details of existing license of license type which is restricted.
|
288
|
+
append_extra_info_to_tui_engine({ license_id: existing_license_keys_in_file.last, license_type: license_type })
|
289
|
+
prompt_fetcher.fetch
|
290
|
+
end
|
291
|
+
|
292
|
+
def unrestricted_license_added?(new_keys, license_type)
|
293
|
+
if license_restricted?(license_type)
|
294
|
+
# Existing license keys of same license type are fetched to compare if old license key or a new one is added.
|
295
|
+
# However, if user is trying to add free license, and user has active trial license, we fetch the trial license key
|
296
|
+
if license_type == :free && file_fetcher.user_has_active_trial_license?
|
297
|
+
existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(:trial)
|
298
|
+
else
|
299
|
+
existing_license_keys_in_file = file_fetcher.fetch_license_keys_based_on_type(license_type)
|
300
|
+
end
|
301
|
+
# Only prompt when a new trial license is added
|
302
|
+
unless existing_license_keys_in_file.last == new_keys.first
|
303
|
+
# prompt the message that this addition of license is restricted.
|
304
|
+
prompt_license_addition_restricted(license_type, existing_license_keys_in_file)
|
305
|
+
return false
|
306
|
+
end
|
307
|
+
true # license type is restricted but not the key since it is the same key hence returning true
|
308
|
+
else
|
309
|
+
persist_and_concat(new_keys, license_type)
|
310
|
+
true
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative "restful_client/v1"
|
2
|
+
require_relative "exceptions/license_generation_failed"
|
3
|
+
|
4
|
+
module ChefLicensing
|
5
|
+
class LicenseKeyGenerator
|
6
|
+
attr_reader :payload
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# @param [Hash] KWARGS keys accepted are [first_name, last_name, email_id, product, company, phone]
|
10
|
+
def generate_trial_license!(kwargs)
|
11
|
+
new(kwargs).generate_trial_license!
|
12
|
+
end
|
13
|
+
|
14
|
+
def generate_free_license!(kwargs)
|
15
|
+
new(kwargs).generate_free_license!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(kwargs, restful_client: ChefLicensing::RestfulClient::V1)
|
20
|
+
# TODO: validate kwargs
|
21
|
+
@payload = build_payload_from(kwargs)
|
22
|
+
@restful_client = restful_client.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate_trial_license!
|
26
|
+
response = @restful_client.generate_trial_license(payload)
|
27
|
+
# need some logic around delivery
|
28
|
+
# how the delivery is decided?
|
29
|
+
response.licenseId
|
30
|
+
rescue RestfulClientError => e
|
31
|
+
raise ChefLicensing::LicenseGenerationFailed, e.message
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate_free_license!
|
35
|
+
response = @restful_client.generate_free_license(payload)
|
36
|
+
response.licenseId
|
37
|
+
rescue RestfulClientError => e
|
38
|
+
raise ChefLicensing::LicenseGenerationFailed, e.message
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def build_payload_from(kwargs)
|
44
|
+
kwargs.slice(:first_name, :last_name, :email_id, :product, :company, :phone)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|