Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The following driver parameters are common to both instance types, but are not r
- `oci_profile_name`, OCI profile to use (default: `DEFAULT`)
- `oci_config`, Hash of additional `OCI::Config` settings. Allows you to test without an oci config file [[more](#use-without-oci-config-file)]
- `ssh_keypath`, SSH public key (default: `~/.ssh/id_rsa.pub`)
- `ssh_keygen`, Automatically generate the rsa key pair for an instance (default: `false`) [[more](#ssh-keygen)]
- `post_create_script`, run a script on an instance after deployment
- `post_create_reboot`, reboot the instance after instance creation (default: `false`)
- `proxy_url`, Connect via the specified proxy URL [[more](#proxy-support)]
Expand Down Expand Up @@ -274,6 +275,26 @@ Alternately, if you simply pass a string to the user_data, it will be base64 enc
gid: 1000
```

## SSH Keygen

The driver can generate an ssh key pair for an instance during creation. In order to turn this feature on, add the `ssh_keygen` property to the `driver` and set the value to `true`. This can be set in the `driver` section on a
per-platform or per-suite basis, but can also be enabled globally for the entire kitchen.yml in the top-level `driver` section.

Ensure that the `transport` section does not contain a path to a private key (the `ssh_key` property). If the `transport` has a value in `ssh_key` property, this will mismatch with the key pair that the driver will create causing your
instance creation to be stuck in an endless loop waiting for `transport` to receive a confirmed ssh connection.

The generated key pair is stored in the `.kitchen/.ssh` directory and is named for the instance that generated it so each instance in your `kitchen.yml` can have its own key pair.

Upon instance termination (`kitchen destroy`), the generated key pair will be removed from the `.kitchen/.ssh` directory along with the state file as should be expected.

```yml
driver:
ssh_keygen: true

transport:
username: opc
```

## Proxy support

If running Kitchen on a private subnet with no public IPs permitted, it may be necessary to connect to the OCI API via a web proxy. The proxy URL can either be specified on the command line:
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ rescue LoadError
puts "chefstyle is not available. (sudo) gem install chefstyle to do style checking."
end

task build: :default do
task :build do
Gem::Package.build(spec, nil, nil, gemfile)
end

Expand Down
38 changes: 9 additions & 29 deletions lib/kitchen/driver/oci.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ module Driver
class Oci < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
require_relative "oci_version"
require_relative "oci/models"
require_relative "oci/volumes"

plugin_version Kitchen::Driver::OCI_VERSION
kitchen_driver_api_version 1
kitchen_driver_api_version 2

# required config items
required_config :availability_domain
Expand All @@ -63,6 +64,7 @@ class Oci < Kitchen::Driver::Base # rubocop:disable Metrics/ClassLength
default_config :display_name, nil
default_keypath = File.expand_path(File.join(%w{~ .ssh id_rsa.pub}))
default_config :ssh_keypath, default_keypath
default_config :ssh_keygen, false
default_config :post_create_script, nil
default_config :proxy_url, nil
default_config :user_data, nil
Expand Down Expand Up @@ -114,6 +116,7 @@ def self.validation_error(message, driver)
end

include Kitchen::Driver::Oci::Models
include Kitchen::Driver::Oci::Volumes

def create(state)
return if state[:server_id]
Expand Down Expand Up @@ -151,33 +154,6 @@ def launch(state, inst)
instance.transport.connection(state).wait_until_ready
end

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

def process_post_script(state)
return if config[:post_create_script].nil?

Expand All @@ -197,14 +173,18 @@ def reboot(state, inst)
def detatch_and_delete_volumes(state, oci, api)
return unless state[:volumes]

bls = Blockstorage.new(config, state, oci, api, :destroy)
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
Expand Down
40 changes: 24 additions & 16 deletions lib/kitchen/driver/oci/blockstorage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@ class Blockstorage < Oci # rubocop:disable Metrics/ClassLength
require_relative "models/iscsi"
require_relative "models/paravirtual"

def initialize(config, state, oci, api, action = :create)
def initialize(opts = {})
super()
@config = config
@state = state
@oci = oci
@api = api
@config = opts[:config]
@state = opts[:state]
@oci = opts[:oci]
@api = opts[:api]
@logger = opts[:logger]
@volume_state = {}
@volume_attachment_state = {}
oci.compartment if action == :create
oci.compartment if opts[:action] == :create
end

#
Expand Down Expand Up @@ -65,6 +66,13 @@ def initialize(config, state, oci, api, action = :create)
#
attr_accessor :api

#
# 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
#
# @return [Hash]
Expand All @@ -78,44 +86,44 @@ def initialize(config, state, oci, api, action = :create)
attr_accessor :volume_attachment_state

def create_volume(volume)
info("Creating <#{volume[:name]}>...")
logger.info("Creating <#{volume[:name]}>...")
result = api.blockstorage.create_volume(volume_details(volume))
response = volume_response(result.data.id)
info("Finished creating <#{volume[:name]}>.")
logger.info("Finished creating <#{volume[:name]}>.")
[response, final_state(response)]
end

def create_clone_volume(volume)
clone_volume_name = clone_volume_display_name(volume[:volume_id])
info("Creating <#{clone_volume_name}>...")
logger.info("Creating <#{clone_volume_name}>...")
result = api.blockstorage.create_volume(volume_clone_details(volume, clone_volume_name))
response = volume_response(result.data.id)
info("Finished creating <#{clone_volume_name}>.")
logger.info("Finished creating <#{clone_volume_name}>.")
[response, final_state(response)]
end

def attach_volume(volume_details, server_id, volume_config)
info("Attaching <#{volume_details.display_name}>...")
logger.info("Attaching <#{volume_details.display_name}>...")
attach_volume = api.compute.attach_volume(attachment_details(volume_details, server_id, volume_config))
response = attachment_response(attach_volume.data.id)
info("Finished attaching <#{volume_details.display_name}>.")
logger.info("Finished attaching <#{volume_details.display_name}>.")
final_state(response)
end

def delete_volume(volume)
info("Deleting <#{volume[:display_name]}>...")
logger.info("Deleting <#{volume[:display_name]}>...")
api.blockstorage.delete_volume(volume[:id])
api.blockstorage.get_volume(volume[:id])
.wait_until(:lifecycle_state, OCI::Core::Models::Volume::LIFECYCLE_STATE_TERMINATED)
info("Finished deleting <#{volume[:display_name]}>.")
logger.info("Finished deleting <#{volume[:display_name]}>.")
end

def detatch_volume(volume_attachment)
info("Detaching <#{attachment_name(volume_attachment)}>...")
logger.info("Detaching <#{attachment_name(volume_attachment)}>...")
api.compute.detach_volume(volume_attachment[:id])
api.compute.get_volume_attachment(volume_attachment[:id])
.wait_until(:lifecycle_state, OCI::Core::Models::VolumeAttachment::LIFECYCLE_STATE_DETACHED)
info("Finished detaching <#{attachment_name(volume_attachment)}>.")
logger.info("Finished detaching <#{attachment_name(volume_attachment)}>.")
end

def final_state(response)
Expand Down
55 changes: 50 additions & 5 deletions lib/kitchen/driver/oci/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ class Instance < Oci # rubocop:disable Metrics/ClassLength

include CommonLaunchDetails

def initialize(config, state, oci, api, action)
def initialize(opts = {})
super()
@config = config
@state = state
@oci = oci
@api = api
@config = opts[:config]
@state = opts[:state]
@oci = opts[:oci]
@api = opts[:api]
@logger = opts[:logger]
end

#
Expand Down Expand Up @@ -66,6 +67,13 @@ def initialize(config, state, oci, api, action)
#
attr_accessor :api

#
# The instance of Kitchen::Logger in use by the active Kitchen::Instance
#
# @return [Kitchen::Logger]
#
attr_accessor :logger

def final_state(state, instance_id)
state.store(:server_id, instance_id)
state.store(:hostname, instance_ip(instance_id))
Expand All @@ -88,6 +96,43 @@ def public_ip_allowed?
!subnet.prohibit_public_ip_on_vnic
end

def public_key_file
if config[:ssh_keygen]
"#{config[:kitchen_root]}/.kitchen/.ssh/#{config[:instance_name]}_rsa.pub"
else
config[:ssh_keypath]
end
end

def private_key_file
public_key_file.gsub(".pub", "")
end

def gen_key_pair
FileUtils.mkdir_p("#{config[:kitchen_root]}/.kitchen/.ssh")
rsa_key = OpenSSL::PKey::RSA.new(4096)
write_private_key(rsa_key)
write_public_key(rsa_key)
state.store(:ssh_key, private_key_file)
end

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

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

def encode_private_key(rsa_key)
prefix = "#{[7].pack("N")}ssh-rsa"
exponent = rsa_key.e.to_s(0)
modulus = rsa_key.n.to_s(0)
["#{prefix}#{exponent}#{modulus}"].pack("m0")
end

def random_password(special_chars)
(Array.new(5) { special_chars.sample } +
Array.new(5) { ("a".."z").to_a.sample } +
Expand Down
2 changes: 1 addition & 1 deletion lib/kitchen/driver/oci/instance/dbaas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def node_count

def pubkey
result = []
result << File.readlines(config[:ssh_keypath]).first.chomp
result << read_public_key
launch_details.ssh_public_keys = result
end

Expand Down
4 changes: 2 additions & 2 deletions lib/kitchen/driver/oci/models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ module Models
require_relative "blockstorage"

def instance_class(config, state, oci, api, action)
Oci::Models.const_get(config[:instance_type].capitalize).new(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, 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
Expand Down
20 changes: 14 additions & 6 deletions lib/kitchen/driver/oci/models/compute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module Models
class Compute < Instance # rubocop:disable Metrics/ClassLength
include ComputeLaunchDetails

def initialize(config, state, oci, api, action)
def initialize(opts = {})
super
@launch_details = OCI::Core::Models::LaunchInstanceDetails.new
end
Expand Down Expand Up @@ -102,10 +102,10 @@ def images(image_list = [], page = nil)
end

def clone_boot_volume
info("Cloning boot volume...")
logger.info("Cloning boot volume...")
cbv = api.blockstorage.create_boot_volume(clone_boot_volume_details)
api.blockstorage.get_boot_volume(cbv.data.id).wait_until(:lifecycle_state, OCI::Core::Models::BootVolume::LIFECYCLE_STATE_AVAILABLE)
info("Finished cloning boot volume.")
logger.info("Finished cloning boot volume.")
cbv.data.id
end

Expand Down Expand Up @@ -159,15 +159,19 @@ def create_vnic_details(name)
end

def pubkey
File.readlines(config[:ssh_keypath]).first.chomp
if config[:ssh_keygen]
logger.info("Generating public/private rsa key pair")
gen_key_pair
end
File.readlines(public_key_file).first.chomp
end

def metadata
md = {}
inject_powershell
config[:custom_metadata]&.each { |k, v| md.store(k, v) }
md.store("ssh_authorized_keys", pubkey)
md.store("user_data", user_data) if config[:user_data] && !config[:user_data].empty?
md.store("ssh_authorized_keys", pubkey) unless config[:setup_winrm]
md.store("user_data", user_data) if user_data?
md
end

Expand All @@ -182,6 +186,10 @@ def windows_state?
config[:setup_winrm] && config[:password].nil? && state[:password].nil?
end

def user_data?
config[:user_data] && !config[:user_data].empty?
end

def winrm_ps1
filename = File.join(__dir__, %w{.. .. .. .. .. tpl setup_winrm.ps1.erb})
tpl = ERB.new(File.read(filename))
Expand Down
10 changes: 9 additions & 1 deletion lib/kitchen/driver/oci/models/dbaas.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module Models
class Dbaas < Instance # rubocop:disable Metrics/ClassLength
include DbaasLaunchDetails

def initialize(config, state, oci, api, action)
def initialize(opts = {})
super
@launch_details = OCI::Database::Models::LaunchDbSystemDetails.new
@database_details = OCI::Database::Models::CreateDatabaseDetails.new
Expand Down Expand Up @@ -97,6 +97,14 @@ def hostname_prefix
def long_hostname_suffix
[random_string(25 - hostname_prefix.length), random_string(3)].compact.join("-")
end

def read_public_key
if config[:ssh_keygen]
logger.info("Generating public/private rsa key pair")
gen_key_pair
end
File.readlines(public_key_file).first.chomp
end
end
end
end
Expand Down
Loading
Loading