vmpooler 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19dd8ce83614c49e92f13e043c2ee7865f1c664d92aa8f13e47fc328eb033690
4
- data.tar.gz: c3618c9c13591b1b06748ac3e5d1c2dc7ba2eb9793ea2f3f014a762fd6deb4ed
3
+ metadata.gz: a3b49b7d743482ad4e81ac6cae5e2d3b3d4c8a15e27024c41afc8938ae3374fe
4
+ data.tar.gz: 6eea091a71b41ac346fae2098633a0e448bcfbad8c357f84bbf17bf8716a16d2
5
5
  SHA512:
6
- metadata.gz: 8dcb8f8765f9c1fac52c2d0088aaa09a589b5145d5a81e901af614b8fcbbd588669b3f14e5c42f4d56d4681165f8814ef11b22073df6f97b85cef6fcb10d04e2
7
- data.tar.gz: d7c917233daee973486a6a8add55222a92bb1708521caaefdf2666824c0785cebd1ce7e298448a66736bf7e0646f02a560067e54cc1d06a07ca6abae645898c9
6
+ metadata.gz: 9b47704b269da351657da3246a6c31be8ecb6aaa20e5a4ab40f69656cccdd7c1ee94bd8661eb159e5654c270cf990afd78ec6964f7efbadca362a325bad47e83
7
+ data.tar.gz: a9481fb784e7d2f525a6c7089d8699a89e2694d43e2952c152358de3d057c3ef174a157452556b01651d6dc631de8e67120b46116c78c692bfd718a8dc008ddb
@@ -7,6 +7,8 @@ config = Vmpooler.config
7
7
  redis_host = config[:redis]['server']
8
8
  redis_port = config[:redis]['port']
9
9
  redis_password = config[:redis]['password']
10
+ redis_connection_pool_size = config[:redis]['connection_pool_size']
11
+ redis_connection_pool_timeout = config[:redis]['connection_pool_timeout']
10
12
  logger_file = config[:config]['logfile']
11
13
 
12
14
  metrics = Vmpooler.new_metrics(config)
@@ -36,7 +38,7 @@ if torun.include? 'manager'
36
38
  Vmpooler::PoolManager.new(
37
39
  config,
38
40
  Vmpooler.new_logger(logger_file),
39
- Vmpooler.new_redis(redis_host, redis_port, redis_password),
41
+ Vmpooler.redis_connection_pool(redis_host, redis_port, redis_password, redis_connection_pool_size, redis_connection_pool_timeout, metrics),
40
42
  metrics
41
43
  ).execute!
42
44
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vmpooler
4
+ require 'concurrent'
4
5
  require 'date'
5
6
  require 'json'
6
7
  require 'net/ldap'
@@ -58,9 +59,14 @@ module Vmpooler
58
59
 
59
60
  # Set some configuration defaults
60
61
  parsed_config[:config]['task_limit'] = string_to_int(ENV['TASK_LIMIT']) || parsed_config[:config]['task_limit'] || 10
62
+ parsed_config[:config]['ondemand_clone_limit'] = string_to_int(ENV['ONDEMAND_CLONE_LIMIT']) || parsed_config[:config]['ondemand_clone_limit'] || 10
63
+ parsed_config[:config]['max_ondemand_instances_per_request'] = string_to_int(ENV['MAX_ONDEMAND_INSTANCES_PER_REQUEST']) || parsed_config[:config]['max_ondemand_instances_per_request'] || 10
61
64
  parsed_config[:config]['migration_limit'] = string_to_int(ENV['MIGRATION_LIMIT']) if ENV['MIGRATION_LIMIT']
62
65
  parsed_config[:config]['vm_checktime'] = string_to_int(ENV['VM_CHECKTIME']) || parsed_config[:config]['vm_checktime'] || 1
63
66
  parsed_config[:config]['vm_lifetime'] = string_to_int(ENV['VM_LIFETIME']) || parsed_config[:config]['vm_lifetime'] || 24
67
+ parsed_config[:config]['max_lifetime_upper_limit'] = string_to_int(ENV['MAX_LIFETIME_UPPER_LIMIT']) || parsed_config[:config]['max_lifetime_upper_limit']
68
+ parsed_config[:config]['ready_ttl'] = string_to_int(ENV['READY_TTL']) || parsed_config[:config]['ready_ttl'] || 60
69
+ parsed_config[:config]['ondemand_request_ttl'] = string_to_int(ENV['ONDEMAND_REQUEST_TTL']) || parsed_config[:config]['ondemand_request_ttl'] || 5
64
70
  parsed_config[:config]['prefix'] = ENV['PREFIX'] || parsed_config[:config]['prefix'] || ''
