vmpooler 0.11.3 → 0.13.3

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: 7ea79e2b20c34ba8ec261bcec65ed74ad0d214fdaaf605b440bfeb6d870016dc
4
- data.tar.gz: 0c880b0c31ab2100aafaf3d6f6b83f50aa723fc44a0dded1a0bfdd0fa6bbc87f
3
+ metadata.gz: fe0ef9bcf3e665cc952fce0975a4381883fca200ef93a883e54c7db7e1fc10e7
4
+ data.tar.gz: d44b61c41a57f940cf0df4bf07aabed10b1a72e91cae5b2105f092a7d98d6d05
5
5
  SHA512:
6
- metadata.gz: 68c5ce2a6a5111824a63c8610c4fd533ac0b538bb29b53064bd09bdd6fbe2b610f3464f5e2270e9de3afbfc1ee6ba0111beed1659e09431c12ebb5124eb305d4
7
- data.tar.gz: 8a69ccbb59c189f8757597bcda0c5ccfe496685582d49992fa604a4fa047c484c424345bff7db60c2830891e48ee8155b4bd0afdc586c7860161f6a8f58da9b8
6
+ metadata.gz: e0e7bbec11ceecc5fdee0fe15b3d37e290a6c884a708a6a5acb2e790d54fb1f1e24ec6966973f02bbecd204a29497e9c17ae3b8be26a0d3f15cec508a0d64532
7
+ data.tar.gz: 7302cf41676a65ff5d0bb4108a8f5ac03a96a92e3cd0ec56deee2f2a60d6e73f09eee569ef0f4140e3c27e81a16c3e2d24b951764a772bdeaa082a96d8bde1fe
@@ -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
@@ -84,29 +84,29 @@ module Vmpooler
84
84
  when 'ldap'
85
85
  ldap_base = auth[:ldap]['base']
86
86
  ldap_port = auth[:ldap]['port'] || 389
87
+ ldap_user_obj = auth[:ldap]['user_object']
88
+ ldap_host = auth[:ldap]['host']
87
89
 
88
- if ldap_base.is_a? Array
89
- ldap_base.each do |search_base|
90
+ unless ldap_base.is_a? Array
91
+ ldap_base = ldap_base.split
92
+ end
93
+
94
+ unless ldap_user_obj.is_a? Array
95
+ ldap_user_obj = ldap_user_obj.split
96
+ end
97
+
98
+ ldap_base.each do |search_base|
99
+ ldap_user_obj.each do |search_user_obj|
90
100
  result = authenticate_ldap(
91
101
  ldap_port,
92
- auth[:ldap]['host'],
93
- auth[:ldap]['user_object'],
102
+ ldap_host,
103
+ search_user_obj,
94
104
  search_base,
95
105
  username_str,
96
106
  password_str
97
107
  )
98
- return true if result == true
108
+ return true if result
99
109
  end
100
- else
101
- result = authenticate_ldap(
102
- ldap_port,
103
- auth[:ldap]['host'],
104
- auth[:ldap]['user_object'],
105
- ldap_base,
106
- username_str,
107
- password_str
108
- )
109
- return result
110
110
  end
111
111
 
112
112
  return false
@@ -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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'vmpooler/util/parsing'
4
+
3
5
  module Vmpooler
4
6
  class API
5
7
  class V1 < Sinatra::Base
@@ -42,6 +44,68 @@ module Vmpooler
42
44
  Vmpooler::API.settings.checkoutlock
43
45
  end
44
46
 
47
+ def get_template_aliases(template)
48
+ result = []
49
+ aliases = Vmpooler::API.settings.config[:alias]
50
+ if aliases
51
+ result += aliases[template] if aliases[template].is_a?(Array)
52
+ template_backends << aliases[template] if aliases[template].is_a?(String)
53
+ end
54
+ result
55
+ end
56
+
57
+ def get_pool_weights(template_backends)
58
+ pool_index = pool_index(pools)
59
+ weighted_pools = {}
60
+ template_backends.each do |t|
61
+ next unless pool_index.key? t
62
+
63
+ index = pool_index[t]
64
+ clone_target = pools[index]['clone_target'] || config['clone_target']
65
+ next unless config.key?('backend_weight')
66
+
67
+ weight = config['backend_weight'][clone_target]
68
+ if weight
69
+ weighted_pools[t] = weight
70
+ end
71
+ end
72
+ weighted_pools
73
+ end
74
+
75
+ def count_selection(selection)
76
+ result = {}
77
+ selection.uniq.each do |poolname|
78
+ result[poolname] = selection.count(poolname)
79
+ end
80
+ result
81
+ end
82
+
83
+ def evaluate_template_aliases(template, count)
84
+ template_backends = []
85
+ template_backends << template if backend.sismember('vmpooler__pools', template)
86
+ selection = []
87
+ aliases = get_template_aliases(template)
88
+ if aliases
89
+ template_backends += aliases
90
+ weighted_pools = get_pool_weights(template_backends)
91
+
92
+ pickup = Pickup.new(weighted_pools) if weighted_pools.count == template_backends.count
93
+ count.to_i.times do
94
+ if pickup
95
+ selection << pickup.pick
96
+ else
97
+ selection << template_backends.sample
98
+ end
99
+ end
100
+ else
101
+ count.to_i.times do
102
+ selection << template
103
+ end
104
+ end
105
+
106
+ count_selection(selection)
107
+ end
108
+
45
109
  def fetch_single_vm(template)
