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