65
71
 
66
72
  parsed_config[:config]['logfile'] = ENV['LOGFILE'] if ENV['LOGFILE']
@@ -72,7 +78,7 @@ module Vmpooler
72
78
  parsed_config[:config]['vm_lifetime_auth'] = string_to_int(ENV['VM_LIFETIME_AUTH']) if ENV['VM_LIFETIME_AUTH']
73
79
  parsed_config[:config]['max_tries'] = string_to_int(ENV['MAX_TRIES']) if ENV['MAX_TRIES']
74
80
  parsed_config[:config]['retry_factor'] = string_to_int(ENV['RETRY_FACTOR']) if ENV['RETRY_FACTOR']
75
- parsed_config[:config]['create_folders'] = ENV['CREATE_FOLDERS'] if ENV['CREATE_FOLDERS']
81
+ parsed_config[:config]['create_folders'] = true?(ENV['CREATE_FOLDERS']) if ENV['CREATE_FOLDERS']
76
82
  parsed_config[:config]['create_template_delta_disks'] = ENV['CREATE_TEMPLATE_DELTA_DISKS'] if ENV['CREATE_TEMPLATE_DELTA_DISKS']
77
83
  set_linked_clone(parsed_config)
78
84
  parsed_config[:config]['experimental_features'] = ENV['EXPERIMENTAL_FEATURES'] if ENV['EXPERIMENTAL_FEATURES']
@@ -84,6 +90,8 @@ module Vmpooler
84
90
  parsed_config[:redis]['port'] = string_to_int(ENV['REDIS_PORT']) if ENV['REDIS_PORT']
85
91
  parsed_config[:redis]['password'] = ENV['REDIS_PASSWORD'] if ENV['REDIS_PASSWORD']
86
92
  parsed_config[:redis]['data_ttl'] = string_to_int(ENV['REDIS_DATA_TTL']) || parsed_config[:redis]['data_ttl'] || 168
93
+ parsed_config[:redis]['connection_pool_size'] = string_to_int(ENV['REDIS_CONNECTION_POOL_SIZE']) || parsed_config[:redis]['connection_pool_size'] || 10
94
+ parsed_config[:redis]['connection_pool_timeout'] = string_to_int(ENV['REDIS_CONNECTION_POOL_TIMEOUT']) || parsed_config[:redis]['connection_pool_timeout'] || 5
87
95
 
88
96
  parsed_config[:statsd] = parsed_config[:statsd] || {} if ENV['STATSD_SERVER']
89
97
  parsed_config[:statsd]['server'] = ENV['STATSD_SERVER'] if ENV['STATSD_SERVER']
@@ -117,6 +125,7 @@ module Vmpooler
117
125
 
118
126
  parsed_config[:pools].each do |pool|
119
127
  parsed_config[:pool_names] << pool['name']
128
+ pool['ready_ttl'] ||= parsed_config[:config]['ready_ttl']
120
129
  if pool['alias']
121
130
  if pool['alias'].is_a?(Array)
122
131
  pool['alias'].each do |pool_alias|
@@ -154,6 +163,19 @@ module Vmpooler
154
163
  pools
155
164
  end
156
165
 
166
+ def self.redis_connection_pool(host, port, password, size, timeout, metrics)
167
+ Vmpooler::PoolManager::GenericConnectionPool.new(
168
+ metrics: metrics,
169
+ metric_prefix: 'redis_connection_pool',
170
+ size: size,
171
+ timeout: timeout
172
+ ) do
173
+ connection = Concurrent::Hash.new
174
+ redis = new_redis(host, port, password)
175
+ connection['connection'] = redis
176
+ end
177
+ end
178
+
157
179
  def self.new_redis(host = 'localhost', port = nil, password = nil)
158
180
  Redis.new(host: host, port: port, password: password)
159
181
  end
@@ -238,7 +238,7 @@ module Vmpooler
238
238
  queue[:running] = get_total_across_pools_redis_scard(pools, 'vmpooler__running__', backend)
239
239
  queue[:completed] = get_total_across_pools_redis_scard(pools, 'vmpooler__completed__', backend)
240
240
 
241
- queue[:cloning] = backend.get('vmpooler__tasks__clone').to_i
241
+ queue[:cloning] = backend.get('vmpooler__tasks__clone').to_i + backend.get('vmpooler__tasks__ondemandclone').to_i
242
242
  queue[:booting] = queue[:pending].to_i - queue[:cloning].to_i
243
243
  queue[:booting] = 0 if queue[:booting] < 0
