morpheus-cli 3.5.2 → 3.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/lib/morpheus/api/api_client.rb +16 -0
  3. data/lib/morpheus/api/blueprints_interface.rb +84 -0
  4. data/lib/morpheus/api/execution_request_interface.rb +33 -0
  5. data/lib/morpheus/api/instances_interface.rb +21 -0
  6. data/lib/morpheus/api/packages_interface.rb +25 -5
  7. data/lib/morpheus/api/processes_interface.rb +34 -0
  8. data/lib/morpheus/api/roles_interface.rb +7 -0
  9. data/lib/morpheus/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/user_settings_interface.rb +76 -0
  11. data/lib/morpheus/cli.rb +5 -1
  12. data/lib/morpheus/cli/alias_command.rb +1 -1
  13. data/lib/morpheus/cli/app_templates.rb +2 -1
  14. data/lib/morpheus/cli/apps.rb +173 -19
  15. data/lib/morpheus/cli/blueprints_command.rb +2134 -0
  16. data/lib/morpheus/cli/cli_command.rb +3 -1
  17. data/lib/morpheus/cli/clouds.rb +4 -10
  18. data/lib/morpheus/cli/coloring_command.rb +14 -8
  19. data/lib/morpheus/cli/containers_command.rb +92 -5
  20. data/lib/morpheus/cli/execution_request_command.rb +313 -0
  21. data/lib/morpheus/cli/hosts.rb +188 -7
  22. data/lib/morpheus/cli/instances.rb +472 -9
  23. data/lib/morpheus/cli/login.rb +1 -1
  24. data/lib/morpheus/cli/mixins/print_helper.rb +8 -0
  25. data/lib/morpheus/cli/mixins/processes_helper.rb +134 -0
  26. data/lib/morpheus/cli/option_types.rb +21 -16
  27. data/lib/morpheus/cli/packages_command.rb +469 -17
  28. data/lib/morpheus/cli/processes_command.rb +313 -0
  29. data/lib/morpheus/cli/remote.rb +20 -9
  30. data/lib/morpheus/cli/roles.rb +186 -6
  31. data/lib/morpheus/cli/shell.rb +10 -1
  32. data/lib/morpheus/cli/tasks.rb +4 -1
  33. data/lib/morpheus/cli/user_settings_command.rb +431 -0
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/whoami.rb +1 -1
  36. data/lib/morpheus/formatters.rb +14 -0
  37. data/lib/morpheus/morpkg.rb +119 -0
  38. data/morpheus-cli.gemspec +1 -0
  39. metadata +26 -2
@@ -762,7 +762,9 @@ module Morpheus
762
762
  end
763
763
 
764
764
  def default_command_name
765
- Morpheus::Cli::CliRegistry.cli_ize(self.name.split('::')[-1])
765
+ class_name = self.name.split('::')[-1]
766
+ #class_name.sub!(/Command$/, '')
767
+ Morpheus::Cli::CliRegistry.cli_ize(class_name)
766
768
  end
767
769
 
768
770
  def command_name
@@ -640,14 +640,6 @@ class Morpheus::Cli::Clouds
640
640
  def print_clouds_table(clouds, opts={})
641
641
  table_color = opts[:color] || cyan
642
642
  rows = clouds.collect do |cloud|
643
- status = nil
644
- if cloud['status'] == 'ok'
645
- status = "#{green}OK#{table_color}"
646
- elsif cloud['status'].nil?
647
- status = "#{white}UNKNOWN#{table_color}"
648
- else
649
- status = "#{red}#{cloud['status'] ? cloud['status'].upcase : 'N/A'}#{cloud['statusMessage'] ? "#{table_color} - #{cloud['statusMessage']}" : ''}#{table_color}"
650
- end
651
643
  cloud_type = cloud_type_for_id(cloud['zoneTypeId'])
