vmfloaty 0.8.2 → 0.9.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
- SHA1:
3
- metadata.gz: 04b7261aa013260aba635e266b003d845e4a2c58
4
- data.tar.gz: 83bbc1c12ace45c6a1ea288121762f13d3b841ae
2
+ SHA256:
3
+ metadata.gz: 7b08c0be5bb4c8a57ce706db190b37bdf89532e30964aca13bd53dc4b2649d46
4
+ data.tar.gz: aeaeb37cde837b342ddd438b6e3c389e9e84b12fd6caa36a40fcd87ff8b0427b
5
5
  SHA512:
6
- metadata.gz: 4e38aedb270708b75d9464744b73b507a6dfdab100567a079287b6b06aa0ac57fabaa03e0e18761c5d1b21e2921f15fbd36ab5beeddcbf3266cac2981d90931c
7
- data.tar.gz: 04ddaf46164b38bf84df47ee8d5a0e96d87b6be5d64941ee72a075e5264511c7556b8fdd85a2875fadfea37820cfbc307adc6dd46eb32621c1cb552d70cbec94
6
+ metadata.gz: d7bb8b68a6dcc391184c910883e4d05c1d7c8580dd7d969f141959fdd46b0e4a9ef4c7ec141a3d4526cab557923d80d0db62e8ae11e8fd76120d2d0e5367fde8
7
+ data.tar.gz: d0bb6b6ff8fd727a110814ebd3836dcd344b30c0e0fb5791ac4485ed0fafe2d715d00173e7d874ec9a41488eaecd0709d1ce53a8c5162419e3285912a1746b2d
data/README.md CHANGED
@@ -53,7 +53,7 @@ $ floaty --help
53
53
  Grabbing a token for authenticated pooler requests:
54
54
 
55
55
  ```
56
- floaty token get --user username --url https://vmpooler.mycompany.net/api/v1
56
+ floaty token get --user username --url https://vmpooler.example.net/api/v1
57
57
  ```
58
58
 
59
59
  This command will then ask you to log in. If successful, it will return a token that you can save either in a dotfile or use with other cli commands.
@@ -61,18 +61,18 @@ This command will then ask you to log in. If successful, it will return a token
61
61
  Grabbing vms:
62
62
 
63
63
  ```
64
- floaty get centos-7-x86_64=2 debian-7-x86_64 windows-10=3 --token mytokenstring --url https://vmpooler.mycompany.net/api/v1
64
+ floaty get centos-7-x86_64=2 debian-7-x86_64 windows-10=3 --token mytokenstring --url https://vmpooler.example.net/api/v1
65
65
  ```
66
66
 
67
67
  ### vmfloaty dotfile
68
68
 
69
- If you do not wish to continuely specify various config options with the cli, you can have a dotfile in your home directory for some defaults. For example:
69
+ If you do not wish to continually specify various config options with the cli, you can have a dotfile in your home directory for some defaults. For example:
70
70
 
71
71
  #### Basic configuration
72
72
 
73
73
  ```yaml
74
74
  # file at /Users/me/.vmfloaty.yml
75
- url: 'https://vmpooler.mycompany.net/api/v1'
75
+ url: 'https://vmpooler.example.net/api/v1'
76
76
  user: 'brian'
77
77
  token: 'tokenstring'
78
78
  ```
@@ -90,10 +90,10 @@ To configure multiple services, you can set up your `~/.vmfloaty.yml` config fil
90
90
  user: 'brian'
91
91
  services:
92
92
  main:
93
- url: 'https://vmpooler.mycompany.net/api/v1'
93
+ url: 'https://vmpooler.example.net/api/v1'
94
94
  token: 'tokenstring'
95
95
  alternate:
96
- url: 'https://vmpooler.alternate.net/api/v1'
96
+ url: 'https://vmpooler.example.com/api/v1'
97
97
  token: 'alternate-tokenstring'
