Skip to content

Commit

Permalink
Implementation of script-based host queueing
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Heinen committed Apr 27, 2020
1 parent 03e45be commit 5bffc8d
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 20 deletions.
10 changes: 7 additions & 3 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Offense count: 36
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
AllCops:
TargetRubyVersion: 2.6
Exclude:
- Guardfile
- Rakefile

Metrics/LineLength:
Max: 147

2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ end
group :debug do
gem "pry"
gem "guard"
gem "guard-shell"
gem "guard-rake"
end

# If you want to load debugging tools into the bundle exec sandbox,
Expand Down
4 changes: 2 additions & 2 deletions Guardfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
notification :terminal_title, display_message: true

guard :shell do
watch(/\.rb$/) { `rake install:local` }
guard 'rake', task: 'install:local' do
watch(/\.rb$/)
end
91 changes: 80 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ This is a Test Kitchen driver for use in cases, where you have an
existing machine, such as a physical server which you want
to use for your tests.

The static driver is directly derived from TK's "proxy" driver,
which is relying on legacy plugin infrastructure - making it directly incompatible
with Windows platforms.
The static driver is directly derived from TK's "proxy" driver,
which is relying on legacy plugin infrastructure - making it directly
incompatible with Windows platforms.

## Usage

Expand All @@ -19,19 +19,88 @@ driver:
# now the rest of your kitchen.yml follows
```

The `host` configuration setting, which specifies the hostname/IP you want tests to run against.
The `host` configuration setting, which specifies the hostname/IP you want tests
to run against.

If you have more than one server, for example when testing specific
hardware drivers, just add a suite for each and override the
`host` value in its section
If you have more than one server, for example when testing specific hardware
drivers, just add a suite for each and override the `host` value in its
section

## Supported Platforms

As this is a pure driver which does not interact with the
instances/VMs, it supports all platforms. Specifically Linux
and Windows work.
As this is a pure driver which does not interact with the instances/VMs, it
supports all platforms. Specifically Linux and Windows are known to work.

## <a name="license"></a> License
## Queueing Feature

As physical machines are a limited resource and are rarely bought or thrown
away in a TestKitchen context, some sort of queueing mechanism is needed in
bigger environments.

To enable this feature, set `queueing` to `true` (default: `false`)

```yaml
driver:
name: static
queueing: true
request:
execute: /usr/local/bin/get-host.sh
release:
execute: /usr/local/bin/release-host.sh $STATIC_HOSTNAME
...
```
Queueing knows two Actions:
* `request` to obtain the hostname or IP of the machine to use
* `release` to return this host into the pool

If you are using non-ephemeral test systems, like physical machines, you will
need to trigger some procedure to reset them back to the defined default. Otherwise,
every test will modify the system further until results get unpredictable.

There currently is just one handler for queueing scenarios:

* the `script` handler, which executes a local script

## Driver Options

| Name | Default | Description |
| ------------------- | --------- | --------------------------------------------- |
| `queueing` | false | If to invoke external actions to get hostname |
| `queueing_timeout` | 3600 | Timeout for queueing operations in seconds. |
| `queueing_handlers` | - | Glob to load external queueing handlers |

## Queueing Handler `static`

This handler only executes local commands. These could query remote databases or
even issue more complex programs to obtain/release machines.

### Parameters for `request`

| Name | Default | Description |
| ---------------- | --------- | ---------------------------------------------------------- |
| `type` | `script` | |
| `execute` | - | Command to execute |
| `match_hostname` | `^(.*)$` | Regex to specify what to grab from output. Default: All |
| `match_banner` | - | Regex to specify optional banner to grab. Default: Nothing |

If a banner is grabbed, it's contents are displaed after the message reporting the
hostname. This field can be used for warnings or additional information like access
to management interfaces (ILO, BMC, ...).

### Parameters for `release`

| Name | Default | Description |
| ---------- | --------- | ------------------------------------------------------- |
| `type` | `script` | |
| `execute` | - | Command to execute |

The executed script gets the following environment variables:

* `STATIC_HOSTNAME`: Hostname or IP of the host to be released

## License

Apache 2.0 (see [LICENSE][license])

Expand Down
11 changes: 11 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ YARD::Rake::YardocTask.new do |t|
end

task default: [:style]

require "guard"
require "guard/commander"

desc "Watch for source changes and redeploy Gem"
task :guard do
Guard.start({ no_interactions: true })
while ::Guard.running do
sleep 0.5
end
end
3 changes: 3 additions & 0 deletions kitchen-static.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = ">= 2.3"

spec.add_dependency "test-kitchen", ">= 1.16", "< 3.0"
spec.add_dependency "mixlib-shellout", "~> 3.0"

spec.add_development_dependency "bundler", ">= 1.16"
spec.add_development_dependency "guard", "~> 2.16"
spec.add_development_dependency "guard-rake", "~> 1.0"
spec.add_development_dependency "rake", "~> 12.0"
spec.add_development_dependency "yard", "~> 0.9"
end
87 changes: 87 additions & 0 deletions lib/kitchen/driver/queueing/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module Kitchen::Driver
class Static
module Queueing
class Base
@options = {}
@request_options = {}
@release_options = {}

@hostname = nil
@banner = nil

@env_vars = {}

attr_reader :options, :request_options, :release_options, :env_vars, :banner

def initialize(options)
@options = {
queueing_timeout: 3600,
}

@request_options = {}
@release_options = {}

setup(options)

process_kitchen_options(options)
end

def request(state)
handle_request(state)
end

def release(state)
@env_vars = {
STATIC_HOSTNAME: state[:hostname],
}

handle_release(state)
end

def banner?
! @banner.nil?
end

def self.descendants
ObjectSpace.each_object(Class).select { |klass| klass < self }
end

private

def setup(_options)
# Add setup and defaults in specific handler
end

def handle_request(_state)
raise "Implement request handler"
end

def handle_release(_state)
raise "Implement release handler"
end

def default_request_options(options = {})
@request_options.merge!(options)
end

def default_release_options(options = {})
@release_options.merge!(options)
end

def process_kitchen_options(kitchen_options)
@options = kitchen_options

@request_options.merge!(options[:request])
@options.delete(:request)

@release_options.merge!(options[:release])
@options.delete(:release)
end

def timeout
options[:queueing_timeout]
end
end
end
end
end
49 changes: 49 additions & 0 deletions lib/kitchen/driver/queueing/script.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
require "mixlib/shellout"

require_relative "base.rb"

module Kitchen::Driver
class Static
module Queueing
class Script < Base
def setup(_kitchen_options)
default_request_options({
match_hostname: "^(.*)$",
match_banner: nil,
})

default_release_options({})
end

def handle_request(_state)
stdout = execute(request_options[:execute])

matched = stdout.match(request_options[:match_hostname])
raise format("Could not extract hostname from '%s' with regular expression /%s/", stdout, request_options[:match_hostname]) unless matched

# Allow additional feedback from command
@banner = stdout.match(request_options[:match_banner])&.captures&.first if request_options[:match_banner]

matched.captures.first
end

def handle_release(_state)
execute(release_options[:execute])
end

private

def execute(command)
raise format("Received empty command") if command.nil? || command.empty?

cmd = Mixlib::ShellOut.new(command, environment: env_vars, timeout: timeout)
cmd.run_command

raise format("Error executing `%s`: %s", command, cmd.stderr) if cmd.status != 0

cmd.stdout.strip
end
end
end
end
end
Loading

0 comments on commit 5bffc8d

Please sign in to comment.