vmcu 0.3.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/LICENSE +24 -0
  2. data/README.md +160 -0
  3. data/Rakefile +101 -0
  4. data/bin/vmcu +6 -0
  5. data/caldecott_helper/Gemfile +10 -0
  6. data/caldecott_helper/Gemfile.lock +48 -0
  7. data/caldecott_helper/server.rb +43 -0
  8. data/config/clients.yml +17 -0
  9. data/config/micro/offline.conf +2 -0
  10. data/config/micro/paths.yml +22 -0
  11. data/config/micro/refresh_ip.rb +20 -0
  12. data/lib/cli.rb +47 -0
  13. data/lib/cli/commands/admin.rb +80 -0
  14. data/lib/cli/commands/apps.rb +1128 -0
  15. data/lib/cli/commands/base.rb +238 -0
  16. data/lib/cli/commands/manifest.rb +56 -0
  17. data/lib/cli/commands/micro.rb +115 -0
  18. data/lib/cli/commands/misc.rb +277 -0
  19. data/lib/cli/commands/services.rb +180 -0
  20. data/lib/cli/commands/user.rb +96 -0
  21. data/lib/cli/config.rb +192 -0
  22. data/lib/cli/console_helper.rb +157 -0
  23. data/lib/cli/core_ext.rb +122 -0
  24. data/lib/cli/errors.rb +19 -0
  25. data/lib/cli/frameworks.rb +244 -0
  26. data/lib/cli/manifest_helper.rb +302 -0
  27. data/lib/cli/runner.rb +543 -0
  28. data/lib/cli/services_helper.rb +84 -0
  29. data/lib/cli/tunnel_helper.rb +332 -0
  30. data/lib/cli/usage.rb +118 -0
  31. data/lib/cli/version.rb +7 -0
  32. data/lib/cli/zip_util.rb +77 -0
  33. data/lib/vmc.rb +3 -0
  34. data/lib/vmc/client.rb +591 -0
  35. data/lib/vmc/const.rb +22 -0
  36. data/lib/vmc/micro.rb +56 -0
  37. data/lib/vmc/micro/switcher/base.rb +97 -0
  38. data/lib/vmc/micro/switcher/darwin.rb +19 -0
  39. data/lib/vmc/micro/switcher/dummy.rb +15 -0
  40. data/lib/vmc/micro/switcher/linux.rb +16 -0
  41. data/lib/vmc/micro/switcher/windows.rb +31 -0
  42. data/lib/vmc/micro/vmrun.rb +158 -0
  43. metadata +263 -0
