chef-licensing 0.4.43

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