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