vmpooler 0.11.2 → 0.13.2

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: b755def48f79be4614be09b1137b7ecb20a59feced1dc521fdc1ba9454a1efa2
4
- data.tar.gz: b3de3d1a6252b6bc3260235421366734f98c031e5895978d22060d770989e0d8
3
+ metadata.gz: a200f3215b389575c605363ed55bb21b9a1779084cee39fef88fe28c2a51e9ab
4
+ data.tar.gz: fd48ee0ff0fd5ea1f19dbd0b19bce49d821b0a17ef6f62aca91804cbdebc7d88
5
5
  SHA512:
6
- metadata.gz: ba6ef4065c811dffbdafc2035c40092f59fc9921a83595bcbe73a42f8b1df74d355a9bc573a9301e6245d70e61132a906feb3b0a22988b4bf11639e921e4d283
7
- data.tar.gz: 457c5212222bf9052cf9ef5e941417d75c85e86ef878910a44a3f35f8b4e4fa5ef22619bc0c10bea8be14091e2d79087b1292bee094719eddb60825e115dc5c6
6
+ metadata.gz: 63df277bab9ab5f049886d30166856ba5adfc85119e3408d91339827f6b05d3b12dde49c78c051c23d90bf123590e14d6cc7e41598cd315f2df9637a5c7c7042
7
+ data.tar.gz: 3f6ce744a9c792c12800d7da0b6738e995d8f9a0cbc86bd3635977b3c463765b371100f5ea3ad509a15b7bd83be5bf37e4ea72cf7004535a36afe263577ed356
@@ -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
@@ -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]
@@ -83,8 +145,13 @@ module Vmpooler
83
145
  vms.reverse.each do |vm|
84
146
  ready = vm_ready?(vm, config['domain'])
85
147
  if ready
86
- backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__running__#{template_backend}", vm)
87
- return [vm, template_backend, template]
148
+ smoved = backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__running__#{template_backend}", vm)
149
+ if smoved
150
+ return [vm, template_backend, template]
151
+ else
152
+ metrics.increment("checkout.smove.failed.#{template_backend}")
153
+ return [nil, nil, nil]
154
+ end
88
155
  else
89
156
  backend.smove("vmpooler__ready__#{template_backend}", "vmpooler__completed__#{template_backend}", vm)
90
157
  metrics.increment("checkout.nonresponsive.#{template_backend}")
@@ -240,11 +307,9 @@ module Vmpooler
240
307
  pool_index = pool_index(pools)
241
308
  template_configs = backend.hgetall('vmpooler__config__template')
242
309
  template_configs&.each do |poolname, template|
243
- if pool_index.include? poolname
244
- unless pools[pool_index[poolname]]['template'] == template
245
- pools[pool_index[poolname]]['template'] = template
246
- end
247
- end
310
+ next unless pool_index.include? poolname
311
+
312
+ pools[pool_index[poolname]]['template'] = template
248
313
  end
249
314
  end
250
315
 
@@ -252,11 +317,9 @@ module Vmpooler
252
317
  pool_index = pool_index(pools)
253
318
  poolsize_configs = backend.hgetall('vmpooler__config__poolsize')
254
319
  poolsize_configs&.each do |poolname, size|
255
- if pool_index.include? poolname
256
- unless pools[pool_index[poolname]]['size'] == size.to_i
257
- pools[pool_index[poolname]]['size'] == size.to_i
258
- end
259
- end
320
+ next unless pool_index.include? poolname
321
+
322
+ pools[pool_index[poolname]]['size'] = size.to_i
260
323
  end
261
324
  end
262
325
 
@@ -264,12 +327,67 @@ module Vmpooler
264
327
  pool_index = pool_index(pools)
265
328
  clone_target_configs = backend.hgetall('vmpooler__config__clone_target')
266
329
  clone_target_configs&.each do |poolname, clone_target|
267
- if pool_index.include? poolname
268
- unless pools[pool_index[poolname]]['clone_target'] == clone_target
269
- pools[pool_index[poolname]]['clone_target'] == clone_target
270
- end
271
- end
330
+ next unless pool_index.include? poolname
331
+
332
+ pools[pool_index[poolname]]['clone_target'] = clone_target
333
+ end
334
+ end
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'))
272
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
273
391
  end
274
392
 
275
393
  get '/' do
@@ -390,7 +508,7 @@ module Vmpooler
390
508
  end
391
509
 
392
510
  # for backwards compatibility, include separate "empty" stats in "status" block
393
- if ready == 0
511
+ if ready == 0 && max != 0
394
512
  result[:status][:empty] ||= []
395
513
  result[:status][:empty].push(pool['name'])
396
514
 
@@ -684,6 +802,88 @@ module Vmpooler
684
802
  JSON.pretty_generate(result)
685
803
  end
686
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
+
687
887
  post "#{api_prefix}/vm/?" do
688
888
  content_type :json
689
889
  result = { 'ok' => false }
@@ -759,6 +959,78 @@ module Vmpooler
759
959
  invalid
760
960
  end
761
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
+
762
1034
  post "#{api_prefix}/vm/:template/?" do
763
1035
  content_type :json
764
1036
  result = { 'ok' => false }
@@ -874,6 +1146,8 @@ module Vmpooler
874
1146
 
875
1147
  status 200
876
1148
  result['ok'] = true
1149
+ else
1150
+ metrics.increment('delete.srem.failed')
877
1151
  end
878
1152
  end
879
1153
 
@@ -916,6 +1190,7 @@ module Vmpooler
916
1190
  unless arg.to_i > 0
917
1191
  failure.push("You provided a lifetime (#{arg}) but you must provide a positive number.")
918
1192
  end
1193
+
919
1194
  when 'tags'
920
1195
  unless arg.is_a?(Hash)
921
1196
  failure.push("You provided tags (#{arg}) as something other than a hash.")
@@ -1040,7 +1315,7 @@ module Vmpooler
1040
1315
  invalid.each do |bad_template|
1041
1316
  metrics.increment("config.invalid.#{bad_template}")
1042
1317
  end
1043
- result[:bad_templates] = invalid
1318
+ result[:not_configured] = invalid
1044
1319
  status 400
1045
1320
  end
1046
1321
  else