beaker-vmpooler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +15 -0
- data/.gitignore +25 -0
- data/.rspec +3 -0
- data/.simplecov +9 -0
- data/Gemfile +27 -0
- data/LICENSE +202 -0
- data/README.md +27 -0
- data/Rakefile +147 -0
- data/beaker-vmpooler.gemspec +38 -0
- data/bin/beaker-vmpooler +32 -0
- data/lib/beaker/hypervisor/vcloud.rb +238 -0
- data/lib/beaker/hypervisor/vmpooler.rb +355 -0
- data/lib/beaker-vmpooler/version.rb +3 -0
- data/spec/beaker/hypervisor/vcloud_spec.rb +79 -0
- data/spec/beaker/hypervisor/vmpooler_spec.rb +276 -0
- data/spec/spec_helper.rb +17 -0
- data/vmpooler.md +45 -0
- metadata +215 -0
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'yaml' unless defined?(YAML)
|
2
|
+
require 'beaker/hypervisor/vmpooler'
|
3
|
+
|
4
|
+
module Beaker
|
5
|
+
class Vcloud < Beaker::Hypervisor
|
6
|
+
|
7
|
+
def self.new(vcloud_hosts, options)
|
8
|
+
if options['pooling_api']
|
9
|
+
Beaker::Vmpooler.new(vcloud_hosts, options)
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(vcloud_hosts, options)
|
16
|
+
@options = options
|
17
|
+
@logger = options[:logger]
|
18
|
+
@hosts = vcloud_hosts
|
19
|
+
|
20
|
+
raise 'You must specify a datastore for vCloud instances!' unless @options['datastore']
|
21
|
+
raise 'You must specify a folder for vCloud instances!' unless @options['folder']
|
22
|
+
raise 'You must specify a datacenter for vCloud instances!' unless @options['datacenter']
|
23
|
+
@vsphere_credentials = VsphereHelper.load_config(@options[:dot_fog])
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect_to_vsphere
|
27
|
+
@logger.notify "Connecting to vSphere at #{@vsphere_credentials[:server]}" +
|
28
|
+
" with credentials for #{@vsphere_credentials[:user]}"
|
29
|
+
|
30
|
+
@vsphere_helper = VsphereHelper.new( @vsphere_credentials )
|
31
|
+
end
|
32
|
+
|
33
|
+
def wait_for_dns_resolution host, try, attempts
|
34
|
+
@logger.notify "Waiting for #{host['vmhostname']} DNS resolution"
|
35
|
+
begin
|
36
|
+
Socket.getaddrinfo(host['vmhostname'], nil)
|
37
|
+
rescue
|
38
|
+
if try <= attempts
|
39
|
+
sleep 5
|
40
|
+
try += 1
|
41
|
+
|
42
|
+
retry
|
43
|
+
else
|
44
|
+
raise "DNS resolution failed after #{@options[:timeout].to_i} seconds"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def booting_host host, try, attempts
|
50
|
+
@logger.notify "Booting #{host['vmhostname']} (#{host.name}) and waiting for it to register with vSphere"
|
51
|
+
until
|
52
|
+
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.toolsRunningStatus == 'guestToolsRunning' and
|
53
|
+
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress != nil
|
54
|
+
if try <= attempts
|
55
|
+
sleep 5
|
56
|
+
try += 1
|
57
|
+
else
|
58
|
+
raise "vSphere registration failed after #{@options[:timeout].to_i} seconds"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Directly borrowed from openstack hypervisor
|
64
|
+
def enable_root(host)
|
65
|
+
if host['user'] != 'root'
|
66
|
+
copy_ssh_to_root(host, @options)
|
67
|
+
enable_root_login(host, @options)
|
68
|
+
host['user'] = 'root'
|
69
|
+
host.close
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_clone_spec host
|
74
|
+
# Add VM annotation
|
75
|
+
configSpec = RbVmomi::VIM.VirtualMachineConfigSpec(
|
76
|
+
:annotation =>
|
77
|
+
'Base template: ' + host['template'] + "\n" +
|
78
|
+
'Creation time: ' + Time.now.strftime("%Y-%m-%d %H:%M") + "\n\n" +
|
79
|
+
'CI build link: ' + ( ENV['BUILD_URL'] || 'Deployed independently of CI' ) +
|
80
|
+
'department: ' + @options[:department] +
|
81
|
+
'project: ' + @options[:project],
|
82
|
+
:extraConfig => [
|
83
|
+
{ :key => 'guestinfo.hostname',
|
84
|
+
:value => host['vmhostname']
|
85
|
+
}
|
86
|
+
]
|
87
|
+
)
|
88
|
+
|
89
|
+
# Are we using a customization spec?
|
90
|
+
customizationSpec = @vsphere_helper.find_customization( host['template'] )
|
91
|
+
|
92
|
+
if customizationSpec
|
93
|
+
# Print a logger message if using a customization spec
|
94
|
+
@logger.notify "Found customization spec for '#{host['template']}', will apply after boot"
|
95
|
+
end
|
96
|
+
|
97
|
+
# Put the VM in the specified folder and resource pool
|
98
|
+
relocateSpec = RbVmomi::VIM.VirtualMachineRelocateSpec(
|
99
|
+
:datastore => @vsphere_helper.find_datastore(@options['datacenter'],@options['datastore']),
|
100
|
+
:pool => @options['resourcepool'] ? @vsphere_helper.find_pool(@options['datacenter'],@options['resourcepool']) : nil,
|
101
|
+
:diskMoveType => :moveChildMostDiskBacking
|
102
|
+
)
|
103
|
+
|
104
|
+
# Create a clone spec
|
105
|
+
spec = RbVmomi::VIM.VirtualMachineCloneSpec(
|
106
|
+
:config => configSpec,
|
107
|
+
:location => relocateSpec,
|
108
|
+
:customization => customizationSpec,
|
109
|
+
:powerOn => true,
|
110
|
+
:template => false
|
111
|
+
)
|
112
|
+
spec
|
113
|
+
end
|
114
|
+
|
115
|
+
def provision
|
116
|
+
connect_to_vsphere
|
117
|
+
begin
|
118
|
+
|
119
|
+
try = 1
|
120
|
+
attempts = @options[:timeout].to_i / 5
|
121
|
+
|
122
|
+
start = Time.now
|
123
|
+
tasks = []
|
124
|
+
@hosts.each_with_index do |h, i|
|
125
|
+
if h['name']
|
126
|
+
h['vmhostname'] = h['name']
|
127
|
+
else
|
128
|
+
h['vmhostname'] = generate_host_name
|
129
|
+
end
|
130
|
+
|
131
|
+
if h['template'].nil? and defined?(ENV['BEAKER_vcloud_template'])
|
132
|
+
h['template'] = ENV['BEAKER_vcloud_template']
|
133
|
+
end
|
134
|
+
|
135
|
+
raise "Missing template configuration for #{h}. Set template in nodeset or set ENV[BEAKER_vcloud_template]" unless h['template']
|
136
|
+
|
137
|
+
if h['template'] =~ /\//
|
138
|
+
templatefolders = h['template'].split('/')
|
139
|
+
h['template'] = templatefolders.pop
|
140
|
+
end
|
141
|
+
|
142
|
+
@logger.notify "Deploying #{h['vmhostname']} (#{h.name}) to #{@options['folder']} from template '#{h['template']}'"
|
143
|
+
|
144
|
+
vm = {}
|
145
|
+
|
146
|
+
if templatefolders
|
147
|
+
vm[h['template']] = @vsphere_helper.find_folder(@options['datacenter'],templatefolders.join('/')).find(h['template'])
|
148
|
+
else
|
149
|
+
vm = @vsphere_helper.find_vms(h['template'])
|
150
|
+
end
|
151
|
+
|
152
|
+
if vm.length == 0
|
153
|
+
raise "Unable to find template '#{h['template']}'!"
|
154
|
+
end
|
155
|
+
|
156
|
+
spec = create_clone_spec(h)
|
157
|
+
|
158
|
+
# Deploy from specified template
|
159
|
+
tasks << vm[h['template']].CloneVM_Task( :folder => @vsphere_helper.find_folder(@options['datacenter'],@options['folder']), :name => h['vmhostname'], :spec => spec )
|
160
|
+
end
|
161
|
+
|
162
|
+
try = (Time.now - start) / 5
|
163
|
+
@vsphere_helper.wait_for_tasks(tasks, try, attempts)
|
164
|
+
@logger.notify 'Spent %.2f seconds deploying VMs' % (Time.now - start)
|
165
|
+
|
166
|
+
try = (Time.now - start) / 5
|
167
|
+
duration = run_and_report_duration do
|
168
|
+
@hosts.each_with_index do |h, i|
|
169
|
+
booting_host(h, try, attempts)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
@logger.notify "Spent %.2f seconds booting and waiting for vSphere registration" % duration
|
173
|
+
|
174
|
+
try = (Time.now - start) / 5
|
175
|
+
duration = run_and_report_duration do
|
176
|
+
@hosts.each do |host|
|
177
|
+
repeat_fibonacci_style_for 8 do
|
178
|
+
@vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress != nil
|
179
|
+
end
|
180
|
+
host[:ip] = @vsphere_helper.find_vms(host['vmhostname'])[host['vmhostname']].summary.guest.ipAddress
|
181
|
+
enable_root(host)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
@logger.notify "Spent %.2f seconds waiting for DNS resolution" % duration
|
186
|
+
|
187
|
+
rescue => e
|
188
|
+
@vsphere_helper.close
|
189
|
+
report_and_raise(@logger, e, "Vcloud.provision")
|
190
|
+
end
|
191
|
+
|
192
|
+
end
|
193
|
+
|
194
|
+
def cleanup
|
195
|
+
@logger.notify "Destroying vCloud boxes"
|
196
|
+
connect_to_vsphere
|
197
|
+
|
198
|
+
vm_names = @hosts.map {|h| h['vmhostname'] }.compact
|
199
|
+
if @hosts.length != vm_names.length
|
200
|
+
@logger.warn "Some hosts did not have vmhostname set correctly! This likely means VM provisioning was not successful"
|
201
|
+
end
|
202
|
+
vms = @vsphere_helper.find_vms vm_names
|
203
|
+
begin
|
204
|
+
vm_names.each do |name|
|
205
|
+
unless vm = vms[name]
|
206
|
+
@logger.warn "Unable to cleanup #{name}, couldn't find VM #{name} in vSphere!"
|
207
|
+
next
|
208
|
+
end
|
209
|
+
|
210
|
+
if vm.runtime.powerState == 'poweredOn'
|
211
|
+
@logger.notify "Shutting down #{vm.name}"
|
212
|
+
duration = run_and_report_duration do
|
213
|
+
vm.PowerOffVM_Task.wait_for_completion
|
214
|
+
end
|
215
|
+
@logger.notify "Spent %.2f seconds halting #{vm.name}" % duration
|
216
|
+
end
|
217
|
+
|
218
|
+
duration = run_and_report_duration do
|
219
|
+
vm.Destroy_Task
|
220
|
+
end
|
221
|
+
@logger.notify "Spent %.2f seconds destroying #{vm.name}" % duration
|
222
|
+
|
223
|
+
end
|
224
|
+
rescue RbVmomi::Fault => ex
|
225
|
+
if ex.fault.is_a?(RbVmomi::VIM::ManagedObjectNotFound)
|
226
|
+
#it's already gone, don't bother trying to delete it
|
227
|
+
name = vms.key(ex.fault.obj)
|
228
|
+
vms.delete(name)
|
229
|
+
vm_names.delete(name)
|
230
|
+
@logger.warn "Unable to destroy #{name}, it was not found in vSphere"
|
231
|
+
retry
|
232
|
+
end
|
233
|
+
end
|
234
|
+
@vsphere_helper.close
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'yaml' unless defined?(YAML)
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
|
5
|
+
module Beaker
|
6
|
+
class Vmpooler < Beaker::Hypervisor
|
7
|
+
|
8
|
+
SSH_EXCEPTIONS = [
|
9
|
+
SocketError,
|
10
|
+
Timeout::Error,
|
11
|
+
Errno::ETIMEDOUT,
|
12
|
+
Errno::EHOSTDOWN,
|
13
|
+
Errno::EHOSTUNREACH,
|
14
|
+
Errno::ECONNREFUSED,
|
15
|
+
Errno::ECONNRESET,
|
16
|
+
Errno::ENETUNREACH,
|
17
|
+
]
|
18
|
+
|
19
|
+
attr_reader :options, :logger, :hosts, :credentials
|
20
|
+
|
21
|
+
def initialize(vmpooler_hosts, options)
|
22
|
+
@options = options
|
23
|
+
@logger = options[:logger]
|
24
|
+
@hosts = vmpooler_hosts
|
25
|
+
@credentials = load_credentials(@options[:dot_fog])
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_credentials(dot_fog = '.fog')
|
29
|
+
creds = {}
|
30
|
+
|
31
|
+
if fog = read_fog_file(dot_fog)
|
32
|
+
if fog[:default] && fog[:default][:vmpooler_token]
|
33
|
+
creds[:vmpooler_token] = fog[:default][:vmpooler_token]
|
34
|
+
else
|
35
|
+
@logger.warn "Credentials file (#{dot_fog}) is missing a :default section with a :vmpooler_token value; proceeding without authentication"
|
36
|
+
end
|
37
|
+
else
|
38
|
+
@logger.warn "Credentials file (#{dot_fog}) is empty; proceeding without authentication"
|
39
|
+
end
|
40
|
+
|
41
|
+
creds
|
42
|
+
|
43
|
+
rescue TypeError, Psych::SyntaxError => e
|
44
|
+
@logger.warn "#{e.class}: Credentials file (#{dot_fog}) has invalid syntax; proceeding without authentication"
|
45
|
+
creds
|
46
|
+
rescue Errno::ENOENT
|
47
|
+
@logger.warn "Credentials file (#{dot_fog}) not found; proceeding without authentication"
|
48
|
+
creds
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_fog_file(dot_fog = '.fog')
|
52
|
+
YAML.load_file(dot_fog)
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_url url
|
56
|
+
begin
|
57
|
+
URI.parse(url)
|
58
|
+
rescue
|
59
|
+
return false
|
60
|
+
end
|
61
|
+
true
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_template_url pooling_api, template
|
65
|
+
if not check_url(pooling_api)
|
66
|
+
raise ArgumentError, "Invalid pooling_api URL: #{pooling_api}"
|
67
|
+
end
|
68
|
+
scheme = ''
|
69
|
+
if not URI.parse(pooling_api).scheme
|
70
|
+
scheme = 'http://'
|
71
|
+
end
|
72
|
+
#check that you have a valid uri
|
73
|
+
template_url = scheme + pooling_api + '/vm/' + template
|
74
|
+
if not check_url(template_url)
|
75
|
+
raise ArgumentError, "Invalid full template URL: #{template_url}"
|
76
|
+
end
|
77
|
+
template_url
|
78
|
+
end
|
79
|
+
|
80
|
+
# Override host tags with presets
|
81
|
+
# @param [Beaker::Host] host Beaker host
|
82
|
+
# @return [Hash] Tag hash
|
83
|
+
def add_tags(host)
|
84
|
+
host[:host_tags].merge(
|
85
|
+
'beaker_version' => Beaker::Version::STRING,
|
86
|
+
'jenkins_build_url' => @options[:jenkins_build_url],
|
87
|
+
'department' => @options[:department],
|
88
|
+
'project' => @options[:project],
|
89
|
+
'created_by' => @options[:created_by],
|
90
|
+
'name' => host.name,
|
91
|
+
'roles' => host.host_hash[:roles].join(', ')
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Get host info hash from parsed json response
|
96
|
+
# @param [Hash] parsed_response hash
|
97
|
+
# @param [String] template string
|
98
|
+
# @return [Hash] Host info hash
|
99
|
+
def get_host_info(parsed_response, template)
|
100
|
+
parsed_response[template]
|
101
|
+
end
|
102
|
+
|
103
|
+
def provision
|
104
|
+
request_payload = {}
|
105
|
+
start = Time.now
|
106
|
+
|
107
|
+
@hosts.each_with_index do |h, i|
|
108
|
+
if not h['template']
|
109
|
+
raise ArgumentError, "You must specify a template name for #{h}"
|
110
|
+
end
|
111
|
+
if h['template'] =~ /\//
|
112
|
+
templatefolders = h['template'].split('/')
|
113
|
+
h['template'] = templatefolders.pop
|
114
|
+
end
|
115
|
+
|
116
|
+
request_payload[h['template']] = (request_payload[h['template']].to_i + 1).to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
last_wait, wait = 0, 1
|
120
|
+
waited = 0 #the amount of time we've spent waiting for this host to provision
|
121
|
+
begin
|
122
|
+
uri = URI.parse(@options['pooling_api'] + '/vm/')
|
123
|
+
|
124
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
125
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
126
|
+
|
127
|
+
if @credentials[:vmpooler_token]
|
128
|
+
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
|
129
|
+
@logger.notify "Requesting VM set from vmpooler (with authentication token)"
|
130
|
+
else
|
131
|
+
@logger.notify "Requesting VM set from vmpooler"
|
132
|
+
end
|
133
|
+
|
134
|
+
request_payload_json = request_payload.to_json
|
135
|
+
@logger.trace( "Request payload json: #{request_payload_json}" )
|
136
|
+
request.body = request_payload_json
|
137
|
+
|
138
|
+
response = http.request(request)
|
139
|
+
parsed_response = JSON.parse(response.body)
|
140
|
+
@logger.trace( "Response parsed json: #{parsed_response}" )
|
141
|
+
|
142
|
+
if parsed_response['ok']
|
143
|
+
domain = parsed_response['domain']
|
144
|
+
request_payload = {}
|
145
|
+
|
146
|
+
@hosts.each_with_index do |h, i|
|
147
|
+
# If the requested host template is not available on vmpooler
|
148
|
+
host_template = h['template']
|
149
|
+
if get_host_info(parsed_response, host_template).nil?
|
150
|
+
request_payload[host_template] ||= 0
|
151
|
+
request_payload[host_template] += 1
|
152
|
+
next
|
153
|
+
end
|
154
|
+
if parsed_response[h['template']]['hostname'].is_a?(Array)
|
155
|
+
hostname = parsed_response[host_template]['hostname'].shift
|
156
|
+
else
|
157
|
+
hostname = parsed_response[host_template]['hostname']
|
158
|
+
end
|
159
|
+
|
160
|
+
h['vmhostname'] = domain ? "#{hostname}.#{domain}" : hostname
|
161
|
+
|
162
|
+
@logger.notify "Using available host '#{h['vmhostname']}' (#{h.name})"
|
163
|
+
end
|
164
|
+
unless request_payload.empty?
|
165
|
+
raise "Vmpooler.provision - requested VM templates #{request_payload.keys} not available"
|
166
|
+
end
|
167
|
+
else
|
168
|
+
if response.code == '401'
|
169
|
+
raise "Vmpooler.provision - response from pooler not ok. Vmpooler token not authorized to make request.\n#{parsed_response}"
|
170
|
+
else
|
171
|
+
raise "Vmpooler.provision - response from pooler not ok. Requested host set #{request_payload.keys} not available in pooler.\n#{parsed_response}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue JSON::ParserError, RuntimeError, *SSH_EXCEPTIONS => e
|
175
|
+
@logger.debug "Failed vmpooler provision: #{e.class} : #{e.message}"
|
176
|
+
if waited <= @options[:timeout].to_i
|
177
|
+
@logger.debug("Retrying provision for vmpooler host after waiting #{wait} second(s)")
|
178
|
+
sleep wait
|
179
|
+
waited += wait
|
180
|
+
last_wait, wait = wait, last_wait + wait
|
181
|
+
retry
|
182
|
+
end
|
183
|
+
report_and_raise(@logger, e, 'Vmpooler.provision')
|
184
|
+
end
|
185
|
+
|
186
|
+
@logger.notify 'Spent %.2f seconds grabbing VMs' % (Time.now - start)
|
187
|
+
|
188
|
+
start = Time.now
|
189
|
+
@logger.notify 'Tagging vmpooler VMs'
|
190
|
+
|
191
|
+
@hosts.each_with_index do |h, i|
|
192
|
+
begin
|
193
|
+
uri = URI.parse(@options[:pooling_api] + '/vm/' + h['vmhostname'].split('.')[0])
|
194
|
+
|
195
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
196
|
+
request = Net::HTTP::Put.new(uri.request_uri)
|
197
|
+
|
198
|
+
# merge pre-defined tags with host tags
|
199
|
+
request.body = { 'tags' => add_tags(h) }.to_json
|
200
|
+
|
201
|
+
response = http.request(request)
|
202
|
+
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
203
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
|
204
|
+
@logger.notify "Failed to connect to vmpooler for tagging!"
|
205
|
+
end
|
206
|
+
|
207
|
+
begin
|
208
|
+
parsed_response = JSON.parse(response.body)
|
209
|
+
|
210
|
+
unless parsed_response['ok']
|
211
|
+
@logger.notify "Failed to tag host '#{h['vmhostname']}'!"
|
212
|
+
end
|
213
|
+
rescue JSON::ParserError => e
|
214
|
+
@logger.notify "Failed to tag host '#{h['vmhostname']}'! (failed with #{e.class})"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
@logger.notify 'Spent %.2f seconds tagging VMs' % (Time.now - start)
|
219
|
+
|
220
|
+
# add additional disks to vm
|
221
|
+
@logger.debug 'Looking for disks to add...'
|
222
|
+
|
223
|
+
@hosts.each do |h|
|
224
|
+
hostname = h['vmhostname'].split(".")[0]
|
225
|
+
|
226
|
+
if h['disks']
|
227
|
+
@logger.debug "Found disks for #{hostname}!"
|
228
|
+
disks = h['disks']
|
229
|
+
|
230
|
+
disks.each_with_index do |disk_size, index|
|
231
|
+
start = Time.now
|
232
|
+
|
233
|
+
add_disk(hostname, disk_size)
|
234
|
+
|
235
|
+
done = wait_for_disk(hostname, disk_size, index)
|
236
|
+
if done
|
237
|
+
@logger.notify "Spent %.2f seconds adding disk #{index}. " % (Time.now - start)
|
238
|
+
else
|
239
|
+
raise "Could not verify disk was added after %.2f seconds" % (Time.now - start)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
else
|
243
|
+
@logger.debug "No disks to add for #{hostname}"
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def cleanup
|
249
|
+
vm_names = @hosts.map {|h| h['vmhostname'] }.compact
|
250
|
+
if @hosts.length != vm_names.length
|
251
|
+
@logger.warn "Some hosts did not have vmhostname set correctly! This likely means VM provisioning was not successful"
|
252
|
+
end
|
253
|
+
|
254
|
+
start = Time.now
|
255
|
+
vm_names.each do |name|
|
256
|
+
@logger.notify "Handing '#{name}' back to vmpooler for VM destruction"
|
257
|
+
|
258
|
+
uri = URI.parse(get_template_url(@options['pooling_api'], name))
|
259
|
+
|
260
|
+
http = Net::HTTP.new( uri.host, uri.port )
|
261
|
+
request = Net::HTTP::Delete.new(uri.request_uri)
|
262
|
+
|
263
|
+
if @credentials[:vmpooler_token]
|
264
|
+
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
|
265
|
+
end
|
266
|
+
|
267
|
+
begin
|
268
|
+
response = http.request(request)
|
269
|
+
rescue *SSH_EXCEPTIONS => e
|
270
|
+
report_and_raise(@logger, e, 'Vmpooler.cleanup (http.request)')
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
@logger.notify "Spent %.2f seconds cleaning up" % (Time.now - start)
|
275
|
+
end
|
276
|
+
|
277
|
+
def add_disk(hostname, disk_size)
|
278
|
+
@logger.notify "Requesting an additional disk of size #{disk_size}GB for #{hostname}"
|
279
|
+
|
280
|
+
if !disk_size.to_s.match /[0123456789]/ || size <= '0'
|
281
|
+
raise NameError.new "Disk size must be an integer greater than zero!"
|
282
|
+
end
|
283
|
+
|
284
|
+
begin
|
285
|
+
uri = URI.parse(@options[:pooling_api] + '/api/v1/vm/' + hostname + '/disk/' + disk_size.to_s)
|
286
|
+
|
287
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
288
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
289
|
+
request['X-AUTH-TOKEN'] = @credentials[:vmpooler_token]
|
290
|
+
|
291
|
+
response = http.request(request)
|
292
|
+
|
293
|
+
parsed = parse_response(response)
|
294
|
+
|
295
|
+
raise "Response from #{hostname} indicates disk was not added" if !parsed['ok']
|
296
|
+
|
297
|
+
rescue NameError, RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
298
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
|
299
|
+
report_and_raise(@logger, e, 'Vmpooler.add_disk')
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def parse_response(response)
|
304
|
+
parsed_response = JSON.parse(response.body)
|
305
|
+
end
|
306
|
+
|
307
|
+
def disk_added?(host, disk_size, index)
|
308
|
+
if host['disk'].nil?
|
309
|
+
false
|
310
|
+
else
|
311
|
+
host['disk'][index] == "+#{disk_size}gb"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def get_vm(hostname)
|
316
|
+
begin
|
317
|
+
uri = URI.parse(@options[:pooling_api] + '/vm/' + hostname)
|
318
|
+
|
319
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
320
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
321
|
+
|
322
|
+
response = http.request(request)
|
323
|
+
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
324
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
|
325
|
+
@logger.notify "Failed to connect to vmpooler while getting VM information!"
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def wait_for_disk(hostname, disk_size, index)
|
330
|
+
response = get_vm(hostname)
|
331
|
+
parsed = parse_response(response)
|
332
|
+
|
333
|
+
@logger.notify "Waiting for disk"
|
334
|
+
|
335
|
+
attempts = 0
|
336
|
+
|
337
|
+
while (!disk_added?(parsed[hostname], disk_size, index) && attempts < 20)
|
338
|
+
sleep 10
|
339
|
+
begin
|
340
|
+
response = get_vm(hostname)
|
341
|
+
parsed = parse_response(response)
|
342
|
+
rescue RuntimeError, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
343
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, *SSH_EXCEPTIONS => e
|
344
|
+
report_and_raise(@logger, e, "Vmpooler.wait_for_disk")
|
345
|
+
end
|
346
|
+
print "."
|
347
|
+
attempts += 1
|
348
|
+
end
|
349
|
+
|
350
|
+
puts " "
|
351
|
+
|
352
|
+
disk_added?(parsed[hostname], disk_size, index)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Beaker
|
4
|
+
describe Vcloud do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
MockVsphereHelper.set_config( fog_file_contents )
|
8
|
+
MockVsphereHelper.set_vms( make_hosts() )
|
9
|
+
stub_const( "VsphereHelper", MockVsphereHelper )
|
10
|
+
stub_const( "Net", MockNet )
|
11
|
+
json = double( 'json' )
|
12
|
+
allow( json ).to receive( :parse ) do |arg|
|
13
|
+
arg
|
14
|
+
end
|
15
|
+
stub_const( "JSON", json )
|
16
|
+
allow( Socket ).to receive( :getaddrinfo ).and_return( true )
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#provision" do
|
20
|
+
|
21
|
+
it 'instantiates vmpooler if pooling api is provided' do
|
22
|
+
opts = make_opts
|
23
|
+
opts[:pooling_api] = 'testpool'
|
24
|
+
hypervisor = Beaker::Vcloud.new( make_hosts, opts)
|
25
|
+
expect( hypervisor.class ).to be Beaker::Vmpooler
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'provisions hosts and add them to the pool' do
|
29
|
+
MockVsphereHelper.powerOff
|
30
|
+
|
31
|
+
opts = make_opts
|
32
|
+
opts[:pooling_api] = nil
|
33
|
+
opts[:datacenter] = 'testdc'
|
34
|
+
|
35
|
+
vcloud = Beaker::Vcloud.new( make_hosts, opts )
|
36
|
+
allow( vcloud ).to receive( :require ).and_return( true )
|
37
|
+
allow( vcloud ).to receive( :sleep ).and_return( true )
|
38
|
+
vcloud.provision
|
39
|
+
|
40
|
+
hosts = vcloud.instance_variable_get( :@hosts )
|
41
|
+
hosts.each do | host |
|
42
|
+
name = host['vmhostname']
|
43
|
+
vm = MockVsphereHelper.find_vm( name )
|
44
|
+
expect( vm.toolsRunningStatus ).to be === "guestToolsRunning"
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#cleanup" do
|
52
|
+
|
53
|
+
it "cleans up hosts not in the pool" do
|
54
|
+
MockVsphereHelper.powerOn
|
55
|
+
|
56
|
+
opts = make_opts
|
57
|
+
opts[:pooling_api] = nil
|
58
|
+
opts[:datacenter] = 'testdc'
|
59
|
+
|
60
|
+
vcloud = Beaker::Vcloud.new( make_hosts, opts )
|
61
|
+
allow( vcloud ).to receive( :require ).and_return( true )
|
62
|
+
allow( vcloud ).to receive( :sleep ).and_return( true )
|
63
|
+
vcloud.provision
|
64
|
+
vcloud.cleanup
|
65
|
+
|
66
|
+
hosts = vcloud.instance_variable_get( :@hosts )
|
67
|
+
vm_names = hosts.map {|h| h['vmhostname'] }.compact
|
68
|
+
vm_names.each do | name |
|
69
|
+
vm = MockVsphereHelper.find_vm( name )
|
70
|
+
expect( vm.runtime.powerState ).to be === "poweredOff"
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|