@@ -0,0 +1,7 @@
1
+ module VMC
2
+ module Cli
3
+ # This version number is used as the RubyGem release version.
4
+ # The internal VMC version number is VMC::VERSION.
5
+ VERSION = '0.3.17'
6
+ end
7
+ end
@@ -0,0 +1,77 @@
1
+
2
+ require 'zip/zipfilesystem'
3
+
4
+ module VMC::Cli
5
+
6
+ class ZipUtil
7
+
8
+ PACK_EXCLUSION_GLOBS = ['..', '.', '*~', '#*#', '*.log']
9
+
10
+ class << self
11
+
12
+ def to_dev_null
13
+ if WINDOWS
14
+ 'nul'
15
+ else
16
+ '/dev/null'
17
+ end
18
+ end
19
+
20
+ def entry_lines(file)
21
+ contents = nil
22
+ unless VMC::Cli::Config.nozip
23
+ contents = `unzip -l #{file} 2> #{to_dev_null}`
24
+ contents = nil if $? != 0
25
+ end
26
+ # Do Ruby version if told to or native version failed
27
+ unless contents
28
+ entries = []
29
+ Zip::ZipFile.foreach(file) { |zentry| entries << zentry }
30
+ contents = entries.join("\n")
31
+ end
32
+ contents
33
+ end
34
+
35
+ def unpack(file, dest)
36
+ unless VMC::Cli::Config.nozip
37
+ FileUtils.mkdir(dest)
38
+ `unzip -q #{file} -d #{dest} 2> #{to_dev_null}`
39
+ return unless $? != 0
40
+ end
41
+ # Do Ruby version if told to or native version failed
42
+ Zip::ZipFile.foreach(file) do |zentry|
43
+ epath = "#{dest}/#{zentry}"
44
+ dirname = File.dirname(epath)
45
+ FileUtils.mkdir_p(dirname) unless File.exists?(dirname)
46
+ zentry.extract(epath) unless File.exists?(epath)
47
+ end
48
+ end
49
+
50
+ def get_files_to_pack(dir)
51
+ Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).select do |f|
52
+ process = true
53
+ PACK_EXCLUSION_GLOBS.each { |e| process = false if File.fnmatch(e, File.basename(f)) }
54
+ process && File.exists?(f)
55
+ end
56
+ end
57
+
58
+ def pack(dir, zipfile)
59
+ unless VMC::Cli::Config.nozip
60
+ excludes = PACK_EXCLUSION_GLOBS.map { |e| "\\#{e}" }
61
+ excludes = excludes.join(' ')
62
+ Dir.chdir(dir) do
63
+ `zip -y -q -r #{zipfile} . -x #{excludes} 2> #{to_dev_null}`
64
+ return unless $? != 0
65
+ end
66
+ end
67
+ # Do Ruby version if told to or native version failed
68
+ Zip::ZipFile::open(zipfile, true) do |zf|
69
+ get_files_to_pack(dir).each do |f|
70
+ zf.add(f.sub("#{dir}/",''), f)
71
+ end
72
+ end
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,3 @@
1
+ module VMC; end
2
+
3
+ require 'vmc/client'
@@ -0,0 +1,591 @@
1
+ # VMC client
2
+ #
3
+ # Example:
4
+ #
5
+ # require 'vmc'
6
+ # client = VMC::Client.new('api.vcap.me')
7
+ # client.login(:user, :pass)
8
+ # client.create('myapplication', manifest)
9
+ # client.create_service('redis', 'my_redis_service', opts);
10
+ #
11
+
12
+ require 'rubygems'
13
+ require 'json/pure'
14
+ require 'open-uri'
15
+
16
+ require File.expand_path('../const', __FILE__)
17
+
18
+ class VMC::Client
19
+
20
+ def self.version
21
+ VMC::VERSION
22
+ end
23
+
24
+ attr_reader :target, :host, :user, :proxy, :auth_token, :via_uhuru_cloud, :cloud_team, :proxy_realm
25
+ attr_accessor :trace
26
+
27
+ # Error codes
28
+ VMC_HTTP_ERROR_CODES = [ 400, 500 ]
29
+
30
+ # Errors
31
+ class BadTarget < RuntimeError; end
32
+ class AuthError < RuntimeError; end
33
+ class TargetError < RuntimeError; end
34
+ class NotFound < RuntimeError; end
35
+ class BadResponse < RuntimeError; end
36
+ class HTTPException < RuntimeError; end
37
+
38
+ # Initialize new client to the target_uri with optional auth_token
39
+ def initialize(target_url=VMC::DEFAULT_TARGET, auth_token=nil)
40
+ target_url = "http://#{target_url}" unless /^https?/ =~ target_url
41
+ target_url = target_url.gsub(/\/+$/, '')
42
+ @target = target_url
43
+ @auth_token = auth_token
44
+ @via_uhuru_cloud = false
45
+ end
46
+
47
+ ######################################################
48
+ # Target info
49
+ ######################################################
50
+
51
+ # Retrieves information on the target cloud, and optionally the logged in user
52
+ def info
53
+ # TODO: Should merge for new version IMO, general, services, user_account
54
+ json_get(VMC::INFO_PATH)
55
+ end
56
+
57
+ def raw_info
58
+ http_get(VMC::INFO_PATH)
59
+ end
60
+
61
+ # Global listing of services that are available on the target system
62
+ def services_info
63
+ check_login_status
64
+ json_get(path(VMC::GLOBAL_SERVICES_PATH))
65
+ end
66
+
67
+ def runtimes_info
68
+ json_get(path(VMC::GLOBAL_RUNTIMES_PATH))
69
+ end
70
+
71
+ ######################################################
72
+ # Apps
73
+ ######################################################
74
+
75
+ def apps
76
+ check_login_status
77
+ json_get(VMC::APPS_PATH)
78
+ end
79
+
80
+ def create_app(name, manifest={})
81
+ check_login_status
82
+ app = manifest.dup
83
+ app[:name] = name
84
+ app[:instances] ||= 1
85
+ json_post(VMC::APPS_PATH, app)
86
+ end
87
+
88
+ def update_app(name, manifest)
89
+ check_login_status
90
+ json_put(path(VMC::APPS_PATH, name), manifest)
91
+ end
92
+
93
+ def upload_app(name, zipfile, resource_manifest=nil)
94
+ #FIXME, manifest should be allowed to be null, here for compatability with old cc's
95
+ resource_manifest ||= []
96
+ check_login_status
97
+ upload_data = {:_method => 'put'}
98
+ if zipfile
99
+ if zipfile.is_a? File
100
+ file = zipfile
101
+ else
102
+ file = File.new(zipfile, 'rb')
103
+ end
104
+ upload_data[:application] = file
105
+ end
106
+ upload_data[:resources] = resource_manifest.to_json if resource_manifest
107
+ http_post(path(VMC::APPS_PATH, name, "application"), upload_data)
108
+ rescue RestClient::ServerBrokeConnection
109
+ retry
110
+ end
111
+
112
+ def delete_app(name)
113
+ check_login_status
114
+ http_delete(path(VMC::APPS_PATH, name))
115
+ end
116
+
117
+ def app_info(name)
118
+ check_login_status
119
+ json_get(path(VMC::APPS_PATH, name))
120
+ end
121
+
122
+ def app_update_info(name)
123
+ check_login_status
124
+ json_get(path(VMC::APPS_PATH, name, "update"))
125
+ end
126
+
127
+ def app_stats(name)
128
+ check_login_status
129
+ stats_raw = json_get(path(VMC::APPS_PATH, name, "stats"))
130
+ stats = []
131
+ stats_raw.each_pair do |k, entry|
132
+ # Skip entries with no stats
133
+ next unless entry[:stats]
134
+ entry[:instance] = k.to_s.to_i
135
+ entry[:state] = entry[:state].to_sym if entry[:state]
136
+ stats << entry
137
+ end
138
+ stats.sort { |a,b| a[:instance] - b[:instance] }
139
+ end
140
+
141
+ def app_instances(name)
142
+ check_login_status
143
+ json_get(path(VMC::APPS_PATH, name, "instances"))
144
+ end
145
+
146
+ def app_crashes(name)
147
+ check_login_status
148
+ json_get(path(VMC::APPS_PATH, name, "crashes"))
149
+ end
150
+
151
+ # List the directory or download the actual file indicated by
152
+ # the path.
153
+ def app_files(name, path, instance='0')
154
+ check_login_status
155
+ path = path.gsub('//', '/')
156
+ url = path(VMC::APPS_PATH, name, "instances", instance, "files", path)
157
+ _, body, headers = http_get(url)
158
+ body
159
+ end
160
+
161
+ ######################################################
162
+ # Services
163
+ ######################################################
164
+
165
+ # listing of services that are available in the system
166
+ def services
167
+ check_login_status
168
+ json_get(VMC::SERVICES_PATH)
169
+ end
170
+
171
+ def create_service(service, name)
172
+ check_login_status
173
+ services = services_info
174
+ services ||= []
175
+ service_hash = nil
176
+
177
+ service = service.to_s
178
+
179
+ # FIXME!
180
+ services.each do |service_type, value|
181
+ value.each do |vendor, version|
182
+ version.each do |version_str, service_descr|
183
+ if service == service_descr[:vendor]
184
+ service_hash = {
185
+ :type => service_descr[:type], :tier => 'free',
186
+ :vendor => service, :version => version_str
187
+ }
188
+ break
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
195
+ service_hash[:name] = name
196
+ json_post(path(VMC::SERVICES_PATH), service_hash)
197
+ end
198
+
199
+ def delete_service(name)
200
+ check_login_status
201
+ svcs = services || []
202
+ names = svcs.collect { |s| s[:name] }
203
+ raise TargetError, "Service [#{name}] not a valid service" unless names.include? name
204
+ http_delete(path(VMC::SERVICES_PATH, name))
205
+ end
206
+
207
+ def bind_service(service, appname)
208
+ check_login_status
209
+ app = app_info(appname)
210
+ services = app[:services] || []
211
+ app[:services] = services << service
212
+ update_app(appname, app)
213
+ end
214
+
215
+ def unbind_service(service, appname)
216
+ check_login_status
217
+ app = app_info(appname)
218
+ services = app[:services] || []
219
+ services.delete(service)
220
+ app[:services] = services
221
+ update_app(appname, app)
222
+ end
223
+
224
+ ######################################################
225
+ # Resources
226
+ ######################################################
227
+
228
+ # Send in a resources manifest array to the system to have
229
+ # it check what is needed to actually send. Returns array
230
+ # indicating what is needed. This returned manifest should be
231
+ # sent in with the upload if resources were removed.
232
+ # E.g. [{:sha1 => xxx, :size => xxx, :fn => filename}]
233
+ def check_resources(resources)
234
+ check_login_status
235
+ status, body, headers = json_post(VMC::RESOURCES_PATH, resources)
236
+ json_parse(body)
237
+ end
238
+
239
+ ######################################################
240
+ # Validation Helpers
241
+ ######################################################
242
+
243
+ # currently not used, kept just for reference
244
+ def generic_target_valid?
245
+ @via_uhuru_cloud = false
246
+ return true if target_valid?
247
+
248
+ @via_uhuru_cloud = true
249
+ return true if uhuru_target_valid?
250
+
251
+ @via_uhuru_cloud = false
252
+ false
253
+ end
254
+
255
+ # Checks that the target is valid
256
+ def target_valid?
257
+ return false unless descr = info
258
+ return false unless descr[:name]
259
+ return false unless descr[:build]
260
+ return false unless descr[:version]
261
+ return false unless descr[:support]
262
+ true
263
+ rescue
264
+ false
265
+ end
266
+
267
+ def uhuru_target_valid?
268
+ # return false unless !via_uhuru_cloud || get_cloud_domain
269
+ return false unless uhuru_version =~ /Uhuru Cloud API, version = \d+.\d+.\d+.\d+/
270
+ true
271
+ rescue
272
+ false
273
+ end
274
+
275
+ # checks if the target is a direct Cloud Foundry target or Uhuru target
276
+ def determine_target_type
277
+ if uhuru_target_valid?
278
+ @via_uhuru_cloud = true
279
+ else
280
+ @via_uhuru_cloud = false
281
+ end
282
+
283
+ end
284
+
285
+ # Checks that the auth_token is valid
286
+ def logged_in?
287
+ descr = info
288
+ if descr
289
+ return false unless descr[:user]
290
+ return false unless descr[:usage]
291
+ @user = descr[:user]
292
+ true
293
+ end
294
+ end
295
+
296
+ ######################################################
297
+ # User login/password
298
+ ######################################################
299
+
300
+ # login and return an auth_token
301
+ # Auth token can be retained and used in creating
302
+ # new clients, avoiding login.
303
+ def login(user, password)
304
+ status, body, headers = json_post(path(VMC::USERS_PATH, user, "tokens"), {:password => password})
305
+ response_info = json_parse(body)
306
+ if response_info
307
+ @user = user
308
+ @auth_token = response_info[:token]
309
+ end
310
+ end
311
+
312
+ # sets the password for the current logged user
313
+ def change_password(new_password)
314
+ check_login_status
315
+ user_info = json_get(path(VMC::USERS_PATH, @user))
316
+ if user_info
317
+ user_info[:password] = new_password
318
+ json_put(path(VMC::USERS_PATH, @user), user_info)
319
+ end
320
+ end
321
+
322
+ ######################################################
323
+ # System administration
324
+ ######################################################
325
+
326
+ def proxy=(proxy)
327
+ @proxy = proxy
328
+ end
329
+
330
+ def proxy_for(proxy)
331
+ @proxy = proxy
332
+ end
333
+
334
+ def users
335
+ check_login_status
336
+ json_get(VMC::USERS_PATH)
337
+ end
338
+
339
+ def add_user(user_email, password)
340
+ json_post(VMC::USERS_PATH, { :email => user_email, :password => password })
341
+ end
342
+
343
+ def delete_user(user_email)
344
+ check_login_status
345
+ http_delete(path(VMC::USERS_PATH, user_email))
346
+ end
347
+
348
+ ######################################################
349
+ # Uhuru extension
350
+ ######################################################
351
+
352
+ def via_uhuru_cloud=(via_uhuru_cloud)
353
+ @via_uhuru_cloud = via_uhuru_cloud
354
+ end
355
+
356
+ def auth_token=(auth_token)
357
+ @auth_token = auth_token
358
+ end
359
+
360
+ def cloud_team=(cloud_team)
361
+ @cloud_team = cloud_team
362
+ end
363
+
364
+ def proxy_realm=(proxy_realm)
365
+ @proxy_realm = proxy_realm
366
+ end
367
+
368
+ def get_user_cloud_teams
369
+ cloud_ids = json_get("../usercloudteams/")
370
+ end
371
+
372
+ def get_cloud_team(id)
373
+ json_get("../cloud_teams/#{id}")
374
+ end
375
+
376
+ def get_detailed_cloud_teams
377
+ ret = []
378
+ get_user_cloud_teams.each { |cloud_team_id|
379
+ ret << get_cloud_team(cloud_team_id)
380
+ }
381
+ ret
382
+ end
383
+
384
+ def get_cloud_entity(cloud_id)
385
+ @cloud_entity = json_get("../clouds/#{cloud_id}")
386
+ end
387
+
388
+ def get_cloud_domain
389
+ ct = get_cloud_team(@cloud_team)
390
+ cloud_id = (get_cloud_team(@cloud_team)[:Cloud] || get_cloud_team(@cloud_team)[:CloudTeamCloud])[:Id]
391
+ json_get("../clouds/#{cloud_id}")[:Domain]
392
+ end
393
+
394
+ def uhuru_version
395
+ if @via_uhuru_cloud
396
+ status, body, headers = http_get("../version/")
397
+ else
398
+ status, body, headers = http_get("version/")
399
+ end
400
+ body
401
+ end
402
+
403
+ def raw_uhuru_version
404
+ if @via_uhuru_cloud
405
+ http_get("../version")
406
+ else
407
+ http_get("version")
408
+ end
409
+ end
410
+
411
+ # login to Uhuru App Cloud with a one time token or a regular authentication token
412
+ def uhuru_login(token)
413
+ @via_uhuru_cloud = true;
414
+ # check for one time token
415
+ if token =~ /\A(\w+-)+\w+\z/
416
+ response_target = json_get("../one_time_tokens/#{token}")
417
+ if response_target
418
+ @auth_token = response_target[:token]
419
+ @cloud_team = response_target[:cloud_team]
420
+ @proxy_realm = response_target[:realm]
421
+ response_target[:cloud_domain] = get_cloud_domain if @cloud_team
422
+ response_target
423
+ end
424
+ else
425
+ @auth_token = token
426
+ body = json_get("../usercloudteams/")
427
+ if body
428
+ {:token => token}
429
+ end
430
+ end
431
+
432
+ end
433
+
434
+ ######################################################
435
+
436
+ def self.path(*path)
437
+ path.flatten.collect { |x|
438
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
439
+ }.join("/")
440
+ end
441
+
442
+ private
443
+
444
+ def path(*args, &blk)
445
+ self.class.path(*args, &blk)
446
+ end
447
+
448
+ def json_get(url)
449
+ status, body, headers = http_get(url, 'application/json')
450
+ json_parse(body)
451
+ rescue JSON::ParserError
452
+ raise BadResponse, "Can't parse response into JSON", body
453
+ end
454
+
455
+ def json_post(url, payload)
456
+ http_post(url, payload.to_json, 'application/json')
457
+ end
458
+
459
+ def json_put(url, payload)
460
+ http_put(url, payload.to_json, 'application/json')
461
+ end
462
+
463
+ def json_parse(str)
464
+ if str
465
+ JSON.parse(str, :symbolize_names => true)
466
+ end
467
+ end
468
+
469
+ require 'rest_client'
470
+
471
+ # HTTP helpers
472
+
473
+ def http_get(path, content_type=nil)
474
+ request(:get, path, content_type)
475
+ end
476
+
477
+ def http_post(path, body, content_type=nil)
478
+ request(:post, path, content_type, body)
479
+ end
480
+
481
+ def http_put(path, body, content_type=nil)
482
+ request(:put, path, content_type, body)
483
+ end
484
+
485
+ def http_delete(path)
486
+ request(:delete, path)
487
+ end
488
+
489
+ def request(method, path, content_type = nil, payload = nil, headers = {})
490
+ headers = headers.dup
491
+ headers['AUTHORIZATION'] = @auth_token if @auth_token
492
+ headers['PROXY-USER'] = @proxy if @proxy
493
+ headers['CloudTeam'] = @cloud_team if @cloud_team
494
+ headers['ProxyRealm'] = @proxy_realm if @proxy_realm
495
+ path = "cf/" + path if @via_uhuru_cloud
496
+
497
+ if content_type
498
+ headers['Content-Type'] = content_type
499
+ headers['Accept'] = content_type
500
+ end
501
+
502
+ req = {
503
+ :method => method, :url => "#{@target}/#{path}",
504
+ :payload => payload, :headers => headers, :multipart => true
505
+ }
506
+ status, body, response_headers = perform_http_request(req)
507
+
508
+ if request_failed?(status)
509
+ # FIXME, old cc returned 400 on not found for file access
510
+ err = (status == 404 || status == 400) ? NotFound : TargetError
511
+ raise err, parse_error_message(status, body)
512
+ else
513
+ return status, body, response_headers
514
+ end
515
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
516
+ raise BadTarget, "Cannot access target (%s)" % [ e.message ]
517
+ end
518
+
519
+ def request_failed?(status)
520
+ VMC_HTTP_ERROR_CODES.detect{|error_code| status >= error_code}
521
+ end
522
+
523
+ def perform_http_request(req)
524
+ proxy_uri = URI.parse(req[:url]).find_proxy()
525
+ RestClient.proxy = proxy_uri.to_s if proxy_uri
526
+
527
+ # Setup tracing if needed
528
+ unless trace.nil?
529
+ req[:headers]['X-VCAP-Trace'] = (trace == true ? '22' : trace)
530
+ end
531
+
532
+ result = nil
533
+ RestClient::Request.execute(req) do |response, request|
534
+ result = [ response.code, response.body, response.headers ]
535
+ unless trace.nil?
536
+ puts '>>>'
537
+ puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
538
+ puts "REQUEST: #{req[:method]} #{req[:url]}"
539
+ puts "RESPONSE_HEADERS:"
540
+ response.headers.each do |key, value|
541
+ puts " #{key} : #{value}"
542
+ end
543
+ puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
544
+ puts "RESPONSE: [#{response.code}]"
545
+ begin
546
+ puts JSON.pretty_generate(JSON.parse(response.body))
547
+ rescue
548
+ puts "#{response.body}"
549
+ end
550
+ puts '<<<'
551
+ end
552
+ end
553
+ result
554
+ rescue Net::HTTPBadResponse => e
555
+ raise BadTarget "Received bad HTTP response from target: #{e}"
556
+ rescue SystemCallError, RestClient::Exception => e
557
+ raise HTTPException, "HTTP exception: #{e.class}:#{e}"
558
+ end
559
+
560
+ def truncate(str, limit = 30)
561
+ etc = '...'
562
+ stripped = str.strip[0..limit]
563
+ if stripped.length > limit
564
+ stripped + etc
565
+ else
566
+ stripped
567
+ end
568
+ end
569
+
570
+ def parse_error_message(status, body)
571
+ parsed_body = json_parse(body.to_s)
572
+ if parsed_body && parsed_body[:code] && parsed_body[:description]
573
+ desc = parsed_body[:description].gsub("\"","'")
574
+ "Error #{parsed_body[:code]}: #{desc}"
575
+ else
576
+ "Error (HTTP #{status}): #{body}"
577
+ end
578
+ rescue JSON::ParserError
579
+ if body.nil? || body.empty?
580
+ "Error (#{status}): No Response Received"
581
+ else
582
+ body_out = trace ? body : truncate(body)
583
+ "Error (JSON #{status}): #{body_out}"
584
+ end
585
+ end
586
+
587
+ def check_login_status
588
+ raise AuthError unless @user || logged_in?
589
+ end
590
+
591
+ end