kitchen-oci 1.8.0 → 1.11.1

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'
@@ -21,7 +21,6 @@
21
21
 
22
22
  # This require fixes bug in ChefDK 4.0.60-1 on Linux.
23
23
  require 'forwardable'
24
-
25
24
  require 'base64'
26
25
  require 'erb'
27
26
  require 'kitchen'
@@ -35,47 +34,34 @@ module Kitchen
35
34
  #
36
35
  # @author Stephen Pearson <stephen.pearson@oracle.com>
37
36
  class Oci < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
37
+ # required config items
38
38
  required_config :compartment_id
39
39
  required_config :availability_domain
40
- required_config :image_id
41
40
  required_config :shape
42
41
  required_config :subnet_id
43
42
 
44
- default_config :use_private_ip, false
45
- default_config :oci_config_file, nil
46
- default_config :oci_profile_name, nil
43
+ # common config items
44
+ default_config :instance_type, 'compute'
47
45
  default_config :hostname_prefix, nil
48
46
  default_keypath = File.expand_path(File.join(%w[~ .ssh id_rsa.pub]))
49
47
  default_config :ssh_keypath, default_keypath
50
48
  default_config :post_create_script, nil
51
49
  default_config :proxy_url, nil
52
- default_config :user_data, []
50
+ default_config :user_data, nil
53
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
54
58
  default_config :setup_winrm, false
55
59
  default_config :winrm_user, 'opc'
56
60
  default_config :winrm_password, nil
61
+ default_config :use_instance_principals, false
57
62
 
58
- def process_freeform_tags(freeform_tags)
59
- prov = instance.provisioner.instance_variable_get(:@config)
60
- tags = %w[run_list policyfile]
61
- tags.each do |tag|
62
- unless prov[tag.to_sym].nil? || prov[tag.to_sym].empty?
63
- freeform_tags[tag] = prov[tag.to_sym].join(',')
64
- end
65
- end
66
- freeform_tags[:kitchen] = true
67
- freeform_tags
68
- end
69
-
70
- def process_windows_options(state)
71
- state[:username] = config[:winrm_user] if config[:setup_winrm]
72
- if config[:setup_winrm] == true &&
73
- config[:password].nil? &&
74
- state[:password].nil?
75
- state[:password] = config[:winrm_password] || random_password
76
- end
77
- state
78
- end
63
+ # dbaas config items
64
+ default_config :dbaas, {}
79
65
 
80
66
  def create(state)
81
67
  return if state[:server_id]
@@ -83,6 +69,7 @@ module Kitchen
83
69
  state = process_windows_options(state)
84
70
 
85
71
  instance_id = launch_instance(state)
72
+
86
73
  state[:server_id] = instance_id
87
74
  state[:hostname] = instance_ip(instance_id)
88
75
 
@@ -99,23 +86,53 @@ module Kitchen
99
86
  return unless state[:server_id]
100
87
 
101
88
  instance.transport.connection(state).close
102
- 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
103
95
 
104
96
  state.delete(:server_id)
105
97
  state.delete(:hostname)
106
98
  end
107
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
+
108
120
  private
109
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
+ ####################
110
131
  def oci_config
111
132
  params = [:load_config]
112
133
  opts = {}
113
- if config[:oci_config_file]
114
- opts[:config_file_location] = config[:oci_config_file]
115
- end
116
- if config[:oci_profile_name]
117
- opts[:profile_name] = config[:oci_profile_name]
118
- 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]
119
136
  params << opts
120
137
  OCI::ConfigFileLoader.send(*params)
121
138
  end
@@ -140,13 +157,19 @@ module Kitchen
140
157
  end
141
158
  end
142
159
 
160
+ #############
161
+ # API setup #
162
+ #############
143
163
  def generic_api(klass)
144
164
  api_prx = api_proxy
145
- if api_prx
146
- 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 }
147
168
  else
148
- klass.new(config: oci_config)
169
+ params = { config: oci_config }
149
170
  end
171
+ params[:proxy_settings] = api_prx if api_prx
172
+ klass.new(**params)
150
173
  end
151
174
 
152
175
  def comp_api
@@ -157,11 +180,90 @@ module Kitchen
157
180
  generic_api(OCI::Core::VirtualNetworkClient)
158
181
  end
159
182
 
183
+ def dbaas_api
184
+ generic_api(OCI::Database::DatabaseClient)
185
+ end
186
+
187
+ ##################
188
+ # Common methods #
189
+ ##################
160
190
  def launch_instance(state)
161
- 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
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
162
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)
163
264
  response = comp_api.launch_instance(request)
164
265
  instance_id = response.data.id
