udn 0.3.23.0.pre

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 +113 -0
  3. data/Rakefile +101 -0
  4. data/bin/udn +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 +1129 -0
  15. data/lib/cli/commands/base.rb +228 -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 +129 -0
  19. data/lib/cli/commands/services.rb +259 -0
  20. data/lib/cli/commands/user.rb +65 -0
  21. data/lib/cli/config.rb +173 -0
  22. data/lib/cli/console_helper.rb +170 -0
  23. data/lib/cli/core_ext.rb +122 -0
  24. data/lib/cli/errors.rb +19 -0
  25. data/lib/cli/frameworks.rb +266 -0
  26. data/lib/cli/manifest_helper.rb +323 -0
  27. data/lib/cli/runner.rb +556 -0
  28. data/lib/cli/services_helper.rb +109 -0
  29. data/lib/cli/tunnel_helper.rb +332 -0
  30. data/lib/cli/usage.rb +124 -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 +562 -0
  35. data/lib/vmc/const.rb +23 -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 +168 -0
  43. metadata +310 -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.23.0.pre'
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
data/lib/vmc.rb ADDED
@@ -0,0 +1,3 @@
1
+ module VMC; end
2
+
3
+ require 'vmc/client'
data/lib/vmc/client.rb ADDED
@@ -0,0 +1,562 @@
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
25
+ attr_accessor :trace, :timeout
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
+ end
45
+
46
+ ######################################################
47
+ # Target info
48
+ ######################################################
49
+
50
+ # Retrieves information on the target cloud, and optionally the logged in user
51
+ def info
52
+ # TODO: Should merge for new version IMO, general, services, user_account
53
+ json_get(VMC::INFO_PATH)
54
+ end
55
+
56
+ def raw_info
57
+ http_get(VMC::INFO_PATH)
58
+ end
59
+
60
+ # Global listing of services that are available on the target system
61
+ def services_info
62
+ check_login_status
63
+ json_get(path(VMC::GLOBAL_SERVICES_PATH))
64
+ end
65
+
66
+ def runtimes_info
67
+ json_get(path(VMC::GLOBAL_RUNTIMES_PATH))
68
+ end
69
+
70
+ ######################################################
71
+ # Apps
72
+ ######################################################
73
+
74
+ def apps
75
+ check_login_status
76
+ json_get(VMC::APPS_PATH)
77
+ end
78
+
79
+ def create_app(name, manifest={})
80
+ check_login_status
81
+ app = manifest.dup
82
+ app[:name] = name
83
+ app[:instances] ||= 1
84
+ json_post(VMC::APPS_PATH, app)
85
+ end
86
+
87
+ def update_app(name, manifest)
88
+ check_login_status
89
+ json_put(path(VMC::APPS_PATH, name), manifest)
90
+ end
91
+
92
+ def upload_app(name, zipfile, resource_manifest=nil)
93
+ #FIXME, manifest should be allowed to be null, here for compatability with old cc's
94
+ resource_manifest ||= []
95
+ check_login_status
96
+ upload_data = {:_method => 'put'}
97
+ if zipfile
98
+ if zipfile.is_a? File
99
+ file = zipfile
100
+ else
101
+ file = File.new(zipfile, 'rb')
102
+ end
103
+ upload_data[:application] = file
104
+ end
105
+ upload_data[:resources] = resource_manifest.to_json if resource_manifest
106
+ http_post(path(VMC::APPS_PATH, name, "application"), upload_data)
107
+ rescue RestClient::ServerBrokeConnection
108
+ retry
109
+ end
110
+
111
+ def delete_app(name)
112
+ check_login_status
113
+ http_delete(path(VMC::APPS_PATH, name))
114
+ end
115
+
116
+ def app_info(name)
117
+ check_login_status
118
+ json_get(path(VMC::APPS_PATH, name))
119
+ end
120
+
121
+ def app_update_info(name)
122
+ check_login_status
123
+ json_get(path(VMC::APPS_PATH, name, "update"))
124
+ end
125
+
126
+ def app_stats(name)
127
+ check_login_status
128
+ stats_raw = json_get(path(VMC::APPS_PATH, name, "stats"))
129
+ stats = []
130
+ stats_raw.each_pair do |k, entry|
131
+ # Skip entries with no stats
132
+ next unless entry[:stats]
133
+ entry[:instance] = k.to_s.to_i
134
+ entry[:state] = entry[:state].to_sym if entry[:state]
135
+ stats << entry
136
+ end
137
+ stats.sort { |a,b| a[:instance] - b[:instance] }
138
+ end
139
+
140
+ def app_instances(name)
141
+ check_login_status
142
+ json_get(path(VMC::APPS_PATH, name, "instances"))
143
+ end
144
+
145
+ def app_crashes(name)
146
+ check_login_status
147
+ json_get(path(VMC::APPS_PATH, name, "crashes"))
148
+ end
149
+
150
+ # List the directory or download the actual file indicated by
151
+ # the path.
152
+ def app_files(name, path, instance='0')
153
+ check_login_status
154
+ path = path.gsub('//', '/')
155
+ url = path(VMC::APPS_PATH, name, "instances", instance, "files", path)
156
+ _, body, headers = http_get(url)
157
+ body
158
+ end
159
+
160
+ ######################################################
161
+ # Services
162
+ ######################################################
163
+
164
+ # listing of services that are available in the system
165
+ def services
166
+ check_login_status
167
+ json_get(VMC::SERVICES_PATH)
168
+ end
169
+
170
+ def create_service(service, name, plan=nil)
171
+ check_login_status
172
+ services = services_info
173
+ services ||= []
174
+ service_hash = nil
175
+
176
+ service = service.to_s
177
+ service_plans = []
178
+ default_plan = nil
179
+
180
+ # FIXME!
181
+ services.each do |service_type, value|
182
+ value.each do |vendor, version|
183
+ version.each do |version_str, service_descr|
184
+ if service == service_descr[:vendor]
185
+ service_plans = service_descr[:tiers].keys.map{|plan_name| plan_name.to_s} if service_descr[:tiers]
186
+ default_plan = service_descr[:default_plan]
187
+ service_hash = {
188
+ :type => service_descr[:type], :tier => 'free',
189
+ :vendor => service, :version => version_str
190
+ }
191
+ break
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
198
+ plan = default_plan || 'free' if plan.nil? || plan.empty?
199
+ service_hash[:tier] = plan
200
+ raise TargetError, "Please specify a plan in (#{service_plans.join(', ')})" if service_plans.size > 0 && (! service_plans.include? plan)
201
+ service_hash[:name] = name
202
+ json_post(path(VMC::SERVICES_PATH), service_hash)
203
+ end
204
+
205
+
206
+ def create_service(service, name, plan=nil)
207
+ check_login_status
208
+ services = services_info
209
+ services ||= []
210
+ service_hash = nil
211
+
212
+ service = service.to_s
213
+ service_plans = []
214
+ default_plan = nil
215
+
216
+ # FIXME!
217
+ services.each do |service_type, value|
218
+ value.each do |vendor, version|
219
+ version.each do |version_str, service_descr|
220
+ if service == service_descr[:vendor]
221
+ service_plans = service_descr[:tiers].keys.map{|plan_name| plan_name.to_s} if service_descr[:tiers]
222
+ default_plan = service_descr[:default_plan]
223
+ service_hash = {
224
+ :type => service_descr[:type], :tier => 'free',
225
+ :vendor => service, :version => version_str
226
+ }
227
+ break
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless service_hash
234
+ plan = default_plan || 'free' if plan.nil? || plan.empty?
235
+ service_hash[:tier] = plan
236
+ raise TargetError, "Please specify a plan in (#{service_plans.join(', ')})" if service_plans.size > 0 && (! service_plans.include? plan)
237
+ service_hash[:name] = name
238
+ json_post(path(VMC::SERVICES_PATH), service_hash)
239
+ end
240
+
241
+ def delete_service(name)
242
+ check_login_status
243
+ svcs = services || []
244
+ names = svcs.collect { |s| s[:name] }
245
+ raise TargetError, "Service [#{name}] not a valid service" unless names.include? name
246
+ http_delete(path(VMC::SERVICES_PATH, name))
247
+ end
248
+
249
+ def bind_service(service, appname)
250
+ check_login_status
251
+ app = app_info(appname)
252
+ services = app[:services] || []
253
+ app[:services] = services << service
254
+ update_app(appname, app)
255
+ end
256
+
257
+ def unbind_service(service, appname)
258
+ check_login_status
259
+ app = app_info(appname)
260
+ services = app[:services] || []
261
+ services.delete(service)
262
+ app[:services] = services
263
+ update_app(appname, app)
264
+ end
265
+
266
+ ######################################################
267
+ # External Services
268
+ ######################################################
269
+
270
+ def external_services_info
271
+ check_login_status
272
+ json_get(path(VMC::EXTERNAL_SERVICES_PATH))
273
+ end
274
+
275
+ def register_service(service, instance, name, options)
276
+ check_login_status
277
+ services = external_services_info.values.collect { |type|
278
+ type.keys.collect(&:to_s)
279
+ }.flatten
280
+ services ||= []
281
+ service_hash = nil
282
+
283
+ service = service.to_s
284
+ raise TargetError, "Service [#{service}] is not a valid service choice" unless services.include?(service)
285
+
286
+ service_hash = {
287
+ :alias => name,
288
+ :name => options.delete(:database),
289
+ :options => options,
290
+ }
291
+
292
+ json_post(path(VMC::EXTERNAL_SERVICES_PATH, service, instance), service_hash)
293
+ end
294
+
295
+ def external_service_instances(service)
296
+ check_login_status
297
+ json_get(path(VMC::EXTERNAL_SERVICES_PATH, service))
298
+ end
299
+
300
+ def rdb_databases(service, instance, credentials)
301
+ check_login_status
302
+ json_get(path(VMC::EXTERNAL_SERVICES_PATH, service, instance),
303
+ {:MasterUsername => credentials[:MasterUsername], :MasterUserPassword => credentials[:MasterUserPassword]})
304
+ end
305
+
306
+ ######################################################
307
+ # Resources
308
+ ######################################################
309
+
310
+ # Send in a resources manifest array to the system to have
311
+ # it check what is needed to actually send. Returns array
312
+ # indicating what is needed. This returned manifest should be
313
+ # sent in with the upload if resources were removed.
314
+ # E.g. [{:sha1 => xxx, :size => xxx, :fn => filename}]
315
+ def check_resources(resources)
316
+ check_login_status
317
+ status, body, headers = json_post(VMC::RESOURCES_PATH, resources)
318
+ json_parse(body)
319
+ end
320
+
321
+ ######################################################
322
+ # Validation Helpers
323
+ ######################################################
324
+
325
+ # Checks that the target is valid
326
+ def target_valid?
327
+ return false unless descr = info
328
+ return false unless descr[:name]
329
+ return false unless descr[:build]
330
+ return false unless descr[:version]
331
+ return false unless descr[:support]
332
+ true
333
+ rescue
334
+ false
335
+ end
336
+
337
+ # Checks that the auth_token is valid
338
+ def logged_in?
339
+ descr = info
340
+ if descr
341
+ return false unless descr[:user]
342
+ return false unless descr[:usage]
343
+ @user = descr[:user]
344
+ true
345
+ end
346
+ end
347
+
348
+ ######################################################
349
+ # User login/password
350
+ ######################################################
351
+
352
+ # login and return an auth_token
353
+ # Auth token can be retained and used in creating
354
+ # new clients, avoiding login.
355
+ def login(user, password)
356
+ status, body, headers = json_post(path(VMC::USERS_PATH, user, "tokens"), {:password => password})
357
+ response_info = json_parse(body)
358
+ if response_info
359
+ @user = user
360
+ @auth_token = response_info[:token]
361
+ end
362
+ end
363
+
364
+ # sets the password for the current logged user
365
+ def change_password(new_password)
366
+ check_login_status
367
+ user_info = json_get(path(VMC::USERS_PATH, @user))
368
+ if user_info
369
+ user_info[:password] = new_password
370
+ json_put(path(VMC::USERS_PATH, @user), user_info)
371
+ end
372
+ end
373
+
374
+ ######################################################
375
+ # System administration
376
+ ######################################################
377
+
378
+ def proxy=(proxy)
379
+ @proxy = proxy
380
+ end
381
+
382
+ def proxy_for(proxy)
383
+ @proxy = proxy
384
+ end
385
+
386
+ def users
387
+ check_login_status
388
+ json_get(VMC::USERS_PATH)
389
+ end
390
+
391
+ def add_user(user_email, password)
392
+ json_post(VMC::USERS_PATH, { :email => user_email, :password => password })
393
+ end
394
+
395
+ def delete_user(user_email)
396
+ check_login_status
397
+ http_delete(path(VMC::USERS_PATH, user_email))
398
+ end
399
+
400
+ ######################################################
401
+
402
+ def self.path(*path)
403
+ path.flatten.collect { |x|
404
+ URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
405
+ }.join("/")
406
+ end
407
+
408
+ private
409
+
410
+ def path(*args, &blk)
411
+ self.class.path(*args, &blk)
412
+ end
413
+
414
+ def json_get(url, params = nil)
415
+ if params
416
+ param = params.map {|key, value| key.to_s + "=" + value}.join("&")
417
+ url = url + "?" + param
418
+ end
419
+ status, body, headers = http_get(url, 'application/json')
420
+ json_parse(body)
421
+ rescue JSON::ParserError
422
+ raise BadResponse, "Can't parse response into JSON", body
423
+ end
424
+
425
+ def json_post(url, payload)
426
+ http_post(url, payload.to_json, 'application/json')
427
+ end
428
+
429
+ def json_put(url, payload)
430
+ http_put(url, payload.to_json, 'application/json')
431
+ end
432
+
433
+ def json_parse(str)
434
+ if str
435
+ JSON.parse(str, :symbolize_names => true)
436
+ end
437
+ end
438
+
439
+ require 'rest_client'
440
+
441
+ # HTTP helpers
442
+
443
+ def http_get(path, content_type=nil)
444
+ request(:get, path, content_type)
445
+ end
446
+
447
+ def http_post(path, body, content_type=nil)
448
+ request(:post, path, content_type, body)
449
+ end
450
+
451
+ def http_put(path, body, content_type=nil)
452
+ request(:put, path, content_type, body)
453
+ end
454
+
455
+ def http_delete(path)
456
+ request(:delete, path)
457
+ end
458
+
459
+ def request(method, path, content_type = nil, payload = nil, headers = {})
460
+ headers = headers.dup
461
+ headers['AUTHORIZATION'] = @auth_token if @auth_token
462
+ headers['PROXY-USER'] = @proxy if @proxy
463
+
464
+ if content_type
465
+ headers['Content-Type'] = content_type
466
+ headers['Accept'] = content_type
467
+ end
468
+
469
+ req = {
470
+ :method => method, :url => "#{@target}/#{path}",
471
+ :payload => payload, :headers => headers, :multipart => true
472
+ }
473
+ req.merge!({:timeout => @timeout}) if @timeout
474
+ status, body, response_headers = perform_http_request(req)
475
+
476
+ if request_failed?(status)
477
+ # FIXME, old cc returned 400 on not found for file access
478
+ err = (status == 404 || status == 400) ? NotFound : TargetError
479
+ raise err, parse_error_message(status, body)
480
+ else
481
+ return status, body, response_headers
482
+ end
483
+ rescue URI::Error, SocketError, Errno::ECONNREFUSED => e
484
+ raise BadTarget, "Cannot access target (%s)" % [ e.message ]
485
+ end
486
+
487
+ def request_failed?(status)
488
+ VMC_HTTP_ERROR_CODES.detect{|error_code| status >= error_code}
489
+ end
490
+
491
+ def perform_http_request(req)
492
+ proxy_uri = URI.parse(req[:url]).find_proxy()
493
+ RestClient.proxy = proxy_uri.to_s if proxy_uri
494
+
495
+ # Setup tracing if needed
496
+ unless trace.nil?
497
+ req[:headers]['X-VCAP-Trace'] = (trace == true ? '22' : trace)
498
+ end
499
+
500
+ start_at = Time.now
501
+ result = nil
502
+ RestClient::Request.execute(req) do |response, request|
503
+ result = [ response.code, response.body, response.headers ]
504
+ unless trace.nil?
505
+ puts '>>>'
506
+ puts "PROXY: #{RestClient.proxy}" if RestClient.proxy
507
+ puts "REQUEST: #{req[:method]} #{req[:url]}"
508
+ puts "RESPONSE_HEADERS:"
509
+ response.headers.each do |key, value|
510
+ puts " #{key} : #{value}"
511
+ end
512
+ puts "REQUEST_BODY: #{req[:payload]}" if req[:payload]
513
+ puts "RESPONSE: [#{response.code}]"
514
+ begin
515
+ puts JSON.pretty_generate(JSON.parse(response.body))
516
+ rescue
517
+ puts "#{response.body}"
518
+ end
519
+ puts '<<<'
520
+ end
521
+ end
522
+ # info: Print response sec if it's over 30sec
523
+ puts "INFO: Took #{Time.now - start_at} sec for request" if Time.now - start_at > 30
524
+ result
525
+ rescue Net::HTTPBadResponse => e
526
+ raise BadTarget "Received bad HTTP response from target: #{e}"
527
+ rescue SystemCallError, RestClient::Exception => e
528
+ raise HTTPException, "HTTP exception: #{e.class}:#{e}"
529
+ end
530
+
531
+ def truncate(str, limit = 30)
532
+ etc = '...'
533
+ stripped = str.strip[0..limit]
534
+ if stripped.length > limit
535
+ stripped + etc
536
+ else
537
+ stripped
538
+ end
539
+ end
540
+
541
+ def parse_error_message(status, body)
542
+ parsed_body = json_parse(body.to_s)
543
+ if parsed_body && parsed_body[:code] && parsed_body[:description]
544
+ desc = parsed_body[:description].gsub("\"","'")
545
+ "Error #{parsed_body[:code]}: #{desc}"
546
+ else
547
+ "Error (HTTP #{status}): #{body}"
548
+ end
549
+ rescue JSON::ParserError
550
+ if body.nil? || body.empty?
551
+ "Error (#{status}): No Response Received"
552
+ else
553
+ body_out = trace ? body : truncate(body)
554
+ "Error (JSON #{status}): #{body_out}"
555
+ end
556
+ end
557
+
558
+ def check_login_status
559
+ raise AuthError unless @user || logged_in?
560
+ end
561
+
562
+ end