Initial commit

This commit is contained in:
Rishi Javia 2017-06-30 09:41:21 -07:00
commit d1f0a7b7ea
15 changed files with 1518 additions and 0 deletions

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
*.swp
log/*
!.gitignore
junit
acceptance-tests
pkg
Gemfile.lock
options.rb
test.cfg
.yardoc
coverage
.bundle
.vendor
_vendor
tmp/
doc
# JetBrains IDEA
*.iml
.idea/
# rbenv file
.ruby-version
.ruby-gemset
# Vagrant folder
.vagrant/
.vagrant_files/

3
.rspec Normal file
View file

@ -0,0 +1,3 @@
--format documentation
--color
--tty

9
.simplecov Normal file
View file

@ -0,0 +1,9 @@
SimpleCov.configure do
add_filter 'spec/'
add_filter 'vendor/'
add_filter do |file|
file.lines_of_code < 10
end
end
SimpleCov.start if ENV['BEAKER_VMPOOLER_COVERAGE']

27
Gemfile Normal file
View file

@ -0,0 +1,27 @@
source ENV['GEM_SOURCE'] || "https://rubygems.org"
gemspec
def location_for(place, fake_version = nil)
if place =~ /^git:([^#]*)#(.*)/
[fake_version, { :git => $1, :branch => $2, :require => false }].compact
elsif place =~ /^file:\/\/(.*)/
['>= 0', { :path => File.expand_path($1), :require => false }]
else
[place, { :require => false }]
end
end
# We don't put beaker in as a test dependency because we
# don't want to create a transitive dependency
group :acceptance_testing do
gem "beaker", *location_for(ENV['BEAKER_VERSION'] || '~> 3.0')
end
if File.exists? "#{__FILE__}.local"
eval(File.read("#{__FILE__}.local"), binding)
end

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

27
README.md Normal file
View file

@ -0,0 +1,27 @@
# beaker-vmpooler
Beaker library to use vmpooler hypervisor
# How to use this wizardry
This is a gem that allows you to use hosts with [vmpooler](vmpooler.md) hypervisor with [beaker](https://github.com/puppetlabs/beaker). This gem is already included as [beaker dependency](https://github.com/puppetlabs/beaker/blob/master/beaker.gemspec#L59) for you, so you don't need to do anything special to use this gem's functionality with beaker.
# Spec tests
Spec test live under the `spec` folder. There are the default rake task and therefore can run with a simple command:
```bash
bundle exec rake test:spec
```
# Acceptance tests
We run beaker's base acceptance tests with this library to see if the hypervisor is working with beaker. There is a simple rake task to invoke acceptance test for the library:
```bash
bundle exec rake test:acceptance
```
# Contributing
Please refer to puppetlabs/beaker's [contributing](https://github.com/puppetlabs/beaker/blob/master/CONTRIBUTING.md) guide.
If you are making changes in beaker-vmpooler and simultaneously in beaker, please *comment and link* your beaker fork repo and branch name in your PR of beaker-vmpooler for testing on CI.

147
Rakefile Normal file
View file

@ -0,0 +1,147 @@
require 'rspec/core/rake_task'
require 'beaker'
require 'beaker/hypervisor/vmpooler'
namespace :test do
namespace :spec do
desc "Run spec tests"
RSpec::Core::RakeTask.new(:run) do |t|
t.rspec_opts = ['--color']
t.pattern = 'spec/'
end
end
desc <<-EOS
Runs the base beaker acceptance test using the hypervisor library
EOS
task :acceptance do
# setup & load_path of beaker's acceptance base and lib directory
beaker_gem_spec = Gem::Specification.find_by_name('beaker')
beaker_gem_dir = beaker_gem_spec.gem_dir
beaker_test_base_dir = File.join(beaker_gem_dir, 'acceptance/tests/base')
load_path_option = File.join(beaker_gem_dir, 'acceptance/lib')
sh("beaker",
"--tests", beaker_test_base_dir,
"--log-level", "verbose",
"--hosts", "redhat7-64af-redhat7-64default.mdcal",
"--load-path", load_path_option,
"--keyfile", ENV['KEY'] || "#{ENV['HOME']}/.ssh/id_rsa-acceptance")
end
end
# namespace-named default tasks.
# these are the default tasks invoked when only the namespace is referenced.
# they're needed because `task :default` in those blocks doesn't work as expected.
task 'test:spec' => 'test:spec:run'
# global defaults
task :test => 'test:spec'
task :default => :test
###########################################################
#
# Documentation Tasks
#
###########################################################
DOCS_DAEMON = "yard server --reload --daemon --server thin"
FOREGROUND_SERVER = 'bundle exec yard server --reload --verbose --server thin lib/beaker'
def running?( cmdline )
ps = `ps -ef`
found = ps.lines.grep( /#{Regexp.quote( cmdline )}/ )
if found.length > 1
raise StandardError, "Found multiple YARD Servers. Don't know what to do."
end
yes = found.empty? ? false : true
return yes, found.first
end
def pid_from( output )
output.squeeze(' ').strip.split(' ')[1]
end
desc 'Start the documentation server in the foreground'
task :docs => 'docs:clear' do
original_dir = Dir.pwd
Dir.chdir( File.expand_path(File.dirname(__FILE__)) )
sh FOREGROUND_SERVER
Dir.chdir( original_dir )
end
namespace :docs do
desc 'Clear the generated documentation cache'
task :clear do
original_dir = Dir.pwd
Dir.chdir( File.expand_path(File.dirname(__FILE__)) )
sh 'rm -rf docs'
Dir.chdir( original_dir )
end
desc 'Generate static documentation'
task :gen => 'docs:clear' do
original_dir = Dir.pwd
Dir.chdir( File.expand_path(File.dirname(__FILE__)) )
output = `bundle exec yard doc`
puts output
if output =~ /\[warn\]|\[error\]/
fail "Errors/Warnings during yard documentation generation"
end
Dir.chdir( original_dir )
end
desc 'Run the documentation server in the background, alias `bg`'
task :background => 'docs:clear' do
yes, output = running?( DOCS_DAEMON )
if yes
puts "Not starting a new YARD Server..."
puts "Found one running with pid #{pid_from( output )}."
else
original_dir = Dir.pwd
Dir.chdir( File.expand_path(File.dirname(__FILE__)) )
sh "bundle exec #{DOCS_DAEMON}"
Dir.chdir( original_dir )
end
end
task(:bg) { Rake::Task['docs:background'].invoke }
desc 'Check the status of the documentation server'
task :status do
yes, output = running?( DOCS_DAEMON )
if yes
pid = pid_from( output )
puts "Found a YARD Server running with pid #{pid}"
else
puts "Could not find a running YARD Server."
end
end
desc "Stop a running YARD Server"
task :stop do
yes, output = running?( DOCS_DAEMON )
if yes
pid = pid_from( output )
puts "Found a YARD Server running with pid #{pid}"
`kill #{pid}`
puts "Stopping..."
yes, output = running?( DOCS_DAEMON )
if yes
`kill -9 #{pid}`
yes, output = running?( DOCS_DAEMON )
if yes
puts "Could not Stop Server!"
else
puts "Server stopped."
end
else
puts "Server stopped."
end
else
puts "Could not find a running YARD Server"
end
end
end

36
beaker-vmpooler.gemspec Normal file
View file

@ -0,0 +1,36 @@
# -*- encoding: utf-8 -*-
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
Gem::Specification.new do |s|
s.name = "beaker-vmpooler"
s.version = '0.0.1'
s.authors = ["Rishi Javia, Kevin Imber, Tony Vu"]
s.email = ["rishi.javia@puppet.com, kevin.imber@puppet.com, tony.vu@puppet.com"]
s.homepage = "https://github.com/puppetlabs/beaker-vmpooler"
s.summary = %q{Beaker DSL Extension Helpers!}
s.description = %q{For use for the Beaker acceptance testing tool}
s.license = 'Apache2'
s.files = `git ls-files`.split("\n")
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.require_paths = ["lib"]
# Testing dependencies
s.add_development_dependency 'rspec', '~> 3.0'
s.add_development_dependency 'rspec-its'
s.add_development_dependency 'fakefs', '~> 0.6'
s.add_development_dependency 'rake', '~> 10.1'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'pry', '~> 0.10'
# Documentation dependencies
s.add_development_dependency 'yard'
s.add_development_dependency 'markdown'
s.add_development_dependency 'thin'
# Run time dependencies
s.add_runtime_dependency 'stringify-hash', '~> 0.0.0'
end

32
bin/beaker-vmpooler Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env ruby
require 'rubygems' unless defined?(Gem)
require 'beaker-vmpooler'
VERSION_STRING =
"
_ .--.
( ` )
beaker-vmpooler .-' `--,
_..----.. ( )`-.
.'_|` _|` _|( .__, )
/_| _| _| _( (_, .-'
;| _| _| _| '-'__,--'`--'
| _| _| _| _| |
_ || _| _| _| _| %s
_( `--.\\_| _| _| _|/
.-' )--,| _| _|.`
(__, (_ ) )_| _| /
`-.__.\\ _,--'\\|__|__/
;____;
\\YT/
||
|\"\"|
'=='
"
puts VERSION_STRING % [Beaker::DSL::Helpers::Vmpooler::Version::STRING]
exit 0

View file

@ -0,0 +1,238 @@
require 'yaml' unless defined?(YAML)
require 'beaker/hypervisor/vmpooler'
module Beaker
class Vcloud < Beaker::Hypervisor
def self.new(vcloud_hosts, options)
if options['pooling_api']
Beaker::Vmpooler.new(vcloud_hosts, options)
else
super
end
end
def initialize(vcloud_hosts, options)
@options = options
@logger = options[:logger]
@hosts = vcloud_hosts
raise 'You must specify a datastore for vCloud instances!' unless @options['datastore']
raise 'You must specify a folder for vCloud instances!' unless @options['folder']
raise 'You must specify a datacenter for vCloud instances!' unless @options['datacenter']
@vsphere_credentials = VsphereHelper.load_config(@options[:dot_fog])
end
def connect_to_vsphere
@logger.notify "Connecting to vSphere at #{@vsphere_credentials[:server]}" +
" with credentials for #{@vsphere_credentials[:user]}"
@vsphere_helper = VsphereHelper.new( @vsphere_credentials )
end
def wait_for_dns_resolution host, try, attempts
@logger.notify "Waiting for #{host['vmhostname']} DNS resolution"
begin
Socket.getaddrinfo(host['vmhostname'], nil)
rescue
if try <= attempts
sleep 5
try += 1
retry
else
raise "DNS resolution failed after #{@options[:timeout].to_i} seconds"
end
end
end
def booting_host host, try, attempts
@logger.notify "Booting #{host['vmhostname']} (#{host.name}) and waiting for it to register with vSphere"
until
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.toolsRunningStatus == 'guestToolsRunning' and
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress != nil
if try <= attempts
sleep 5
try += 1
else
raise "vSphere registration failed after #{@options[:timeout].to_i} seconds"
end
end
end
# Directly borrowed from openstack hypervisor
def enable_root(host)
if host['user'] != 'root'
copy_ssh_to_root(host, @options)
enable_root_login(host, @options)
host['user'] = 'root'
host.close
end
end
def create_clone_spec host
# Add VM annotation
configSpec = RbVmomi::VIM.VirtualMachineConfigSpec(
:annotation =>
'Base template: ' + host['template'] + "\n" +
'Creation time: ' + Time.now.strftime("%Y-%m-%d %H:%M") + "\n\n" +
'CI build link: ' + ( ENV['BUILD_URL'] || 'Deployed independently of CI' ) +
'department: ' + @options[:department] +
'project: ' + @options[:project],
:extraConfig => [
{ :key => 'guestinfo.hostname',
:value => host['vmhostname']
}
]
)
# Are we using a customization spec?
customizationSpec = @vsphere_helper.find_customization( host['template'] )
if customizationSpec
# Print a logger message if using a customization spec
@logger.notify "Found customization spec for '#{host['template']}', will apply after boot"
end
# Put the VM in the specified folder and resource pool
relocateSpec = RbVmomi::VIM.VirtualMachineRelocateSpec(
:datastore => @vsphere_helper.find_datastore(@options['datacenter'],@options['datastore']),
:pool => @options['resourcepool'] ? @vsphere_helper.find_pool(@options['datacenter'],@options['resourcepool']) : nil,
:diskMoveType => :moveChildMostDiskBacking
)
# Create a clone spec
spec = RbVmomi::VIM.VirtualMachineCloneSpec(
:config => configSpec,
:location => relocateSpec,
:customization => customizationSpec,
:powerOn => true,
:template => false
)
spec
end
def provision
connect_to_vsphere
begin
try = 1
attempts = @options[:timeout].to_i / 5
start = Time.now
tasks = []
@hosts.each_with_index do |h, i|
if h['name']
h['vmhostname'] = h['name']
else
h['vmhostname'] = generate_host_name
end
if h['template'].nil? and defined?(ENV['BEAKER_vcloud_template'])
h['template'] = ENV['BEAKER_vcloud_template']
end
raise "Missing template configuration for #{h}. Set template in nodeset or set ENV[BEAKER_vcloud_template]" unless h['template']
if h['template'] =~ /\//
templatefolders = h['template'].split('/')
h['template'] = templatefolders.pop
end
@logger.notify "Deploying #{h['vmhostname']} (#{h.name}) to #{@options['folder']} from template '#{h['template']}'"
vm = {}
if templatefolders
vm[h['template']] = @vsphere_helper.find_folder(@options['datacenter'],templatefolders.join('/')).find(h['template'])
else
vm = @vsphere_helper.find_vms(h['template'])
end
if vm.length == 0
raise "Unable to find template '#{h['template']}'!"
end
spec = create_clone_spec(h)
# Deploy from specified template
tasks << vm[h['template']].CloneVM_Task( :folder => @vsphere_helper.find_folder(@options['datacenter'],@options['folder']), :name => h['vmhostname'], :spec => spec )
end
try = (Time.now - start) / 5
@vsphere_helper.wait_for_tasks(tasks, try, attempts)
@logger.notify 'Spent %.2f seconds deploying VMs' % (Time.now - start)
try = (Time.now - start) / 5
duration = run_and_report_duration do
@hosts.each_with_index do |h, i|
booting_host(h, try, attempts)
end
end
@logger.notify "Spent %.2f seconds booting and waiting for vSphere registration" % duration
try = (Time.now - start) / 5
duration = run_and_report_duration do
@hosts.each do |host|
repeat_fibonacci_style_for 8 do
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress != nil
end
host[:ip] = @vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress
enable_root(host)
end
end
@logger.notify "Spent %.2f seconds waiting for DNS resolution" % duration
rescue => e
@vsphere_helper.close
report_and_raise(@logger, e, "Vcloud.provision")
end
end
def cleanup
@logger.notify "Destroying vCloud boxes"
connect_to_vsphere
vm_names = @hosts.map {|h| h['vmhostname'] }.compact
if @hosts.length != vm_names.length
@logger.warn "Some hosts did not have vmhostname set correctly! This likely means VM provisioning was not successful"
end
vms = @vsphere_helper.find_vms vm_names
begin
vm_names.each do |name|
unless vm = vms[name]
@logger.warn "Unable to cleanup #{name}, couldn't find VM #{name} in vSphere!"
next
end
if vm.runtime.powerState == 'poweredOn'
@logger.notify "Shutting down #{vm.name}"
duration = run_and_report_duration do
vm.PowerOffVM_Task.wait_for_completion
end
@logger.notify "Spent %.2f seconds halting #{vm.name}" % duration
end
duration = run_and_report_duration do
vm.Destroy_Task
end
@logger.notify "Spent %.2f seconds destroying #{vm.name}" % duration
end
rescue RbVmomi::Fault => ex
if ex.fault.is_a?(RbVmomi::VIM::ManagedObjectNotFound)
#it's already gone, don't bother trying to delete it
name = vms.key(ex.fault.obj)
vms.delete(name)
vm_names.delete(name)
@logger.warn "Unable to destroy #{name}, it was not found in vSphere"
retry
end
end
@vsphere_helper.close
end
end
end

View file

@ -0,0 +1,355 @@
require 'yaml' unless defined?(YAML)
require 'json'
require 'net/http'
module Beaker
class Vmpooler < Beaker::Hypervisor
SSH_EXCEPTIONS = [
SocketError,
Timeout::Error,
Errno::ETIMEDOUT,
Errno::EHOSTDOWN,
Errno::EHOSTUNREACH,
Errno::ECONNREFUSED,
Errno::ECONNRESET,
Errno::ENETUNREACH,
]
attr_reader :options, :logger, :hosts, :credentials
def initialize(vmpooler_hosts, options)
@options = options
@logger = options[:logger]
@hosts = vmpooler_hosts
@credentials = load_credentials(@options[:dot_fog])
end
def load_credentials(dot_fog = '.fog')
creds = {}
if fog = read_fog_file(dot_fog)
if fog[:default] && fog[:default][:vmpooler_token]
creds[:vmpooler_token] = fog[:default][:vmpooler_token]
else
@logger.warn "Credentials file (#{dot_fog}) is missing a :default section with a :vmpooler_token value; proceeding without authentication"
end
else
@logger.warn "Credentials file (#{dot_fog}) is empty; proceeding without authentication"
end
creds
rescue TypeError, Psych::SyntaxError => e
@logger.warn "#{e.class}: Credentials file (#{dot_fog}) has invalid syntax; proceeding without authentication"
creds
rescue Errno::ENOENT
@logger.warn "Credentials file (#{dot_fog}) not found; proceeding without authentication"
creds
end
def read_fog_file(dot_fog = '.fog')
YAML.load_file(dot_fog)
end
def check_url url
begin
URI.parse(url)
rescue
return false
end
true
end
def get_template_url pooling_api, template
if not check_url(pooling_api)
raise ArgumentError, "Invalid pooling_api URL: #{pooling_api}"
end
scheme = ''
if not URI.parse(pooling_api).scheme
scheme = 'http://'
end
#check that you have a valid uri
template_url = scheme + pooling_api + '/vm/' + template
if not check_url(template_url)
raise ArgumentError, "Invalid full template URL: #{template_url}"
end
template_url
end
# Override host tags with presets
# @param [Beaker::Host] host Beaker host
# @return [Hash] Tag hash
def add_tags(host)
host[:host_tags].merge(
'beaker_version' => Beaker::Version::STRING,
'jenkins_build_url' => @options[:jenkins_build_url],
'department' => @options[:department],
'project' => @options[:project],
'created_by' => @options[:created_by],
'name' => host.name,
'roles' => host.host_hash[:roles].join(', ')
)
end
# Get host info hash from parsed json response
# @param [Hash] parsed_response hash
# @param [String] template string
# @return [Hash] Host info hash
def get_host_info(parsed_response, template)
parsed_response[template]
end
def provision
request_payload = {}
start = Time.now
@hosts.each_with_index do |h, i|
if not h['template']
raise ArgumentError, "You must specify a template name for #{h}"
end
if h['template'] =~ /\//
templatefolders = h['template'].split('/')
h['template'] = templatefolders.pop
end
request_payload[h['template']] = (request_payload[h['template']].to_i + 1).to_s
end
last_wait, wait = 0, 1
waited = 0 #the amount of time we've spent waiting for this host to provision
begin
uri = URI.parse(@options['pooling_api'] + '/vm/')
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
if @credentials[:vmpooler_token]
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
@logger.notify "Requesting VM set from vmpooler (with authentication token)"
else
@logger.notify "Requesting VM set from vmpooler"
end
request_payload_json = request_payload.to_json
@logger.trace( "Request payload json: #{request_payload_json}" )
request.body = request_payload_json
response = http.request(request)
parsed_response = JSON.parse(response.body)
@logger.trace( "Response parsed json: #{parsed_response}" )
if parsed_response['ok']
domain = parsed_response['domain']
request_payload = {}
@hosts.each_with_index do |h, i|
# If the requested host template is not available on vmpooler
host_template = h['template']
if get_host_info(parsed_response, host_template).nil?
request_payload[host_template] ||= 0
request_payload[host_template] += 1
next
end
if parsed_response[h['template']]['hostname'].is_a?(Array)
hostname = parsed_response[host_template]['hostname'].shift
else
hostname = parsed_response[host_template]['hostname']
end
h['vmhostname'] = domain ? "#{hostname}.#{domain}" : hostname
@logger.notify "Using available host '#{h['vmhostname']}' (#{h.name})"
end
unless request_payload.empty?
raise "Vmpooler.provision - requested VM templates #{request_payload.keys} not available"
end
else
if response.code == '401'
raise "Vmpooler.provision - response from pooler not ok. Vmpooler token not authorized to make request.\n#{parsed_response}"
else
raise "Vmpooler.provision - response from pooler not ok. Requested host set #{request_payload.keys} not available in pooler.\n#{parsed_response}"
end
end
rescue JSON::ParserError, RuntimeError, *SSH_EXCEPTIONS => e
@logger.debug "Failed vmpooler provision: #{e.class} : #{e.message}"
if waited <= @options[:timeout].to_i
@logger.debug("Retrying provision for vmpooler host after waiting #{wait} second(s)")
sleep wait
waited += wait
last_wait, wait = wait, last_wait + wait
retry
end
report_and_raise(@logger, e, 'Vmpooler.provision')
end
@logger.notify 'Spent %.2f seconds grabbing VMs' % (Time.now - start)
start = Time.now
@logger.notify 'Tagging vmpooler VMs'
@hosts.each_with_index do |h, i|
begin
uri = URI.parse(@options[:pooling_api] + '/vm/' + h['vmhostname'].split('.')[0])
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Put.new(uri.request_uri)
# merge pre-defined tags with host tags
request.body = { 'tags' => add_tags(h) }.to_json
response = http.request(request)
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
@logger.notify "Failed to connect to vmpooler for tagging!"
end
begin
parsed_response = JSON.parse(response.body)
unless parsed_response['ok']
@logger.notify "Failed to tag host '#{h['vmhostname']}'!"
end
rescue JSON::ParserError => e
@logger.notify "Failed to tag host '#{h['vmhostname']}'! (failed with #{e.class})"
end
end
@logger.notify 'Spent %.2f seconds tagging VMs' % (Time.now - start)
# add additional disks to vm
@logger.debug 'Looking for disks to add...'
@hosts.each do |h|
hostname = h['vmhostname'].split(".")[0]
if h['disks']
@logger.debug "Found disks for #{hostname}!"
disks = h['disks']
disks.each_with_index do |disk_size, index|
start = Time.now
add_disk(hostname, disk_size)
done = wait_for_disk(hostname, disk_size, index)
if done
@logger.notify "Spent %.2f seconds adding disk #{index}. " % (Time.now - start)
else
raise "Could not verify disk was added after %.2f seconds" % (Time.now - start)
end
end
else
@logger.debug "No disks to add for #{hostname}"
end
end
end
def cleanup
vm_names = @hosts.map {|h| h['vmhostname'] }.compact
if @hosts.length != vm_names.length
@logger.warn "Some hosts did not have vmhostname set correctly! This likely means VM provisioning was not successful"
end
start = Time.now
vm_names.each do |name|
@logger.notify "Handing '#{name}' back to vmpooler for VM destruction"
uri = URI.parse(get_template_url(@options['pooling_api'], name))
http = Net::HTTP.new( uri.host, uri.port )
request = Net::HTTP::Delete.new(uri.request_uri)
if @credentials[:vmpooler_token]
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
end
begin
response = http.request(request)
rescue *SSH_EXCEPTIONS => e
report_and_raise(@logger, e, 'Vmpooler.cleanup (http.request)')
end
end
@logger.notify "Spent %.2f seconds cleaning up" % (Time.now - start)
end
def add_disk(hostname, disk_size)
@logger.notify "Requesting an additional disk of size #{disk_size}GB for #{hostname}"
if !disk_size.to_s.match /[0123456789]/ || size <= '0'
raise NameError.new "Disk size must be an integer greater than zero!"
end
begin
uri = URI.parse(@options[:pooling_api] + '/api/v1/vm/' + hostname + '/disk/' + disk_size.to_s)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
response = http.request(request)
parsed = parse_response(response)
raise "Response from #{hostname} indicates disk was not added" if !parsed['ok']
rescue NameError, RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
report_and_raise(@logger, e, 'Vmpooler.add_disk')
end
end
def parse_response(response)
parsed_response = JSON.parse(response.body)
end
def disk_added?(host, disk_size, index)
if host['disk'].nil?
false
else
host['disk'][index] == "+#{disk_size}gb"
end
end
def get_vm(hostname)
begin
uri = URI.parse(@options[:pooling_api] + '/vm/' + hostname)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)
response = http.request(request)
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
@logger.notify "Failed to connect to vmpooler while getting VM information!"
end
end
def wait_for_disk(hostname, disk_size, index)
response = get_vm(hostname)
parsed = parse_response(response)
@logger.notify "Waiting for disk"
attempts = 0
while (!disk_added?(parsed[hostname], disk_size, index) && attempts < 20)
sleep 10
begin
response = get_vm(hostname)
parsed = parse_response(response)
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
report_and_raise(@logger, e, "Vmpooler.wait_for_disk")
end
print "."
attempts += 1
end
puts " "
disk_added?(parsed[hostname], disk_size, index)
end
end
end

View file

@ -0,0 +1,79 @@
require 'spec_helper'
module Beaker
describe Vcloud do
before :each do
MockVsphereHelper.set_config( fog_file_contents )
MockVsphereHelper.set_vms( make_hosts() )
stub_const( "VsphereHelper", MockVsphereHelper )
stub_const( "Net", MockNet )
json = double( 'json' )
allow( json ).to receive( :parse ) do |arg|
arg
end
stub_const( "JSON", json )
allow( Socket ).to receive( :getaddrinfo ).and_return( true )
end
describe "#provision" do
it 'instantiates vmpooler if pooling api is provided' do
opts = make_opts
opts[:pooling_api] = 'testpool'
hypervisor = Beaker::Vcloud.new( make_hosts, opts)
expect( hypervisor.class ).to be Beaker::Vmpooler
end
it 'provisions hosts and add them to the pool' do
MockVsphereHelper.powerOff
opts = make_opts
opts[:pooling_api] = nil
opts[:datacenter] = 'testdc'
vcloud = Beaker::Vcloud.new( make_hosts, opts )
allow( vcloud ).to receive( :require ).and_return( true )
allow( vcloud ).to receive( :sleep ).and_return( true )
vcloud.provision
hosts = vcloud.instance_variable_get( :@hosts )
hosts.each do | host |
name = host['vmhostname']
vm = MockVsphereHelper.find_vm( name )
expect( vm.toolsRunningStatus ).to be === "guestToolsRunning"
end
end
end
describe "#cleanup" do
it "cleans up hosts not in the pool" do
MockVsphereHelper.powerOn
opts = make_opts
opts[:pooling_api] = nil
opts[:datacenter] = 'testdc'
vcloud = Beaker::Vcloud.new( make_hosts, opts )
allow( vcloud ).to receive( :require ).and_return( true )
allow( vcloud ).to receive( :sleep ).and_return( true )
vcloud.provision
vcloud.cleanup
hosts = vcloud.instance_variable_get( :@hosts )
vm_names = hosts.map {|h| h['vmhostname'] }.compact
vm_names.each do | name |
vm = MockVsphereHelper.find_vm( name )
expect( vm.runtime.powerState ).to be === "poweredOff"
end
end
end
end
end

View file

@ -0,0 +1,276 @@
require 'spec_helper'
module Beaker
describe Vmpooler do
before :each do
vms = make_hosts()
MockVsphereHelper.set_config( fog_file_contents )
MockVsphereHelper.set_vms( vms )
stub_const( "VsphereHelper", MockVsphereHelper )
stub_const( "Net", MockNet )
allow( JSON ).to receive( :parse ) do |arg|
arg
end
allow( Socket ).to receive( :getaddrinfo ).and_return( true )
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:load_credentials).and_return(fog_file_contents)
end
describe '#get_template_url' do
it 'works returns the valid url when passed valid pooling_api and template name' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
uri = vmpooler.get_template_url("http://pooling.com", "template")
expect( uri ).to be === "http://pooling.com/vm/template"
end
it 'adds a missing scheme to a given URL' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
uri = vmpooler.get_template_url("pooling.com", "template")
expect( URI.parse(uri).scheme ).to_not be === nil
end
it 'raises an error on an invalid pooling api url' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect{ vmpooler.get_template_url("pooling### ", "template")}.to raise_error ArgumentError
end
it 'raises an error on an invalide template name' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect{ vmpooler.get_template_url("pooling.com", "t!e&m*p(l\\a/t e")}.to raise_error ArgumentError
end
end
describe '#add_tags' do
let(:vmpooler) { Beaker::Vmpooler.new(make_hosts({:host_tags => {'test_tag' => 'test_value'}}), make_opts) }
it 'merges tags correctly' do
vmpooler.instance_eval {
@options = @options.merge({:project => 'vmpooler-spec'})
}
host = vmpooler.instance_variable_get(:@hosts)[0]
merged_tags = vmpooler.add_tags(host)
expected_hash = {
test_tag: 'test_value',
beaker_version: Beaker::Version::STRING,
project: 'vmpooler-spec'
}
expect(merged_tags).to include(expected_hash)
end
end
describe '#disk_added?' do
let(:vmpooler) { Beaker::Vmpooler.new(make_hosts, make_opts) }
let(:response_hash_no_disk) {
{
"ok" => "true",
"hostname" => {
"template"=>"redhat-7-x86_64",
"domain"=>"delivery.puppetlabs.net"
}
}
}
let(:response_hash_disk) {
{
"ok" => "true",
"hostname" => {
"disk" => [
'+16gb',
'+8gb'
],
"template"=>"redhat-7-x86_64",
"domain"=>"delivery.puppetlabs.net"
}
}
}
it 'returns false when there is no disk' do
host = response_hash_no_disk['hostname']
expect(vmpooler.disk_added?(host, "8", 0)).to be(false)
end
it 'returns true when there is a disk' do
host = response_hash_disk["hostname"]
expect(vmpooler.disk_added?(host, "16", 0)).to be(true)
expect(vmpooler.disk_added?(host, "8", 1)).to be(true)
end
end
describe "#provision" do
it 'provisions hosts from the pool' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
allow( vmpooler ).to receive( :require ).and_return( true )
allow( vmpooler ).to receive( :sleep ).and_return( true )
vmpooler.provision
hosts = vmpooler.instance_variable_get( :@hosts )
hosts.each do | host |
expect( host['vmhostname'] ).to be === 'pool'
end
end
it 'raises an error when a host template is not found in returned json' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
allow( vmpooler ).to receive( :require ).and_return( true )
allow( vmpooler ).to receive( :sleep ).and_return( true )
allow( vmpooler ).to receive( :get_host_info ).and_return( nil )
expect {
vmpooler.provision
}.to raise_error( RuntimeError,
/Vmpooler\.provision - requested VM templates \[.*\,.*\,.*\] not available/
)
end
it 'repeats asking only for failed hosts' do
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
allow( vmpooler ).to receive( :require ).and_return( true )
allow( vmpooler ).to receive( :sleep ).and_return( true )
allow( vmpooler ).to receive( :get_host_info ).with(
anything, "vm1_has_a_template" ).and_return( nil )
allow( vmpooler ).to receive( :get_host_info ).with(
anything, "vm2_has_a_template" ).and_return( 'y' )
allow( vmpooler ).to receive( :get_host_info ).with(
anything, "vm3_has_a_template" ).and_return( 'y' )
expect {
vmpooler.provision
}.to raise_error( RuntimeError,
/Vmpooler\.provision - requested VM templates \[[^\,]*\] not available/
) # should be only one item in the list, no commas
end
end
describe "#cleanup" do
it "cleans up hosts in the pool" do
MockVsphereHelper.powerOn
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
allow( vmpooler ).to receive( :require ).and_return( true )
allow( vmpooler ).to receive( :sleep ).and_return( true )
vmpooler.provision
vmpooler.cleanup
hosts = vmpooler.instance_variable_get( :@hosts )
hosts.each do | host |
name = host.name
vm = MockVsphereHelper.find_vm( name )
expect( vm.runtime.powerState ).to be === "poweredOn" #handed back to the pool, stays on
end
end
end
end
describe Vmpooler do
before :each do
vms = make_hosts()
MockVsphereHelper.set_config( fog_file_contents )
MockVsphereHelper.set_vms( vms )
stub_const( "VsphereHelper", MockVsphereHelper )
stub_const( "Net", MockNet )
allow( JSON ).to receive( :parse ) do |arg|
arg
end
allow( Socket ).to receive( :getaddrinfo ).and_return( true )
end
describe "#load_credentials" do
it 'continues without credentials when fog file is missing' do
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_raise(Errno::ENOENT.new)
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == {}
end
it 'continues without credentials when fog file is empty' do
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_return(false)
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == {}
end
it 'continues without credentials when fog file contains no :default section' do
data = { :some => { :other => :data } }
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_return(data)
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { }
end
it 'continues without credentials when fog file :default section has no :vmpooler_token' do
data = { :default => { :something_else => "TOKEN" } }
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_return(data)
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { }
end
it 'continues without credentials when there are formatting errors in the fog file' do
data = { "'default'" => { :vmpooler_token => "b2wl8prqe6ddoii70md" } }
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_return(data)
logger = double('logger')
expect(logger).to receive(:warn).with(/is missing a :default section with a :vmpooler_token value/)
make_opts = {:logger => logger}
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { }
end
it 'throws a TypeError and continues without credentials when there are syntax errors in the fog file' do
data = "'default'\n :vmpooler_token: z2wl8prqe0ddoii70ad"
allow( File ).to receive( :open ).and_yield( StringIO.new(data) )
logger = double('logger')
expect(logger).to receive(:warn).with(/TypeError: .* has invalid syntax/)
make_opts = {:logger => logger}
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { }
end
it 'throws a Psych::SyntaxError and continues without credentials when there are syntax errors in the fog file' do
data = ";default;\n :vmpooler_token: z2wl8prqe0ddoii707d"
allow( File ).to receive( :open ).and_yield( StringIO.new(data) )
logger = double('logger')
expect(logger).to receive(:warn).with(/Psych::SyntaxError: .* invalid syntax/)
make_opts = {:logger => logger}
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { }
end
it 'stores vmpooler token when found in fog file' do
data = { :default => { :vmpooler_token => "TOKEN" } }
allow_any_instance_of( Beaker::Vmpooler ).to \
receive(:read_fog_file).and_return(data)
vmpooler = Beaker::Vmpooler.new( make_hosts, make_opts )
expect( vmpooler.credentials ).to be == { :vmpooler_token => "TOKEN" }
end
end
end
end

17
spec/spec_helper.rb Normal file
View file

@ -0,0 +1,17 @@
require 'simplecov'
require 'rspec/its'
require 'beaker'
require 'beaker/hypervisor/vmpooler'
require 'beaker/hypervisor/vcloud'
# setup & require beaker's spec_helper.rb
beaker_gem_spec = Gem::Specification.find_by_name('beaker')
beaker_gem_dir = beaker_gem_spec.gem_dir
beaker_spec_path = File.join(beaker_gem_dir, 'spec')
$LOAD_PATH << beaker_spec_path
require File.join(beaker_spec_path, 'spec_helper.rb')
RSpec.configure do |config|
config.include TestFileHelpers
config.include HostHelpers
end

45
vmpooler.md Normal file
View file

@ -0,0 +1,45 @@
[vmpooler](https://github.com/puppetlabs/vmpooler) is a puppet-built abstraction
layer over vSphere infrastructure that pools VMs to be used by beaker & other
systems.
beaker's vmpooler hypervisor interacts with vmpooler to get Systems Under Test
(SUTs) for testing purposes.
**Note** that if you're a puppet-internal user, you'll have to setup your SSH
keys to communicate with vmpooler SUTs. To do that, refer to our
[internal doc](https://confluence.puppetlabs.com/display/SRE/SSH+access+to+vmpooler+VMs).
# Tokens
Using tokens will allow you to extend your VMs lifetime, as well as interact
with vmpooler and your VMs in more complex ways. You can have beaker do these
same things by providing your `vmpooler_token` in the `~/.fog` file. For more
info about how the `.fog` file works, please refer to the beaker
[hypervisor README](https://github.com/puppetlabs/beaker/blob/master/docs/how_to/hypervisors/README.md).
An example of a `.fog` file with just the vmpooler details is below:
```yaml
:default:
:vmpooler_token: 'randomtokentext'
```
# Additional Disks
Using the vmpooler API, Beaker enables you to attach additional storage disks in the host configuration file. The disks are added at the time the VM is created. Logic for using the disk must go into your tests.
Simply add the `disks` key and a list containing the sizes(in GB) of the disks you want to create and attach to that host.
For example, to create 2 disks sized 8GB and 16GB to example-box:
```yaml
example-box:
disks:
- 8
- 16
roles:
- satellite
platform: el-7-x86_64
hypervisor: vmpooler
template: redhat-7-x86_64
```
Users with Puppet credentials can follow our instructions for getting & using
vmpooler tokens in our
[internal documentation](https://confluence.puppetlabs.com/pages/viewpage.action?spaceKey=SRE&title=Generating+and+using+vmpooler+tokens).