beaker 3.21.1 → 3.22.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|