98
98
  ```
99
99
 
@@ -125,12 +125,17 @@ vmfloaty is capable of working with Puppet's [nonstandard pooler](https://github
125
125
  user: 'brian'
126
126
  services:
127
127
  vm:
128
- url: 'https://vmpooler.mycompany.net/api/v1'
128
+ url: 'https://vmpooler.example.net/api/v1'
129
129
  token: 'tokenstring'
130
130
  ns:
131
- url: 'https://nspooler.mycompany.net/api/v1'
131
+ url: 'https://nspooler.example.net/api/v1'
132
132
  token: 'nspooler-tokenstring'
133
133
  type: 'nonstandard' # <-- 'type' is necessary for any non-vmpooler service
134
+ abs:
135
+ url: 'https://abs.example.net/'
136
+ token: 'abs-tokenstring'
137
+ type: 'abs' # <-- 'type' is necessary for any non-vmpooler service
138
+
134
139
  ```
135
140
 
136
141
  With this configuration, you could list available OS types from nspooler like this:
data/bin/floaty CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- $LOAD_PATH.unshift(File.expand_path('../../lib', __FILE__))
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
4
5
 
5
6
  require 'vmfloaty'
6
7
 
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'rubygems'
4
4
  require 'commander'
@@ -17,7 +17,7 @@ require 'vmfloaty/ssh'
17
17
  class Vmfloaty
18
18
  include Commander::Methods
19
19
 
20
- def run
20
+ def run # rubocop:disable Metrics/AbcSize
21
21
  program :version, Vmfloaty::VERSION
22
22
  program :description, 'A CLI helper tool for Puppet Labs VM poolers to help you stay afloat'
23
23
 
@@ -33,6 +33,7 @@ class Vmfloaty
33
33
  c.option '--user STRING', String, 'User to authenticate with'
34
34
  c.option '--url STRING', String, 'URL of pooler service'
35
35
  c.option '--token STRING', String, 'Token for pooler service'
36
+ c.option '--priority STRING', 'Priority for supported backends(ABS) (High(1), Medium(2), Low(3))'
36
37
  c.option '--notoken', 'Makes a request without a token'
37
38
  c.option '--force', 'Forces vmfloaty to get requested vms'
38
39
  c.option '--json', 'Prints retrieved vms in JSON format'
@@ -43,22 +44,22 @@ class Vmfloaty
43
44
  force = options.force
44
45
 
45
46
  if args.empty?
46
- STDERR.puts "No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs."
47
+ STDERR.puts 'No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs.'
47
48
  exit 1
48
49
  end
49
50
 
50
51
  os_types = Utils.generate_os_hash(args)
51
52
 
52
53
  max_pool_request = 5
53
- large_pool_requests = os_types.select{|_,v| v > max_pool_request}
54
- if ! large_pool_requests.empty? and ! force
54
+ large_pool_requests = os_types.select { |_, v| v > max_pool_request }
55
+ if !large_pool_requests.empty? && !force
55
56
  STDERR.puts "Requesting vms over #{max_pool_request} requires a --force flag."
56
- STDERR.puts "Try again with `floaty get --force`"
57
+ STDERR.puts 'Try again with `floaty get --force`'
57
58
  exit 1
58
59
  end
59
60
 
60
61
  if os_types.empty?
61
- STDERR.puts "No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs."
62
+ STDERR.puts 'No operating systems provided to obtain. See `floaty get --help` for more information on how to get VMs.'
62
63
  exit 1
63
64
  end
64
65
 
@@ -84,6 +85,7 @@ class Vmfloaty
84
85
  c.option '--url STRING', String, 'URL of pooler service'
85
86
  c.action do |args, options|
86
87
  verbose = options.verbose || config['verbose']
88
+
87
89
  service = Service.new(options, config)
88
90
  filter = args[0]
89
91
 
@@ -143,18 +145,18 @@ class Vmfloaty
143
145
  hostname = args[0]
144
146
  modify_all = options.all
145
147
 
146
- if hostname.nil? and !modify_all
147
- STDERR.puts "ERROR: Provide a hostname or specify --all."
148
+ if hostname.nil? && !modify_all
149
+ STDERR.puts 'ERROR: Provide a hostname or specify --all.'
148
150
  exit 1
149
151
  end
150
- running_vms = modify_all ? service.list_active(verbose) : hostname.split(",")
152
+ running_vms = modify_all ? service.list_active(verbose) : hostname.split(',')
151
153
 
