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.
@@ -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