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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +1 -0
  3. data/chef-licensing.gemspec +35 -0
  4. data/lib/chef-licensing/api/client.rb +39 -0
  5. data/lib/chef-licensing/api/describe.rb +62 -0
  6. data/lib/chef-licensing/api/license_feature_entitlement.rb +55 -0
  7. data/lib/chef-licensing/api/license_software_entitlement.rb +53 -0
  8. data/lib/chef-licensing/api/list_licenses.rb +30 -0
  9. data/lib/chef-licensing/api/parser/client.rb +100 -0
  10. data/lib/chef-licensing/api/parser/describe.rb +118 -0
  11. data/lib/chef-licensing/cli_flags/mixlib_cli.rb +28 -0
  12. data/lib/chef-licensing/cli_flags/thor.rb +21 -0
  13. data/lib/chef-licensing/config.rb +44 -0
  14. data/lib/chef-licensing/config_fetcher/arg_fetcher.rb +38 -0
  15. data/lib/chef-licensing/config_fetcher/env_fetcher.rb +21 -0
  16. data/lib/chef-licensing/context.rb +98 -0
  17. data/lib/chef-licensing/exceptions/client_error.rb +9 -0
  18. data/lib/chef-licensing/exceptions/describe_error.rb +9 -0
  19. data/lib/chef-licensing/exceptions/error.rb +4 -0
  20. data/lib/chef-licensing/exceptions/feature_not_entitled.rb +9 -0
  21. data/lib/chef-licensing/exceptions/invalid_license.rb +10 -0
  22. data/lib/chef-licensing/exceptions/license_generation_failed.rb +9 -0
  23. data/lib/chef-licensing/exceptions/license_generation_rejected.rb +7 -0
  24. data/lib/chef-licensing/exceptions/list_licenses_error.rb +12 -0
  25. data/lib/chef-licensing/exceptions/missing_api_credentials_error.rb +7 -0
  26. data/lib/chef-licensing/exceptions/restful_client_connection_error.rb +9 -0
  27. data/lib/chef-licensing/exceptions/restful_client_error.rb +9 -0
  28. data/lib/chef-licensing/exceptions/software_not_entitled.rb +9 -0
  29. data/lib/chef-licensing/license.rb +151 -0
  30. data/lib/chef-licensing/license_key_fetcher/base.rb +28 -0
  31. data/lib/chef-licensing/license_key_fetcher/chef_licensing_interactions.yaml +534 -0
  32. data/lib/chef-licensing/license_key_fetcher/file.rb +275 -0
  33. data/lib/chef-licensing/license_key_fetcher/prompt.rb +43 -0
  34. data/lib/chef-licensing/license_key_fetcher.rb +314 -0
  35. data/lib/chef-licensing/license_key_generator.rb +47 -0
  36. data/lib/chef-licensing/license_key_validator.rb +24 -0
  37. data/lib/chef-licensing/licensing_service/local.rb +29 -0
  38. data/lib/chef-licensing/list_license_keys.rb +142 -0
  39. data/lib/chef-licensing/restful_client/base.rb +139 -0
  40. data/lib/chef-licensing/restful_client/middleware/exceptions_handler.rb +16 -0
  41. data/lib/chef-licensing/restful_client/v1.rb +17 -0
  42. data/lib/chef-licensing/tui_engine/tui_actions.rb +238 -0
  43. data/lib/chef-licensing/tui_engine/tui_engine.rb +174 -0
  44. data/lib/chef-licensing/tui_engine/tui_engine_state.rb +62 -0
  45. data/lib/chef-licensing/tui_engine/tui_exceptions.rb +17 -0
  46. data/lib/chef-licensing/tui_engine/tui_interaction.rb +17 -0
  47. data/lib/chef-licensing/tui_engine/tui_prompt.rb +117 -0
  48. data/lib/chef-licensing/tui_engine.rb +2 -0
  49. data/lib/chef-licensing/version.rb +3 -0
  50. data/lib/chef-licensing.rb +70 -0
  51. 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