152
154
  tags = options.tags ? JSON.parse(options.tags) : nil
153
155
  modify_hash = {
154
- lifetime: options.lifetime,
155
- disk: options.disk,
156
- tags: tags,
157
- reason: options.reason
156
+ :lifetime => options.lifetime,
157
+ :disk => options.disk,
158
+ :tags => tags,
159
+ :reason => options.reason,
158
160
  }
159
161
  modify_hash.delete_if { |_, value| value.nil? }
160
162
 
@@ -171,11 +173,11 @@ class Vmfloaty
171
173
  end
172
174
  if ok
173
175
  if modify_all
174
- puts "Successfully modified all VMs."
176
+ puts 'Successfully modified all VMs.'
175
177
  else
176
178
  puts "Successfully modified VM #{hostname}."
177
179
  end
178
- puts "Use `floaty list --active` to see the results."
180
+ puts 'Use `floaty list --active` to see the results.'
179
181
  end
180
182
  end
181
183
  end
@@ -205,15 +207,13 @@ class Vmfloaty
205
207
  if delete_all
206
208
  running_vms = service.list_active(verbose)
207
209
  if running_vms.empty?
208
- STDERR.puts "You have no running VMs."
210
+ STDERR.puts 'You have no running VMs.'
209
211
  else
210
212
  Utils.pretty_print_hosts(verbose, service, running_vms)
211
213
  # Confirm deletion
212
214
  puts
213
215
  confirmed = true
214
- unless force
215
- confirmed = agree('Delete all these VMs? [y/N]')
216
- end
216
+ confirmed = agree('Delete all these VMs? [y/N]') unless force
217
217
  if confirmed
218
218
  response = service.delete(verbose, running_vms)
219
219
  response.each do |hostname, result|
@@ -236,7 +236,7 @@ class Vmfloaty
236
236
  end
237
237
  end
238
238
  else
239
- STDERR.puts "You did not provide any hosts to delete"
239
+ STDERR.puts 'You did not provide any hosts to delete'
240
240
  exit 1
241
241
  end
242
242
 
@@ -302,9 +302,7 @@ class Vmfloaty
302
302
  hostname = args[0]
303
303
  snapshot_sha = args[1] || options.snapshot
304
304
 
305
- if args[1] && options.snapshot
306
- STDERR.puts "Two snapshot arguments were given....using snapshot #{snapshot_sha}"
307
- end
305
+ STDERR.puts "Two snapshot arguments were given....using snapshot #{snapshot_sha}" if args[1] && options.snapshot
308
306
 
309
307
  begin
310
308
  revert_req = service.revert(verbose, hostname, snapshot_sha)
@@ -372,25 +370,23 @@ class Vmfloaty
372
370
 
373
371
  begin
374
372
  case action
375
- when 'get'
376
- token = service.get_new_token(verbose)
377
- puts token
378
- when 'delete'
379
- result = service.delete_token(verbose, options.token)
380
- puts result
381
- when 'status'
382
- token_value = options.token
383
- if token_value.nil?
384
- token_value = args[1]
385
- end
386
- status = service.token_status(verbose, token_value)
387
- puts status
388
- when nil
389
- STDERR.puts 'No action provided'
390
- exit 1
391
- else
392
- STDERR.puts "Unknown action: #{action}"
393
- exit 1
373
+ when 'get'
374
+ token = service.get_new_token(verbose)
375
+ puts token
376
+ when 'delete'
377
+ result = service.delete_token(verbose, options.token)
378
+ puts result
379
+ when 'status'
380
+ token_value = options.token
381
+ token_value = args[1] if token_value.nil?
382
+ status = service.token_status(verbose, token_value)
383
+ puts status
384
+ when nil
385
+ STDERR.puts 'No action provided'
386
+ exit 1
387
+ else
388
+ STDERR.puts "Unknown action: #{action}"
389
+ exit 1
394
390
  end
395
391
  rescue TokenError => e
396
392
  STDERR.puts e
@@ -417,15 +413,13 @@ class Vmfloaty
417
413
  use_token = !options.notoken
