Skip to content

Commit dbb0e6e

Browse files
committed
feature: openstack provider
1 parent 94be1b4 commit dbb0e6e

File tree

2 files changed

+273
-1
lines changed

2 files changed

+273
-1
lines changed

lib/travis/worker/job/runner.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def run_script
278278

279279
def start_session
280280
announce("Using worker: #{host_name}\n\n")
281-
retryable(:tries => 5, :sleep => 3) do
281+
retryable(:tries => 20, :sleep => 15) do
282282
Timeout.timeout(10) do
283283
session.connect
284284
end
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
require 'fog'
2+
require 'shellwords'
3+
require 'securerandom'
4+
require 'benchmark'
5+
require 'travis/support'
6+
require 'travis/worker/ssh/session'
7+
require 'resolv'
8+
#require 'travis/worker/virtual_machine/blue_box/template'
9+
10+
module Travis
11+
module Worker
12+
module VirtualMachine
13+
class OpenStack
14+
include Retryable
15+
include Logging
16+
17+
DUPLICATE_MATCH = /testing-(\w*-?\w+-?\d*-?\d*-\d+-\w+-\d+)-(\d+)/
18+
19+
DEFAULT_TEMPLATE_ID = Travis::Worker.config.open_stack.default_template_id
20+
21+
USER_NAME = Travis::Worker.config.open_stack.username
22+
23+
class << self
24+
def vm_count
25+
Travis::Worker.config.vms.count
26+
end
27+
28+
def vm_names
29+
vm_count.times.map { |num| "#{Travis::Worker.config.vms.name_prefix}-#{num + 1}" }
30+
end
31+
end
32+
33+
log_header { "#{name}:worker:virtual_machine:open_stack" }
34+
35+
attr_reader :name, :password, :server, :ip_address
36+
37+
def initialize(name)
38+
@name = name
39+
end
40+
41+
# create a connection
42+
def connection
43+
@connection ||= Fog::Compute.new(
44+
provider: :openstack,
45+
openstack_api_key: Travis::Worker.config.open_stack.access.api_key,
46+
openstack_username: Travis::Worker.config.open_stack.access.username,
47+
openstack_auth_url: Travis::Worker.config.open_stack.access.auth_url,
48+
openstack_tenant: Travis::Worker.config.open_stack.access.tenant,
49+
connection_options: Travis::Worker.config.open_stack.access.connection_options || {}
50+
)
51+
end
52+
53+
def create_server(opts = {})
54+
hostname = hostname(opts[:job_id])
55+
56+
config = open_stack_vm_defaults.merge(opts.merge({
57+
:image_ref => template_id(opts),
58+
:name => hostname,
59+
:key_name => Travis::Worker.config.open_stack.key_pair_name
60+
}))
61+
62+
retryable(tries: 3, sleep: 5) do
63+
destroy_duplicate_servers
64+
create_new_server(config)
65+
end
66+
end
67+
68+
def create_new_server(opts)
69+
@password = (opts[:password] ||= Travis::Worker.config.open_stack.password || generate_password)
70+
71+
opts[:user_data] = user_data(opts[:name], USER_NAME, opts[:password])
72+
73+
@server = connection.servers.create(opts)
74+
instrument do
75+
Fog.wait_for(300, 3) do
76+
begin
77+
server.reload
78+
server.ready?
79+
rescue Excon::Errors::HTTPStatusError => e
80+
mark_api_error(e)
81+
false
82+
end
83+
end
84+
end
85+
86+
allocate_and_associate_ip_address_for(server)
87+
88+
info "Booted #{server.name} (#{ip_address})"
89+
rescue Timeout::Error, Fog::Errors::TimeoutError => e
90+
if server
91+
error "OpenStack VM would not boot within 240 seconds : id=#{server.id} state=#{server.state} vsh=#{server.vsh_id}"
92+
end
93+
Metriks.meter("worker.vm.provider.openstack.boot.timeout.#{server.vsh_id}").mark
94+
release_floating_ip(ip_address) if ip_address
95+
raise
96+
rescue Excon::Errors::HTTPStatusError => e
97+
mark_api_error(e)
98+
release_floating_ip(ip_address) if ip_address
99+
raise
100+
rescue Exception => e
101+
Metriks.meter('worker.vm.provider.openstack.boot.error').mark
102+
error "Booting an OpenStack VM failed with the following error: #{e.inspect}"
103+
release_floating_ip(ip_address) if ip_address
104+
raise
105+
end
106+
107+
def hostname(suffix)
108+
prefix = Worker.config.host.split('.').first
109+
"testing-#{prefix}-#{Process.pid}-#{name}-#{suffix}"
110+
end
111+
112+
def session
113+
unless server
114+
raise StandardError, 'VM is not currently available'
115+
end
116+
@session ||= Ssh::Session.new(name,
117+
:host => ip_address,
118+
:port => 22,
119+
:username => USER_NAME,
120+
:password => Travis::Worker.config.open_stack.password,
121+
:private_key_path => Travis::Worker.config.open_stack.private_key_path,
122+
:buffer => Travis::Worker.config.shell.buffer,
123+
:timeouts => Travis::Worker.config.timeouts
124+
)
125+
end
126+
127+
def sandboxed(opts = {})
128+
create_server(opts)
129+
yield
130+
ensure
131+
session.close if @session
132+
destroy_server if server
133+
end
134+
135+
def open_stack_vm_defaults
136+
{
137+
:username => USER_NAME,
138+
:flavor_ref => Travis::Worker.config.open_stack.flavor_id,
139+
:nics => [{ net_id: Travis::Worker.config.open_stack.internal_network_id }]
140+
}
141+
end
142+
143+
def full_name
144+
"#{Travis::Worker.config.host}:travis-#{name}"
145+
end
146+
147+
def allocate_and_associate_ip_address_for(srv)
148+
unless srv.ready?
149+
info "#{srv.name} is not ready"
150+
return
151+
end
152+
153+
if Travis::Worker.config.open_stack.use_floating_ip
154+
ip = connection.allocate_address(Travis::Worker.config.open_stack.external_network_id)
155+
addr = ip.body["floating_ip"]["ip"]
156+
connection.associate_address(srv.id, addr)
157+
else
158+
addr = srv.addresses[Travis::Worker.config.open_stack.access.tenant].first["addr"]
159+
end
160+
debug "Allocated #{addr} and assigned it to #{srv.name}"
161+
162+
@ip_address = addr
163+
end
164+
165+
def destroy_server(opts = {})
166+
release_floating_ip(ip_address) if Travis::Worker.config.use_floating_ip
167+
destroy_vm(server)
168+
ensure
169+
server = nil
170+
@session = nil
171+
end
172+
173+
def prepare
174+
info "OpenStack API adapter prepared"
175+
end
176+
177+
private
178+
179+
def template_name(opts)
180+
if Travis::Worker.config.open_stack.image_override
181+
Travis::Worker.config.open_stack.image_override
182+
else
183+
raise "Could not construct templateName, dist field must not be empty" unless opts[:dist]
184+
[ Travis::Worker.config.open_stack.template_name_prefix,
185+
opts[:dist],
186+
opts[:group]
187+
].select(&:present?).join('_')
188+
end
189+
end
190+
191+
def template_id(opts)
192+
connection.images.find_all() do |img|
193+
img.name == template_name(opts)
194+
end.first.id || DEFAULT_TEMPLATE_ID
195+
end
196+
197+
def destroy_duplicate_servers
198+
duplicate_servers.each do |server|
199+
info "destroying duplicate server #{server.name}"
200+
destroy_vm(server)
201+
end
202+
end
203+
204+
def duplicate_servers
205+
connection.servers.select do |server|
206+
DUPLICATE_MATCH.match(server.name) do |match|
207+
match[1] == "#{Worker.config.host.split('.').first}-#{Process.pid}-#{name}"
208+
end
209+
end
210+
rescue Excon::Errors::HTTPStatusError => e
211+
warn "could not retrieve the current VM list : #{e.inspect}"
212+
mark_api_error(e)
213+
raise
214+
end
215+
216+
def instrument
217+
info "Provisioning an OpenStack VM"
218+
time = Benchmark.realtime { yield }
219+
info "OpenStack VM provisioned in #{time.round(2)} seconds"
220+
Metriks.timer('worker.vm.provider.openstack.boot').update(time)
221+
end
222+
223+
def mark_api_error(error)
224+
Metriks.meter("worker.vm.provider.openstack.api.error.#{error.response[:status]}").mark
225+
error "OpenStack API returned error code #{error.response[:status]} : #{error.inspect}"
226+
end
227+
228+
def destroy_vm(vm)
229+
debug "vm is in #{vm.state} state"
230+
info "destroying the VM"
231+
retryable(tries: 3, sleep: 5) do
232+
vm.destroy
233+
end
234+
rescue Fog::Compute::OpenStack::NotFound => e
235+
warn "went to destroy the VM but it didn't exist :/ : #{e.inspect}"
236+
rescue Excon::Errors::HTTPStatusError => e
237+
warn "went to destroy the VM but there was an http status error : #{e.inspect}"
238+
rescue Excon::Errors::InternalServerError => e
239+
warn "went to destroy the VM but there was an internal server error : #{e.inspect}"
240+
mark_api_error(e)
241+
end
242+
243+
def release_floating_ip(address)
244+
if ip_obj = connection.addresses.detect {|addr| addr.ip == address }
245+
info "releasing floating IP #{address}"
246+
connection.release_address(ip_obj.id)
247+
end
248+
end
249+
250+
def generate_password
251+
SecureRandom.base64 12
252+
end
253+
254+
def user_data(hostname, username, passwd)
255+
user_data = %Q{#! /bin/bash\n}
256+
user_data += %Q{cat /etc/hosts | sed -e 's/^\\(127\\.0\\.0\\.1.*\\)localhost\\s*\\(.*\\)$/\\1localhost #{hostname} \\2/' | sudo tee /etc/hosts >/dev/null\n}
257+
user_data += %Q{cat /etc/hosts | sed -e 's/^\\(::1.*\\)localhost\\s*\\(.*\\)$/\\1localhost #{hostname} \\2/' | sudo tee /etc/hosts >/dev/null\n}
258+
user_data += %Q{sudo useradd #{username} -m -s /bin/bash || true\n}
259+
user_data += %Q{echo #{username}:#{passwd} | sudo chpasswd\n} if passwd
260+
user_data += %Q{sudo sed -i '/#{username}/d' /etc/sudoers\n}
261+
user_data += %Q{echo "#{username} ALL=(ALL) NOPASSWD:ALL" | sudo tee -a /etc/sudoers >/dev/null\n}
262+
user_data += %Q{sudo sed -i '/PasswordAuthentication/ d' /etc/ssh/sshd_config\n}
263+
user_data += %Q{echo 'PasswordAuthentication yes' | tee -a /etc/ssh/sshd_config >/dev/null\n}
264+
user_data += %Q{sudo sed -i '/UseDNS/ d' /etc/ssh/sshd_config\n}
265+
user_data += %Q{echo 'UseDNS no' | tee -a /etc/ssh/sshd_config >/dev/null\n}
266+
user_data += %Q{sudo service ssh restart}
267+
end
268+
269+
end
270+
end
271+
end
272+
end

0 commit comments

Comments
 (0)