vmpooler 0.12.0 → 0.13.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.
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