46
110
  template_backends = [template]
47
111
  aliases = Vmpooler::API.settings.config[:alias]
@@ -245,11 +309,9 @@ module Vmpooler
245
309
  pool_index = pool_index(pools)
246
310
  template_configs = backend.hgetall('vmpooler__config__template')
247
311
  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
312
+ next unless pool_index.include? poolname
313
+
314
+ pools[pool_index[poolname]]['template'] = template
253
315
  end
254
316
  end
255
317
 
@@ -257,11 +319,9 @@ module Vmpooler
257
319
  pool_index = pool_index(pools)
258
320
  poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
259
321
  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
322
+ next unless pool_index.include? poolname
323
+
324
+ pools[pool_index[poolname]]['size'] = size.to_i
265
325
  end
266
326
  end
267
327
 
@@ -269,12 +329,70 @@ module Vmpooler
269
329
  pool_index = pool_index(pools)
270
330
  clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
271
331
  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
332
+ next unless pool_index.include? poolname
333
+
334
+ pools[pool_index[poolname]]['clone_target'] = clone_target
335
+ end
336
+ end
337
+
338
+ def too_many_requested?(payload)
339
+ payload&.each do |poolname, count|
340
+ next unless count.to_i > config['max_ondemand_instances_per_request']
341
+
342
+ metrics.increment('ondemandrequest.toomanyrequests.' + poolname)
343
+ return true
344
+ end
345
+ false
346
+ end
347
+
348
+ def generate_ondemand_request(payload)
349
+ result = { 'ok': false }
350
+
351
+ requested_instances = payload.reject { |k, _v| k == 'request_id' }
352
+ if too_many_requested?(requested_instances)
353
+ result['message'] = "requested amount of instances exceeds the maximum #{config['max_ondemand_instances_per_request']}"
354
+ status 403
355
+ return result
356
+ end
357
+
358
+ score = Time.now.to_i
359
+ request_id = payload['request_id']
360
+ request_id ||= generate_request_id
361
+ result['request_id'] = request_id
362
+
363
+ if backend.exists("vmpooler__odrequest__#{request_id}")
364
+ result['message'] = "request_id '#{request_id}' has already been created"
365
+ status 409
366
+ metrics.increment('ondemandrequest.generate.duplicaterequests')
367
+ return result
368
+ end
369
+
370
+ status 201
371
+
372
+ platforms_with_aliases = []
373
+ requested_instances.each do |poolname, count|
374
+ selection = evaluate_template_aliases(poolname, count)
375
+ selection.map { |selected_pool, selected_pool_count| platforms_with_aliases << "#{poolname}:#{selected_pool}:#{selected_pool_count}" }
376
+ end
377
+ platforms_string = platforms_with_aliases.join(',')
378
+
379
+ return result unless backend.zadd('vmpooler__provisioning__request', score, request_id)
380
+
381
+ backend.hset("vmpooler__odrequest__#{request_id}", 'requested', platforms_string)
382
+ if Vmpooler::API.settings.config[:auth] and has_token?
383
+ backend.hset("vmpooler__odrequest__#{request_id}", 'token:token', request.env['HTTP_X_AUTH_TOKEN'])
384
+ backend.hset("vmpooler__odrequest__#{request_id}", 'token:user',
385
+ backend.hget('vmpooler__token__' + request.env['HTTP_X_AUTH_TOKEN'], 'user'))
277
386
  end
387
+
388
+ result['domain'] = config['domain'] if config['domain']
389
+ result[:ok] = true
390
+ metrics.increment('ondemandrequest.generate.success')
391
+ result
392
+ end
393
+
394
+ def generate_request_id
395
+ SecureRandom.uuid
278
396
  end
279
397
 
280
398
  get '/' do
@@ -395,7 +513,7 @@ module Vmpooler
395
513
  end
396
514
 
397
515
  # for backwards compatibility, include separate "empty" stats in "status" block
398
- if ready == 0
516
+ if ready == 0 && max != 0
399
517
  result[:status][:empty] ||= []
400
518
  result[:status][:empty].push(pool['name'])
401
519
 
@@ -689,6 +807,88 @@ module Vmpooler
689
807
  JSON.pretty_generate(result)
690
808
  end
691
809
 