266
+
165
267
  comp_api.get_instance(instance_id).wait_until(
166
268
  :lifecycle_state,
167
269
  OCI::Core::Models::Instance::LIFECYCLE_STATE_RUNNING
@@ -169,36 +271,32 @@ module Kitchen
169
271
  instance_id
170
272
  end
171
273
 
172
- def vnic_attachments(instance_id)
173
- att = comp_api.list_vnic_attachments(
174
- config[:compartment_id],
175
- instance_id: instance_id
176
- ).data
177
-
178
- raise 'Could not find any VNIC attachments' unless att.any?
274
+ def compute_instance_request(state)
275
+ request = compute_launch_details
179
276
 
180
- att
181
- end
277
+ inject_powershell(state) if config[:setup_winrm] == true
182
278
 
183
- def vnics(instance_id)
184
- vnic_attachments(instance_id).map do |att|
185
- net_api.get_vnic(att.vnic_id).data
186
- 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
187
285
  end
188
286
 
189
- def instance_ip(instance_id)
190
- vnic = vnics(instance_id).select(&:is_primary).first
191
- if public_ip_allowed?
192
- config[:use_private_ip] ? vnic.private_ip : vnic.public_ip
193
- else
194
- 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])
195
297
  end
196
298
  end
197
299
 
198
- def pubkey
199
- File.readlines(config[:ssh_keypath]).first.chomp
200
- end
201
-
202
300
  def instance_source_details
203
301
  OCI::Core::Models::InstanceSourceViaImageDetails.new(
204
302
  sourceType: 'image',
@@ -206,11 +304,6 @@ module Kitchen
206
304
  )
207
305
  end
208
306
 
209
- def public_ip_allowed?
210
- subnet = net_api.get_subnet(config[:subnet_id]).data
211
- !subnet.prohibit_public_ip_on_vnic
212
- end
213
-
214
307
  def create_vnic_details(name)
215
308
  OCI::Core::Models::CreateVnicDetails.new(
216
309
  assign_public_ip: public_ip_allowed?,
@@ -220,12 +313,48 @@ module Kitchen
220
313
  )
221
314
  end
222
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
+
223
342
  def winrm_ps1(state)
224
343
  filename = File.join(__dir__, %w[.. .. .. tpl setup_winrm.ps1.erb])
225
344
  tpl = ERB.new(File.read(filename))
226
345
  tpl.result(binding)
227
346
  end
228
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
+
229
358
  def read_part(part)
230
359
  if part[:path]
231
360
  content = File.read part[:path]
@@ -250,71 +379,125 @@ module Kitchen
250
379
  msg
251
380
  end
252
381
 
253
- def user_data
254
- boundary = "MIMEBOUNDARY_#{random_string(20)}"
255
- msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"",
256
- 'MIME-Version: 1.0', '']
257
- msg += mime_parts(boundary)
258
- txt = msg.join("\n") + "\n"
259
- gzip = Zlib::GzipWriter.new(StringIO.new)
260
- gzip << txt
261
- 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
262
395
  end
263
396
 
264
- def inject_powershell(state)
265
- data = winrm_ps1(state)
266
- config[:user_data] ||= []
267
- config[:user_data] << {
268
- type: 'x-shellscript',
269
- inline: data,
270
- filename: 'setup_winrm.ps1'
271
- }
272
- end
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
273
404
 
274
- def generate_hostname
275
- prefix = config[:hostname_prefix]
276
- [prefix, random_hostname(instance.name)].compact.join('-')
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
277
412
  end
278
413
 
279
- def base_oci_launch_details
280
- OCI::Core::Models::LaunchInstanceDetails.new.tap do |l|
281
- hostname = generate_hostname
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|
282
421
  l.availability_domain = config[:availability_domain]
283
422
  l.compartment_id = config[:compartment_id]
284
- l.display_name = hostname
285
- l.source_details = instance_source_details
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
286
428
  l.shape = config[:shape]
287
- l.create_vnic_details = create_vnic_details(hostname)
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]
288
435
  l.freeform_tags = process_freeform_tags(config[:freeform_tags])
289
436
  end
290
437
  end
291
438
 
292
- def compute_instance_request(state)
293
- request = base_oci_launch_details
439
+ def create_db_home_details
440
+ raise 'db_version cannot be nil!' if config[:dbaas][:db_version].nil?
294
441
 
295
- 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
296
448
 
297
- metadata = {}
298
- metadata.store('ssh_authorized_keys', pubkey)
299
- data = user_data
300
- metadata.store('user_data', data) if config[:user_data].any?
301
- request.metadata = metadata
302
- 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
303
465
  end
304
466
 
305
- def random_hostname(prefix)
306
- "#{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
307
471
  end
308
472
 
309
- def random_password
310
- (Array.new(5) { %w[! " # & ( ) * + , - . /].sample } +
311
- Array.new(5) { ('a'..'z').to_a.sample } +
312
- Array.new(5) { ('A'..'Z').to_a.sample } +
313
- 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
314
481
  end
315
482
 
316
- def random_string(length)
317
- 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
318
501
  end
319
502
  end
320
503
  end