-
Notifications
You must be signed in to change notification settings - Fork 90
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
Add VMware transport #321
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,190 @@ | ||||
# encoding: utf-8 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fun shorthand: try
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this constant? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.