kitchen-oci 1.6.1 → 1.11.0

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.
data/Rakefile CHANGED
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2020 Stephen Pearson <stephen.pearson@oracle.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  require 'bundler/gem_tasks'
4
18
  require 'cane/rake_task'
5
19
  require 'tailor/rake_task'
@@ -1,5 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Copyright 2020 Stephen Pearson <stephen.pearson@oracle.com>
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
3
17
  lib = File.expand_path('lib', __dir__)
4
18
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
19
  require 'kitchen/driver/oci_version'
@@ -21,7 +35,7 @@ Gem::Specification.new do |spec|
21
35
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
36
  spec.require_paths = ['lib']
23
37
 
24
- spec.add_dependency 'oci', '~> 2.5.9'
38
+ spec.add_dependency 'oci', '~> 2.9.0'
25
39
  spec.add_dependency 'test-kitchen'
26
40
 
27
41
  spec.add_development_dependency 'bundler'
@@ -17,9 +17,10 @@
17
17
  # See the License for the specific language governing permissions and
18
18
  # limitations under the License.
19
19
 
20
+ # rubocop:disable Metrics/AbcSize
21
+
20
22
  # This require fixes bug in ChefDK 4.0.60-1 on Linux.
21
23
  require 'forwardable'
22
-
23
24
  require 'base64'
24
25
  require 'erb'
25
26
  require 'kitchen'
@@ -33,40 +34,42 @@ module Kitchen
33
34
  #
34
35
  # @author Stephen Pearson <stephen.pearson@oracle.com>
35
36
  class Oci < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
37
+ # required config items
36
38
  required_config :compartment_id
37
39
  required_config :availability_domain
38
- required_config :image_id
39
40
  required_config :shape
40
41
  required_config :subnet_id
41
42
 
42
- default_config :use_private_ip, false
43
- default_config :oci_config_file, nil
44
- default_config :oci_profile_name, nil
43
+ # common config items
44
+ default_config :instance_type, 'compute'
45
+ default_config :hostname_prefix, nil
45
46
  default_keypath = File.expand_path(File.join(%w[~ .ssh id_rsa.pub]))
46
47
  default_config :ssh_keypath, default_keypath
47
48
  default_config :post_create_script, nil
48
49
  default_config :proxy_url, nil
49
- default_config :user_data, []
50
+ default_config :user_data, nil
51
+ default_config :freeform_tags, {}
52
+
53
+ # compute config items
54
+ default_config :image_id
55
+ default_config :use_private_ip, false
56
+ default_config :oci_config_file, nil
57
+ default_config :oci_profile_name, nil
50
58
  default_config :setup_winrm, false
51
59
  default_config :winrm_user, 'opc'
52
60
  default_config :winrm_password, nil
61
+ default_config :use_instance_principals, false
53
62
 
54
- def process_windows_options(state)
55
- state[:username] = config[:winrm_user] if config[:setup_winrm]
56
- if config[:setup_winrm] == true &&
57
- config[:password].nil? &&
58
- state[:password].nil?
59
- state[:password] = config[:winrm_password] || random_password
60
- end
61
- state
62
- end
63
+ # dbaas config items
64
+ default_config :dbaas, {}
63
65
 
64
- def create(state) # rubocop:disable Metrics/AbcSize
66
+ def create(state)
65
67
  return if state[:server_id]
66
68
 
67
69
  state = process_windows_options(state)
68
70
 
69
71
  instance_id = launch_instance(state)
72
+
70
73
  state[:server_id] = instance_id
71
74
  state[:hostname] = instance_ip(instance_id)
72
75
 
@@ -83,23 +86,53 @@ module Kitchen
83
86
  return unless state[:server_id]
84
87
 
85
88
  instance.transport.connection(state).close
86
- comp_api.terminate_instance(state[:server_id])
89
+
90
+ if instance_type == 'compute'
91
+ comp_api.terminate_instance(state[:server_id])
92
+ elsif instance_type == 'dbaas'
93
+ dbaas_api.terminate_db_system(state[:server_id])
94
+ end
87
95
 
88
96
  state.delete(:server_id)
89
97
  state.delete(:hostname)
90
98
  end
91
99
 