418
414
 
419
415
  if args.empty?
420
- STDERR.puts "No operating systems provided to obtain. See `floaty ssh --help` for more information on how to get VMs."
416
+ STDERR.puts 'No operating systems provided to obtain. See `floaty ssh --help` for more information on how to get VMs.'
421
417
  exit 1
422
418
  end
423
419
 
424
420
  host_os = args.first
425
421
 
426
- if args.length > 1
427
- STDERR.puts "Can't ssh to multiple hosts; Using #{host_os} only..."
428
- end
422
+ STDERR.puts "Can't ssh to multiple hosts; Using #{host_os} only..." if args.length > 1
429
423
 
430
424
  service.ssh(verbose, host_os, use_token)
431
425
  exit 0
@@ -435,13 +429,13 @@ class Vmfloaty
435
429
  command :completion do |c|
436
430
  c.syntax = 'floaty completion [options]'
437
431
  c.summary = 'Outputs path to completion script'
438
- c.description = Utils.strip_heredoc(<<-EOF)
432
+ c.description = Utils.strip_heredoc(<<-DESCRIPTION)
439
433
  Outputs path to a completion script for the specified shell (or 'bash' if not specified). This makes it easy to add the completion script to your profile:
440
434
 
441
435
  source $(floaty completion --shell bash)
442
436
 
443
437
  This subcommand will exit non-zero with an error message if no completion script is available for the requested shell.
444
- EOF
438
+ DESCRIPTION
445
439
  c.example 'Gets path to bash tab completion script', 'floaty completion --shell bash'
446
440
  c.option '--shell STRING', String, 'Shell to request completion script for'
