-
Notifications
You must be signed in to change notification settings - Fork 102
/
create_server.rb
368 lines (316 loc) · 14.1 KB
/
create_server.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
require 'log4r'
require 'socket'
require 'timeout'
require 'sshkey'
require 'vagrant/util/retryable'
module VagrantPlugins
module Openstack
module Action
class CreateServer
include Vagrant::Util::Retryable
def initialize(app, _env)
@app = app
@logger = Log4r::Logger.new('vagrant_openstack::action::create_server')
end
def call(env)
@logger.info 'Start create server action'
config = env[:machine].provider_config
fail Errors::MissingBootOption if config.image.nil? && config.volume_boot.nil?
fail Errors::ConflictBootOption unless config.image.nil? || config.volume_boot.nil?
nova = env[:openstack_client].nova
options = {
flavor: resolve_flavor(env),
image: resolve_image(env),
volume_boot: resolve_volume_boot(env),
networks: resolve_networks(env),
volumes: resolve_volumes(env),
keypair_name: resolve_keypair(env),
availability_zone: env[:machine].provider_config.availability_zone,
scheduler_hints: env[:machine].provider_config.scheduler_hints,
security_groups: env[:machine].provider_config.security_groups,
user_data: env[:machine].provider_config.user_data,
metadata: env[:machine].provider_config.metadata
}
server_id = create_server(env, options)
# Store the ID right away so we can track it
env[:machine].id = server_id
waiting_for_server_to_be_build(env, server_id)
floating_ip = resolve_floating_ip(env)
if floating_ip && !floating_ip.empty?
@logger.info "Using floating IP #{floating_ip}"
env[:ui].info(I18n.t('vagrant_openstack.using_floating_ip', floating_ip: floating_ip))
nova.add_floating_ip(env, server_id, floating_ip)
end
attach_volumes(env, server_id, options[:volumes]) unless options[:volumes].empty?
unless env[:interrupted]
# Clear the line one more time so the progress is removed
env[:ui].clear_line
# Wait for SSH to become available
ssh_timeout = env[:machine].provider_config.ssh_timeout
unless port_open?(env, floating_ip, resolve_ssh_port(env), ssh_timeout)
env[:ui].error(I18n.t('vagrant_openstack.timeout'))
fail Errors::SshUnavailable, host: floating_ip, timeout: ssh_timeout
end
@logger.info 'The server is ready'
env[:ui].info(I18n.t('vagrant_openstack.ready'))
end
@app.call(env)
end
private
def resolve_ssh_port(env)
machine_config = env[:machine].config
return machine_config.ssh.port if machine_config.ssh.port
22
end
# 1. if floating_ip is set, use it
# 2. if floating_ip_pool is set
# GET v2/{{tenant_id}}/os-floating-ips
# If any IP with the same pool is available, use it
# Else Allocate a new IP from the pool
# Manage error case
# 3. GET v2/{{tenant_id}}/os-floating-ips
# If any IP is available, use it
# Else fail
def resolve_floating_ip(env)
config = env[:machine].provider_config
nova = env[:openstack_client].nova
return config.floating_ip if config.floating_ip
floating_ips = nova.get_all_floating_ips(env)
if config.floating_ip_pool
floating_ips.each do |single|
return single.ip if single.pool == config.floating_ip_pool && single.instance_id.nil?
end unless config.floating_ip_pool_always_allocate
return nova.allocate_floating_ip(env, config.floating_ip_pool).ip
else
floating_ips.each do |ip|
return ip.ip if ip.instance_id.nil?
end
end
fail Errors::UnableToResolveFloatingIP
end
def resolve_keypair(env)
config = env[:machine].provider_config
nova = env[:openstack_client].nova
return config.keypair_name if config.keypair_name
return nova.import_keypair_from_file(env, config.public_key_path) if config.public_key_path
generate_keypair(env)
end
def generate_keypair(env)
key = SSHKey.generate
nova = env[:openstack_client].nova
generated_keyname = nova.import_keypair(env, key.ssh_public_key)
File.write("#{env[:machine].data_dir}/#{generated_keyname}", key.private_key)
generated_keyname
end
def resolve_flavor(env)
@logger.info 'Resolving flavor'
config = env[:machine].provider_config
nova = env[:openstack_client].nova
env[:ui].info(I18n.t('vagrant_openstack.finding_flavor'))
flavors = nova.get_all_flavors(env)
@logger.info "Finding flavor matching name '#{config.flavor}'"
flavor = find_matching(flavors, config.flavor)
fail Errors::NoMatchingFlavor unless flavor
flavor
end
def resolve_image(env)
@logger.info 'Resolving image'
config = env[:machine].provider_config
return nil if config.image.nil?
nova = env[:openstack_client].nova
env[:ui].info(I18n.t('vagrant_openstack.finding_image'))
images = nova.get_all_images(env)
@logger.info "Finding image matching name '#{config.image}'"
image = find_matching(images, config.image)
fail Errors::NoMatchingImage unless image
image
end
def resolve_networks(env)
@logger.info 'Resolving network(s)'
config = env[:machine].provider_config
return [] if config.networks.nil? || config.networks.empty?
env[:ui].info(I18n.t('vagrant_openstack.finding_networks'))
private_networks = env[:openstack_client].neutron.get_private_networks(env)
private_network_ids = private_networks.map { |n| n.id }
networks = []
config.networks.each do |network|
if private_network_ids.include?(network)
networks << network
next
end
net_id = nil
private_networks.each do |n| # Bad algorithm complexity, but here we don't care...
next unless n.name.eql? network
fail "Multiple networks with name '#{n.id}'" unless net_id.nil?
net_id = n.id
end
fail "No matching network with name '#{network}'" if net_id.nil?
networks << net_id
end
networks
end
def resolve_volume_boot(env)
@logger.info 'Resolving image'
config = env[:machine].provider_config
return nil if config.volume_boot.nil?
volume_list = env[:openstack_client].cinder.get_all_volumes(env)
volume_ids = volume_list.map { |v| v.id }
@logger.debug(volume_list)
volume = resolve_volume(config.volume_boot, volume_list, volume_ids)
device = volume[:device].nil? ? 'vda' : volume[:device]
{ id: volume[:id], device: device }
end
def resolve_volumes(env)
@logger.info 'Resolving volume(s)'
config = env[:machine].provider_config
return [] if config.volumes.nil? || config.volumes.empty?
env[:ui].info(I18n.t('vagrant_openstack.finding_volumes'))
volume_list = env[:openstack_client].cinder.get_all_volumes(env)
volume_ids = volume_list.map { |v| v.id }
@logger.debug(volume_list)
volumes = []
config.volumes.each do |volume|
volumes << resolve_volume(volume, volume_list, volume_ids)
end
@logger.debug("Resolved volumes : #{volumes.to_json}")
volumes
end
def resolve_volume(volume, volume_list, volume_ids)
return resolve_volume_from_string(volume, volume_list) if volume.is_a? String
return resolve_volume_from_hash(volume, volume_list, volume_ids) if volume.is_a? Hash
fail Errors::InvalidVolumeObject, volume: volume
end
def resolve_volume_from_string(volume, volume_list)
found_volume = find_matching(volume_list, volume)
fail Errors::UnresolvedVolume, volume: volume if found_volume.nil?
{ id: found_volume.id, device: nil }
end
def resolve_volume_from_hash(volume, volume_list, volume_ids)
device = nil
device = volume[:device] if volume.key?(:device)
if volume.key?(:id)
fail Errors::ConflictVolumeNameId, volume: volume if volume.key?(:name)
volume_id = volume[:id]
fail Errors::UnresolvedVolumeId, id: volume_id unless volume_ids.include? volume_id
elsif volume.key?(:name)
volume_list.each do |v|
next unless v.name.eql? volume[:name]
fail Errors::MultipleVolumeName, name: volume[:name] unless volume_id.nil?
volume_id = v.id
end
fail Errors::UnresolvedVolumeName, name: volume[:name] unless volume_ids.include? volume_id
else
fail Errors::ConflictVolumeNameId, volume: volume
end
{ id: volume_id, device: device }
end
def create_server(env, options)
config = env[:machine].provider_config
nova = env[:openstack_client].nova
server_name = config.server_name || env[:machine].name
env[:ui].info(I18n.t('vagrant_openstack.launching_server'))
env[:ui].info(" -- Tenant : #{config.tenant_name}")
env[:ui].info(" -- Name : #{server_name}")
env[:ui].info(" -- Flavor : #{options[:flavor].name}")
env[:ui].info(" -- FlavorRef : #{options[:flavor].id}")
unless options[:image].nil?
env[:ui].info(" -- Image : #{options[:image].name}")
env[:ui].info(" -- ImageRef : #{options[:image].id}")
end
env[:ui].info(" -- Boot volume : #{options[:volume_boot][:id]} (#{options[:volume_boot][:device]})") unless options[:volume_boot].nil?
env[:ui].info(" -- KeyPair : #{options[:keypair_name]}")
unless options[:networks].empty?
if options[:networks].size == 1
env[:ui].info(" -- Network : #{options[:networks][0]}")
else
env[:ui].info(" -- Networks : #{options[:networks]}")
end
end
unless options[:volumes].empty?
options[:volumes].each do |volume|
device = volume[:device]
device = :auto if device.nil?
env[:ui].info(" -- Volume attached : #{volume[:id]} => #{device}")
end
end
log = "Lauching server '#{server_name}' in project '#{config.tenant_name}' "
log << "with flavor '#{options[:flavor].name}' (#{options[:flavor].id}), "
unless options[:image].nil?
log << "image '#{options[:image].name}' (#{options[:image].id}) "
end
log << "and keypair '#{options[:keypair_name]}'"
@logger.info(log)
image_ref = options[:image].id unless options[:image].nil?
create_opts = {
name: server_name,
image_ref: image_ref,
volume_boot: options[:volume_boot],
flavor_ref: options[:flavor].id,
keypair: options[:keypair_name],
availability_zone: options[:availability_zone],
networks: options[:networks],
scheduler_hints: options[:scheduler_hints],
security_groups: options[:security_groups],
user_data: options[:user_data],
metadata: options[:metadata]
}
nova.create_server(env, create_opts)
end
def waiting_for_server_to_be_build(env, server_id)
@logger.info 'Waiting for the server to be built...'
env[:ui].info(I18n.t('vagrant_openstack.waiting_for_build'))
nova = env[:openstack_client].nova
timeout(200) do
while nova.get_server_details(env, server_id)['status'] != 'ACTIVE'
sleep 3
@logger.debug('Waiting for server to be ACTIVE')
end
end
end
def attach_volumes(env, server_id, volumes)
@logger.info("Attaching volumes #{volumes} to server #{server_id}")
nova = env[:openstack_client].nova
volumes.each do |volume|
@logger.debug("Attaching volumes #{volume}")
nova.attach_volume(env, server_id, volume[:id], volume[:device])
end
end
def port_open?(env, ip, port, timeout)
start_time = Time.now
current_time = start_time
nb_retry = 0
while (current_time - start_time) <= timeout
begin
@logger.debug "Checking if SSH port is open... Attempt number #{nb_retry}"
if nb_retry % 5 == 0
@logger.info 'Waiting for SSH to become available...'
env[:ui].info(I18n.t('vagrant_openstack.waiting_for_ssh'))
end
TCPSocket.new(ip, port)
return true
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT
@logger.debug 'SSH port is not open... new retry in in 1 second'
nb_retry += 1
sleep 1
end
current_time = Time.now
end
false
end
# This method finds a matching _thing_ in a collection of
# _things_. This works matching if the ID or NAME equals to
# `name`. Or, if `name` is a regexp, a partial match is chosen
# as well.
def find_matching(collection, name)
collection.each do |single|
return single if single.id == name
return single if single.name == name
return single if name.is_a?(Regexp) && name =~ single.name
end
@logger.error "Element '#{name}' not found in collection #{collection}"
nil
end
end
end
end
end