652
644
  {
653
645
  id: cloud['id'],
@@ -656,7 +648,7 @@ class Morpheus::Cli::Clouds
656
648
  location: cloud['location'],
657
649
  groups: (cloud['groups'] || []).collect {|it| it.instance_of?(Hash) ? it['name'] : it.to_s }.join(', '),
658
650
  servers: cloud['serverCount'],
659
- status: status
651
+ status: format_cloud_status(cloud)
660
652
  }
661
653
  end
662
654
  columns = [
@@ -714,7 +706,9 @@ class Morpheus::Cli::Clouds
714
706
  def format_cloud_status(cloud, return_color=cyan)
715
707
  out = ""
716
708
  status_string = cloud['status']
717
- if status_string.nil? || status_string.empty? || status_string == "unknown"
709
+ if cloud['enabled'] == false
710
+ out << "#{red}DISABLED#{return_color}"
711
+ elsif status_string.nil? || status_string.empty? || status_string == "unknown"
718
712
  out << "#{white}UNKNOWN#{return_color}"
719
713
  elsif status_string == 'ok'
720
714
  out << "#{green}#{status_string.upcase}#{return_color}"
@@ -22,18 +22,24 @@ class Morpheus::Cli::ColoringCommand
22
22
  opts.footer = "Enable [on] or Disable [off] ANSI Colors for all output."
23
23
  end
24
24
  optparse.parse!(args)
25
- if args.count != 1
25
+ if args.count > 1
26
26
  puts optparse
27
27
  exit 1
28
28
  end
29
- is_on = ["on","true", "1"].include?(args[0].to_s.strip.downcase)
30
- is_off = ["off","false", "0"].include?(args[0].to_s.strip.downcase)
31
- if !is_on && !is_off
32
- puts optparse
33
- exit 1
29
+ if args.count == 1
30
+ is_on = ["on","true", "1"].include?(args[0].to_s.strip.downcase)
31
+ is_off = ["off","false", "0"].include?(args[0].to_s.strip.downcase)
32
+ if !is_on && !is_off
33
+ puts optparse
34
+ exit 1
35
+ end
36
+ Term::ANSIColor::coloring = is_on
37
+ end
38
+ if Term::ANSIColor::coloring?
39
+ puts "#{cyan}coloring is #{bold}#{green}on#{reset}"
40
+ else
41
+ puts "coloring is off"
34
42
  end
35
- Term::ANSIColor::coloring = is_on
36
- return true
37
43
  end
38
44
 
39
45
  end
@@ -14,10 +14,12 @@ class Morpheus::Cli::ContainersCommand
14
14
  set_command_name :containers
15
15
 
16
16
  register_subcommands :get, :stop, :start, :restart, :suspend, :eject, :action, :actions
17
+ register_subcommands :exec => :execution_request
17
18
 
18
19
  def connect(opts)
19
20
  @api_client = establish_remote_appliance_connection(opts)
20
21
  @containers_interface = @api_client.containers
22
+ @execution_request_interface = @api_client.execution_request
21
23
  end
22
24
 
23
25
  def handle(args)
@@ -31,9 +33,9 @@ class Morpheus::Cli::ContainersCommand
31
33
  opts.on( nil, '--actions', "Display Available Actions" ) do
32
34
  options[:include_available_actions] = true
33
35
  end
34
- opts.on('--refresh-until [status]', String, "Refresh until status is reached. Default status is running.") do |val|
36
+ opts.on('--refresh [status]', String, "Refresh until status is reached. Default status is running.") do |val|
35
37
  if val.to_s.empty?
36
- options[:refresh_until_status] = "running"
38
+ options[:refresh_until_status] = "running,failed"
37
39
  else
38
40
  options[:refresh_until_status] = val.to_s.downcase
39
41
  end
@@ -135,10 +137,13 @@ class Morpheus::Cli::ContainersCommand
135
137
  if options[:refresh_interval].nil? || options[:refresh_interval].to_f < 0
136
138
  options[:refresh_interval] = 5
137
139
  end
138
- while container['status'].to_s.downcase != options[:refresh_until_status].to_s.downcase
140
+ statuses = options[:refresh_until_status].to_s.downcase.split(",").collect {|s| s.strip }.select {|s| !s.to_s.empty? }
141
+ if !statuses.include?(container['status'])
139
142
  print cyan
140
- print "Refreshing until status #{options[:refresh_until_status]} ..."
141
- sleep(options[:refresh_interval])
143
+ print "Status is #{container['status'] || 'unknown'}. Refreshing in #{options[:refresh_interval]} seconds"
144
+ #sleep(options[:refresh_interval])
145
+ sleep_with_dots(options[:refresh_interval])
146
+ print "\n"
142
147
  _get(arg, options)
143
148
  end
144
149
  end
@@ -493,6 +498,88 @@ class Morpheus::Cli::ContainersCommand
493
498
  return 0
494
499
  end
495
500
 
501
+ def execution_request(args)
502
+ options = {}
503
+ params = {}
504
+ script_content = nil
505
+ do_refresh = true
506
+ optparse = Morpheus::Cli::OptionParser.new do|opts|
507
+ opts.banner = subcommand_usage("[id] [options]")
508
+ opts.on('--script SCRIPT', "Script to be executed" ) do |val|
509
+ script_content = val
510
+ end
511
+ opts.on('--file FILE', "File containing the script. This can be used instead of --script" ) do |filename|
512
+ full_filename = File.expand_path(filename)
513
+ if File.exists?(full_filename)
514
+ script_content = File.read(full_filename)
515
+ else
516
+ print_red_alert "File not found: #{full_filename}"
517
+ exit 1
518
+ end
519
+ end
520
+ opts.on(nil, '--no-refresh', "Do not refresh until finished" ) do
521
+ do_refresh = false
522
+ end
523
+ #build_option_type_options(opts, options, add_user_source_option_types())
524
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
525
+ opts.footer = "Execute an arbitrary command or script on a container." + "\n" +
526
+ "[id] is required. This is the id a container." + "\n" +
527
+ "[script] is required. This is the script that is to be executed."
528
+ end
529
+ optparse.parse!(args)
530
+ connect(options)
531
+ if args.count != 1
532
+ print_error Morpheus::Terminal.angry_prompt
533
+ puts_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.inspect}\n#{optparse}"
534
+ return 1
535
+ end
536
+
537
+
538
+ begin
539
+ container = find_container_by_id(args[0])
540
+ return 1 if container.nil?
541
+ params['containerId'] = container['id']
542
+ # construct payload
543
+ payload = {}
544
+ if options[:payload]
545
+ payload = options[:payload]
546
+ else
547
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
548
+ # prompt for Script
549
+ if script_content.nil?
550
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'script', 'type' => 'code-editor', 'fieldLabel' => 'Script', 'required' => true, 'description' => 'The script content'}], options[:options])
551
+ script_content = v_prompt['script']
552
+ end
553
+ payload['script'] = script_content
554
+ end
555
+ # dry run?
556
+ if options[:dry_run]
557
+ print_dry_run @execution_request_interface.dry.create(params, payload)
558
+ return 0
559
+ end
560
+ # do it
561
+ json_response = @execution_request_interface.create(params, payload)
562
+ # print and return result
563
+ if options[:quiet]
564
+ return 0
565
+ elsif options[:json]
566
+ puts as_json(json_response, options)
567
+ return 0
568
+ end
569
+ execution_request = json_response['executionRequest']
570
+ print_green_success "Executing request #{execution_request['uniqueId']}"
571
+ if do_refresh
572
+ Morpheus::Cli::ExecutionRequestCommand.new.handle(["get", execution_request['uniqueId'], "--refresh"])
573
+ else
574
+ Morpheus::Cli::ExecutionRequestCommand.new.handle(["get", execution_request['uniqueId']])
575
+ end
576
+ return 0
577
+ rescue RestClient::Exception => e
578
+ print_rest_exception(e, options)
579
+ exit 1
580
+ end
581
+ end
582
+
496
583
  private
