Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VMware transport #321

Merged
merged 2 commits into from
Jul 12, 2018
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
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Train supports:
* Mock (for testing and debugging)
* AWS as an API
* Azure as an API
* VMware via PowerCLI

# Examples

Expand Down Expand Up @@ -79,6 +80,20 @@ require 'train'
train = Train.create('aws')
```

**VMware**

```ruby
require 'train'
Train.create('vmware', viserver: '10.0.0.10', user: 'demouser', password: 'securepassword')
```

You may also use environment variables by setting `VISERVER`, `VISERVER__USERNAME`, and `VISERVER_PASSWORD`

```ruby
require 'train'
Train.create('vmware')
```

## Configuration

To get a list of available options for a plugin:
Expand Down Expand Up @@ -151,7 +166,7 @@ bundle exec ruby -I .\test\windows\ .\test\windows\local_test.rb

Train is heavily based on the work of:

* [test-kitchen](https://github.com/test-kitchen/test-kitchen)
* [test-kitchen](https://github.com/test-kitchen/test-kitchen)

by [Fletcher Nichol](fnichol@nichol.ca)
and [a great community of contributors](https://github.com/test-kitchen/test-kitchen/graphs/contributors)
Expand Down
2 changes: 2 additions & 0 deletions lib/train/platforms/detect/specifications/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def self.load
plat.name('aws').in_family('cloud')
plat.name('azure').in_family('cloud')
plat.name('gcp').in_family('cloud')
plat.name('vmware').in_family('cloud')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Methinks vmware might be a member of iaas, not cloud. But that may be a marketing distinction, here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I struggled with this as well. After discussing, @jquick and @chris-rock decided that is should be under cloud.


plat.family('iaas').in_family('api')
plat.name('oneview').in_family('iaas')
end
Expand Down
190 changes: 190 additions & 0 deletions lib/train/transports/vmware.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# encoding: utf-8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot going on here that I really wish we had integration tests for. The unit tests are solid - I believe that the accessors and option parsers work. I'm sure info about integration testing is coming, likely in the resource pack.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, I too would like integration tests, I'm just not quite sure how to approach it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That said, pleas update the README.md to include vmware, and provide configuration information there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added sections covering basic usage. Detailed info (such as PWSH/PowerCLI installation will be in the resource pack).

Does that work for you? 😄

require 'train/plugins'
require 'open3'
require 'ostruct'
require 'json'
require 'mkmf'

module Train::Transports
class VMware < Train.plugin(1)
name 'vmware'
option :viserver, default: ENV['VISERVER']
option :username, default: ENV['VISERVER_USERNAME']
option :password, default: ENV['VISERVER_PASSWORD']
option :insecure, default: false

def connection(_ = nil)
@connection ||= Connection.new(@options)
end

class Connection < BaseConnection # rubocop:disable ClassLength
POWERSHELL_PROMPT_REGEX = /PS\s.*> $/

def initialize(options)
super(options)

options[:viserver] = options[:viserver] || options[:host]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fun shorthand: try ||=. Like +=, but for or. It's great for setting defaults.

options[:viserver] ||= options[:host]
     

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed it is, will do!

options[:username] = options[:username] || options[:user]

@username = options[:username]
@viserver = options[:viserver]
@session = nil
@stdout_buffer = ''
@stderr_buffer = ''

@powershell_binary = detect_powershell_binary

if @powershell_binary == :powershell
require 'train/transports/local'
@powershell = Train::Transports::Local::Connection.new(options)
end

if options[:insecure] == true
run_command_via_connection('Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Scope Session -Confirm:$False')
end

@platform_details = {
release: "vmware-powercli-#{powercli_version}",
}

connect
end

def connect
login_command = "Connect-VIServer #{options[:viserver]} -User #{options[:username]} -Password #{options[:password]} | Out-Null"
result = run_command_via_connection(login_command)

if result.exit_status != 0
message = "Unable to connect to VIServer at #{options[:viserver]}. "
case result.stderr
when /Invalid server certificate/
message += 'Certification verification failed. Please use `--insecure` or set `Set-PowerCLIConfiguration -InvalidCertificateAction Ignore` in PowerShell'
when /incorrect user name or password/
message += 'Incorrect username or password'
else
message += result.stderr.gsub(/-Password .*\s/, '-Password REDACTED')
end

raise message
end
end

def local?
true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this constant?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, I'm trying to override:

end

def platform
direct_platform('vmware', @platform_details)
end

def run_command_via_connection(cmd)
if @powershell_binary == :pwsh
result = parse_pwsh_output(cmd)

# Attach exit status to result
exit_status = parse_pwsh_output('echo $?').stdout.chomp
result.exit_status = exit_status == 'True' ? 0 : 1

result
else
@powershell.run_command(cmd)
end
end

def unique_identifier
uuid_command = '(Get-VMHost | Get-View).hardware.systeminfo.uuid'
run_command_via_connection(uuid_command).stdout.chomp
end

def uri
"vmware://#{@username}@#{@viserver}"
end

private

def detect_powershell_binary
if find_executable0('pwsh')
:pwsh
elsif find_executable0('powershell')
:powershell
else
raise 'Cannot find PowerShell binary, is `pwsh` installed?'
end
end

# Read from stdout pipe until prompt is received
def flush_stdout(pipe)
while @stdout_buffer !~ POWERSHELL_PROMPT_REGEX
@stdout_buffer += pipe.read_nonblock(1)
end
@stdout_buffer
rescue IO::EAGAINWaitReadable
# We cannot know when the stdout pipe is finished so we keep reading
retry
ensure
@stdout_buffer = ''
end

# This must be called after `flush_stdout` to ensure buffer is full
def flush_stderr(pipe)
loop do
@stderr_buffer += pipe.read_nonblock(1)
end
rescue IO::EAGAINWaitReadable
# If `flush_stderr` is ran after reading stdout we know that all of
# stderr is in the pipe. Thus, we can return the buffer once the pipe
# is unreadable.
@stderr_buffer
ensure
@stderr_buffer = ''
end

def parse_pwsh_output(cmd)
session.stdin.puts(cmd)

stdout = flush_stdout(session.stdout)

# Remove stdin from stdout (including trailing newline)
stdout.slice!(0, cmd.length+1)

# Remove prompt from stdout
stdout.gsub!(POWERSHELL_PROMPT_REGEX, '')

# Grab stderr
stderr = flush_stderr(session.stderr)

CommandResult.new(
stdout,
stderr,
nil # exit_status is attached in `run_command_via_connection`
)
end

def powercli_version
version_command = '[string](Get-Module -Name VMware.PowerCLI -ListAvailable | Select -ExpandProperty Version)'
result = run_command_via_connection(version_command)
if result.stdout.empty? || result.exit_status != 0
raise 'Unable to determine PowerCLI Module version, is it installed?'
end

result.stdout.chomp
end

def session
return @session unless @session.nil?

stdin, stdout, stderr = Open3.popen3('pwsh')

# Remove leading prompt and intro text
flush_stdout(stdout)

@session = OpenStruct.new
@session.stdin = stdin
@session.stdout = stdout
@session.stderr = stderr

@session
end
end
end
end
Loading