100
+ def process_freeform_tags(freeform_tags)
101
+ prov = instance.provisioner.instance_variable_get(:@config)
102
+ tags = %w[run_list policyfile]
103
+ tags.each do |tag|
104
+ freeform_tags[tag] = prov[tag.to_sym].join(',') unless prov[tag.to_sym].nil? || prov[tag.to_sym].empty?
105
+ end
106
+ freeform_tags[:kitchen] = true
107
+ freeform_tags
108
+ end
109
+
110
+ def process_windows_options(state)
111
+ state[:username] = config[:winrm_user] if config[:setup_winrm]
112
+ if config[:setup_winrm] == true &&
113
+ config[:password].nil? &&
114
+ state[:password].nil?
115
+ state[:password] = config[:winrm_password] || random_password
116
+ end
117
+ state
118
+ end
119
+
92
120
  private
93
121
 
122
+ def instance_type
123
+ raise 'instance_type must be either compute or dbaas!' unless %w[compute dbaas].include?(config[:instance_type].downcase)
124
+
125
+ config[:instance_type].downcase
126
+ end
127
+
128
+ ####################
129
+ # OCI config setup #
130
+ ####################
94
131
  def oci_config
95
132
  params = [:load_config]
96
133
  opts = {}
97
- if config[:oci_config_file]
98
- opts[:config_file_location] = config[:oci_config_file]
99
- end
100
- if config[:oci_profile_name]
101
- opts[:profile_name] = config[:oci_profile_name]
102
- end
134
+ opts[:config_file_location] = config[:oci_config_file] if config[:oci_config_file]
135
+ opts[:profile_name] = config[:oci_profile_name] if config[:oci_profile_name]
103
136
  params << opts
104
137
  OCI::ConfigFileLoader.send(*params)
105
138
  end
@@ -124,13 +157,19 @@ module Kitchen
124
157
  end
125
158
  end
126
159
 
160
+ #############
161
+ # API setup #
162
+ #############
127
163
  def generic_api(klass)
128
164
  api_prx = api_proxy
129
- if api_prx
130
- klass.new(config: oci_config, proxy_settings: api_prx)
165
+ if config[:use_instance_principals]
166
+ sign = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
167
+ params = { signer: sign }
131
168
  else
132
- klass.new(config: oci_config)
169
+ params = { config: oci_config }
133
170
  end
171
+ params[:proxy_settings] = api_prx if api_prx
172
+ klass.new(**params)
134
173
  end
135
174
 
136
175
  def comp_api
@@ -141,11 +180,90 @@ module Kitchen
141
180
  generic_api(OCI::Core::VirtualNetworkClient)
142
181
  end
143
182
 
183
+ def dbaas_api
184
+ generic_api(OCI::Database::DatabaseClient)
185
+ end
186
+
187
+ ##################
188
+ # Common methods #
189
+ ##################
144
190
  def launch_instance(state)
145
- request = compute_instance_request(state)
191
+ if instance_type == 'compute'
192
+ launch_compute_instance(state)
193
+ elsif instance_type == 'dbaas'
194
+ launch_dbaas_instance
195
+ end
196
+ end
197
+
198
+ def public_ip_allowed?
199
+ subnet = net_api.get_subnet(config[:subnet_id]).data
200
+ !subnet.prohibit_public_ip_on_vnic
201
+ end
146
202
 