244
244
  queue[:total] = queue[:pending].to_i + queue[:ready].to_i + queue[:running].to_i + queue[:completed].to_i
@@ -42,6 +42,68 @@ module Vmpooler
42
42
  Vmpooler::API.settings.checkoutlock
43
43
  end
44
44
 
45
+ def get_template_aliases(template)
46
+ result = []
47
+ aliases = Vmpooler::API.settings.config[:alias]
48
+ if aliases
49
+ result += aliases[template] if aliases[template].is_a?(Array)
50
+ template_backends << aliases[template] if aliases[template].is_a?(String)
51
+ end
52
+ result
53
+ end
54
+
55
+ def get_pool_weights(template_backends)
56
+ pool_index = pool_index(pools)
57
+ weighted_pools = {}
58
+ template_backends.each do |t|
59
+ next unless pool_index.key? t
60
+
61
+ index = pool_index[t]
62
+ clone_target = pools[index]['clone_target'] || config['clone_target']
63
+ next unless config.key?('backend_weight')
64
+
65
+ weight = config['backend_weight'][clone_target]
66
+ if weight
67
+ weighted_pools[t] = weight
68
+ end
69
+ end
70
+ weighted_pools
71
+ end
72
+
73
+ def count_selection(selection)
74
+ result = {}
75
+ selection.uniq.each do |poolname|
76
+ result[poolname] = selection.count(poolname)
77
+ end
78
+ result
79
+ end
80
+
81
+ def evaluate_template_aliases(template, count)
82
+ template_backends = []
83
+ template_backends << template if backend.sismember('vmpooler__pools', template)
84
+ selection = []
85
+ aliases = get_template_aliases(template)
86
+ if aliases
87
+ template_backends += aliases
88
+ weighted_pools = get_pool_weights(template_backends)
89
+
90
+ pickup = Pickup.new(weighted_pools) if weighted_pools.count == template_backends.count
91
+ count.to_i.times do
92
+ if pickup
93
+ selection << pickup.pick
94
+ else
95
+ selection << template_backends.sample
96
+ end
97
+ end
98
+ else
99
+ count.to_i.times do
100
+ selection << template
101
+ end
102
+ end
103
+
104
+ count_selection(selection)
105
+ end
106
+
45
107
  def fetch_single_vm(template)
46
108
  template_backends = [template]
47
109
  aliases = Vmpooler::API.settings.config[:alias]
@@ -245,11 +307,9 @@ module Vmpooler
245
307
  pool_index = pool_index(pools)
246
308
  template_configs = backend.hgetall('vmpooler__config__template')
247
309
  template_configs&.each do |poolname, template|
248
- if pool_index.include? poolname
249
- unless pools[pool_index[poolname]]['template'] == template
250
- pools[pool_index[poolname]]['template'] = template
251
- end
252
- end
310
+ next unless pool_index.include? poolname
311
+
312
+ pools[pool_index[poolname]]['template'] = template
253
313
  end
254
314
  end
255
315
 
@@ -257,11 +317,9 @@ module Vmpooler
257
317
  pool_index = pool_index(pools)
258
318
  poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
259
319
  poolsize_configs&.each do |poolname, size|
260
- if pool_index.include? poolname
261
- unless pools[pool_index[poolname]]['size'] == size.to_i
262
- pools[pool_index[poolname]]['size'] == size.to_i
263
- end
264
- end
320
+ next unless pool_index.include? poolname
321
+
322
+ pools[pool_index[poolname]]['size'] = size.to_i
265
323
  end
266
324
  end
267
325
 
@@ -269,14 +327,69 @@ module Vmpooler
269
327
  pool_index = pool_index(pools)
270
328
  clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
271
329
  clone_target_configs&.each do |poolname, clone_target|
272
- if pool_index.include? poolname
273
- unless pools[pool_index[poolname]]['clone_target'] == clone_target
274
- pools[pool_index[poolname]]['clone_target'] == clone_target
275
- end
276
- end
330
+ next unless pool_index.include? poolname
331
+
332
+ pools[pool_index[poolname]]['clone_target'] = clone_target
277
333
  end
278
334
  end
279
335
 