447
441
  c.action do |_, options|
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'vmfloaty/errors'
4
+ require 'vmfloaty/http'
5
+ require 'faraday'
6
+ require 'json'
7
+
8
+ class ABS
9
+ # List active VMs in ABS
10
+ # This is what a job request looks like:
11
+ # {
12
+ # "state":"filled",
13
+ # "last_processed":"2019-10-31 20:59:33 +0000",
14
+ # "allocated_resources": [
15
+ # {
16
+ # "hostname":"h3oyntawjm7xdch.delivery.puppetlabs.net",
17
+ # "type":"centos-7.2-tmpfs-x86_64",
18
+ # "engine":"vmpooler"}
19
+ # ],
20
+ # "audit_log":{
21
+ # "2019-10-30 20:33:12 +0000":"Allocated h3oyntawjm7xdch.delivery.puppetlabs.net for job 1572467589"
22
+ # },
23
+ # "request":{
24
+ # "resources":{
25
+ # "centos-7.2-tmpfs-x86_64":1
26
+ # },
27
+ # "job": {
28
+ # "id":1572467589,
29
+ # "tags": {
30
+ # "user":"mikker",
31
+ # "url_string":"floaty://mikker/1572467589"
32
+ # },
33
+ # "user":"mikker",
34
+ # "time-received":1572467589
35
+ # }
36
+ # }
37
+ # }
38
+ #
39
+
40
+ @active_hostnames = {}
41
+
42
+ def self.list_active(verbose, url, _token, user)
43
+ all_jobs = []
44
+ @active_hostnames = {}
45
+
46
+ get_active_requests(verbose, url, user).each do |req_hash|
47
+ all_jobs.push(req_hash['request']['job']['id'])
48
+ @active_hostnames[req_hash['request']['job']['id']] = req_hash
49
+ end
50
+
51
+ all_jobs
52
+ end
53
+
54
+ def self.get_active_requests(verbose, url, user)
55
+ conn = Http.get_conn(verbose, url)
56
+ res = conn.get 'status/queue'
57
+ requests = JSON.parse(res.body)
58
+
59
+ ret_val = []
60
+
61
+ requests.each do |req|
62
+ next if req == 'null'
63
+
64
+ req_hash = JSON.parse(req)
65
+
66
+ begin
67
+ next unless user == req_hash['request']['job']['user']
68
+
69
+ ret_val.push(req_hash)
70
+ rescue NoMethodError
71
+ puts "Warning: couldn't parse line returned from abs/status/queue: ".yellow
72
+ end
73
+ end
74
+
75
+ ret_val
76
+ end
77
+
78
+ def self.all_job_resources_accounted_for(allocated_resources, hosts)
79
+ allocated_host_list = allocated_resources.map { |ar| ar['hostname'] }
80
+ (allocated_host_list - hosts).empty?
81
+ end
82
+
83
+ def self.delete(verbose, url, hosts, token, user)
84
+ # In ABS terms, this is a "returned" host.
85
+ conn = Http.get_conn(verbose, url)
86
+ conn.headers['X-AUTH-TOKEN'] = token if token
87
+
88
+ puts "Trying to delete hosts #{hosts}" if verbose
89
+ requests = get_active_requests(verbose, url, user)
90
+
91
+ jobs_to_delete = []
92
+
93
+ ret_status = {}
94
+ hosts.each do |host|
95
+ ret_status[host] = {
96
+ 'ok' => false,
97
+ }
98
+ end
99
+
100
+ requests.each do |req_hash|
101
+ next unless req_hash['state'] == 'allocated' || req_hash['state'] == 'filled'
102
+
103
+ req_hash['allocated_resources'].each do |vm_name, _i|
104
+ if hosts.include? vm_name['hostname']
105
+ if all_job_resources_accounted_for(req_hash['allocated_resources'], hosts)
106
+ ret_status[vm_name['hostname']] = {
107
+ 'ok' => true,
108
+ }
109
+ jobs_to_delete.push(req_hash)
110
+ else
111
+ puts "When using ABS you must delete all vms that you requested at the same time: Can't delete #{req_hash['request']['job']['id']}: #{hosts} does not include all of #{req_hash['allocated_resources']}"
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+ response_body = {}
118
+
119
+ jobs_to_delete.each do |job|
120
+ req_obj = {
121
+ 'job_id' => job['request']['job']['id'],
122
+ 'hosts' => job['allocated_resources'],
123
+ }
124
+
125
+ puts "Deleting #{req_obj}" if verbose
126
+
127
+ return_result = conn.post 'return', req_obj.to_json
128
+ req_obj['hosts'].each do |host|
129
+ response_body[host['hostname']] = { 'ok' => true } if return_result.body == 'OK'
130
+ end
131
+ end
132
+
133
+ response_body
134
+ end
135
+
136
+ # List available VMs in ABS
137
+ def self.list(verbose, url, os_filter = nil)
138
+ conn = Http.get_conn(verbose, url)
139
+
140
+ os_list = []
141
+
142
+ res = conn.get 'status/platforms/vmpooler'
143
+
144
+ res_body = JSON.parse(res.body)
145
+ os_list << '*** VMPOOLER Pools ***'
146
+ os_list += JSON.parse(res_body['vmpooler_platforms'])
147
+
148
+ res = conn.get 'status/platforms/nspooler'
149
+ res_body = JSON.parse(res.body)
150
+ os_list << ''
151
+ os_list << '*** NSPOOLER Pools ***'
152
+ os_list += JSON.parse(res_body['nspooler_platforms'])
153
+
154
+ res = conn.get 'status/platforms/aws'
155
+ res_body = JSON.parse(res.body)
156
+ os_list << ''
157
+ os_list << '*** AWS Pools ***'
158
+ os_list += JSON.parse(res_body['aws_platforms'])
159
+
160
+ os_list.delete 'ok'
161
+
162
+ os_filter ? os_list.select { |i| i[/#{os_filter}/] } : os_list
163
+ end
164
+
165
+ # Retrieve an OS from ABS.
166
+ def self.retrieve(verbose, os_types, token, url, user, options)
167
+ #
168
+ # Contents of post must be like:
169
+ #
170
+ # {
171
+ # "resources": {
172
+ # "centos-7-i386": 1,
173
+ # "ubuntu-1404-x86_64": 2
174
+ # },
175
+ # "job": {
176
+ # "id": "12345",
177
+ # "tags": {
178
+ # "user": "username",
179
+ # }
180
+ # }
181
+ # }
182
+
183
+ conn = Http.get_conn(verbose, url)
184
+ conn.headers['X-AUTH-TOKEN'] = token if token
185
+
186
+ saved_job_id = DateTime.now.strftime('%Q')
187
+
188
+ req_obj = {
189
+ :resources => os_types,
190
+ :job => {
191
+ :id => saved_job_id,
192
+ :tags => {
193
+ :user => user,
194
+ },
195
+ },
196
+ }
197
+
198
+ if options['priority']
199
+ req_obj[:priority] = if options['priority'] == 'high'
200
+ 1
201
+ elsif options['priority'] == 'medium'
202
+ 2
203
+ elsif options['priority'] == 'low'
204
+ 3
205
+ else
206
+ options['priority'].to_i
207
+ end
208
+ end
209
+
210
+ puts "Posting to ABS #{req_obj.to_json}" if verbose
211
+
212
+ # os_string = os_type.map { |os, num| Array(os) * num }.flatten.join('+')
213
+ # raise MissingParamError, 'No operating systems provided to obtain.' if os_string.empty?
214
+ puts "Requesting VMs with job_id: #{saved_job_id}. Will retry for up to an hour."
215
+ res = conn.post 'request', req_obj.to_json
216
+
217
+ retries = 360
218
+
219
+ raise AuthError, "HTTP #{res.status}: The token provided could not authenticate to the pooler.\n#{res_body}" if res.status == 401
220
+
221
+ (1..retries).each do |i|
222
+ queue_place, res_body = check_queue(conn, saved_job_id, req_obj)
223
+ return translated(res_body) if res_body
224
+
225
+ sleep_seconds = 10 if i >= 10
226
+ sleep_seconds = i if i < 10
227
+ puts "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. Queue Position: #{queue_place}... (x#{i})"
228
+
229
+ sleep(sleep_seconds)
230
+ end
231
+ nil
232
+ end
233
+
234
+ #
235
+ # We should fix the ABS API to be more like the vmpooler or nspooler api, but for now
236
+ #
237
+ def self.translated(res_body)
238
+ vmpooler_formatted_body = {}
239
+
240
+ res_body.each do |host|
241
+ if vmpooler_formatted_body[host['type']] && vmpooler_formatted_body[host['type']]['hostname'].class == Array
242
+ vmpooler_formatted_body[host['type']]['hostname'] << host['hostname']
243
+ else
244
+ vmpooler_formatted_body[host['type']] = { 'hostname' => [host['hostname']] }
245
+ end
246
+ end
247
+ vmpooler_formatted_body['ok'] = true
248
+
249
+ vmpooler_formatted_body
250
+ end
251
+
252
+ def self.check_queue(conn, job_id, req_obj)
253
+ queue_info_res = conn.get "status/queue/info/#{job_id}"
254
+ queue_info = JSON.parse(queue_info_res.body)
255
+
256
+ res = conn.post 'request', req_obj.to_json
257
+
258
+ unless res.body.empty?
259
+ res_body = JSON.parse(res.body)
260
+ return queue_info['queue_place'], res_body
261
+ end
262
+ [queue_info['queue_place'], nil]
263
+ end
264
+
265
+ def self.snapshot(_verbose, _url, _hostname, _token)
266
+ puts "Can't snapshot with ABS, use '--service vmpooler' (even for vms checked out with ABS)"
267
+ end
268
+
269
+ def self.status(verbose, url)
270
+ conn = Http.get_conn(verbose, url)
271
+
272
+ res = conn.get 'status'
273
+
274
+ res.body == 'OK'
275
+ end
276
+
277
+ def self.summary(verbose, url)
278
+ conn = Http.get_conn(verbose, url)
279
+
280
+ res = conn.get 'summary'
281
+ JSON.parse(res.body)
282
+ end
283
+
284
+ def self.query(verbose, url, hostname)
285
+ return @active_hostnames if @active_hostnames
286
+
287
+ puts "For vmpooler/snapshot information, use '--service vmpooler' (even for vms checked out with ABS)"
288
+ conn = Http.get_conn(verbose, url)
289
+
290
+ res = conn.get "host/#{hostname}"
291
+ JSON.parse(res.body)
292
+ end
293
+ end