diff --git a/kitchen-oci.gemspec b/kitchen-oci.gemspec index e6ca3c0..d4dcd1a 100644 --- a/kitchen-oci.gemspec +++ b/kitchen-oci.gemspec @@ -18,7 +18,7 @@ lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "kitchen/driver/oci_version" -Gem::Specification.new do |spec| +Gem::Specification.new do |spec| # rubocop: disable Metrics/BlockLength spec.name = "kitchen-oci" spec.version = Kitchen::Driver::OCI_VERSION spec.authors = ["Stephen Pearson", "Justin Steele"] @@ -31,13 +31,20 @@ Gem::Specification.new do |spec| spec.files = `git ls-files`.split($/).grep(/LICENSE|^lib|^tpl/) spec.executables = [] spec.require_paths = ["lib"] - + spec.metadata = { + "bug_tracker_uri" => "https://github.com/stephenpearson/kitchen-oci/issues", + "changelog_uri" => "https://github.com/stephenpearson/kitchen-oci/blob/master/CHANGELOG.md", + "documentation_uri" => "https://github.com/stephenpearson/kitchen-oci/blob/master/README.md", + "homepage_uri" => "https://github.com/stephenpearson/kitchen-oci", + "source_code_uri" => "https://github.com/stephenpearson/kitchen-oci", + "rubygems_mfa_required" => "true", + } spec.add_dependency "oci", "~> 2.18" spec.add_dependency "test-kitchen" - spec.add_development_dependency "bundler" spec.add_development_dependency "chefstyle" spec.add_development_dependency "pry" spec.add_development_dependency "rake" spec.add_development_dependency "rspec" -end + spec.add_development_dependency "yard" +end # rubocop: enable Metrics/BlockLength diff --git a/lib/kitchen/driver/oci.rb b/lib/kitchen/driver/oci.rb index b058661..2d289e7 100644 --- a/lib/kitchen/driver/oci.rb +++ b/lib/kitchen/driver/oci.rb @@ -32,10 +32,11 @@ module Driver # Oracle OCI driver for Kitchen. # # @author Stephen Pearson - class Oci < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength + class Oci < Kitchen::Driver::Base require_relative "oci_version" - require_relative "oci/models" - require_relative "oci/volumes" + require_relative "oci/mixin/actions" + require_relative "oci/mixin/models" + require_relative "oci/mixin/volumes" plugin_version Kitchen::Driver::OCI_VERSION kitchen_driver_api_version 2 @@ -115,9 +116,10 @@ def self.validation_error(message, driver) raise UserError, "#{driver.class}<#{driver.instance.name}>#config#{message}" end - include Kitchen::Driver::Oci::Models - include Kitchen::Driver::Oci::Volumes - + # Creates an instance. + # (see Kitchen::Driver::Base#create) + # + # @param state [Hash] (see Kitchen::StateFile) def create(state) return if state[:server_id] @@ -130,6 +132,10 @@ def create(state) reboot(state, inst) end + # Destorys an instance. + # (see Kitchen::Driver::Base#destroy) + # + # @param state [Hash] (see Kitchen::StateFile) def destroy(state) return unless state[:server_id] @@ -141,51 +147,20 @@ def destroy(state) private + include Kitchen::Driver::Oci::Mixin::Actions + include Kitchen::Driver::Oci::Mixin::Models + include Kitchen::Driver::Oci::Mixin::Volumes + + # Creates the OCI config and API clients. + # + # @param action [Symbol] the name of the method that called this method. + # @return [Oci::Config, Oci::Api] def auth(action) oci = Oci::Config.new(config) api = Oci::Api.new(oci.config, config) oci.compartment if action == :create [oci, api] end - - def launch(state, inst) - state_details = inst.launch - state.merge!(state_details) - instance.transport.connection(state).wait_until_ready - end - - def process_post_script(state) - return if config[:post_create_script].nil? - - info("Running post create script") - script = config[:post_create_script] - instance.transport.connection(state).execute(script) - end - - def reboot(state, inst) - return unless config[:post_create_reboot] - - instance.transport.connection(state).close - inst.reboot - instance.transport.connection(state).wait_until_ready - end - - def detatch_and_delete_volumes(state, oci, api) - return unless state[:volumes] - - bls = Blockstorage.new(config: config, state: state, oci: oci, api: api, action: :destroy, logger: instance.logger) - state[:volume_attachments].each { |att| bls.detatch_volume(att) } - state[:volumes].each { |vol| bls.delete_volume(vol) } - end - - def terminate(state, inst) - instance.transport.connection(state).close - inst.terminate - if state[:ssh_key] - FileUtils.rm_f(state[:ssh_key]) - FileUtils.rm_f("#{state[:ssh_key]}.pub") - end - end end end end diff --git a/lib/kitchen/driver/oci/api.rb b/lib/kitchen/driver/oci/api.rb index fe29761..d34a3e9 100644 --- a/lib/kitchen/driver/oci/api.rb +++ b/lib/kitchen/driver/oci/api.rb @@ -20,49 +20,66 @@ module Kitchen module Driver class Oci - # Api class that defines the various API classes used to interact with OCI + # Defines the various API classes used to interact with OCI. + # + # @author Justin Steele class Api def initialize(oci_config, config) @oci_config = oci_config @config = config end - # - # The config used to authenticate to OCI + # The config used to authenticate to OCI. # # @return [OCI::Config] - # attr_reader :oci_config - # - # The config provided by the driver + # The config provided by the driver. # # @return [Kitchen::LazyHash] - # attr_reader :config + # Creates a Compute API client. + # + # @return [OCI::Core::ComputeClient] def compute generic_api(OCI::Core::ComputeClient) end + # Creates a Network API client. + # + # @return [OCI::Core::VirtualNetworkClient] def network generic_api(OCI::Core::VirtualNetworkClient) end + # Creates a Database API client. + # + # @return [OCI::Core::DatabaseClient] def dbaas generic_api(OCI::Database::DatabaseClient) end + # Creates an Identity API client. + # + # @return [OCI::Core::IdentityClient] def identity generic_api(OCI::Identity::IdentityClient) end + # Creates a Blockstorage API client. + # + # @return [OCI::Core::BlockstorageClient] def blockstorage generic_api(OCI::Core::BlockstorageClient) end private + # Instantiates the specified client class. + # + # @param klass [Class] The client class to instantiate. + # @return [Object] an instance of klass. def generic_api(klass) params = {} params[:proxy_settings] = api_proxy if api_proxy @@ -73,6 +90,9 @@ def generic_api(klass) klass.new(**params) end + # Determines the signing method if one is specified. + # + # @return [OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner, OCI::Auth::Signers::SecurityTokenSigner] an instance of the specified token signer. def signer if config[:use_instance_principals] OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new @@ -81,6 +101,9 @@ def signer end end + # Creates the token signer with a provided key. + # + # @return [OCI::Auth::Signers::SecurityTokenSigner] def token_signer pkey_content = oci_config.key_content || File.read(oci_config.key_file).strip pkey = OpenSSL::PKey::RSA.new(pkey_content, oci_config.pass_phrase) @@ -89,6 +112,9 @@ def token_signer OCI::Auth::Signers::SecurityTokenSigner.new(token, pkey) end + # Parse any specified proxy from either the kitchen config or the environment. + # + # @return [URI] a parsed proxy host. def proxy_config if config[:proxy_url] URI.parse(config[:proxy_url]) @@ -97,6 +123,9 @@ def proxy_config end end + # Create the proxy settings for the OCI API. + # + # @return [OCI::ApiClientProxySettings] def api_proxy prx = proxy_config return unless prx diff --git a/lib/kitchen/driver/oci/blockstorage.rb b/lib/kitchen/driver/oci/blockstorage.rb index 28879dd..9e9c182 100644 --- a/lib/kitchen/driver/oci/blockstorage.rb +++ b/lib/kitchen/driver/oci/blockstorage.rb @@ -19,7 +19,9 @@ module Kitchen module Driver class Oci - # generic class for blockstorage + # Base class for blockstorage models. + # + # @author Justin Steele class Blockstorage < Oci # rubocop:disable Metrics/ClassLength require_relative "api" require_relative "config" @@ -38,53 +40,45 @@ def initialize(opts = {}) oci.compartment if opts[:action] == :create end - # - # The config provided by the driver + # The config provided by the driver. # # @return [Kitchen::LazyHash] - # attr_accessor :config - # - # The definition of the state of the instance from the statefile + # The definition of the state of the instance from the statefile. # # @return [Hash] - # attr_accessor :state - # - # The config object that contains properties of the authentication to OCI + # The config object that contains properties of the authentication to OCI. # # @return [Kitchen::Driver::Oci::Config] - # attr_accessor :oci - # - # The API object that contains each of the authenticated clients for interfacing with OCI + # The API object that contains each of the authenticated clients for interfacing with OCI. # # @return [Kitchen::Driver::Oci::Api] - # attr_accessor :api - # - # The instance of Kitchen::Logger in use by the active Kitchen::Instance + # The instance of Kitchen::Logger in use by the active Kitchen::Instance. # # @return [Kitchen::Logger] - # attr_accessor :logger - # The definition of the state of a volume + # The definition of the state of a volume. # # @return [Hash] - # attr_accessor :volume_state - # The definition of the state of a volume attachment + # The definition of the state of a volume attachment. # # @return [Hash] - # attr_accessor :volume_attachment_state + # Create the volume as specified in the kitchen config. + # + # @param volume [Hash] the state of the current volume being created. + # @return [Array(OCI::Core::Models::Volume, Hash)] returns the actual volume response from OCI for the created volume and the state hash. def create_volume(volume) logger.info("Creating <#{volume[:name]}>...") result = api.blockstorage.create_volume(volume_details(volume)) @@ -93,6 +87,10 @@ def create_volume(volume) [response, final_state(response)] end + # Clones the specified volume. + # + # @param volume [Hash] the state of the current volume being cloned. + # @return [Array(OCI::Core::Models::Volume, Hash)] returns the actual volume response from OCI for the cloned volume and the state hash. def create_clone_volume(volume) clone_volume_name = clone_volume_display_name(volume[:volume_id]) logger.info("Creating <#{clone_volume_name}>...") @@ -102,6 +100,11 @@ def create_clone_volume(volume) [response, final_state(response)] end + # Attaches the volume to the instance. + # + # @param volume_details [OCI::Core::Models::Volume] + # @param server_id [String] the ocid of the compute instance we are attaching the volume to. + # @return [Hash] the updated state hash. def attach_volume(volume_details, server_id, volume_config) logger.info("Attaching <#{volume_details.display_name}>...") attach_volume = api.compute.attach_volume(attachment_details(volume_details, server_id, volume_config)) @@ -110,6 +113,9 @@ def attach_volume(volume_details, server_id, volume_config) final_state(response) end + # Deletes the specified volume. + # + # @param volume [Hash] the state of the current volume being deleted from the state file. def delete_volume(volume) logger.info("Deleting <#{volume[:display_name]}>...") api.blockstorage.delete_volume(volume[:id]) @@ -118,6 +124,9 @@ def delete_volume(volume) logger.info("Finished deleting <#{volume[:display_name]}>.") end + # Detaches the specified volume. + # + # @param volume_attachment [Hash] the state of the current volume being deleted from the state file. def detatch_volume(volume_attachment) logger.info("Detaching <#{attachment_name(volume_attachment)}>...") api.compute.detach_volume(volume_attachment[:id]) @@ -126,6 +135,10 @@ def detatch_volume(volume_attachment) logger.info("Finished detaching <#{attachment_name(volume_attachment)}>.") end + # Adds the volume and attachment info into the state. + # + # @param response [OCI::Core::Models::Volume, OCI::Core::Models::VolumeAttachment] The response from volume creation or attachment. + # @return [Hash] def final_state(response) case response when OCI::Core::Models::Volume @@ -137,16 +150,26 @@ def final_state(response) private + # The response from creating a volume. + # + # @return [OCI::Core::Models::Volume] def volume_response(volume_id) api.blockstorage.get_volume(volume_id) .wait_until(:lifecycle_state, OCI::Core::Models::Volume::LIFECYCLE_STATE_AVAILABLE).data end + # The response from attaching a volume. + # + # @return [OCI::Core::Models::VolumeAttachment] def attachment_response(attachment_id) api.compute.get_volume_attachment(attachment_id) .wait_until(:lifecycle_state, OCI::Core::Models::VolumeAttachment::LIFECYCLE_STATE_ATTACHED).data end + # The details of the volume that is being created. + # + # @param volume [Hash] the state of the current volume being created. + # @return [OCI::Core::Models::CreateVolumeDetails] def volume_details(volume) OCI::Core::Models::CreateVolumeDetails.new( compartment_id: oci.compartment, @@ -158,6 +181,11 @@ def volume_details(volume) ) end + # The details of a volume that is being created as a clone of an existing volume. + # + # @param volume [Hash] the state of the current volume being cloned. + # @param clone_volume_name [String] the desired name of the new volume. + # @return [OCI::Core::Models::CreateVolumeDetails] def volume_clone_details(volume, clone_volume_name) OCI::Core::Models::CreateVolumeDetails.new( compartment_id: oci.compartment, @@ -170,21 +198,37 @@ def volume_clone_details(volume, clone_volume_name) ) end + # Returns a somewhat prettier display name for the volume attachment. + # + # @param attachment [Hash] the state of the current volume attachment being created. + # @return [String] def attachment_name(attachment) attachment[:display_name].gsub(/(?:paravirtual|iscsi)-/, "") end + # Returns the operating system from the instance. + # + # @param server_id [String] the ocid of the compute instance. + # @return [String] def server_os(server_id) image_id = api.compute.get_instance(server_id).data.image_id api.compute.get_image(image_id).data.operating_system end + # Adds the ocid and display name of the volume to the state. + # + # @param response [OCI::Core::Models::Volume] + # @return [Hash] def final_volume_state(response) volume_state.store(:id, response.id) volume_state.store(:display_name, response.display_name) volume_state end + # Appends the (Clone) string to the display name of the block volume that is being cloned. + # + # @param volume_id [String] the ocid of the volume being cloned. + # @return [String] the display name of the cloned volume. def clone_volume_display_name(volume_id) "#{api.blockstorage.get_volume(volume_id).data.to_hash[:displayName]} (Clone)" end diff --git a/lib/kitchen/driver/oci/config.rb b/lib/kitchen/driver/oci/config.rb index 6ba1cca..4821d03 100644 --- a/lib/kitchen/driver/oci/config.rb +++ b/lib/kitchen/driver/oci/config.rb @@ -22,20 +22,23 @@ module Kitchen module Driver class Oci - # Config class that defines the oci config that will be used for the API calls + # Config class that defines the oci config that will be used for the API calls. + # + # @author Justin Steele class Config def initialize(driver_config) setup_driver_config(driver_config) @config = oci_config end - # - # The config used to authenticate to OCI + # The config used to authenticate to OCI. # # @return [OCI::Config] - # attr_reader :config + # Creates a new instance of OCI::Config to be used to authenticate to OCI. + # + # @return [OCI::Config] def oci_config # OCI::Config is missing this OCI::Config.class_eval { attr_accessor :security_token_file } if @driver_config[:use_token_auth] @@ -46,6 +49,12 @@ def oci_config conf end + # The ocid of the compartment where the Kitchen instance will be created. + # * If compartment_id is specified in the kitchen.yml, that will be returned. + # * If compartment_name is specified in the kitchen.yml, lookup with the Identity API to find the ocid by the compartment name. + # + # @return [String] the ocid of the compartment where instances will be created. + # @raise [StandardError] if neither compartment_id nor compartment_name are specified OR if lookup by name fails to find a match. def compartment @compartment ||= @compartment_id return @compartment if @compartment @@ -58,12 +67,19 @@ def compartment private + # Sets up instance variables from the driver config (parsed kitchen.yml) and compartment. + # + # @param config [Hash] the parsed config from the kitchen.yml. def setup_driver_config(config) @driver_config = config @compartment_id = config[:compartment_id] @compartment_name = config[:compartment_name] end + # Creates a new instance of OCI::Config either by loading the config from a file or returning a new instance that will be set. + # + # @param opts [Hash] + # @return [OCI::Config] def config_loader(opts = {}) # this is to accommodate old versions of ruby that do not have a compact method on a Hash opts.reject! { |_, v| v.nil? } @@ -72,6 +88,9 @@ def config_loader(opts = {}) OCI::Config.new end + # Returns the ocid of the tenancy from either the provided ocid or from your instance principals. + # + # @return [String] def tenancy if @driver_config[:use_instance_principals] sign = OCI::Auth::Signers::InstancePrincipalsSecurityTokenSigner.new @@ -81,11 +100,17 @@ def tenancy end end + # Looks up the compartment ocid by name by recursively querying the list of compartments with the Identity API. + # + # @return [String] the ocid of the compartment. def compartment_id_by_name(name) api = Oci::Api.new(config, @driver_config).identity all_compartments(api, config.tenancy).select { |c| c.name == name }&.first&.id end + # Pages through all of the compartments in the tenancy. This has to be a recursive process because the list_compartments API only returns 99 entries at a time. + # + # @return [Array] An array of OCI::Identity::Models::Compartment def all_compartments(api, tenancy, compartments = [], page = nil) current_compartments = api.list_compartments(tenancy, page: page) next_page = current_compartments.next_page diff --git a/lib/kitchen/driver/oci/instance.rb b/lib/kitchen/driver/oci/instance.rb index 940e1e9..cd584f6 100644 --- a/lib/kitchen/driver/oci/instance.rb +++ b/lib/kitchen/driver/oci/instance.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -# # Author:: Justin Steele () # # Copyright (C) 2024, Stephen Pearson @@ -20,7 +19,9 @@ module Kitchen module Driver class Oci - # generic class for instance models + # Base class for instance models. + # + # @author Justin Steele class Instance < Oci # rubocop:disable Metrics/ClassLength require_relative "api" require_relative "config" @@ -39,41 +40,35 @@ def initialize(opts = {}) @logger = opts[:logger] end - # - # The config provided by the driver + # The config provided by the driver. # # @return [Kitchen::LazyHash] - # attr_accessor :config - # - # The definition of the state of the instance from the statefile + # The definition of the state of the instance from the statefile. # # @return [Hash] - # attr_accessor :state - # - # The config object that contains properties of the authentication to OCI + # The config object that contains properties of the authentication to OCI. # # @return [Kitchen::Driver::Oci::Config] - # attr_accessor :oci - # - # The API object that contains each of the authenticated clients for interfacing with OCI + # The API object that contains each of the authenticated clients for interfacing with OCI. # # @return [Kitchen::Driver::Oci::Api] - # attr_accessor :api - # - # The instance of Kitchen::Logger in use by the active Kitchen::Instance + # The instance of Kitchen::Logger in use by the active Kitchen::Instance. # # @return [Kitchen::Logger] - # attr_accessor :logger + # Adds the instance info into the state. + # + # @param state [Hash] The state from kitchen. + # @return [Hash] def final_state(state, instance_id) state.store(:server_id, instance_id) state.store(:hostname, instance_ip(instance_id)) @@ -82,6 +77,9 @@ def final_state(state, instance_id) private + # Calls all of the setter methods for the self. + # + # @return [OCI::Core::Models::LaunchInstanceDetails, OCI::Database::Models::LaunchDbSystemDetails] the fully populated launch details for the specific instance type. def launch_instance_details launch_methods = [] self.class.ancestors.reverse.select { |m| m.is_a?(Module) && m.name.start_with?("#{self.class.superclass}::") }.each do |klass| @@ -91,11 +89,17 @@ def launch_instance_details launch_details end + # Checks if public IP addresses are allowed in the specified subnet. + # + # @return [Boolean] def public_ip_allowed? subnet = api.network.get_subnet(config[:subnet_id]).data !subnet.prohibit_public_ip_on_vnic end + # Returns the location of the public ssh key. + # + # @return [String] def public_key_file if config[:ssh_keygen] "#{config[:kitchen_root]}/.kitchen/.ssh/#{config[:instance_name]}_rsa.pub" @@ -104,10 +108,14 @@ def public_key_file end end + # Returns the name of the private key file. + # + # @return [String] def private_key_file public_key_file.gsub(".pub", "") end + # Generates an RSA key pair to be used to SSH to the instance and updates the state with the full path to the private key. def gen_key_pair FileUtils.mkdir_p("#{config[:kitchen_root]}/.kitchen/.ssh") rsa_key = OpenSSL::PKey::RSA.new(4096) @@ -116,16 +124,25 @@ def gen_key_pair state.store(:ssh_key, private_key_file) end + # Writes the private key. + # + # @param rsa_key [OpenSSL::PKey::RSA] the generated RSA key. def write_private_key(rsa_key) File.open(private_key_file, "wb") { |k| k.write(rsa_key.to_pem) } File.chmod(0600, private_key_file) end + # Writes the encoded private key as a public key. + # + # @param rsa_key [OpenSSL::PKey::RSA] the generated RSA key. def write_public_key(rsa_key) File.open(public_key_file, "wb") { |k| k.write("ssh-rsa #{encode_private_key(rsa_key)} #{config[:instance_name]}") } File.chmod(0600, public_key_file) end + # Encodes the private key. + # + # @param rsa_key [OpenSSL::PKey::RSA] the generated RSA key. def encode_private_key(rsa_key) prefix = "#{[7].pack("N")}ssh-rsa" exponent = rsa_key.e.to_s(0) @@ -133,6 +150,10 @@ def encode_private_key(rsa_key) ["#{prefix}#{exponent}#{modulus}"].pack("m0") end + # Generates a random password. + # + # @param special_chars [Array] an array of special characters to include in the random password. + # @return [String] def random_password(special_chars) (Array.new(5) { special_chars.sample } + Array.new(5) { ("a".."z").to_a.sample } + @@ -140,14 +161,25 @@ def random_password(special_chars) Array.new(5) { ("0".."9").to_a.sample }).shuffle.join end + # Generates a random string of letters. + # + # @param length [Integer] how many characters to randomize. + # @return [String] def random_string(length) Array.new(length) { ("a".."z").to_a.sample }.join end + # Generates a random string of numbers. + # + # @param length [Integer] how many numbers to randomize. + # @return [String] def random_number(length) Array.new(length) { ("0".."9").to_a.sample }.join end + # Parses freeform tags to be added to the instance by the setter method. + # + # @return [Hash] def process_freeform_tags tags = %w{run_list policyfile} fft = config[:freeform_tags] @@ -161,6 +193,9 @@ def process_freeform_tags fft end + # Encodes specified user_data to be added to cloud-init. + # + # @return [Base64] def user_data case config[:user_data] when Array @@ -170,6 +205,9 @@ def user_data end end + # GZips processed user_data prior to being encoded to allow for multi-part inclusions. + # + # @return [Zlib::GzipWriter] def multi_part_user_data boundary = "MIMEBOUNDARY_#{random_string(20)}" msg = ["Content-Type: multipart/mixed; boundary=\"#{boundary}\"", @@ -180,6 +218,10 @@ def multi_part_user_data gzip << txt end + # Joins all of the bits provided by each itema in user_data with the provided boundary and content headers. + # + # @param boundary [String] + # @return [Array] def mime_parts(boundary) msg = [] config[:user_data].each do |m| @@ -193,6 +235,10 @@ def mime_parts(boundary) msg end + # Reads either the specified file or the text provided inline. + # + # @param part [Hash] the current item in the user_data hash being processed. + # @return [Array] def read_part(part) if part[:path] content = File.read part[:path] diff --git a/lib/kitchen/driver/oci/instance/common.rb b/lib/kitchen/driver/oci/instance/common.rb index 67c1416..fa35c88 100644 --- a/lib/kitchen/driver/oci/instance/common.rb +++ b/lib/kitchen/driver/oci/instance/common.rb @@ -20,24 +20,31 @@ module Kitchen module Driver class Oci class Instance - # setter methods that populate launch details common to all instance models + # Setter methods that populate launch details common to all instance models. + # + # @author Justin Steele module CommonLaunchDetails + # Assigns the ocid of the compartment to the launch details. def compartment_id launch_details.compartment_id = oci.compartment end + # Assigns the availability_domain to the launch details. def availability_domain launch_details.availability_domain = config[:availability_domain] end + # Assigns the defined_tags to the launch details. def defined_tags launch_details.defined_tags = config[:defined_tags] end + # Assigns the shape to the launch_details. def shape launch_details.shape = config[:shape] end + # Assigns the freeform_tags to the launch_details. def freeform_tags launch_details.freeform_tags = process_freeform_tags end @@ -46,4 +53,3 @@ def freeform_tags end end end - diff --git a/lib/kitchen/driver/oci/instance/compute.rb b/lib/kitchen/driver/oci/instance/compute.rb index 19c9267..47ac1c0 100644 --- a/lib/kitchen/driver/oci/instance/compute.rb +++ b/lib/kitchen/driver/oci/instance/compute.rb @@ -20,14 +20,20 @@ module Kitchen module Driver class Oci class Instance - # setter methods that populate the details of OCI::Core::Models::LaunchInstanceDetails + # Setter methods that populate the details of OCI::Core::Models::LaunchInstanceDetails. + # + # @author Justin Steele module ComputeLaunchDetails + # Assigns the display_name and create_vnic_details to the launch_details. + # * display_name is either the literal display_name provided in the kitchen config or a randomly generated one. + # * create_vnic_details is a populated instance of OCI::Core::Models::CreateVnicDetails. def hostname_display_name display_name = config[:display_name] || hostname launch_details.display_name = display_name launch_details.create_vnic_details = create_vnic_details(display_name) end + # Adds the preemptible_instance_config property tot he launch_details by creating a new instance of OCI::Core::Models::PreemptibleInstanceConfigDetails. def preemptible_instance_config return unless config[:preemptible_instance] @@ -39,6 +45,7 @@ def preemptible_instance_config ) end + # Adds the shape_config property to the launch_details by creating a new instance of OCI::Core::Models::LaunchInstanceShapeConfigDetails. def shape_config return if config[:shape_config].empty? @@ -49,10 +56,12 @@ def shape_config ) end + # Adds the capacity_reservation_id property to the launch_details if an ocid is provided. def capacity_reservation launch_details.capacity_reservation_id = config[:capacity_reservation_id] end + # Adds the agent_config property to the launch_details. def agent_config launch_details.agent_config = OCI::Core::Models::LaunchInstanceAgentConfigDetails.new( are_all_plugins_disabled: config[:all_plugins_disabled], @@ -61,6 +70,7 @@ def agent_config ) end + # Adds the source_details property to the launch_details for an instance that is being created from an image. def instance_source_via_image return if config[:boot_volume_id] @@ -71,6 +81,7 @@ def instance_source_via_image ) end + # Adds the source_details property to the launch_details for an instance that is being created from a boot volume. def instance_source_via_boot_volume return unless config[:boot_volume_id] @@ -80,6 +91,7 @@ def instance_source_via_boot_volume ) end + # Adds the metadata property to the launch_details. def instance_metadata launch_details.metadata = metadata end diff --git a/lib/kitchen/driver/oci/instance/database.rb b/lib/kitchen/driver/oci/instance/database.rb index 25c40fd..1f67e38 100644 --- a/lib/kitchen/driver/oci/instance/database.rb +++ b/lib/kitchen/driver/oci/instance/database.rb @@ -20,39 +20,49 @@ module Kitchen module Driver class Oci class Instance - # setter methods that populate the details of OCI::Database::Models::CreateDatabaseDetails + # Setter methods that populate the details of OCI::Database::Models::CreateDatabaseDetails. + # + # @author Justin Steele module DatabaseDetails + # Adds the database_software_image_id property to the database_details if provided. def database_software_image return unless config[:dbaas][:db_software_image_id] database_details.database_software_image_id = config[:dbaas][:db_software_image_id] end + # Adds the character_set property to the database_details. def character_set database_details.character_set = config[:dbaas][:character_set] ||= "AL32UTF8" end + # Adds the ncharacter_set property to the database_details. def ncharacter_set database_details.ncharacter_set = config[:dbaas][:ncharacter_set] ||= "AL16UTF16" end + # Adds the db_workload property to the database details. def db_workload workload = config[:dbaas][:db_workload] ||= OCI::Database::Models::CreateDatabaseDetails::DB_WORKLOAD_OLTP database_details.db_workload = workload end + # Adds the admin_password property to the database details. def admin_password database_details.admin_password = config[:dbaas][:admin_password] ||= random_password(%w{# _ -}) end + # Adds the db_name property to the database_details. def db_name database_details.db_name = config[:dbaas][:db_name] ||= "dbaas1" end + # Adds the pdb_name property to the database_details. def pdb_name database_details.pdb_name = config[:dbaas][:pdb_name] end + # Adds the db_backup_config property to the database_details by creating a new instance of OCI::Database::Models::DbBackupConfig. def db_backup_config database_details.db_backup_config = OCI::Database::Models::DbBackupConfig.new.tap do |l| l.auto_backup_enabled = false @@ -60,6 +70,7 @@ def db_backup_config database_details end + # Adds the defined tags property to the database_details. def db_defined_tags database_details.defined_tags = config[:defined_tags] end diff --git a/lib/kitchen/driver/oci/instance/db_home.rb b/lib/kitchen/driver/oci/instance/db_home.rb index f9f7ddb..d8f44a2 100644 --- a/lib/kitchen/driver/oci/instance/db_home.rb +++ b/lib/kitchen/driver/oci/instance/db_home.rb @@ -20,28 +20,36 @@ module Kitchen module Driver class Oci class Instance - # setter methods that populate the details of OCI::Database::Models::CreateDbHomeDetails + # Setter methods that populate the details of OCI::Database::Models::CreateDbHomeDetails. + # + # @author Justin Steele module DbHomeDetails + # Adds the database property to the db_home_details. def database db_home_details.database = database_details end + # Adds the db_version property to the db_home_details. + # @raise [StandardError] if a version has not been provided. def db_version raise "db_version cannot be nil!" if config[:dbaas][:db_version].nil? db_home_details.db_version = config[:dbaas][:db_version] end + # Adds the display_name property to db_home_details. def db_home_display_name db_home_details.display_name = ["dbhome", random_number(10)].compact.join end + # Adds the database_software_image_id to the db_home_details. def db_home_software_image return unless config[:dbaas][:db_software_image_id] db_home_details.database_software_image_id = config[:dbaas][:db_software_image_id] end + # Adds the defined_tags to the db_home_details. def db_home_defined_tags db_home_details.defined_tags = config[:defined_tags] end diff --git a/lib/kitchen/driver/oci/instance/dbaas.rb b/lib/kitchen/driver/oci/instance/dbaas.rb index 3dd6546..1263f8d 100644 --- a/lib/kitchen/driver/oci/instance/dbaas.rb +++ b/lib/kitchen/driver/oci/instance/dbaas.rb @@ -23,69 +23,85 @@ module Kitchen module Driver class Oci class Instance - # setter methods that populate the details of OCI::Database::Models::LaunchDbSystemDetails + # Setter methods that populate the details of OCI::Database::Models::LaunchDbSystemDetails. + # + # @author Justin Steele module DbaasLaunchDetails include DatabaseDetails include DbHomeDetails # # TODO: add support for the #domain property # + + # Adds the db_home property to the launch_details. def db_home launch_details.db_home = db_home_details end + # Adds the subnet_id property to the launch_details. def subnet_id launch_details.subnet_id = config[:subnet_id] end + # Adds the nsg_ids property to the launch_details. def nsg_ids launch_details.nsg_ids = config[:nsg_ids] end + # Adds the hostname property to the launch_details. + # The hostname must begin with an alphabetic character, and can contain alphanumeric characters and hyphens (-). + # The maximum length of the hostname is 16 characters. def hostname - # The hostname must begin with an alphabetic character, and can contain alphanumeric characters and hyphens (-). - # The maximum length of the hostname is 16 characters long_name = [hostname_prefix, long_hostname_suffix].compact.join("-") trimmed_name = [hostname_prefix[0, 12], random_string(3)].compact.join("-") launch_details.hostname = [long_name, trimmed_name].min { |l, t| l.size <=> t.size } end + # Adds the display_name property to the launch details. + # The user-friendly name for the DB system. The name does not have to be unique. def display_name - # The user-friendly name for the DB system. The name does not have to be unique. launch_details.display_name = [config[:hostname_prefix], random_string(4), random_number(2)].compact.join("-") end + # Adds the node_count property to the launch_details. def node_count launch_details.node_count = 1 end + # Adds the ssh_public_keys property to the launch_details. def pubkey result = [] result << read_public_key launch_details.ssh_public_keys = result end + # Adds the cpu_core_count property to the launch_details. def cpu_core_count launch_details.cpu_core_count = config[:dbaas][:cpu_core_count] ||= 2 end + # Adds the license_model property to the launch_details. def license_model license = config[:dbaas][:license_model] ||= OCI::Database::Models::DbSystem::LICENSE_MODEL_BRING_YOUR_OWN_LICENSE launch_details.license_model = license end + # Adds the initial_data_size_in_gb property to the launch_details. def initial_data_storage_size_in_gb launch_details.initial_data_storage_size_in_gb = config[:dbaas][:initial_data_storage_size_in_gb] ||= 256 end + # Adds the database_edition property to the launch_details. def database_edition db_edition = config[:dbaas][:database_edition] ||= OCI::Database::Models::DbSystem::DATABASE_EDITION_ENTERPRISE_EDITION launch_details.database_edition = db_edition end + # Adds the cluster_name property to the launch_details. + # 11 character limit for cluster_name in DBaaS. def cluster_name prefix = config[:hostname_prefix].split("-")[0] - # 11 character limit for cluster_name in DBaaS + cn = if prefix.length >= 11 prefix[0, 11] else @@ -98,4 +114,3 @@ def cluster_name end end end - diff --git a/lib/kitchen/driver/oci/mixin/actions.rb b/lib/kitchen/driver/oci/mixin/actions.rb new file mode 100644 index 0000000..0a233a2 --- /dev/null +++ b/lib/kitchen/driver/oci/mixin/actions.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# +# Author:: Justin Steele () +# +# Copyright (C) 2025, Stephen Pearson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Kitchen + module Driver + class Oci + module Mixin + # Actions that can be performed on an instance. + # + # @author Justin Steele + module Actions + # Launches an instance. + # + # @param state [Hash] (see Kitchen::StateFile) + # @param inst [Class] the specific class of instance being launched. + def launch(state, inst) + state_details = inst.launch + state.merge!(state_details) + instance.transport.connection(state).wait_until_ready + end + + # Executes the post script on the instance. + # + # @param state [Hash] (see Kitchen::StateFile) + def process_post_script(state) + return if config[:post_create_script].nil? + + info("Running post create script") + script = config[:post_create_script] + instance.transport.connection(state).execute(script) + end + + # Reboots an instance. + # + # @param state [Hash] (see Kitchen::StateFile) + # @param inst [Class] the specific class of instance being rebooted. + def reboot(state, inst) + return unless config[:post_create_reboot] + + instance.transport.connection(state).close + inst.reboot + instance.transport.connection(state).wait_until_ready + end + + # Terminates an instance. + # + # @param state [Hash] (see Kitchen::StateFile) + # @param inst [Class] the specific class of instance being launched. + def terminate(state, inst) + instance.transport.connection(state).close + inst.terminate + if state[:ssh_key] + FileUtils.rm_f(state[:ssh_key]) + FileUtils.rm_f("#{state[:ssh_key]}.pub") + end + end + end + end + end + end +end diff --git a/lib/kitchen/driver/oci/mixin/models.rb b/lib/kitchen/driver/oci/mixin/models.rb new file mode 100644 index 0000000..bb003b9 --- /dev/null +++ b/lib/kitchen/driver/oci/mixin/models.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +# +# Author:: Justin Steele () +# +# Copyright (C) 2024, Stephen Pearson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Kitchen + module Driver + class Oci + module Mixin + # Instantiates the class of the specified model. + # + # @author Justin Steele + module Models + require_relative "../instance" + require_relative "../blockstorage" + + # Instantiates an instance model. + # + # @param config [Kitchen::LazyHash] The config provided by the driver. + # @param state [Hash] (see Kitchen::StateFile) + # @param oci [Kitchen::Driver::Oci::Config] a populated OCI config class. + # @param api [Kitchen::Driver::Oci::Api] an authenticated API class. + # @return [Class] the instantiated model class. + def instance_class(config, state, oci, api, action) + Oci::Models.const_get(config[:instance_type].capitalize).new(config: config, state: state, oci: oci, api: api, action: action, logger: instance.logger) + end + + # Instantiates a blockstorage volume model. + # + # @param type [String] The type of volume that will be created. + # @param state [Hash] (see Kitchen::StateFile) + # @param oci [Kitchen::Driver::Oci::Config] a populated OCI config class. + # @param api [Kitchen::Driver::Oci::Api] an authenticated API class. + # @return [Class] the instantiated model class. + def volume_class(type, config, state, oci, api) + Oci::Models.const_get(volume_attachment_type(type)).new(config: config, state: state, oci: oci, api: api, logger: instance.logger) + end + + private + + # Returns the class name of the attachment type. + # + # @param type [String, nil] + # @return [String] + def volume_attachment_type(type) + if type.nil? + "Paravirtual" + else + type.capitalize + end + end + end + end + end + end +end diff --git a/lib/kitchen/driver/oci/mixin/volumes.rb b/lib/kitchen/driver/oci/mixin/volumes.rb new file mode 100644 index 0000000..c11f601 --- /dev/null +++ b/lib/kitchen/driver/oci/mixin/volumes.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# +# Author:: Justin Steele () +# +# Copyright (C) 2024, Stephen Pearson +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Kitchen + module Driver + class Oci + module Mixin + # Mixins for working with volumes and attachments. + # + # @author Justin Steele + module Volumes + # Create and attach volumes. + # + # @param config [Kitchen::LazyHash] The config provided by the driver. + # @param state [Hash] (see Kitchen::StateFile) + # @param oci [Kitchen::Driver::Oci::Config] a populated OCI config class. + # @param api [Kitchen::Driver::Oci::Api] an authenticated API class. + def create_and_attach_volumes(config, state, oci, api) + return if config[:volumes].empty? + + volume_state = process_volumes(config, state, oci, api) + state.merge!(volume_state) + end + + # Detatch and delete volumes. + # + # @param state [Hash] (see Kitchen::StateFile) + # @param oci [Kitchen::Driver::Oci::Config] a populated OCI config class. + # @param api [Kitchen::Driver::Oci::Api] an authenticated API class. + def detatch_and_delete_volumes(state, oci, api) + return unless state[:volumes] + + bls = Blockstorage.new(config: config, state: state, oci: oci, api: api, action: :destroy, logger: instance.logger) + state[:volume_attachments].each { |att| bls.detatch_volume(att) } + state[:volumes].each { |vol| bls.delete_volume(vol) } + end + + # Process volumes specified in the kitchen config. + # + # @param config [Kitchen::LazyHash] The config provided by the driver. + # @param state [Hash] (see Kitchen::StateFile) + # @param oci [Kitchen::Driver::Oci::Config] a populated OCI config class. + # @param api [Kitchen::Driver::Oci::Api] an authenticated API class. + # @return [Hash] the finalized state after the volume(s) have been created and attached. + def process_volumes(config, state, oci, api) + volume_state = { volumes: [], volume_attachments: [] } + config[:volumes].each do |volume| + vol = volume_class(volume[:type], config, state, oci, api) + volume_details, vol_state = create_volume(vol, volume) + attach_state = vol.attach_volume(volume_details, state[:server_id], volume) + volume_state[:volumes] << vol_state + volume_state[:volume_attachments] << attach_state + end + volume_state + end + + # Creates or clones the volume. + # + # @param vol [OCI::Core::Models::Volume] the volume that has been created. + # @param volume [Hash] the state of the current volume being cloned. + def create_volume(vol, volume) + if volume.key?(:volume_id) + vol.create_clone_volume(volume) + else + vol.create_volume(volume) + end + end + end + end + end + end +end diff --git a/lib/kitchen/driver/oci/models.rb b/lib/kitchen/driver/oci/models.rb deleted file mode 100644 index 58e3f86..0000000 --- a/lib/kitchen/driver/oci/models.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -# -# Author:: Justin Steele () -# -# Copyright (C) 2024, Stephen Pearson -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Kitchen - module Driver - class Oci - # models definitions - module Models - require_relative "instance" - require_relative "blockstorage" - - def instance_class(config, state, oci, api, action) - Oci::Models.const_get(config[:instance_type].capitalize).new(config: config, state: state, oci: oci, api: api, action: action, logger: instance.logger) - end - - def volume_class(type, config, state, oci, api) - Oci::Models.const_get(volume_attachment_type(type)).new(config: config, state: state, oci: oci, api: api, logger: instance.logger) - end - - private - - def volume_attachment_type(type) - if type.nil? - "Paravirtual" - else - type.capitalize - end - end - end - end - end -end diff --git a/lib/kitchen/driver/oci/models/compute.rb b/lib/kitchen/driver/oci/models/compute.rb index 0625c4e..d4e0b6f 100644 --- a/lib/kitchen/driver/oci/models/compute.rb +++ b/lib/kitchen/driver/oci/models/compute.rb @@ -22,7 +22,9 @@ module Kitchen module Driver class Oci module Models - # Compute instance model + # Compute instance model. + # + # @author Justin Steele class Compute < Instance # rubocop:disable Metrics/ClassLength include ComputeLaunchDetails @@ -31,13 +33,14 @@ def initialize(opts = {}) @launch_details = OCI::Core::Models::LaunchInstanceDetails.new end - # - # The details model that describes a compute instance + # The details model that describes a compute instance. # # @return [OCI::Core::Models::LaunchInstanceDetails] - # attr_accessor :launch_details + # Launches a compute instance. + # + # @return [Hash] the finalized state after the instance has been launched and is running. def launch process_windows_options response = api.compute.launch_instance(launch_instance_details) @@ -46,11 +49,13 @@ def launch final_state(state, instance_id) end + # Terminates a compute instance. def terminate api.compute.terminate_instance(state[:server_id]) api.compute.get_instance(state[:server_id]).wait_until(:lifecycle_state, OCI::Core::Models::Instance::LIFECYCLE_STATE_TERMINATING) end + # Reboots a compute instance. def reboot api.compute.instance_action(state[:server_id], "SOFTRESET") api.compute.get_instance(state[:server_id]).wait_until(:lifecycle_state, OCI::Core::Models::Instance::LIFECYCLE_STATE_RUNNING) @@ -58,6 +63,10 @@ def reboot private + # The ocid of the image to be used when creating the instance. + # * If image_id is specified in the kitchen.yml, that will be returned. + # * If image_name is specified in the kitchen.yml, lookup with the Compute API to find the ocid of the image by name. + # @raise [StandardError] if neither image_id nor image_name are specified OR the image lookup by name fails to find a match. def image_id return config[:image_id] if config[:image_id] @@ -66,6 +75,9 @@ def image_id image_id_by_name end + # Looks up the image ocid by name by recursively querying the list of images with the Compute API. + # + # @return [String] the ocid of the image. def image_id_by_name image_name = image_name_conversion image_list = images.select { |i| i.display_name.match(/#{image_name}/) } @@ -77,22 +89,35 @@ def image_id_by_name latest_image_id(image_list) end + # Automatically append aarch64 to a specified image name if an ARM shape is specified. + # + # @return [String] the modified image name. def image_name_conversion image_name = config[:image_name].gsub(" ", "-") - if config[:shape] =~ /^VM\.Standard\.A\d+\.Flex$/ && !config[:image_name].include?("aarch64") - image_name = "#{image_name}-aarch64" - end + image_name = "#{image_name}-aarch64" if config[:shape] =~ /^VM\.Standard\.A\d+\.Flex$/ && !config[:image_name].include?("aarch64") image_name end + # Filter images by name. + # + # @param image_list [Array] a list of the display names of all available images. + # @param image_name [String] the image name or regular expression provided in the config. + # @return [Array] all display names that match the image_name. def filter_image_list(image_list, image_name) image_list.select { |i| i.display_name.match(/#{image_name}-[0-9]{4}\.[0-9]{2}\.[0-9]{2}/) } end + # Finds the ocid of the most recent image by time created. + # + # @param image_list [Array] a list of all of the display names that matched the search string. + # @return [String] the ocid of the latest matching image. def latest_image_id(image_list) image_list.sort_by! { |o| ((DateTime.parse(Time.now.utc.to_s) - o.time_created) * 24 * 60 * 60).to_i }.first.id end + # Pages through all of the images in the compartment. This has to be a recursive process because the list_images API only returns 99 entries at a time. + # + # @return [Array] An array of OCI::Core::Models::Image. def images(image_list = [], page = nil) current_images = api.compute.list_images(oci.compartment, page: page) next_page = current_images.next_page @@ -101,6 +126,9 @@ def images(image_list = [], page = nil) image_list.flatten end + # Clone the specified boot volume and return the new ocid. + # + # @return [String] def clone_boot_volume logger.info("Cloning boot volume...") cbv = api.blockstorage.create_boot_volume(clone_boot_volume_details) @@ -109,6 +137,9 @@ def clone_boot_volume cbv.data.id end + # Create a new instance of OCI::Core::Models::CreateBootVolumeDetails. + # + # @return [OCI::Core::Models::CreateBootVolumeDetails] def clone_boot_volume_details OCI::Core::Models::CreateBootVolumeDetails.new( source_details: OCI::Core::Models::BootVolumeSourceFromBootVolumeDetails.new( @@ -120,10 +151,17 @@ def clone_boot_volume_details ) end + # Create the display name of the cloned boot volume. + # + # @return [String] def boot_volume_display_name "#{api.blockstorage.get_boot_volume(config[:boot_volume_id]).data.display_name} (Clone)" end + # Get the IP address of the instance from the vnic. + # + # @param instance_id [String] the ocid of the instance. + # @return [String] def instance_ip(instance_id) vnic = vnics(instance_id).select(&:is_primary).first if public_ip_allowed? @@ -133,10 +171,18 @@ def instance_ip(instance_id) end end + # Get a list of all vnics attached to the instance. + # + # @param instance_id [String] the ocid of the instance. + # @return [Array] a list of OCI::Core::Models::Vnic. def vnics(instance_id) vnic_attachments(instance_id).map { |att| api.network.get_vnic(att.vnic_id).data } end + # Get a list of all vnic attachments associated with the instance. + # + # @param instance_id [String] the ocid of the instance. + # @return [Array] a list of OCI::Core::Models::VnicAttachment. def vnic_attachments(instance_id) att = api.compute.list_vnic_attachments(oci.compartment, instance_id: instance_id).data raise "Could not find any VNIC attachments" unless att.any? @@ -144,10 +190,16 @@ def vnic_attachments(instance_id) att end + # Generate a hostname that includes some randomness. + # + # @return [String] def hostname %W{#{config[:hostname_prefix]} #{config[:instance_name]} #{random_string(6)}}.uniq.compact.join("-") end + # Create the details of the vnic that will be created. + # + # @param name [String] the display name of the instance being created. def create_vnic_details(name) OCI::Core::Models::CreateVnicDetails.new( assign_public_ip: public_ip_allowed?, @@ -158,6 +210,9 @@ def create_vnic_details(name) ) end + # Read in the public ssh key. + # + # @return [String] def pubkey if config[:ssh_keygen] logger.info("Generating public/private rsa key pair") @@ -166,6 +221,7 @@ def pubkey File.readlines(public_key_file).first.chomp end + # Add our special sauce to the instance metadata to be executed by cloud-init. def metadata md = {} inject_powershell @@ -175,6 +231,7 @@ def metadata md end + # Piece together options that a required for Windows instances. def process_windows_options return unless windows_state? @@ -182,20 +239,32 @@ def process_windows_options state.store(:password, config[:winrm_password] || random_password(%w{@ - ( ) .})) end + # Do the windows-y things exist in the kitchen config or the state? + # + # @return [Boolean] def windows_state? config[:setup_winrm] && config[:password].nil? && state[:password].nil? end + # Has custom user_data been provided in the config? + # + # @return [Boolean] def user_data? config[:user_data] && !config[:user_data].empty? end + # Read in and bind our winrm setup script. + # + # @return [String] def winrm_ps1 filename = File.join(__dir__, %w{.. .. .. .. .. tpl setup_winrm.ps1.erb}) tpl = ERB.new(File.read(filename)) tpl.result(binding) end + # Inject all of the winrm setup stuff into cloud-init. + # + # @return [Hash] the user_data config hash with the winrm stuff injected. def inject_powershell return unless config[:setup_winrm] diff --git a/lib/kitchen/driver/oci/models/dbaas.rb b/lib/kitchen/driver/oci/models/dbaas.rb index 29f6f78..be1a189 100644 --- a/lib/kitchen/driver/oci/models/dbaas.rb +++ b/lib/kitchen/driver/oci/models/dbaas.rb @@ -22,8 +22,10 @@ module Kitchen module Driver class Oci module Models - # dbaas model - class Dbaas < Instance # rubocop:disable Metrics/ClassLength + # Database system model. + # + # @author Justin Steele + class Dbaas < Instance include DbaasLaunchDetails def initialize(opts = {}) @@ -33,27 +35,24 @@ def initialize(opts = {}) @db_home_details = OCI::Database::Models::CreateDbHomeDetails.new end - # - # The details model that describes the db system + # The details model that describes the db system. # # @return [OCI::Database::Models::LaunchDbSystemDetails] - # attr_accessor :launch_details - # - # The details model that describes the database + # The details model that describes the database. # # @return [OCI::Database::Models::CreateDatabaseDetails] - # attr_accessor :database_details - # - # The details model that describes the database home + # The details model that describes the database home. # # @return [OCI::Database::Models::CreateDbHomeDetails] - # attr_accessor :db_home_details + # Launches a database system. + # + # @return [Hash] the finalized state after the instance has been launched and is running. def launch response = api.dbaas.launch_db_system(launch_instance_details) instance_id = response.data.id @@ -63,12 +62,14 @@ def launch final_state(state, instance_id) end + # Terminates a DBaaS system. def terminate api.dbaas.terminate_db_system(state[:server_id]) api.dbaas.get_db_system(state[:server_id]).wait_until(:lifecycle_state, OCI::Database::Models::DbSystem::LIFECYCLE_STATE_TERMINATING, max_interval_seconds: 900, max_wait_seconds: 21_600) end + # Reboots a DBaaS node. def reboot db_node_id = dbaas_node(state[:server_id]).first.id api.dbaas.db_node_action(db_node_id, "SOFTRESET") @@ -77,6 +78,10 @@ def reboot private + # Get the IP address of the instance from the vnic. + # + # @param instance_id [String] the ocid of the instance. + # @return [String] def instance_ip(instance_id) vnic = dbaas_node(instance_id).select(&:vnic_id).first.vnic_id if public_ip_allowed? @@ -86,18 +91,30 @@ def instance_ip(instance_id) end end + # Get the ocid of the database node associated with the database system. + # + # @param instance_id [String] the ocid of the database system. def dbaas_node(instance_id) api.dbaas.list_db_nodes(oci.compartment, db_system_id: instance_id).data end + # Sets the hostname_prefix as defined in the kitchen config. + # + # @return [String] def hostname_prefix config[:hostname_prefix] end + # Generates a random suffix to the hostname prefix. + # + # @return [String] def long_hostname_suffix [random_string(25 - hostname_prefix.length), random_string(3)].compact.join("-") end + # Read in the public ssh key. + # + # @return [String] def read_public_key if config[:ssh_keygen] logger.info("Generating public/private rsa key pair") diff --git a/lib/kitchen/driver/oci/models/iscsi.rb b/lib/kitchen/driver/oci/models/iscsi.rb index 3de37f2..ae94485 100644 --- a/lib/kitchen/driver/oci/models/iscsi.rb +++ b/lib/kitchen/driver/oci/models/iscsi.rb @@ -21,20 +21,26 @@ module Kitchen module Driver class Oci module Models - # iscsi volume attachment model + # iSCSI volume model. + # + # @author Justin Steele class Iscsi < Blockstorage def initialize(opts = {}) super @attachment_type = "iscsi" end - # - # The type of attachment being created + # The type of attachment being created. # # @return [String] - # attr_reader :attachment_type + # Creates the attachment details for an iSCSI volume. + # + # @param volume_details [OCI::Core::Models::Volume] + # @param server_id [String] the ocid of the compute instance to which the volume will be attached. + # @param volume_config [Hash] the state of the current volume being processed as specified in the kitchen.yml. + # @return [OCI::Core::Models::AttachIScsiVolumeDetails] def attachment_details(volume_details, server_id, volume_config) device = volume_config[:device] unless server_os(server_id).downcase =~ /windows/ OCI::Core::Models::AttachIScsiVolumeDetails.new( @@ -45,6 +51,10 @@ def attachment_details(volume_details, server_id, volume_config) ) end + # Adds the volume attachment info into the state. + # + # @param response [OCI::Core::Models::VolumeAttachment] + # @return [Hash] def final_volume_attachment_state(response) volume_attachment_state.store(:id, response.id) volume_attachment_state.store(:display_name, response.display_name) diff --git a/lib/kitchen/driver/oci/models/paravirtual.rb b/lib/kitchen/driver/oci/models/paravirtual.rb index c5663eb..79c32cf 100644 --- a/lib/kitchen/driver/oci/models/paravirtual.rb +++ b/lib/kitchen/driver/oci/models/paravirtual.rb @@ -21,20 +21,26 @@ module Kitchen module Driver class Oci module Models - # paravirtual attachment model + # Paravirtual volume model. + # + # @author Justin Steele class Paravirtual < Blockstorage def initialize(opts = {}) super @attachment_type = "paravirtual" end - # - # The type of attachment being created + # The type of attachment being created. # # @return [String] - # attr_reader :attachment_type + # Creates the attachment details for a Paravirtual volume. + # + # @param volume_details [OCI::Core::Models::Volume] + # @param server_id [String] the ocid of the compute instance to which the volume will be attached. + # @param volume_config [Hash] the state of the current volume being processed as specified in the kitchen.yml. + # @return [OCI::Core::Models::AttachParavirtualizedVolumeDetails] def attachment_details(volume_details, server_id, volume_config) device = volume_config[:device] unless server_os(server_id).downcase =~ /windows/ OCI::Core::Models::AttachParavirtualizedVolumeDetails.new( @@ -45,6 +51,10 @@ def attachment_details(volume_details, server_id, volume_config) ) end + # Adds the volume attachment info into the state. + # + # @param response [OCI::Core::Models::VolumeAttachment] + # @return [Hash] def final_volume_attachment_state(response) volume_attachment_state.store(:id, response.id) volume_attachment_state.store(:display_name, response.display_name) diff --git a/lib/kitchen/driver/oci/volumes.rb b/lib/kitchen/driver/oci/volumes.rb deleted file mode 100644 index 20603b0..0000000 --- a/lib/kitchen/driver/oci/volumes.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -# -# Author:: Justin Steele () -# -# Copyright (C) 2024, Stephen Pearson -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Kitchen - module Driver - class Oci - # mixin for working with volumes and attachments - module Volumes - def create_and_attach_volumes(config, state, oci, api) - return if config[:volumes].empty? - - volume_state = process_volumes(config, state, oci, api) - state.merge!(volume_state) - end - - def process_volumes(config, state, oci, api) - volume_state = { volumes: [], volume_attachments: [] } - config[:volumes].each do |volume| - vol = volume_class(volume[:type], config, state, oci, api) - volume_details, vol_state = create_volume(vol, volume) - attach_state = vol.attach_volume(volume_details, state[:server_id], volume) - volume_state[:volumes] << vol_state - volume_state[:volume_attachments] << attach_state - end - volume_state - end - - def create_volume(vol, volume) - if volume.key?(:volume_id) - vol.create_clone_volume(volume) - else - vol.create_volume(volume) - end - end - end - end - end -end