beaker 3.21.1 → 3.22.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 +8 -8
- data/acceptance/tests/base/dsl/helpers/host_helpers/archive_file_from_test.rb +1 -1
- data/beaker.gemspec +9 -3
- data/docs/how_to/hypervisors/README.md +9 -7
- data/lib/beaker/dsl/helpers/host_helpers.rb +2 -2
- data/lib/beaker/host/pswindows/exec.rb +1 -1
- data/lib/beaker/hypervisor.rb +1 -15
- data/lib/beaker/version.rb +1 -1
- data/spec/beaker/host/pswindows/exec_spec.rb +2 -2
- metadata +26 -18
- data/docs/how_to/hypervisors/docker.md +0 -148
- data/docs/how_to/hypervisors/openstack.md +0 -166
- data/lib/beaker/hypervisor/docker.rb +0 -336
- data/lib/beaker/hypervisor/openstack.rb +0 -363
- data/spec/beaker/hypervisor/docker_spec.rb +0 -491
- data/spec/beaker/hypervisor/openstack_spec.rb +0 -238
@@ -1,336 +0,0 @@
|
|
1
|
-
module Beaker
|
2
|
-
class Docker < Beaker::Hypervisor
|
3
|
-
|
4
|
-
# Docker hypvervisor initializtion
|
5
|
-
# Env variables supported:
|
6
|
-
# DOCKER_REGISTRY: Docker registry URL
|
7
|
-
# DOCKER_HOST: Remote docker host
|
8
|
-
# DOCKER_BUILDARGS: Docker buildargs map
|
9
|
-
# @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon,
|
10
|
-
# or a role (String or Symbol) that identifies one or more hosts.
|
11
|
-
# @param [Hash{Symbol=>String}] options Options to pass on to the hypervisor
|
12
|
-
def initialize(hosts, options)
|
13
|
-
require 'docker'
|
14
|
-
@options = options
|
15
|
-
@logger = options[:logger]
|
16
|
-
@hosts = hosts
|
17
|
-
|
18
|
-
# increase the http timeouts as provisioning images can be slow
|
19
|
-
default_docker_options = { :write_timeout => 300, :read_timeout => 300 }.merge(::Docker.options || {})
|
20
|
-
# Merge docker options from the entry in hosts file
|
21
|
-
::Docker.options = default_docker_options.merge(@options[:docker_options] || {})
|
22
|
-
# assert that the docker-api gem can talk to your docker
|
23
|
-
# enpoint. Will raise if there is a version mismatch
|
24
|
-
begin
|
25
|
-
::Docker.validate_version!
|
26
|
-
rescue Excon::Errors::SocketError => e
|
27
|
-
raise "Docker instance not connectable.\nError was: #{e}\nCheck your DOCKER_HOST variable has been set\nIf you are on OSX or Windows, you might not have Docker Machine setup correctly: https://docs.docker.com/machine/\n"
|
28
|
-
end
|
29
|
-
|
30
|
-
# Pass on all the logging from docker-api to the beaker logger instance
|
31
|
-
::Docker.logger = @logger
|
32
|
-
|
33
|
-
# Find out what kind of remote instance we are talking against
|
34
|
-
if ::Docker.version['Version'] =~ /swarm/
|
35
|
-
@docker_type = 'swarm'
|
36
|
-
unless ENV['DOCKER_REGISTRY']
|
37
|
-
raise "Using Swarm with beaker requires a private registry. Please setup the private registry and set the 'DOCKER_REGISTRY' env var"
|
38
|
-
else
|
39
|
-
@registry = ENV['DOCKER_REGISTRY']
|
40
|
-
end
|
41
|
-
else
|
42
|
-
@docker_type = 'docker'
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
def provision
|
48
|
-
@logger.notify "Provisioning docker"
|
49
|
-
|
50
|
-
@hosts.each do |host|
|
51
|
-
@logger.notify "provisioning #{host.name}"
|
52
|
-
|
53
|
-
@logger.debug("Creating image")
|
54
|
-
image = ::Docker::Image.build(dockerfile_for(host), {
|
55
|
-
:rm => true, :buildargs => buildargs_for(host)
|
56
|
-
})
|
57
|
-
|
58
|
-
if @docker_type == 'swarm'
|
59
|
-
image_name = "#{@registry}/beaker/#{image.id}"
|
60
|
-
ret = ::Docker::Image.search(:term => image_name)
|
61
|
-
if ret.first.nil?
|
62
|
-
@logger.debug("Image does not exist on registry. Pushing.")
|
63
|
-
image.tag({:repo => image_name, :force => true})
|
64
|
-
image.push
|
65
|
-
end
|
66
|
-
else
|
67
|
-
image_name = image.id
|
68
|
-
end
|
69
|
-
|
70
|
-
container_opts = {
|
71
|
-
'Image' => image_name,
|
72
|
-
'Hostname' => host.name,
|
73
|
-
}
|
74
|
-
container = find_container(host)
|
75
|
-
|
76
|
-
# If the specified container exists, then use it rather creating a new one
|
77
|
-
if container.nil?
|
78
|
-
unless host['mount_folders'].nil?
|
79
|
-
container_opts['HostConfig'] ||= {}
|
80
|
-
container_opts['HostConfig']['Binds'] = host['mount_folders'].values.map do |mount|
|
81
|
-
a = [ File.expand_path(mount['host_path']), mount['container_path'] ]
|
82
|
-
a << mount['opts'] if mount.has_key?('opts')
|
83
|
-
a.join(':')
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
if @options[:provision]
|
88
|
-
if host['docker_container_name']
|
89
|
-
container_opts['name'] = host['docker_container_name']
|
90
|
-
end
|
91
|
-
|
92
|
-
@logger.debug("Creating container from image #{image_name}")
|
93
|
-
container = ::Docker::Container.create(container_opts)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
if container.nil?
|
98
|
-
raise RuntimeError, 'Cannot continue because no existing container ' +
|
99
|
-
'could be found and provisioning is disabled.'
|
100
|
-
end
|
101
|
-
|
102
|
-
fix_ssh(container) if @options[:provision] == false
|
103
|
-
|
104
|
-
@logger.debug("Starting container #{container.id}")
|
105
|
-
container.start({"PublishAllPorts" => true, "Privileged" => true})
|
106
|
-
|
107
|
-
# Find out where the ssh port is from the container
|
108
|
-
# When running on swarm DOCKER_HOST points to the swarm manager so we have to get the
|
109
|
-
# IP of the swarm slave via the container data
|
110
|
-
# When we are talking to a normal docker instance DOCKER_HOST can point to a remote docker instance.
|
111
|
-
|
112
|
-
# Talking against a remote docker host which is a normal docker host
|
113
|
-
if @docker_type == 'docker' && ENV['DOCKER_HOST']
|
114
|
-
ip = URI.parse(ENV['DOCKER_HOST']).host
|
115
|
-
else
|
116
|
-
# Swarm or local docker host
|
117
|
-
ip = container.json["NetworkSettings"]["Ports"]["22/tcp"][0]["HostIp"]
|
118
|
-
end
|
119
|
-
|
120
|
-
@logger.info("Using docker server at #{ip}")
|
121
|
-
port = container.json["NetworkSettings"]["Ports"]["22/tcp"][0]["HostPort"]
|
122
|
-
|
123
|
-
forward_ssh_agent = @options[:forward_ssh_agent] || false
|
124
|
-
|
125
|
-
# Update host metadata
|
126
|
-
host['ip'] = ip
|
127
|
-
host['port'] = port
|
128
|
-
host['ssh'] = {
|
129
|
-
:password => root_password,
|
130
|
-
:port => port,
|
131
|
-
:forward_agent => forward_ssh_agent,
|
132
|
-
}
|
133
|
-
|
134
|
-
@logger.debug("node available as ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@#{ip} -p #{port}")
|
135
|
-
host['docker_container'] = container
|
136
|
-
host['docker_image'] = image
|
137
|
-
host['vm_ip'] = container.json["NetworkSettings"]["IPAddress"].to_s
|
138
|
-
|
139
|
-
end
|
140
|
-
|
141
|
-
hack_etc_hosts @hosts, @options
|
142
|
-
|
143
|
-
end
|
144
|
-
|
145
|
-
def cleanup
|
146
|
-
@logger.notify "Cleaning up docker"
|
147
|
-
@hosts.each do |host|
|
148
|
-
if container = host['docker_container']
|
149
|
-
@logger.debug("stop container #{container.id}")
|
150
|
-
begin
|
151
|
-
container.kill
|
152
|
-
sleep 2 # avoid a race condition where the root FS can't unmount
|
153
|
-
rescue Excon::Errors::ClientError => e
|
154
|
-
@logger.warn("stop of container #{container.id} failed: #{e.response.body}")
|
155
|
-
end
|
156
|
-
@logger.debug("delete container #{container.id}")
|
157
|
-
begin
|
158
|
-
container.delete
|
159
|
-
rescue Excon::Errors::ClientError => e
|
160
|
-
@logger.warn("deletion of container #{container.id} failed: #{e.response.body}")
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
# Do not remove the image if docker_reserve_image is set to true, otherwise remove it
|
165
|
-
if image = (host['docker_preserve_image'] ? nil : host['docker_image'])
|
166
|
-
@logger.debug("delete image #{image.id}")
|
167
|
-
begin
|
168
|
-
image.delete
|
169
|
-
rescue Excon::Errors::ClientError => e
|
170
|
-
@logger.warn("deletion of image #{image.id} failed: #{e.response.body}")
|
171
|
-
rescue ::Docker::Error::DockerError => e
|
172
|
-
@logger.warn("deletion of image #{image.id} caused internal Docker error: #{e.message}")
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
private
|
179
|
-
|
180
|
-
def root_password
|
181
|
-
'root'
|
182
|
-
end
|
183
|
-
|
184
|
-
def buildargs_for(host)
|
185
|
-
docker_buildargs = {}
|
186
|
-
docker_buildargs_env = ENV['DOCKER_BUILDARGS']
|
187
|
-
if docker_buildargs_env != nil
|
188
|
-
docker_buildargs_env.split(/ +|\t+/).each do |arg|
|
189
|
-
key,value=arg.split(/=/)
|
190
|
-
if key
|
191
|
-
docker_buildargs[key]=value
|
192
|
-
else
|
193
|
-
@logger.warn("DOCKER_BUILDARGS environment variable appears invalid, no key found for value #{value}" )
|
194
|
-
end
|
195
|
-
end
|
196
|
-
end
|
197
|
-
if docker_buildargs.empty?
|
198
|
-
buildargs = host['docker_buildargs'] || {}
|
199
|
-
else
|
200
|
-
buildargs = docker_buildargs
|
201
|
-
end
|
202
|
-
@logger.debug("Docker build buildargs: #{buildargs}")
|
203
|
-
JSON.generate(buildargs)
|
204
|
-
end
|
205
|
-
|
206
|
-
def dockerfile_for(host)
|
207
|
-
if host['dockerfile'] then
|
208
|
-
@logger.debug("attempting to load user Dockerfile from #{host['dockerfile']}")
|
209
|
-
if File.exist?(host['dockerfile']) then
|
210
|
-
dockerfile = File.read(host['dockerfile'])
|
211
|
-
else
|
212
|
-
raise "requested Dockerfile #{host['dockerfile']} does not exist"
|
213
|
-
end
|
214
|
-
else
|
215
|
-
raise("Docker image undefined!") if (host['image']||= nil).to_s.empty?
|
216
|
-
|
217
|
-
# specify base image
|
218
|
-
dockerfile = <<-EOF
|
219
|
-
FROM #{host['image']}
|
220
|
-
ENV container docker
|
221
|
-
EOF
|
222
|
-
|
223
|
-
# additional options to specify to the sshd
|
224
|
-
# may vary by platform
|
225
|
-
sshd_options = ''
|
226
|
-
|
227
|
-
# add platform-specific actions
|
228
|
-
service_name = "sshd"
|
229
|
-
case host['platform']
|
230
|
-
when /ubuntu/, /debian/
|
231
|
-
service_name = "ssh"
|
232
|
-
dockerfile += <<-EOF
|
233
|
-
RUN apt-get update
|
234
|
-
RUN apt-get install -y openssh-server openssh-client #{Beaker::HostPrebuiltSteps::DEBIAN_PACKAGES.join(' ')}
|
235
|
-
EOF
|
236
|
-
when /cumulus/
|
237
|
-
dockerfile += <<-EOF
|
238
|
-
RUN apt-get update
|
239
|
-
RUN apt-get install -y openssh-server openssh-client #{Beaker::HostPrebuiltSteps::CUMULUS_PACKAGES.join(' ')}
|
240
|
-
EOF
|
241
|
-
when /fedora-(2[2-9])/
|
242
|
-
dockerfile += <<-EOF
|
243
|
-
RUN dnf clean all
|
244
|
-
RUN dnf install -y sudo openssh-server openssh-clients #{Beaker::HostPrebuiltSteps::UNIX_PACKAGES.join(' ')}
|
245
|
-
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
246
|
-
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
247
|
-
EOF
|
248
|
-
when /^el-/, /centos/, /fedora/, /redhat/, /eos/
|
249
|
-
dockerfile += <<-EOF
|
250
|
-
RUN yum clean all
|
251
|
-
RUN yum install -y sudo openssh-server openssh-clients #{Beaker::HostPrebuiltSteps::UNIX_PACKAGES.join(' ')}
|
252
|
-
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
253
|
-
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
254
|
-
EOF
|
255
|
-
when /opensuse/, /sles/
|
256
|
-
dockerfile += <<-EOF
|
257
|
-
RUN zypper -n in openssh #{Beaker::HostPrebuiltSteps::SLES_PACKAGES.join(' ')}
|
258
|
-
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
|
259
|
-
RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
|
260
|
-
RUN sed -ri 's/^#?UsePAM .*/UsePAM no/' /etc/ssh/sshd_config
|
261
|
-
EOF
|
262
|
-
else
|
263
|
-
# TODO add more platform steps here
|
264
|
-
raise "platform #{host['platform']} not yet supported on docker"
|
265
|
-
end
|
266
|
-
|
267
|
-
# Make sshd directory, set root password
|
268
|
-
dockerfile += <<-EOF
|
269
|
-
RUN mkdir -p /var/run/sshd
|
270
|
-
RUN echo root:#{root_password} | chpasswd
|
271
|
-
EOF
|
272
|
-
|
273
|
-
# Configure sshd service to allowroot login using password
|
274
|
-
# Also, disable reverse DNS lookups to prevent every. single. ssh
|
275
|
-
# operation taking 30 seconds while the lookup times out.
|
276
|
-
dockerfile += <<-EOF
|
277
|
-
RUN sed -ri 's/^#?PermitRootLogin .*/PermitRootLogin yes/' /etc/ssh/sshd_config
|
278
|
-
RUN sed -ri 's/^#?PasswordAuthentication .*/PasswordAuthentication yes/' /etc/ssh/sshd_config
|
279
|
-
RUN sed -ri 's/^#?UseDNS .*/UseDNS no/' /etc/ssh/sshd_config
|
280
|
-
EOF
|
281
|
-
|
282
|
-
|
283
|
-
# Any extra commands specified for the host
|
284
|
-
dockerfile += (host['docker_image_commands'] || []).map { |command|
|
285
|
-
"RUN #{command}\n"
|
286
|
-
}.join('')
|
287
|
-
|
288
|
-
# Override image entrypoint
|
289
|
-
if host['docker_image_entrypoint']
|
290
|
-
dockerfile += "ENTRYPOINT #{host['docker_image_entrypoint']}\n"
|
291
|
-
end
|
292
|
-
|
293
|
-
# How to start a sshd on port 22. May be an init for more supervision
|
294
|
-
# Ensure that the ssh server can be restarted (done from set_env) and container keeps running
|
295
|
-
cmd = host['docker_cmd'] || ["sh","-c","service #{service_name} start ; tail -f /dev/null"]
|
296
|
-
dockerfile += <<-EOF
|
297
|
-
EXPOSE 22
|
298
|
-
CMD #{cmd}
|
299
|
-
EOF
|
300
|
-
|
301
|
-
end
|
302
|
-
|
303
|
-
@logger.debug("Dockerfile is #{dockerfile}")
|
304
|
-
return dockerfile
|
305
|
-
end
|
306
|
-
|
307
|
-
# a puppet run may have changed the ssh config which would
|
308
|
-
# keep us out of the container. This is a best effort to fix it.
|
309
|
-
def fix_ssh(container)
|
310
|
-
@logger.debug("Fixing ssh on container #{container.id}")
|
311
|
-
container.exec(['sed','-ri',
|
312
|
-
's/^#?PermitRootLogin .*/PermitRootLogin yes/',
|
313
|
-
'/etc/ssh/sshd_config'])
|
314
|
-
container.exec(['sed','-ri',
|
315
|
-
's/^#?PasswordAuthentication .*/PasswordAuthentication yes/',
|
316
|
-
'/etc/ssh/sshd_config'])
|
317
|
-
container.exec(['sed','-ri',
|
318
|
-
's/^#?UseDNS .*/UseDNS no/',
|
319
|
-
'/etc/ssh/sshd_config'])
|
320
|
-
container.exec(%w(service ssh restart))
|
321
|
-
end
|
322
|
-
|
323
|
-
|
324
|
-
# return the existing container if we're not provisioning
|
325
|
-
# and docker_container_name is set
|
326
|
-
def find_container(host)
|
327
|
-
return nil if host['docker_container_name'].nil? || @options[:provision]
|
328
|
-
@logger.debug("Looking for an existing container called #{host['docker_container_name']}")
|
329
|
-
|
330
|
-
::Docker::Container.all.select do |c|
|
331
|
-
c.info['Names'].include? "/#{host['docker_container_name']}"
|
332
|
-
end.first
|
333
|
-
end
|
334
|
-
|
335
|
-
end
|
336
|
-
end
|
@@ -1,363 +0,0 @@
|
|
1
|
-
module Beaker
|
2
|
-
#Beaker support for OpenStack
|
3
|
-
#This code is EXPERIMENTAL!
|
4
|
-
#Please file any issues/concerns at https://github.com/puppetlabs/beaker/issues
|
5
|
-
class OpenStack < Beaker::Hypervisor
|
6
|
-
|
7
|
-
SLEEPWAIT = 5
|
8
|
-
|
9
|
-
#Create a new instance of the OpenStack hypervisor object
|
10
|
-
#@param [<Host>] openstack_hosts The array of OpenStack hosts to provision
|
11
|
-
#@param [Hash{Symbol=>String}] options The options hash containing configuration values
|
12
|
-
#@option options [String] :openstack_api_key The key to access the OpenStack instance with (required)
|
13
|
-
#@option options [String] :openstack_username The username to access the OpenStack instance with (required)
|
14
|
-
#@option options [String] :openstack_auth_url The URL to access the OpenStack instance with (required)
|
15
|
-
#@option options [String] :openstack_tenant The tenant to access the OpenStack instance with (required)
|
16
|
-
#@option options [String] :openstack_region The region that each OpenStack instance should be provisioned on (optional)
|
17
|
-
#@option options [String] :openstack_network The network that each OpenStack instance should be contacted through (required)
|
18
|
-
#@option options [String] :openstack_keyname The name of an existing key pair that should be auto-loaded onto each
|
19
|
-
#@option options [Hash] :security_group An array of security groups to associate with the instance
|
20
|
-
# OpenStack instance (optional)
|
21
|
-
#@option options [String] :jenkins_build_url Added as metadata to each OpenStack instance
|
22
|
-
#@option options [String] :department Added as metadata to each OpenStack instance
|
23
|
-
#@option options [String] :project Added as metadata to each OpenStack instance
|
24
|
-
#@option options [Integer] :timeout The amount of time to attempt execution before quiting and exiting with failure
|
25
|
-
def initialize(openstack_hosts, options)
|
26
|
-
require 'fog'
|
27
|
-
@options = options
|
28
|
-
@logger = options[:logger]
|
29
|
-
@hosts = openstack_hosts
|
30
|
-
@vms = []
|
31
|
-
|
32
|
-
raise 'You must specify an Openstack API key (:openstack_api_key) for OpenStack instances!' unless @options[:openstack_api_key]
|
33
|
-
raise 'You must specify an Openstack username (:openstack_username) for OpenStack instances!' unless @options[:openstack_username]
|
34
|
-
raise 'You must specify an Openstack auth URL (:openstack_auth_url) for OpenStack instances!' unless @options[:openstack_auth_url]
|
35
|
-
raise 'You must specify an Openstack tenant (:openstack_tenant) for OpenStack instances!' unless @options[:openstack_tenant]
|
36
|
-
raise 'You must specify an Openstack network (:openstack_network) for OpenStack instances!' unless @options[:openstack_network]
|
37
|
-
|
38
|
-
# Common keystone authentication credentials
|
39
|
-
@credentials = {
|
40
|
-
:provider => :openstack,
|
41
|
-
:openstack_auth_url => @options[:openstack_auth_url],
|
42
|
-
:openstack_api_key => @options[:openstack_api_key],
|
43
|
-
:openstack_username => @options[:openstack_username],
|
44
|
-
:openstack_tenant => @options[:openstack_tenant],
|
45
|
-
:openstack_region => @options[:openstack_region],
|
46
|
-
}
|
47
|
-
|
48
|
-
# Keystone version 3 requires users and projects to be scoped
|
49
|
-
if @credentials[:openstack_auth_url].include?('/v3/')
|
50
|
-
@credentials[:openstack_user_domain] = @options[:openstack_user_domain] || 'Default'
|
51
|
-
@credentials[:openstack_project_domain] = @options[:openstack_project_domain] || 'Default'
|
52
|
-
end
|
53
|
-
|
54
|
-
@compute_client ||= Fog::Compute.new(@credentials)
|
55
|
-
|
56
|
-
if not @compute_client
|
57
|
-
raise "Unable to create OpenStack Compute instance (api key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]})"
|
58
|
-
end
|
59
|
-
|
60
|
-
@network_client ||= Fog::Network.new(@credentials)
|
61
|
-
|
62
|
-
if not @network_client
|
63
|
-
raise "Unable to create OpenStack Network instance (api_key: #{@options[:openstack_api_key]}, username: #{@options[:openstack_username]}, auth_url: #{@options[:openstack_auth_url]}, tenant: #{@options[:openstack_tenant]})"
|
64
|
-
end
|
65
|
-
|
66
|
-
# Validate openstack_volume_support setting value, reset to boolean if passed via ENV value string
|
67
|
-
@options[:openstack_volume_support] = true if @options[:openstack_volume_support].to_s.match(/\btrue\b/i)
|
68
|
-
@options[:openstack_volume_support] = false if @options[:openstack_volume_support].to_s.match(/\bfalse\b/i)
|
69
|
-
[true,false].include? @options[:openstack_volume_support] or raise "Invalid openstack_volume_support setting, current: @options[:openstack_volume_support]"
|
70
|
-
|
71
|
-
end
|
72
|
-
|
73
|
-
#Provided a flavor name return the OpenStack id for that flavor
|
74
|
-
#@param [String] f The flavor name
|
75
|
-
#@return [String] Openstack id for provided flavor name
|
76
|
-
def flavor f
|
77
|
-
@logger.debug "OpenStack: Looking up flavor '#{f}'"
|
78
|
-
@compute_client.flavors.find { |x| x.name == f } || raise("Couldn't find flavor: #{f}")
|
79
|
-
end
|
80
|
-
|
81
|
-
#Provided an image name return the OpenStack id for that image
|
82
|
-
#@param [String] i The image name
|
83
|
-
#@return [String] Openstack id for provided image name
|
84
|
-
def image i
|
85
|
-
@logger.debug "OpenStack: Looking up image '#{i}'"
|
86
|
-
@compute_client.images.find { |x| x.name == i } || raise("Couldn't find image: #{i}")
|
87
|
-
end
|
88
|
-
|
89
|
-
#Provided a network name return the OpenStack id for that network
|
90
|
-
#@param [String] n The network name
|
91
|
-
#@return [String] Openstack id for provided network name
|
92
|
-
def network n
|
93
|
-
@logger.debug "OpenStack: Looking up network '#{n}'"
|
94
|
-
@network_client.networks.find { |x| x.name == n } || raise("Couldn't find network: #{n}")
|
95
|
-
end
|
96
|
-
|
97
|
-
#Provided an array of security groups return that array if all
|
98
|
-
#security groups are present
|
99
|
-
#@param [Array] sgs The array of security group names
|
100
|
-
#@return [Array] The array of security group names
|
101
|
-
def security_groups sgs
|
102
|
-
for sg in sgs
|
103
|
-
@logger.debug "Openstack: Looking up security group '#{sg}'"
|
104
|
-
@compute_client.security_groups.find { |x| x.name == sg } || raise("Couldn't find security group: #{sg}")
|
105
|
-
sgs
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
# Create a volume client on request
|
110
|
-
# @return [Fog::OpenStack::Volume] OpenStack volume client
|
111
|
-
def volume_client_create
|
112
|
-
@volume_client ||= Fog::Volume.new(@credentials)
|
113
|
-
unless @volume_client
|
114
|
-
raise "Unable to create OpenStack Volume instance"\
|
115
|
-
" (api_key: #{@options[:openstack_api_key]},"\
|
116
|
-
" username: #{@options[:openstack_username]},"\
|
117
|
-
" auth_url: #{@options[:openstack_auth_url]},"\
|
118
|
-
" tenant: #{@options[:openstack_tenant]})"
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Get a hash of volumes from the host
|
123
|
-
def get_volumes host
|
124
|
-
return host['volumes'] if host['volumes']
|
125
|
-
{}
|
126
|
-
end
|
127
|
-
|
128
|
-
# Get the API version
|
129
|
-
def get_volume_api_version
|
130
|
-
case @volume_client
|
131
|
-
when Fog::Volume::OpenStack::V1
|
132
|
-
1
|
133
|
-
else
|
134
|
-
-1
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
# Create and attach dynamic volumes
|
139
|
-
#
|
140
|
-
# Creates an array of volumes and attaches them to the current host.
|
141
|
-
# The host bus type is determined by the image type, so by default
|
142
|
-
# devices appear as /dev/vdb, /dev/vdc etc. Setting the glance
|
143
|
-
# properties hw_disk_bus=scsi, hw_scsi_model=virtio-scsi will present
|
144
|
-
# them as /dev/sdb, /dev/sdc (or 2:0:0:1, 2:0:0:2 in SCSI addresses)
|
145
|
-
#
|
146
|
-
# @param host [Hash] thet current host defined in the nodeset
|
147
|
-
# @param vm [Fog::Compute::OpenStack::Server] the server to attach to
|
148
|
-
def provision_storage host, vm
|
149
|
-
volumes = get_volumes(host)
|
150
|
-
if !volumes.empty?
|
151
|
-
# Lazily create the volume client if needed
|
152
|
-
volume_client_create
|
153
|
-
volumes.keys.each_with_index do |volume, index|
|
154
|
-
@logger.debug "Creating volume #{volume} for OpenStack host #{host.name}"
|
155
|
-
|
156
|
-
# The node defintion file defines volume sizes in MB (due to precedent
|
157
|
-
# with the vagrant virtualbox implementation) however OpenStack requires
|
158
|
-
# this translating into GB
|
159
|
-
openstack_size = volumes[volume]['size'].to_i / 1000
|
160
|
-
|
161
|
-
# Set up the volume creation arguments
|
162
|
-
args = {
|
163
|
-
:size => openstack_size,
|
164
|
-
:description => "Beaker volume: host=#{host.name} volume=#{volume}",
|
165
|
-
}
|
166
|
-
|
167
|
-
# Between version 1 and subsequent versions the API was updated to
|
168
|
-
# rename 'display_name' to just 'name' for better consistency
|
169
|
-
if get_volume_api_version == 1
|
170
|
-
args[:display_name] = volume
|
171
|
-
else
|
172
|
-
args[:name] = volume
|
173
|
-
end
|
174
|
-
|
175
|
-
# Create the volume and wait for it to become available
|
176
|
-
vol = @volume_client.volumes.create(**args)
|
177
|
-
vol.wait_for { ready? }
|
178
|
-
|
179
|
-
# Fog needs a device name to attach as, so invent one. The guest
|
180
|
-
# doesn't pay any attention to this
|
181
|
-
device = "/dev/vd#{('b'.ord + index).chr}"
|
182
|
-
vm.attach_volume(vol.id, device)
|
183
|
-
end
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# Detach and delete guest volumes
|
188
|
-
# @param vm [Fog::Compute::OpenStack::Server] the server to detach from
|
189
|
-
def cleanup_storage vm
|
190
|
-
vm.volumes.each do |vol|
|
191
|
-
@logger.debug "Deleting volume #{vol.name} for OpenStack host #{vm.name}"
|
192
|
-
vm.detach_volume(vol.id)
|
193
|
-
vol.wait_for { ready? }
|
194
|
-
vol.destroy
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
# Get a floating IP address to associate with the instance, try
|
199
|
-
# to allocate a new one from the specified pool if none are available
|
200
|
-
def get_ip
|
201
|
-
begin
|
202
|
-
@logger.debug "Creating IP"
|
203
|
-
ip = @compute_client.addresses.create
|
204
|
-
rescue Fog::Compute::OpenStack::NotFound
|
205
|
-
# If there are no more floating IP addresses, allocate a
|
206
|
-
# new one and try again.
|
207
|
-
@compute_client.allocate_address(@options[:floating_ip_pool])
|
208
|
-
ip = @compute_client.addresses.find { |ip| ip.instance_id.nil? }
|
209
|
-
end
|
210
|
-
raise 'Could not find or allocate an address' if not ip
|
211
|
-
ip
|
212
|
-
end
|
213
|
-
|
214
|
-
#Create new instances in OpenStack
|
215
|
-
def provision
|
216
|
-
@logger.notify "Provisioning OpenStack"
|
217
|
-
|
218
|
-
@hosts.each do |host|
|
219
|
-
ip = get_ip
|
220
|
-
hostname = ip.ip.gsub('.','-')
|
221
|
-
host[:vmhostname] = hostname + '.rfc1918.puppetlabs.net'
|
222
|
-
create_or_associate_keypair(host, hostname)
|
223
|
-
@logger.debug "Provisioning #{host.name} (#{host[:vmhostname]})"
|
224
|
-
options = {
|
225
|
-
:flavor_ref => flavor(host[:flavor]).id,
|
226
|
-
:image_ref => image(host[:image]).id,
|
227
|
-
:nics => [ {'net_id' => network(@options[:openstack_network]).id } ],
|
228
|
-
:name => host[:vmhostname],
|
229
|
-
:hostname => host[:vmhostname],
|
230
|
-
:user_data => host[:user_data] || "#cloud-config\nmanage_etc_hosts: true\n",
|
231
|
-
:key_name => host[:keyname],
|
232
|
-
}
|
233
|
-
options[:security_groups] = security_groups(@options[:security_group]) unless @options[:security_group].nil?
|
234
|
-
vm = @compute_client.servers.create(options)
|
235
|
-
|
236
|
-
#wait for the new instance to start up
|
237
|
-
try = 1
|
238
|
-
attempts = @options[:timeout].to_i / SLEEPWAIT
|
239
|
-
|
240
|
-
while try <= attempts
|
241
|
-
begin
|
242
|
-
vm.wait_for(5) { ready? }
|
243
|
-
break
|
244
|
-
rescue Fog::Errors::TimeoutError => e
|
245
|
-
if try >= attempts
|
246
|
-
@logger.debug "Failed to connect to new OpenStack instance #{host.name} (#{host[:vmhostname]})"
|
247
|
-
raise e
|
248
|
-
end
|
249
|
-
@logger.debug "Timeout connecting to instance #{host.name} (#{host[:vmhostname]}), trying again..."
|
250
|
-
end
|
251
|
-
sleep SLEEPWAIT
|
252
|
-
try += 1
|
253
|
-
end
|
254
|
-
|
255
|
-
# Associate a public IP to the server
|
256
|
-
ip.server = vm
|
257
|
-
host[:ip] = ip.ip
|
258
|
-
|
259
|
-
@logger.debug "OpenStack host #{host.name} (#{host[:vmhostname]}) assigned ip: #{host[:ip]}"
|
260
|
-
|
261
|
-
#set metadata
|
262
|
-
vm.metadata.update({:jenkins_build_url => @options[:jenkins_build_url].to_s,
|
263
|
-
:department => @options[:department].to_s,
|
264
|
-
:project => @options[:project].to_s })
|
265
|
-
@vms << vm
|
266
|
-
|
267
|
-
# Wait for the host to accept ssh logins
|
268
|
-
host.wait_for_port(22)
|
269
|
-
|
270
|
-
#enable root if user is not root
|
271
|
-
enable_root(host)
|
272
|
-
|
273
|
-
provision_storage(host, vm) if @options[:openstack_volume_support]
|
274
|
-
@logger.notify "OpenStack Volume Support Disabled, can't provision volumes" if not @options[:openstack_volume_support]
|
275
|
-
end
|
276
|
-
|
277
|
-
hack_etc_hosts @hosts, @options
|
278
|
-
|
279
|
-
end
|
280
|
-
|
281
|
-
#Destroy any OpenStack instances
|
282
|
-
def cleanup
|
283
|
-
@logger.notify "Cleaning up OpenStack"
|
284
|
-
@vms.each do |vm|
|
285
|
-
cleanup_storage(vm) if @options[:openstack_volume_support]
|
286
|
-
@logger.debug "Release floating IPs for OpenStack host #{vm.name}"
|
287
|
-
floating_ips = vm.all_addresses # fetch and release its floating IPs
|
288
|
-
floating_ips.each do |address|
|
289
|
-
@compute_client.disassociate_address(vm.id, address['ip'])
|
290
|
-
@compute_client.release_address(address['id'])
|
291
|
-
end
|
292
|
-
@logger.debug "Destroying OpenStack host #{vm.name}"
|
293
|
-
vm.destroy
|
294
|
-
if @options[:openstack_keyname].nil?
|
295
|
-
@logger.debug "Deleting random keypair"
|
296
|
-
@compute_client.delete_key_pair vm.key_name
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
|
-
# Enables root access for a host when username is not root
|
302
|
-
# This method ripped from the aws_sdk implementation and is probably wrong
|
303
|
-
# because it iterates on a collection when there's no guarantee the collection
|
304
|
-
# has all been brought up in openstack yet and will thus explode
|
305
|
-
# @return [void]
|
306
|
-
# @api private
|
307
|
-
def enable_root_on_hosts
|
308
|
-
@hosts.each do |host|
|
309
|
-
enable_root(host)
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
# enable root on a single host (the current one presumably) but only
|
314
|
-
# if the username isn't 'root'
|
315
|
-
def enable_root(host)
|
316
|
-
if host['user'] != 'root'
|
317
|
-
copy_ssh_to_root(host, @options)
|
318
|
-
enable_root_login(host, @options)
|
319
|
-
host['user'] = 'root'
|
320
|
-
host.close
|
321
|
-
end
|
322
|
-
end
|
323
|
-
|
324
|
-
#Get key_name from options or generate a new rsa key and add it to
|
325
|
-
#OpenStack keypairs
|
326
|
-
#
|
327
|
-
#@param [Host] host The OpenStack host to provision
|
328
|
-
#@api private
|
329
|
-
def create_or_associate_keypair(host, keyname)
|
330
|
-
if @options[:openstack_keyname]
|
331
|
-
@logger.debug "Adding optional key_name #{@options[:openstack_keyname]} to #{host.name} (#{host[:vmhostname]})"
|
332
|
-
keyname = @options[:openstack_keyname]
|
333
|
-
else
|
334
|
-
@logger.debug "Generate a new rsa key"
|
335
|
-
|
336
|
-
# There is apparently an error that can occur when generating RSA keys, probably
|
337
|
-
# due to some timing issue, probably similar to the issue described here:
|
338
|
-
# https://github.com/negativecode/vines/issues/34
|
339
|
-
# In order to mitigate this error, we will simply try again up to three times, and
|
340
|
-
# then fail if we continue to error out.
|
341
|
-
begin
|
342
|
-
retries ||= 0
|
343
|
-
key = OpenSSL::PKey::RSA.new 2048
|
344
|
-
rescue OpenSSL::PKey::RSAError => e
|
345
|
-
retries += 1
|
346
|
-
if retries > 2
|
347
|
-
@logger.notify "error generating RSA key #{retries} times, exiting"
|
348
|
-
raise e
|
349
|
-
end
|
350
|
-
retry
|
351
|
-
end
|
352
|
-
|
353
|
-
type = key.ssh_type
|
354
|
-
data = [ key.to_blob ].pack('m0')
|
355
|
-
@logger.debug "Creating Openstack keypair '#{keyname}' for public key '#{type} #{data}'"
|
356
|
-
@compute_client.create_key_pair keyname, "#{type} #{data}"
|
357
|
-
host['ssh'][:key_data] = [ key.to_pem ]
|
358
|
-
end
|
359
|
-
|
360
|
-
host[:keyname] = keyname
|
361
|
-
end
|
362
|
-
end
|
363
|
-
end
|