class Kitchen::Driver::Oci

Oracle OCI driver for Kitchen.

@author Stephen Pearson <stephen.pearson@oracle.com>

Public Instance Methods

create(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 68
def create(state)
  return if state[:server_id]

  state = process_windows_options(state)

  instance_id = launch_instance(state)

  state[:server_id] = instance_id
  state[:hostname] = instance_ip(instance_id)

  instance.transport.connection(state).wait_until_ready

  return unless config[:post_create_script]

  info('Running post create script')
  script = config[:post_create_script]
  instance.transport.connection(state).execute(script)
end
destroy(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 87
def destroy(state)
  return unless state[:server_id]

  instance.transport.connection(state).close

  if instance_type == 'compute'
    comp_api.terminate_instance(state[:server_id])
  elsif instance_type == 'dbaas'
    dbaas_api.terminate_db_system(state[:server_id])
  end

  state.delete(:server_id)
  state.delete(:hostname)
end
process_freeform_tags(freeform_tags) click to toggle source
# File lib/kitchen/driver/oci.rb, line 102
def process_freeform_tags(freeform_tags)
  prov = instance.provisioner.instance_variable_get(:@config)
  tags = %w[run_list policyfile]
  tags.each do |tag|
    freeform_tags[tag] = prov[tag.to_sym].join(',') unless prov[tag.to_sym].nil? || prov[tag.to_sym].empty?
  end
  freeform_tags[:kitchen] = true
  freeform_tags
end
process_windows_options(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 112
def process_windows_options(state)
  state[:username] = config[:winrm_user] if config[:setup_winrm]
  if config[:setup_winrm] == true &&
     config[:password].nil? &&
     state[:password].nil?
    state[:password] = config[:winrm_password] || random_password
  end
  state
end

Private Instance Methods

api_proxy() click to toggle source
# File lib/kitchen/driver/oci.rb, line 148
def api_proxy
  prx = proxy_config
  return nil unless prx

  if prx.user
    OCI::ApiClientProxySettings.new(prx.host, prx.port, prx.user,
                                    prx.password)
  else
    OCI::ApiClientProxySettings.new(prx.host, prx.port)
  end
end
comp_api() click to toggle source
# File lib/kitchen/driver/oci.rb, line 175
def comp_api
  generic_api(OCI::Core::ComputeClient)
end
compute_instance_ip(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 352
def compute_instance_ip(instance_id)
  vnic = vnics(instance_id).select(&:is_primary).first
  if public_ip_allowed?
    config[:use_private_ip] ? vnic.private_ip : vnic.public_ip
  else
    vnic.private_ip
  end
end
compute_instance_request(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 274
def compute_instance_request(state)
  request = compute_launch_details

  inject_powershell(state) if config[:setup_winrm] == true

  metadata = {}
  metadata.store('ssh_authorized_keys', pubkey)
  data = user_data
  metadata.store('user_data', data) if config[:user_data] && !config[:user_data].empty?
  request.metadata = metadata
  request
end
compute_launch_details() click to toggle source
# File lib/kitchen/driver/oci.rb, line 287
def compute_launch_details # rubocop:disable Metrics/MethodLength
  OCI::Core::Models::LaunchInstanceDetails.new.tap do |l|
    hostname = generate_hostname
    l.availability_domain = config[:availability_domain]
    l.compartment_id = config[:compartment_id]
    l.display_name = hostname
    l.source_details = instance_source_details
    l.shape = config[:shape]
    l.create_vnic_details = create_vnic_details(hostname)
    l.freeform_tags = process_freeform_tags(config[:freeform_tags])
    l.preemptible_instance_config = preemptible_instance_config if config[:preemptible_instance]
    l.shape_config = shape_config unless config[:shape_config].empty?
  end
end
create_database_details() click to toggle source
# File lib/kitchen/driver/oci.rb, line 468
def create_database_details # rubocop:disable Metrics/MethodLength
  character_set = config[:dbaas][:character_set] ||= 'AL32UTF8'
  ncharacter_set = config[:dbaas][:ncharacter_set] ||= 'AL16UTF16'
  db_workload = config[:dbaas][:db_workload] ||= OCI::Database::Models::CreateDatabaseDetails::DB_WORKLOAD_OLTP
  admin_password = config[:dbaas][:admin_password] ||= random_password
  db_name = config[:dbaas][:db_name] ||= 'dbaas1'

  OCI::Database::Models::CreateDatabaseDetails.new.tap do |l|
    l.admin_password = admin_password
    l.character_set = character_set
    l.db_name = db_name
    l.db_workload = db_workload
    l.ncharacter_set = ncharacter_set
    l.pdb_name = config[:dbaas][:pdb_name]
    l.db_backup_config = db_backup_config
  end
end
create_db_home_details() click to toggle source
# File lib/kitchen/driver/oci.rb, line 458
def create_db_home_details
  raise 'db_version cannot be nil!' if config[:dbaas][:db_version].nil?

  OCI::Database::Models::CreateDbHomeDetails.new.tap do |l|
    l.database = create_database_details
    l.db_version = config[:dbaas][:db_version]
    l.display_name = ['dbhome', random_number(10)].compact.join('')
  end
end
create_vnic_details(name) click to toggle source
# File lib/kitchen/driver/oci.rb, line 326
def create_vnic_details(name)
  OCI::Core::Models::CreateVnicDetails.new(
    assign_public_ip: public_ip_allowed?,
    display_name: name,
    hostname_label: name,
    subnetId: config[:subnet_id]
  )
end
db_backup_config() click to toggle source
# File lib/kitchen/driver/oci.rb, line 486
def db_backup_config
  OCI::Database::Models::DbBackupConfig.new.tap do |l|
    l.auto_backup_enabled = false
  end
end
dbaas_api() click to toggle source
# File lib/kitchen/driver/oci.rb, line 183
def dbaas_api
  generic_api(OCI::Database::DatabaseClient)
end
dbaas_instance_ip(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 513
def dbaas_instance_ip(instance_id)
  vnic = dbaas_node(instance_id).select(&:vnic_id).first.vnic_id
  if public_ip_allowed?
    net_api.get_vnic(vnic).data.public_ip
  else
    net_api.get_vnic(vnic).data.private_ip
  end
end
dbaas_launch_details() click to toggle source
# File lib/kitchen/driver/oci.rb, line 433
def dbaas_launch_details # rubocop:disable Metrics/MethodLength
  cpu_core_count = config[:dbaas][:cpu_core_count] ||= 2
  database_edition = config[:dbaas][:database_edition] ||= OCI::Database::Models::DbSystem::DATABASE_EDITION_ENTERPRISE_EDITION
  initial_data_storage_size_in_gb = config[:dbaas][:initial_data_storage_size_in_gb] ||= 256
  license_model = config[:dbaas][:license_model] ||= OCI::Database::Models::DbSystem::LICENSE_MODEL_BRING_YOUR_OWN_LICENSE

  OCI::Database::Models::LaunchDbSystemDetails.new.tap do |l|
    l.availability_domain = config[:availability_domain]
    l.compartment_id = config[:compartment_id]
    l.cpu_core_count = cpu_core_count
    l.database_edition = database_edition
    l.db_home = create_db_home_details
    l.display_name = [config[:hostname_prefix], random_string(4), random_number(2)].compact.join('-')
    l.hostname = generate_hostname
    l.shape = config[:shape]
    l.ssh_public_keys = pubkey
    l.cluster_name = generate_cluster_name
    l.initial_data_storage_size_in_gb = initial_data_storage_size_in_gb
    l.node_count = 1
    l.license_model = license_model
    l.subnet_id = config[:subnet_id]
    l.freeform_tags = process_freeform_tags(config[:freeform_tags])
  end
end
dbaas_node(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 502
def dbaas_node(instance_id)
  dbaas_api.list_db_nodes(
    config[:compartment_id],
    db_system_id: instance_id
  ).data
end
dbaas_vnic(node_ocid) click to toggle source
# File lib/kitchen/driver/oci.rb, line 509
def dbaas_vnic(node_ocid)
  dbaas_api.get_db_node(node_ocid).data
end
generate_cluster_name() click to toggle source
# File lib/kitchen/driver/oci.rb, line 492
def generate_cluster_name
  prefix = config[:hostname_prefix].split('-')[0]
  # 11 character limit for cluster_name in DBaaS
  if prefix.length >= 11
    prefix[0, 11]
  else
    [prefix, random_string(10 - prefix.length)].compact.join('-')
  end
end
generate_hostname() click to toggle source
# File lib/kitchen/driver/oci.rb, line 220
def generate_hostname
  prefix = config[:hostname_prefix]
  if instance_type == 'compute'
    [prefix, random_hostname(instance.name)].compact.join('-')
  elsif instance_type == 'dbaas'
    # 30 character limit for hostname in DBaaS
    if prefix.length >= 30
      [prefix[0, 26], 'db1'].compact.join('-')
    else
      [prefix, random_string(25 - prefix.length), 'db1'].compact.join('-')
    end
  end
end
generic_api(klass) click to toggle source

API setup #

# File lib/kitchen/driver/oci.rb, line 163
def generic_api(klass)
  api_prx = api_proxy
  if config[:use_instance_principals]
    sign = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new
    params = { signer: sign }
  else
    params = { config: oci_config }
  end
  params[:proxy_settings] = api_prx if api_prx
  klass.new(**params)
end
inject_powershell(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 367
def inject_powershell(state)
  data = winrm_ps1(state)
  config[:user_data] ||= []
  config[:user_data] << {
    type: 'x-shellscript',
    inline: data,
    filename: 'setup_winrm.ps1'
  }
end
instance_ip(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 203
def instance_ip(instance_id)
  if instance_type == 'compute'
    compute_instance_ip(instance_id)
  elsif instance_type == 'dbaas'
    dbaas_instance_ip(instance_id)
  end
end
instance_source_details() click to toggle source
# File lib/kitchen/driver/oci.rb, line 302
def instance_source_details
  OCI::Core::Models::InstanceSourceViaImageDetails.new(
    sourceType: 'image',
    imageId: config[:image_id]
  )
end
instance_type() click to toggle source
# File lib/kitchen/driver/oci.rb, line 124
def instance_type
  raise 'instance_type must be either compute or dbaas!' unless %w[compute dbaas].include?(config[:instance_type].downcase)

  config[:instance_type].downcase
end
launch_compute_instance(state) click to toggle source

Compute methods #

# File lib/kitchen/driver/oci.rb, line 262
def launch_compute_instance(state)
  request = compute_instance_request(state)
  response = comp_api.launch_instance(request)
  instance_id = response.data.id

  comp_api.get_instance(instance_id).wait_until(
    :lifecycle_state,
    OCI::Core::Models::Instance::LIFECYCLE_STATE_RUNNING
  )
  instance_id
end
launch_dbaas_instance() click to toggle source

DBaaS methods #

# File lib/kitchen/driver/oci.rb, line 419
def launch_dbaas_instance
  request = dbaas_launch_details
  response = dbaas_api.launch_db_system(request)
  instance_id = response.data.id

  dbaas_api.get_db_system(instance_id).wait_until(
    :lifecycle_state,
    OCI::Database::Models::DbSystem::LIFECYCLE_STATE_AVAILABLE,
    max_interval_seconds: 900,
    max_wait_seconds: 21600
  )
  instance_id
end
launch_instance(state) click to toggle source

Common methods #

# File lib/kitchen/driver/oci.rb, line 190
def launch_instance(state)
  if instance_type == 'compute'
    launch_compute_instance(state)
  elsif instance_type == 'dbaas'
    launch_dbaas_instance
  end
end
mime_parts(boundary) click to toggle source
# File lib/kitchen/driver/oci.rb, line 388
def mime_parts(boundary)
  msg = []
  config[:user_data].each do |m|
    msg << "--#{boundary}"
    msg << "Content-Disposition: attachment; filename=\"#{m[:filename]}\""
    msg << 'Content-Transfer-Encoding: 7bit'
    msg << "Content-Type: text/#{m[:type]}" << 'Mime-Version: 1.0' << ''
    msg << read_part(m) << ''
  end
  msg << "--#{boundary}--"
  msg
end
net_api() click to toggle source
# File lib/kitchen/driver/oci.rb, line 179
def net_api
  generic_api(OCI::Core::VirtualNetworkClient)
end
oci_config() click to toggle source

OCI config setup #

# File lib/kitchen/driver/oci.rb, line 133
def oci_config
  opts = {}
  opts[:config_file_location] = config[:oci_config_file] if config[:oci_config_file]
  opts[:profile_name] = config[:oci_profile_name] if config[:oci_profile_name]
  OCI::ConfigFileLoader.load_config(**opts)
end
preemptible_instance_config() click to toggle source
# File lib/kitchen/driver/oci.rb, line 309
def preemptible_instance_config
  OCI::Core::Models::PreemptibleInstanceConfigDetails.new(
    preemption_action:
      OCI::Core::Models::TerminatePreemptionAction.new(
        type: 'TERMINATE', preserve_boot_volume: true
      )
  )
end
proxy_config() click to toggle source
# File lib/kitchen/driver/oci.rb, line 140
def proxy_config
  if config[:proxy_url]
    URI.parse(config[:proxy_url])
  else
    URI.parse('http://').find_proxy
  end
end
pubkey() click to toggle source
# File lib/kitchen/driver/oci.rb, line 211
def pubkey
  if instance_type == 'compute'
    File.readlines(config[:ssh_keypath]).first.chomp
  elsif instance_type == 'dbaas'
    result = []
    result << File.readlines(config[:ssh_keypath]).first.chomp
  end
end
public_ip_allowed?() click to toggle source
# File lib/kitchen/driver/oci.rb, line 198
def public_ip_allowed?
  subnet = net_api.get_subnet(config[:subnet_id]).data
  !subnet.prohibit_public_ip_on_vnic
end
random_hostname(prefix) click to toggle source
# File lib/kitchen/driver/oci.rb, line 234
def random_hostname(prefix)
  "#{prefix}-#{random_string(6)}"
end
random_number(length) click to toggle source
# File lib/kitchen/driver/oci.rb, line 255
def random_number(length)
  Array.new(length) { ('0'..'9').to_a.sample }.join
end
random_password() click to toggle source
# File lib/kitchen/driver/oci.rb, line 238
def random_password
  if instance_type == 'compute'
    special_chars = %w[@ - ( ) .]
  elsif instance_type == 'dbaas'
    special_chars = %w[# _ -]
  end

  (Array.new(5) { special_chars.sample } +
   Array.new(5) { ('a'..'z').to_a.sample } +
   Array.new(5) { ('A'..'Z').to_a.sample } +
   Array.new(5) { ('0'..'9').to_a.sample }).shuffle.join
end
random_string(length) click to toggle source
# File lib/kitchen/driver/oci.rb, line 251
def random_string(length)
  Array.new(length) { ('a'..'z').to_a.sample }.join
end
read_part(part) click to toggle source
# File lib/kitchen/driver/oci.rb, line 377
def read_part(part)
  if part[:path]
    content = File.read part[:path]
  elsif part[:inline]
    content = part[:inline]
  else
    raise 'Invalid user data'
  end
  content.split("\n")
end
shape_config() click to toggle source
# File lib/kitchen/driver/oci.rb, line 318
def shape_config
  OCI::Core::Models::LaunchInstanceShapeConfigDetails.new(
    ocpus: config[:shape_config][:ocpus],
    memory_in_gbs: config[:shape_config][:memory_in_gbs],
    baseline_ocpu_utilization: config[:shape_config][:baseline_ocpu_utilization] || 'BASELINE_1_1'
  )
end
user_data() click to toggle source
# File lib/kitchen/driver/oci.rb, line 401
def user_data # rubocop:disable Metrics/MethodLength
  if config[:user_data].is_a? Array
    boundary = "MIMEBOUNDARY_#{random_string(20)}"
    msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"",
           'MIME-Version: 1.0', '']
    msg += mime_parts(boundary)
    txt = msg.join("\n") + "\n"
    gzip = Zlib::GzipWriter.new(StringIO.new)
    gzip << txt
    Base64.encode64(gzip.close.string).delete("\n")
  elsif config[:user_data].is_a? String
    Base64.encode64(config[:user_data]).delete("\n")
  end
end
vnic_attachments(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 341
def vnic_attachments(instance_id)
  att = comp_api.list_vnic_attachments(
    config[:compartment_id],
    instance_id: instance_id
  ).data

  raise 'Could not find any VNIC attachments' unless att.any?

  att
end
vnics(instance_id) click to toggle source
# File lib/kitchen/driver/oci.rb, line 335
def vnics(instance_id)
  vnic_attachments(instance_id).map do |att|
    net_api.get_vnic(att.vnic_id).data
  end
end
winrm_ps1(state) click to toggle source
# File lib/kitchen/driver/oci.rb, line 361
def winrm_ps1(state)
  filename = File.join(__dir__, %w[.. .. .. tpl setup_winrm.ps1.erb])
  tpl = ERB.new(File.read(filename))
  tpl.result(binding)
end