497
584
 
498
585
  def find_container_by_id(id)
@@ -0,0 +1,313 @@
1
+ require 'morpheus/cli/cli_command'
2
+ # require 'morpheus/cli/mixins/provisioning_helper'
3
+ # require 'morpheus/cli/mixins/infrastructure_helper'
4
+
5
+ class Morpheus::Cli::ExecutionRequestCommand
6
+ include Morpheus::Cli::CliCommand
7
+ # include Morpheus::Cli::InfrastructureHelper
8
+ # include Morpheus::Cli::ProvisioningHelper
9
+
10
+ set_command_name :'execution-request'
11
+
12
+ register_subcommands :get, :execute
13
+ #register_subcommands :'execute-against-lease' => :execute_against_lease
14
+
15
+ # set_default_subcommand :list
16
+
17
+ def initialize()
18
+ # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
19
+ end
20
+
21
+ def connect(opts)
22
+ @api_client = establish_remote_appliance_connection(opts)
23
+ # @instances_interface = @api_client.instances
24
+ # @containers_interface = @api_client.containers
25
+ # @servers_interface = @api_client.servers
26
+ @execution_request_interface = @api_client.execution_request
27
+ end
28
+
29
+ def handle(args)
30
+ handle_subcommand(args)
31
+ end
32
+
33
+ def get(args)
34
+ raw_args = args
35
+ options = {}
36
+ params = {}
37
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
38
+ opts.banner = subcommand_usage("[uid]")
39
+ build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :dry_run, :remote])
40
+ opts.on('--refresh', String, "Refresh until execution is finished.") do |val|
41
+ options[:refresh_until_finished] = true
42
+ end
43
+ opts.on('--refresh-interval seconds', String, "Refresh interval. Default is 5 seconds.") do |val|
44
+ options[:refresh_interval] = val.to_f
45
+ end
46
+ opts.footer = "Get details about an execution request." + "\n" +
47
+ "[uid] is required. This is the unique id of an execution request."
48
+ end
49
+ optparse.parse!(args)
50
+ connect(options)
51
+ if args.count != 1
52
+ print_error Morpheus::Terminal.angry_prompt
53
+ puts_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.inspect}\n#{optparse}"
54
+ return 1
55
+ end
56
+ execution_request_id = args[0]
57
+ begin
58
+ params.merge!(parse_list_options(options))
59
+ if options[:dry_run]
60
+ print_dry_run @execution_request_interface.dry.get(execution_request_id, params)
61
+ return
62
+ end
63
+ json_response = @execution_request_interface.get(execution_request_id, params)
64
+ if options[:json]
65
+ puts as_json(json_response, options, "executionRequest")
66
+ return 0
67
+ elsif options[:yaml]
68
+ puts as_yaml(json_response, options, "executionRequest")
69
+ return 0
70
+ elsif options[:csv]
71
+ puts records_as_csv([json_response['executionRequest']], options)
72
+ return 0
73
+ end
74
+
75
+ execution_request = json_response['executionRequest']
76
+
77
+ # refresh until a status is reached
78
+ if options[:refresh_until_finished]
79
+ if options[:refresh_interval].nil? || options[:refresh_interval].to_f < 0
80
+ options[:refresh_interval] = 5
81
+ end
82
+ if execution_request['exitCode'] || ['complete','failed','expired'].include?(execution_request['status'])
83
+ # it is finished
84
+ else
85
+ print cyan
86
+ print "Execution request has not yet finished. Refreshing every #{options[:refresh_interval]} seconds"
87
+ while execution_request['exitCode'].nil? do
88
+ sleep(options[:refresh_interval])
89
+ print cyan,".",reset
90
+ json_response = @execution_request_interface.get(execution_request_id, params)
91
+ execution_request = json_response['executionRequest']
92
+ end
93
+ #sleep_with_dots(options[:refresh_interval])
94
+ print "\n", reset
95
+ # get(raw_args)
96
+ end
97
+ end
98
+
99
+ print_h1 "Execution Request Details"
100
+ print cyan
101
+ description_cols = {
102
+ #"ID" => lambda {|it| it['id'] },
103
+ "Unique ID" => lambda {|it| it['uniqueId'] },
104
+ "Server ID" => lambda {|it| it['serverId'] },
105
+ "Instance ID" => lambda {|it| it['instanceId'] },
106
+ "Container ID" => lambda {|it| it['containerId'] },
107
+ "Expires At" => lambda {|it| format_local_dt it['expiresAt'] },
108
+ "Exit Code" => lambda {|it| it['exitCode'] },
109
+ "Status" => lambda {|it| format_execution_request_status(it) },
110
+ #"Created By" => lambda {|it| it['createdById'] },
111
+ #"Subdomain" => lambda {|it| it['subdomain'] },
112
+ }
113
+ print_description_list(description_cols, execution_request)
114
+
115
+ if execution_request['stdErr']
116
+ print_h2 "Error"
117
+ puts execution_request['stdErr'].to_s.strip
118
+ end
119
+ if execution_request['stdOut']
120
+ print_h2 "Output"
121
+ puts execution_request['stdOut'].to_s.strip
122
+ end
123
+ print reset, "\n"
124
+ return 0
125
+ rescue RestClient::Exception => e
126
+ print_rest_exception(e, options)
127
+ return 1
128
+ end
129
+ end
130
+
131
+ def execute(args)
132
+ options = {}
133
+ params = {}
134
+ script_content = nil
135
+ do_refresh = true
136
+ optparse = Morpheus::Cli::OptionParser.new do|opts|
137
+ opts.banner = subcommand_usage("[options]")
138
+ opts.on('--server ID', String, "Server ID") do |val|
139
+ params['serverId'] = val
140
+ end
141
+ opts.on('--instance ID', String, "Instance ID") do |val|
142
+ params['instanceId'] = val
143
+ end
144
+ opts.on('--container ID', String, "Container ID") do |val|
145
+ params['containerId'] = val
146
+ end
147
+ opts.on('--request ID', String, "Execution Request ID") do |val|
148
+ params['requestId'] = val
149
+ end
150
+ opts.on('--script SCRIPT', "Script to be executed" ) do |val|
151
+ script_content = val
152
+ end
153
+ opts.on('--file FILE', "File containing the script. This can be used instead of --script" ) do |filename|
154
+ full_filename = File.expand_path(filename)
155
+ if File.exists?(full_filename)
156
+ script_content = File.read(full_filename)
157
+ else
158
+ print_red_alert "File not found: #{full_filename}"
159
+ exit 1
160
+ end
161
+ end
162
+ opts.on(nil, '--no-refresh', "Do not refresh until finished" ) do
163
+ do_refresh = false
164
+ end
165
+ #build_option_type_options(opts, options, add_user_source_option_types())
166
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
167
+ opts.footer = "Execute an arbitrary script." + "\n" +
168
+ "[server] or [instance] or [container] is required. This is the id of a server, instance or container." + "\n" +
169
+ "[script] is required. This is the script that is to be executed."
170
+ end
171
+ optparse.parse!(args)
172
+ connect(options)
173
+ if args.count != 0
174
+ print_error Morpheus::Terminal.angry_prompt
175
+ puts_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.inspect}\n#{optparse}"
176
+ return 1
177
+ end
178
+ if params['serverId'].nil? && params['instanceId'].nil? && params['containerId'].nil? && params['requestId'].nil?
179
+ puts_error "#{Morpheus::Terminal.angry_prompt}missing required option: --server or --instance or --container\n#{optparse}"
180
+ return 1
181
+ end
182
+ begin
183
+ # construct payload
184
+ payload = {}
185
+ if options[:payload]
186
+ payload = options[:payload]
187
+ else
188
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
189
+ # could prompt for Server or Container or Instance
190
+ # prompt for Script
191
+ if script_content.nil?
192
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'script', 'type' => 'code-editor', 'fieldLabel' => 'Script', 'required' => true, 'description' => 'The script content'}], options[:options])
193
+ script_content = v_prompt['script']
194
+ end
195
+ payload['script'] = script_content
196
+ end
197
+ # dry run?
198
+ if options[:dry_run]
199
+ print_dry_run @execution_request_interface.dry.create(params, payload)
200
+ return 0
201
+ end
202
+ # do it
203
+ json_response = @execution_request_interface.create(params, payload)
204
+ # print and return result
205
+ if options[:quiet]
206
+ return 0
207
+ elsif options[:json]
208
+ puts as_json(json_response, options)
209
+ return 0
210
+ end
211
+ execution_request = json_response['executionRequest']
212
+ print_green_success "Executing request #{execution_request['uniqueId']}"
213
+ if do_refresh
214
+ get([execution_request['uniqueId'], "--refresh"])
215
+ else
216
+ get([execution_request['uniqueId']])
217
+ end
218
+ return 0
219
+ rescue RestClient::Exception => e
220
+ print_rest_exception(e, options)
221
+ exit 1
222
+ end
223
+ end
224
+
225
+ def execute_against_lease(args)
226
+ options = {}
227
+ params = {}
228
+ do_refresh = true
229
+ script_content = nil
230
+ optparse = Morpheus::Cli::OptionParser.new do|opts|
231
+ opts.banner = subcommand_usage("[uid] [options]")
232
+ opts.on('--script SCRIPT', "Script to be executed" ) do |val|
233
+ script_content = val
234
+ end
235
+ opts.on('--file FILE', "File containing the script. This can be used instead of --script" ) do |filename|
236
+ full_filename = File.expand_path(filename)
237
+ if File.exists?(full_filename)
238
+ script_content = File.read(full_filename)
239
+ else
240
+ print_red_alert "File not found: #{full_filename}"
241
+ exit 1
242
+ end
243
+ end
244
+ opts.on(nil, '--no-refresh', "Do not refresh until finished" ) do
245
+ do_refresh = false
246
+ end
247
+ #build_option_type_options(opts, options, add_user_source_option_types())
248
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
249
+ opts.footer = "Execute request against lease.\n" +
250
+ "[uid] is required. This is the unique id of the execution request.\n" +
251
+ "[script] is required. This is the script that is to be executed."
252
+ end
253
+ optparse.parse!(args)
254
+ connect(options)
255
+ if args.count != 1
256
+ print_error Morpheus::Terminal.angry_prompt
257
+ puts_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.inspect}\n#{optparse}"
258
+ return 1
259
+ end
260
+ execution_request_id = args[0]
261
+ begin
262
+ # construct payload
263
+ payload = {}
264
+ if options[:payload]
265
+ payload = options[:payload]
266
+ else
267
+ payload.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
268
+ if script_content
269
+ payload['script'] = script_content
270
+ end
271
+ end
272
+ # dry run?
273
+ if options[:dry_run]
274
+ print_dry_run @execution_request_interface.dry.execute_against_lease(execution_request_id, params, payload)
275
+ return 0
276
+ end
277
+ # do it
278
+ json_response = @execution_request_interface.execute_against_lease(execution_request_id, params, payload)
279
+ # print and return result
280
+ if options[:quiet]
281
+ return 0
282
+ elsif options[:json]
283
+ puts as_json(json_response, options)
284
+ return 0
285
+ end
286
+ execution_request = json_response['executionRequest']
287
+ print_green_success "Executing request #{execution_request['uniqueId']} against lease"
288
+ if do_refresh
289
+ get([execution_request['uniqueId'], "--refresh"])
290
+ else
291
+ get([execution_request['uniqueId']])
292
+ end
293
+ return 0
294
+ rescue RestClient::Exception => e
295
+ print_rest_exception(e, options)
296
+ exit 1
297
+ end
298
+ end
299
+
300
+ def format_execution_request_status(execution_request, return_color=cyan)
301
+ out = ""
302
+ status_str = execution_request['status']
303
+ if status_str == 'complete'
304
+ out << "#{green}#{status_str.upcase}#{return_color}"
305
+ elsif status_str == 'failed' || status_str == 'expired'
306
+ out << "#{red}#{status_str.upcase}#{return_color}"
307
+ else
308
+ out << "#{cyan}#{status_str.upcase}#{return_color}"
309
+ end
310
+ out
311
+ end
312
+
313
+ end