336
+ def too_many_requested?(payload)
337
+ payload&.each do |_poolname, count|
338
+ next unless count.to_i > config['max_ondemand_instances_per_request']
339
+
340
+ return true
341
+ end
342
+ false
343
+ end
344
+
345
+ def generate_ondemand_request(payload)
346
+ result = { 'ok': false }
347
+
348
+ requested_instances = payload.reject { |k, _v| k == 'request_id' }
349
+ if too_many_requested?(requested_instances)
350
+ result['message'] = "requested amount of instances exceeds the maximum #{config['max_ondemand_instances_per_request']}"
351
+ status 403
352
+ return result
353
+ end
354
+
355
+ score = Time.now.to_i
356
+ request_id = payload['request_id']
357
+ request_id ||= generate_request_id
358
+ result['request_id'] = request_id
359
+
360
+ if backend.exists("vmpooler__odrequest__#{request_id}")
361
+ result['message'] = "request_id '#{request_id}' has already been created"
362
+ status 409
363
+ return result
364
+ end
365
+
366
+ status 201
367
+
368
+ platforms_with_aliases = []
369
+ requested_instances.each do |poolname, count|
370
+ selection = evaluate_template_aliases(poolname, count)
371
+ selection.map { |selected_pool, selected_pool_count| platforms_with_aliases << "#{poolname}:#{selected_pool}:#{selected_pool_count}" }
372
+ end
373
+ platforms_string = platforms_with_aliases.join(',')
374
+
375
+ return result unless backend.zadd('vmpooler__provisioning__request', score, request_id)
376
+
377
+ backend.hset("vmpooler__odrequest__#{request_id}", 'requested', platforms_string)
378
+ if Vmpooler::API.settings.config[:auth] and has_token?
379
+ backend.hset("vmpooler__odrequest__#{request_id}", 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
380
+ backend.hset("vmpooler__odrequest__#{request_id}", 'token:user',
381
+ backend.hget('vmpooler__token__' + request.env['HTTP_X_AUTH_TOKEN'], 'user'))
382
+ end
383
+
384
+ result['domain'] = config['domain'] if config['domain']
385
+ result[:ok] = true
386
+ result
387
+ end
388
+
389
+ def generate_request_id
390
+ SecureRandom.uuid
391
+ end
392
+
280
393
  get '/' do
281
394
  sync_pool_sizes
282
395
  redirect to('/dashboard/')
@@ -395,7 +508,7 @@ module Vmpooler
395
508
  end
396
509
 
397
510
  # for backwards compatibility, include separate "empty" stats in "status" block
398
- if ready == 0
511
+ if ready == 0 && max != 0
399
512
  result[:status][:empty] ||= []
400
513
  result[:status][:empty].push(pool['name'])
401
514
 
@@ -689,6 +802,88 @@ module Vmpooler
689
802
  JSON.pretty_generate(result)
690
803
  end
691
804
 
805
+ post "#{api_prefix}/ondemandvm/?" do
806
+ content_type :json
807
+
808
+ need_token! if Vmpooler::API.settings.config[:auth]
809
+
810
+ result = { 'ok' => false }
811
+
812
+ begin
813
+ payload = JSON.parse(request.body.read)
814
+
815
+ if payload
816
+ invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
817
+ if invalid.empty?
818
+ result = generate_ondemand_request(payload)
819
+ else
820
+ result[:bad_templates] = invalid
821
+ invalid.each do |bad_template|
822
+ metrics.increment('ondemandrequest.invalid.' + bad_template)
823
+ end
824
+ status 404
825
+ end
826
+ else
827
+ metrics.increment('ondemandrequest.invalid.unknown')
828
+ status 404
829
+ end
830
+ rescue JSON::ParserError
831
+ status 400
832
+ result = {
833
+ 'ok' => false,
834
+ 'message' => 'JSON payload could not be parsed'
835
+ }
836
+ end
837
+
838
+ JSON.pretty_generate(result)
839
+ end
840
+
841
+ post "#{api_prefix}/ondemandvm/:template/?" do
842
+ content_type :json
843
+ result = { 'ok' => false }
844
+
845
+ need_token! if Vmpooler::API.settings.config[:auth]
846
+
847
+ payload = extract_templates_from_query_params(params[:template])
848
+
849
+ if payload
850
+ invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
851
+ if invalid.empty?
852
+ result = generate_ondemand_request(payload)
853
+ else
854
+ result[:bad_templates] = invalid
855
+ invalid.each do |bad_template|
856
+ metrics.increment('ondemandrequest.invalid.' + bad_template)
857
+ end
858
+ status 404
859
+ end
860
+ else
861
+ metrics.increment('ondemandrequest.invalid.unknown')
862
+ status 404
863
+ end
864
+
865
+ JSON.pretty_generate(result)
866
+ end
867
+
868
+ get "#{api_prefix}/ondemandvm/:requestid/?" do
869
+ content_type :json
870
+
871
+ status 404
872
+ result = check_ondemand_request(params[:requestid])
873
+
874
+ JSON.pretty_generate(result)
875
+ end
876
+
877
+ delete "#{api_prefix}/ondemandvm/:requestid/?" do
878
+ content_type :json
879
+ need_token! if Vmpooler::API.settings.config[:auth]
880
+
881
+ status 404
882
+ result = delete_ondemand_request(params[:requestid])
883
+
884
+ JSON.pretty_generate(result)
885
+ end
886
+
692
887
  post "#{api_prefix}/vm/?" do
693
888
  content_type :json
694
889
  result = { 'ok' => false }
@@ -764,6 +959,78 @@ module Vmpooler
764
959
  invalid
765
960
  end
766
961
 
962
+ def check_ondemand_request(request_id)
963
+ result = { 'ok' => false }
964
+ request_hash = backend.hgetall("vmpooler__odrequest__#{request_id}")
965
+ if request_hash.empty?
966
+ result['message'] = "no request found for request_id '#{request_id}'"
967
+ return result
968
+ end
969
+
970
+ result['request_id'] = request_id
971
+ result['ready'] = false
972
+ result['ok'] = true
973
+ status 202
974
+
975
+ if request_hash['status'] == 'ready'
976
+ result['ready'] = true
977
+ platform_parts = request_hash['requested'].split(',')
978
+ platform_parts.each do |platform|
979
+ pool_alias, pool, _count = platform.split(':')
980
+ instances = backend.smembers("vmpooler__#{request_id}__#{pool_alias}__#{pool}")
981
+ result[pool_alias] = { 'hostname': instances }
982
+ end
983
+ result['domain'] = config['domain'] if config['domain']
984
+ status 200
985
+ elsif request_hash['status'] == 'failed'
986
+ result['message'] = "The request failed to provision instances within the configured ondemand_request_ttl '#{config['ondemand_request_ttl']}'"
987
+ status 200
988
+ elsif request_hash['status'] == 'deleted'
989
+ result['message'] = 'The request has been deleted'
990
+ status 200
991
+ else
992
+ platform_parts = request_hash['requested'].split(',')
993
+ platform_parts.each do |platform|
994
+ pool_alias, pool, count = platform.split(':')
995
+ instance_count = backend.scard("vmpooler__#{request_id}__#{pool_alias}__#{pool}")
996
+ result[pool_alias] = {
997
+ 'ready': instance_count.to_s,
998
+ 'pending': (count.to_i - instance_count.to_i).to_s
999
+ }
1000
+ end
1001
+ end
1002
+
1003
+ result
1004
+ end
1005
+
1006
+ def delete_ondemand_request(request_id)
1007
+ result = { 'ok' => false }
1008
+
1009
+ platforms = backend.hget("vmpooler__odrequest__#{request_id}", 'requested')
1010
+ unless platforms
1011
+ result['message'] = "no request found for request_id '#{request_id}'"
1012
+ return result
1013
+ end
1014
+
1015
+ if backend.hget("vmpooler__odrequest__#{request_id}", 'status') == 'deleted'
1016
+ result['message'] = 'the request has already been deleted'
1017
+ else
1018
+ backend.hset("vmpooler__odrequest__#{request_id}", 'status', 'deleted')
1019
+
1020
+ platforms.split(',').each do |platform|
1021
+ pool_alias, pool, _count = platform.split(':')
1022
+ backend.smembers("vmpooler__#{request_id}__#{pool_alias}__#{pool}")&.each do |vm|
1023
+ backend.smove("vmpooler__running__#{pool}", "vmpooler__completed__#{pool}", vm)
1024
+ end
1025
+ backend.del("vmpooler__#{request_id}__#{pool_alias}__#{pool}")
1026
+ end
1027
+ backend.expire("vmpooler__odrequest__#{request_id}", 129_600_0)
1028
+ end
1029
+ status 200
1030
+ result['ok'] = true
1031
+ result
1032
+ end
1033
+
767
1034
  post "#{api_prefix}/vm/:template/?" do
768
1035
  content_type :json
769
1036
  result = { 'ok' => false }
@@ -923,6 +1190,7 @@ module Vmpooler
923
1190
  unless arg.to_i > 0
924
1191
  failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.")
925
1192
  end
1193
+
926
1194
  when 'tags'
927
1195
  unless arg.is_a?(Hash)
928
1196
  failure.push("You provided tags (#{arg}) as something other than a hash.")
@@ -1047,7 +1315,7 @@ module Vmpooler
1047
1315
  invalid.each do |bad_template|
1048
1316
  metrics.increment("config.invalid.#{bad_template}")
1049
1317
  end
1050
- result[:bad_templates] = invalid
1318
+ result[:not_configured] = invalid
1051
1319
  status 400
1052
1320
  end
1053
1321
  else