vmcu 0.3.17

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.
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