203
+ def instance_ip(instance_id)
204
+ if instance_type == 'compute'
205
+ compute_instance_ip(instance_id)
206
+ elsif instance_type == 'dbaas'
207
+ dbaas_instance_ip(instance_id)
208
+ end
209
+ end
210
+
211
+ def pubkey
212
+ if instance_type == 'compute'
213
+ File.readlines(config[:ssh_keypath]).first.chomp
214
+ elsif instance_type == 'dbaas'
215
+ result = []
216
+ result << File.readlines(config[:ssh_keypath]).first.chomp
217
+ end
218
+ end
219
+
220
+ def generate_hostname
221
+ prefix = config[:hostname_prefix]
222
+ if instance_type == 'compute'
223
+ [prefix, random_hostname(instance.name)].compact.join('-')
224
+ elsif instance_type == 'dbaas'
225
+ # 30 character limit for hostname in DBaaS
226
+ if prefix.length >= 30
227
+ [prefix[0, 26], 'db1'].compact.join('-')
228
+ else
229
+ [prefix, random_string(25 - prefix.length), 'db1'].compact.join('-')
230
+ end
231
+ end
232
+ end
233
+
234
+ def random_hostname(prefix)
235
+ "#{prefix}-#{random_string(6)}"
236
+ end
237
+
238
+ def random_password
239
+ if instance_type == 'compute'
240
+ special_chars = %w[! " # & ( ) * + , - . /]
241
+ elsif instance_type == 'dbaas'
242
+ special_chars = %w[# _ -]
243
+ end
244
+
245
+ (Array.new(5) { special_chars.sample } +
246
+ Array.new(5) { ('a'..'z').to_a.sample } +
247
+ Array.new(5) { ('A'..'Z').to_a.sample } +
248
+ Array.new(5) { ('0'..'9').to_a.sample }).shuffle.join
249
+ end
250
+
251
+ def random_string(length)
252
+ Array.new(length) { ('a'..'z').to_a.sample }.join
253
+ end
254
+
255
+ def random_number(length)
256
+ Array.new(length) { ('0'..'9').to_a.sample }.join
257
+ end
258
+
259
+ ###################
260
+ # Compute methods #
261
+ ###################
262
+ def launch_compute_instance(state)
263
+ request = compute_instance_request(state)
147
264
  response = comp_api.launch_instance(request)
148
265
  instance_id = response.data.id
266
+
149
267
  comp_api.get_instance(instance_id).wait_until(
150
268
  :lifecycle_state,
151
269
  OCI::Core::Models::Instance::LIFECYCLE_STATE_RUNNING
@@ -153,36 +271,32 @@ module Kitchen
153
271
  instance_id
154
272
  end
155
273
 
156
- def vnic_attachments(instance_id)
157
- att = comp_api.list_vnic_attachments(
158
- config[:compartment_id],
159
- instance_id: instance_id
160
- ).data
161
-
162
- raise 'Could not find any VNIC attachments' unless att.any?
274
+ def compute_instance_request(state)
275
+ request = compute_launch_details
163
276
 
164
- att
165
- end
277
+ inject_powershell(state) if config[:setup_winrm] == true
166
278
 
167
- def vnics(instance_id)
168
- vnic_attachments(instance_id).map do |att|
169
- net_api.get_vnic(att.vnic_id).data
170
- end
279
+ metadata = {}
280
+ metadata.store('ssh_authorized_keys', pubkey)
281
+ data = user_data
282
+ metadata.store('user_data', data) if config[:user_data] && !config[:user_data].empty?
283
+ request.metadata = metadata
284
+ request
171
285
  end
172
286
 
173
- def instance_ip(instance_id)
174
- vnic = vnics(instance_id).select(&:is_primary).first
175
- if public_ip_allowed?
176
- config[:use_private_ip] ? vnic.private_ip : vnic.public_ip
177
- else
178
- vnic.private_ip
287
+ def compute_launch_details
288
+ OCI::Core::Models::LaunchInstanceDetails.new.tap do |l|
289
+ hostname = generate_hostname
290
+ l.availability_domain = config[:availability_domain]
291
+ l.compartment_id = config[:compartment_id]
292
+ l.display_name = hostname
293
+ l.source_details = instance_source_details
294
+ l.shape = config[:shape]
295
+ l.create_vnic_details = create_vnic_details(hostname)
296
+ l.freeform_tags = process_freeform_tags(config[:freeform_tags])
179
297
  end
180
298
  end
181
299
 
182
- def pubkey
183
- File.readlines(config[:ssh_keypath]).first.chomp
184
- end
185
-
186
300
  def instance_source_details
187
301
  OCI::Core::Models::InstanceSourceViaImageDetails.new(
188
302
  sourceType: 'image',
@@ -190,11 +304,6 @@ module Kitchen
190
304
  )
191
305
  end
192
306
 
193
- def public_ip_allowed?
194
- subnet = net_api.get_subnet(config[:subnet_id]).data
195
- !subnet.prohibit_public_ip_on_vnic
196
- end
197
-
198
307
  def create_vnic_details(name)
199
308
  OCI::Core::Models::CreateVnicDetails.new(
200
309
  assign_public_ip: public_ip_allowed?,
@@ -204,12 +313,48 @@ module Kitchen
204
313
  )
205
314
  end
206
315
 
316
+ def vnics(instance_id)
317
+ vnic_attachments(instance_id).map do |att|
318
+ net_api.get_vnic(att.vnic_id).data
319
+ end
320
+ end
321
+
322
+ def vnic_attachments(instance_id)
323
+ att = comp_api.list_vnic_attachments(
324
+ config[:compartment_id],
325
+ instance_id: instance_id
326
+ ).data
327
+
328
+ raise 'Could not find any VNIC attachments' unless att.any?
329
+
330
+ att
331
+ end
332
+
333
+ def compute_instance_ip(instance_id)
334
+ vnic = vnics(instance_id).select(&:is_primary).first
335
+ if public_ip_allowed?
336
+ config[:use_private_ip] ? vnic.private_ip : vnic.public_ip
337
+ else
338
+ vnic.private_ip
339
+ end
340
+ end
341
+
207
342
  def winrm_ps1(state)
208
343
  filename = File.join(__dir__, %w[.. .. .. tpl setup_winrm.ps1.erb])
209
344
  tpl = ERB.new(File.read(filename))
210
345
  tpl.result(binding)
211
346
  end
212
347
 
348
+ def inject_powershell(state)
349
+ data = winrm_ps1(state)
350
+ config[:user_data] ||= []
351
+ config[:user_data] << {
352
+ type: 'x-shellscript',
353
+ inline: data,
354
+ filename: 'setup_winrm.ps1'
355
+ }
356
+ end
357
+
213
358
  def read_part(part)
214
359
  if part[:path]
215
360
  content = File.read part[:path]
@@ -221,7 +366,7 @@ module Kitchen
221
366
  content.split("\n")
222
367
  end
223
368
 
224
- def mime_parts(boundary) # rubocop:disable Metrics/AbcSize
369
+ def mime_parts(boundary)
225
370
  msg = []
226
371
  config[:user_data].each do |m|
227
372
  msg << "--#{boundary}"
@@ -234,66 +379,128 @@ module Kitchen
234
379
  msg
235
380
  end
236
381
 
237
- def user_data
238
- boundary = "MIMEBOUNDARY_#{random_string(20)}"
239
- msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"",
240
- 'MIME-Version: 1.0', '']
241
- msg += mime_parts(boundary)
242
- txt = msg.join("\n") + "\n"
243
- gzip = Zlib::GzipWriter.new(StringIO.new)
244
- gzip << txt
245
- Base64.encode64(gzip.close.string).delete("\n")
382
+ def user_data # rubocop:disable Metrics/MethodLength
383
+ if config[:user_data].is_a? Array
384
+ boundary = "MIMEBOUNDARY_#{random_string(20)}"
385
+ msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"",
386
+ 'MIME-Version: 1.0', '']
387
+ msg += mime_parts(boundary)
388
+ txt = msg.join("\n") + "\n"
389
+ gzip = Zlib::GzipWriter.new(StringIO.new)
390
+ gzip << txt
391
+ Base64.encode64(gzip.close.string).delete("\n")
392
+ elsif config[:user_data].is_a? String
393
+ Base64.encode64(config[:user_data]).delete("\n")
394
+ end
246
395
  end
247
396
 
248
- def inject_powershell(state)
249
- data = winrm_ps1(state)
250
- config[:user_data] ||= []
251
- config[:user_data] << {
252
- type: 'x-shellscript',
253
- inline: data,
254
- filename: 'setup_winrm.ps1'
255
- }
397
+ #################
398
+ # DBaaS methods #
399
+ #################
400
+ def launch_dbaas_instance
401
+ request = dbaas_launch_details
402
+ response = dbaas_api.launch_db_system(request)
403
+ instance_id = response.data.id
404
+
405
+ dbaas_api.get_db_system(instance_id).wait_until(
406
+ :lifecycle_state,
407
+ OCI::Database::Models::DbSystem::LIFECYCLE_STATE_AVAILABLE,
408
+ max_interval_seconds: 900,
409
+ max_wait_seconds: 21600
410
+ )
411
+ instance_id
256
412
  end
257
413
 
258
- def base_oci_launch_details
259
- request = OCI::Core::Models::LaunchInstanceDetails.new
260
- hostname = random_hostname(instance.name)
261
- request.availability_domain = config[:availability_domain]
262
- request.compartment_id = config[:compartment_id]
263
- request.display_name = hostname
264
- request.source_details = instance_source_details
265
- request.shape = config[:shape]
266
- request.create_vnic_details = create_vnic_details(hostname)
267
- request
414
+ def dbaas_launch_details # rubocop:disable Metrics/MethodLength
415
+ cpu_core_count = config[:dbaas][:cpu_core_count] ||= 2
416
+ database_edition = config[:dbaas][:database_edition] ||= OCI::Database::Models::DbSystem::DATABASE_EDITION_ENTERPRISE_EDITION
417
+ initial_data_storage_size_in_gb = config[:dbaas][:initial_data_storage_size_in_gb] ||= 256
418
+ license_model = config[:dbaas][:license_model] ||= OCI::Database::Models::DbSystem::LICENSE_MODEL_BRING_YOUR_OWN_LICENSE
419
+
420
+ OCI::Database::Models::LaunchDbSystemDetails.new.tap do |l|
421
+ l.availability_domain = config[:availability_domain]
422
+ l.compartment_id = config[:compartment_id]
423
+ l.cpu_core_count = cpu_core_count
424
+ l.database_edition = database_edition
425
+ l.db_home = create_db_home_details
426
+ l.display_name = [config[:hostname_prefix], random_string(4), random_number(2)].compact.join('-')
427
+ l.hostname = generate_hostname
428
+ l.shape = config[:shape]
429
+ l.ssh_public_keys = pubkey
430
+ l.cluster_name = generate_cluster_name
431
+ l.initial_data_storage_size_in_gb = initial_data_storage_size_in_gb
432
+ l.node_count = 1
433
+ l.license_model = license_model
434
+ l.subnet_id = config[:subnet_id]
435
+ l.freeform_tags = process_freeform_tags(config[:freeform_tags])
436
+ end
268
437
  end
269
438
 
270
- def compute_instance_request(state)
271
- request = base_oci_launch_details
439
+ def create_db_home_details
440
+ raise 'db_version cannot be nil!' if config[:dbaas][:db_version].nil?
272
441
 
273
- inject_powershell(state) if config[:setup_winrm] == true
442
+ OCI::Database::Models::CreateDbHomeDetails.new.tap do |l|
443
+ l.database = create_database_details
444
+ l.db_version = config[:dbaas][:db_version]
445
+ l.display_name = ['dbhome', random_number(10)].compact.join('')
446
+ end
447
+ end
274
448
 
275
- metadata = {}
276
- metadata.store('ssh_authorized_keys', pubkey)
277
- data = user_data
278
- metadata.store('user_data', data) if config[:user_data].any?
279
- request.metadata = metadata
280
- request
449
+ def create_database_details # rubocop:disable Metrics/MethodLength
450
+ character_set = config[:dbaas][:character_set] ||= 'AL32UTF8'
451
+ ncharacter_set = config[:dbaas][:ncharacter_set] ||= 'AL16UTF16'
452
+ db_workload = config[:dbaas][:db_workload] ||= OCI::Database::Models::CreateDatabaseDetails::DB_WORKLOAD_OLTP
453
+ admin_password = config[:dbaas][:admin_password] ||= random_password
454
+ db_name = config[:dbaas][:db_name] ||= 'dbaas1'
455
+
456
+ OCI::Database::Models::CreateDatabaseDetails.new.tap do |l|
457
+ l.admin_password = admin_password
458
+ l.character_set = character_set
459
+ l.db_name = db_name
460
+ l.db_workload = db_workload
461
+ l.ncharacter_set = ncharacter_set
462
+ l.pdb_name = config[:dbaas][:pdb_name]
463
+ l.db_backup_config = db_backup_config
464
+ end
281
465
  end
282
466
 
283
- def random_hostname(prefix)
284
- "#{prefix}-#{random_string(6)}"
467
+ def db_backup_config
468
+ OCI::Database::Models::DbBackupConfig.new.tap do |l|
469
+ l.auto_backup_enabled = false
470
+ end
285
471
  end
286
472
 
287
- def random_password # rubocop:disable Metrics/AbcSize
288
- (Array.new(5) { %w[! " # & ( ) * + , - . /].sample } +
289
- Array.new(5) { ('a'..'z').to_a.sample } +
290
- Array.new(5) { ('A'..'Z').to_a.sample } +
291
- Array.new(5) { ('0'..'9').to_a.sample }).shuffle.join
473
+ def generate_cluster_name
474
+ prefix = config[:hostname_prefix].split('-')[0]
475
+ # 11 character limit for cluster_name in DBaaS
476
+ if prefix.length >= 11
477
+ prefix[0, 11]
478
+ else
479
+ [prefix, random_string(10 - prefix.length)].compact.join('-')
480
+ end
292
481
  end
293
482
 
294
- def random_string(length)
295
- Array.new(length) { ('a'..'z').to_a.sample }.join
483
+ def dbaas_node(instance_id)
484
+ dbaas_api.list_db_nodes(
485
+ config[:compartment_id],
486
+ db_system_id: instance_id
487
+ ).data
488
+ end
489
+
490
+ def dbaas_vnic(node_ocid)
491
+ dbaas_api.get_db_node(node_ocid).data
492
+ end
493
+
494
+ def dbaas_instance_ip(instance_id)
495
+ vnic = dbaas_node(instance_id).select(&:vnic_id).first.vnic_id
496
+ if public_ip_allowed?
497
+ net_api.get_vnic(vnic).data.public_ip
498
+ else
499
+ net_api.get_vnic(vnic).data.private_ip
500
+ end
296
501
  end
297
502
  end
298
503
  end
299
504
  end
505
+
506
+ # rubocop:enable Metrics/AbcSize