810
+ post "#{api_prefix}/ondemandvm/?" do
811
+ content_type :json
812
+
813
+ need_token! if Vmpooler::API.settings.config[:auth]
814
+
815
+ result = { 'ok' => false }
816
+
817
+ begin
818
+ payload = JSON.parse(request.body.read)
819
+
820
+ if payload
821
+ invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
822
+ if invalid.empty?
823
+ result = generate_ondemand_request(payload)
824
+ else
825
+ result[:bad_templates] = invalid
826
+ invalid.each do |bad_template|
827
+ metrics.increment('ondemandrequest.invalid.' + bad_template)
828
+ end
829
+ status 404
830
+ end
831
+ else
832
+ metrics.increment('ondemandrequest.invalid.unknown')
833
+ status 404
834
+ end
835
+ rescue JSON::ParserError
836
+ status 400
837
+ result = {
838
+ 'ok' => false,
839
+ 'message' => 'JSON payload could not be parsed'
840
+ }
841
+ end
842
+
843
+ JSON.pretty_generate(result)
844
+ end
845
+
846
+ post "#{api_prefix}/ondemandvm/:template/?" do
847
+ content_type :json
848
+ result = { 'ok' => false }
849
+
850
+ need_token! if Vmpooler::API.settings.config[:auth]
851
+
852
+ payload = extract_templates_from_query_params(params[:template])
853
+
854
+ if payload
855
+ invalid = invalid_templates(payload.reject { |k, _v| k == 'request_id' })
856
+ if invalid.empty?
857
+ result = generate_ondemand_request(payload)
858
+ else
859
+ result[:bad_templates] = invalid
860
+ invalid.each do |bad_template|
861
+ metrics.increment('ondemandrequest.invalid.' + bad_template)
862
+ end
863
+ status 404
864
+ end
865
+ else
866
+ metrics.increment('ondemandrequest.invalid.unknown')
867
+ status 404
868
+ end
869
+
870
+ JSON.pretty_generate(result)
871
+ end
872
+
873
+ get "#{api_prefix}/ondemandvm/:requestid/?" do
874
+ content_type :json
875
+
876
+ status 404
877
+ result = check_ondemand_request(params[:requestid])
878
+
879
+ JSON.pretty_generate(result)
880
+ end
881
+
882
+ delete "#{api_prefix}/ondemandvm/:requestid/?" do
883
+ content_type :json
884
+ need_token! if Vmpooler::API.settings.config[:auth]
885
+
886
+ status 404
887
+ result = delete_ondemand_request(params[:requestid])
888
+
889
+ JSON.pretty_generate(result)
890
+ end
891
+
692
892
  post "#{api_prefix}/vm/?" do
693
893
  content_type :json
694
894
  result = { 'ok' => false }
@@ -764,6 +964,73 @@ module Vmpooler
764
964
  invalid
765
965
  end
766
966
 
967
+ def check_ondemand_request(request_id)
968
+ result = { 'ok' => false }
969
+ request_hash = backend.hgetall("vmpooler__odrequest__#{request_id}")
970
+ if request_hash.empty?
971
+ result['message'] = "no request found for request_id '#{request_id}'"
972
+ return result
973
+ end
974
+
975
+ result['request_id'] = request_id
976
+ result['ready'] = false
977
+ result['ok'] = true
978
+ status 202
979
+
980
+ if request_hash['status'] == 'ready'
981
+ result['ready'] = true
982
+ Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, _count|
983
+ instances = backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
984
+ result[platform_alias] = { 'hostname': instances }
985
+ end
986
+ result['domain'] = config['domain'] if config['domain']
987
+ status 200
988
+ elsif request_hash['status'] == 'failed'
989
+ result['message'] = "The request failed to provision instances within the configured ondemand_request_ttl '#{config['ondemand_request_ttl']}'"
990
+ status 200
991
+ elsif request_hash['status'] == 'deleted'
992
+ result['message'] = 'The request has been deleted'
993
+ status 200
994
+ else
995
+ Parsing.get_platform_pool_count(request_hash['requested']) do |platform_alias, pool, count|
996
+ instance_count = backend.scard("vmpooler__#{request_id}__#{platform_alias}__#{pool}")
997
+ result[platform_alias] = {
998
+ 'ready': instance_count.to_s,
999
+ 'pending': (count.to_i - instance_count.to_i).to_s
1000
+ }
1001
+ end
1002
+ end
1003
+
1004
+ result
1005
+ end
1006
+
1007
+ def delete_ondemand_request(request_id)
1008
+ result = { 'ok' => false }
1009
+
1010
+ platforms = backend.hget("vmpooler__odrequest__#{request_id}", 'requested')
1011
+ unless platforms
1012
+ result['message'] = "no request found for request_id '#{request_id}'"
1013
+ return result
1014
+ end
1015
+
1016
+ if backend.hget("vmpooler__odrequest__#{request_id}", 'status') == 'deleted'
1017
+ result['message'] = 'the request has already been deleted'
1018
+ else
1019
+ backend.hset("vmpooler__odrequest__#{request_id}", 'status', 'deleted')
1020
+
1021
+ Parsing.get_platform_pool_count(platforms) do |platform_alias, pool, _count|
1022
+ backend.smembers("vmpooler__#{request_id}__#{platform_alias}__#{pool}")&.each do |vm|
1023
+ backend.smove("vmpooler__running__#{pool}", "vmpooler__completed__#{pool}", vm)
1024
+ end
1025
+ backend.del("vmpooler__#{request_id}__#{platform_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