morpheus-cli 0.1.1 → 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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/morpheus/api/accounts_interface.rb +55 -0
  3. data/lib/morpheus/api/api_client.rb +48 -3
  4. data/lib/morpheus/api/apps_interface.rb +13 -13
  5. data/lib/morpheus/api/{zones_interface.rb → clouds_interface.rb} +10 -10
  6. data/lib/morpheus/api/deploy_interface.rb +4 -4
  7. data/lib/morpheus/api/groups_interface.rb +3 -3
  8. data/lib/morpheus/api/instance_types_interface.rb +2 -2
  9. data/lib/morpheus/api/instances_interface.rb +35 -19
  10. data/lib/morpheus/api/key_pairs_interface.rb +60 -0
  11. data/lib/morpheus/api/license_interface.rb +29 -0
  12. data/lib/morpheus/api/load_balancers_interface.rb +72 -0
  13. data/lib/morpheus/api/logs_interface.rb +37 -0
  14. data/lib/morpheus/api/options_interface.rb +20 -0
  15. data/lib/morpheus/api/provision_types_interface.rb +27 -0
  16. data/lib/morpheus/api/roles_interface.rb +73 -0
  17. data/lib/morpheus/api/security_group_rules_interface.rb +3 -3
  18. data/lib/morpheus/api/security_groups_interface.rb +5 -5
  19. data/lib/morpheus/api/servers_interface.rb +67 -3
  20. data/lib/morpheus/api/task_sets_interface.rb +46 -0
  21. data/lib/morpheus/api/tasks_interface.rb +72 -0
  22. data/lib/morpheus/api/users_interface.rb +72 -0
  23. data/lib/morpheus/cli.rb +27 -4
  24. data/lib/morpheus/cli/accounts.rb +306 -0
  25. data/lib/morpheus/cli/apps.rb +58 -1
  26. data/lib/morpheus/cli/cli_command.rb +87 -0
  27. data/lib/morpheus/cli/cli_registry.rb +6 -1
  28. data/lib/morpheus/cli/{zones.rb → clouds.rb} +99 -70
  29. data/lib/morpheus/cli/credentials.rb +23 -11
  30. data/lib/morpheus/cli/error_handler.rb +31 -11
  31. data/lib/morpheus/cli/groups.rb +1 -0
  32. data/lib/morpheus/cli/hosts.rb +567 -0
  33. data/lib/morpheus/cli/instances.rb +588 -292
  34. data/lib/morpheus/cli/key_pairs.rb +393 -0
  35. data/lib/morpheus/cli/license.rb +118 -0
  36. data/lib/morpheus/cli/load_balancers.rb +366 -0
  37. data/lib/morpheus/cli/mixins/accounts_helper.rb +193 -0
  38. data/lib/morpheus/cli/option_types.rb +260 -0
  39. data/lib/morpheus/cli/roles.rb +164 -0
  40. data/lib/morpheus/cli/security_group_rules.rb +4 -9
  41. data/lib/morpheus/cli/shell.rb +108 -0
  42. data/lib/morpheus/cli/tasks.rb +370 -0
  43. data/lib/morpheus/cli/users.rb +325 -0
  44. data/lib/morpheus/cli/version.rb +1 -1
  45. data/lib/morpheus/cli/workflows.rb +100 -0
  46. data/lib/morpheus/formatters.rb +43 -0
  47. data/morpheus-cli.gemspec +1 -1
  48. metadata +33 -10
  49. data/lib/morpheus/cli/servers.rb +0 -265
@@ -0,0 +1,366 @@
1
+ # require 'yaml'
2
+ require 'io/console'
3
+ require 'rest_client'
4
+ require 'term/ansicolor'
5
+ require 'optparse'
6
+ require 'table_print'
7
+ require 'morpheus/cli/cli_command'
8
+
9
+ class Morpheus::Cli::LoadBalancers
10
+ include Morpheus::Cli::CliCommand
11
+ include Term::ANSIColor
12
+ def initialize()
13
+ @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
14
+ end
15
+
16
+ def connect(opts)
17
+ if opts[:remote]
18
+ @appliance_url = opts[:remote]
19
+ @appliance_name = opts[:remote]
20
+ @access_token = Morpheus::Cli::Credentials.new(@appliance_name,@appliance_url).request_credentials(opts)
21
+ else
22
+ @access_token = Morpheus::Cli::Credentials.new(@appliance_name,@appliance_url).request_credentials(opts)
23
+ end
24
+ @api_client = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url)
25
+ @load_balancers_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).load_balancers
26
+
27
+ if @access_token.empty?
28
+ print red,bold, "\nInvalid Credentials. Unable to acquire access token. Please verify your credentials and try again.\n\n",reset
29
+ exit 1
30
+ end
31
+ end
32
+
33
+
34
+ def handle(args)
35
+ if args.empty?
36
+ puts "\nUsage: morpheus load-balancers [list,add, update,remove, details, lb-types]\n\n"
37
+ return
38
+ end
39
+
40
+ case args[0]
41
+ when 'list'
42
+ list(args[1..-1])
43
+ when 'add'
44
+ add(args[1..-1])
45
+ when 'update'
46
+ # update(args[1..-1])
47
+ when 'details'
48
+ details(args[1..-1])
49
+ when 'remove'
50
+ remove(args[1..-1])
51
+ when 'lb-types'
52
+ lb_types(args[1..-1])
53
+ else
54
+ puts "\nUsage: morpheus load-balancers [list,add, update,remove, details, lb-types]\n\n"
55
+ exit 127
56
+ end
57
+ end
58
+
59
+ def list(args)
60
+ options = {}
61
+ optparse = OptionParser.new do|opts|
62
+ opts.banner = "Usage: morpheus load-balancers list [-s] [-o] [-m]"
63
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
64
+ end
65
+ optparse.parse(args)
66
+ connect(options)
67
+ begin
68
+ params = {}
69
+ if options[:offset]
70
+ params[:offset] = options[:offset]
71
+ end
72
+ if options[:max]
73
+ params[:max] = options[:max]
74
+ end
75
+ if options[:phrase]
76
+ params[:phrase] = options[:phrase]
77
+ end
78
+ json_response = @load_balancers_interface.get(params)
79
+ if options[:json]
80
+ print JSON.pretty_generate(json_response)
81
+ else
82
+ lbs = json_response['loadBalancers']
83
+ print "\n" ,cyan, bold, "Morpheus Load Balancers\n","==================", reset, "\n\n"
84
+ if lbs.empty?
85
+ puts yellow,"No load balancers currently configured.",reset
86
+ else
87
+ print cyan
88
+ lb_table_data = lbs.collect do |lb|
89
+ {name: lb['name'], id: lb['id'], type: lb['type']['name']}
90
+ end
91
+ tp lb_table_data, :id, :name, :type
92
+ end
93
+ print reset,"\n\n"
94
+ end
95
+
96
+
97
+ rescue RestClient::Exception => e
98
+ if e.response.code == 400
99
+ error = JSON.parse(e.response.to_s)
100
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error,options)
101
+ else
102
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
103
+ end
104
+ exit 1
105
+ end
106
+ end
107
+
108
+ def details(args)
109
+ lb_name = args[0]
110
+ options = {}
111
+ optparse = OptionParser.new do|opts|
112
+ opts.banner = "Usage: morpheus load-balancers details [name]"
113
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
114
+ end
115
+ if args.count < 1
116
+ puts "\n#{optparse.banner}\n\n"
117
+ exit 1
118
+ end
119
+ optparse.parse(args)
120
+ connect(options)
121
+ begin
122
+ lb = find_lb_by_name(lb_name)
123
+
124
+ exit 1 if lb.nil?
125
+ lb_type = find_lb_type_by_name(lb['type']['name'])
126
+ if options[:json]
127
+ puts JSON.pretty_generate({task:task})
128
+ else
129
+ print "\n", cyan, "Lb #{lb['name']} - #{lb['type']['name']}\n\n"
130
+ lb_type['optionTypes'].sort { |x,y| x['displayOrder'].to_i <=> y['displayOrder'].to_i }.each do |optionType|
131
+ puts " #{optionType['fieldLabel']} : " + (optionType['type'] == 'password' ? "#{task['taskOptions'][optionType['fieldName']] ? '************' : ''}" : "#{task['taskOptions'][optionType['fieldName']] || optionType['defaultValue']}")
132
+ end
133
+ print reset,"\n\n"
134
+ end
135
+ rescue RestClient::Exception => e
136
+ if e.response.code == 400
137
+ error = JSON.parse(e.response.to_s)
138
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error,options)
139
+ else
140
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
141
+ end
142
+ exit 1
143
+ end
144
+ end
145
+
146
+ def update(args)
147
+ lb_name = args[0]
148
+ options = {}
149
+ account_name = nil
150
+ optparse = OptionParser.new do|opts|
151
+ opts.banner = "Usage: morpheus tasks update [task] [options]"
152
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
153
+ end
154
+ if args.count < 1
155
+ puts "\n#{optparse.banner}\n\n"
156
+ exit 1
157
+ end
158
+ optparse.parse(args)
159
+
160
+ connect(options)
161
+
162
+ begin
163
+
164
+
165
+ task = find_task_by_name_or_code_or_id(lb_name)
166
+ exit 1 if task.nil?
167
+ lb_type = find_lb_type_by_name(task['type']['name'])
168
+
169
+ #params = Morpheus::Cli::OptionTypes.prompt(add_user_option_types, options[:options], @api_client, options[:params]) # options[:params] is mysterious
170
+ params = options[:options] || {}
171
+
172
+ if params.empty?
173
+ puts "\n#{optparse.banner}\n\n"
174
+ option_lines = update_task_option_types(lb_type).collect {|it| "\t-O #{it['fieldContext'] ? (it['fieldContext'] + '.') : ''}#{it['fieldName']}=\"value\"" }.join("\n")
175
+ puts "\nAvailable Options:\n#{option_lines}\n\n"
176
+ exit 1
177
+ end
178
+
179
+ #puts "parsed params is : #{params.inspect}"
180
+ task_keys = ['name']
181
+ changes_payload = (params.select {|k,v| task_keys.include?(k) })
182
+ task_payload = task
183
+ if changes_payload
184
+ task_payload.merge!(changes_payload)
185
+ end
186
+ puts params
187
+ if params['taskOptions']
188
+ task_payload['taskOptions'].merge!(params['taskOptions'])
189
+ end
190
+
191
+ request_payload = {task: task_payload}
192
+ response = @load_balancers_interface.update(task['id'], request_payload)
193
+ if options[:json]
194
+ print JSON.pretty_generate(json_response)
195
+ if !response['success']
196
+ exit 1
197
+ end
198
+ else
199
+ print "\n", cyan, "Task #{response['task']['name']} updated", reset, "\n\n"
200
+ end
201
+ rescue RestClient::Exception => e
202
+ if e.response.code == 400
203
+ error = JSON.parse(e.response.to_s)
204
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error)
205
+ else
206
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
207
+ end
208
+ exit 1
209
+ end
210
+ end
211
+
212
+
213
+ def lb_types(args)
214
+ options = {}
215
+ optparse = OptionParser.new do|opts|
216
+ opts.banner = "Usage: morpheus load-balancers lb-types"
217
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
218
+ end
219
+ optparse.parse(args)
220
+ connect(options)
221
+ begin
222
+ json_response = @load_balancers_interface.load_balancer_types()
223
+ if options[:json]
224
+ print JSON.pretty_generate(json_response)
225
+ else
226
+ lb_types = json_response['loadBalancerTypes']
227
+ print "\n" ,cyan, bold, "Morpheus Load Balancer Types\n","============================", reset, "\n\n"
228
+ if lb_types.nil? || lb_types.empty?
229
+ puts yellow,"No lb types currently exist on this appliance. This could be a seed issue.",reset
230
+ else
231
+ print cyan
232
+ lb_table_data = lb_types.collect do |lb_type|
233
+ {name: lb_type['name'], id: lb_type['id'], code: lb_type['code']}
234
+ end
235
+ tp lb_table_data, :id, :name, :code
236
+ end
237
+
238
+ print reset,"\n\n"
239
+ end
240
+
241
+
242
+ rescue => e
243
+ if e.response.code == 400
244
+ error = JSON.parse(e.response.to_s)
245
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error,options)
246
+ else
247
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
248
+ end
249
+ exit 1
250
+ end
251
+ end
252
+
253
+ def add(args)
254
+ lb_name = args[0]
255
+ lb_type_name = nil
256
+ options = {}
257
+ optparse = OptionParser.new do|opts|
258
+ opts.banner = "Usage: morpheus load-balancers add [lb] -t LB_TYPE"
259
+ opts.on( '-t', '--type LB_TYPE', "Lb Type" ) do |val|
260
+ lb_type_name = val
261
+ end
262
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
263
+ end
264
+ if args.count < 1
265
+ puts "\n#{optparse.banner}\n\n"
266
+ exit 1
267
+ end
268
+ optparse.parse(args)
269
+ connect(options)
270
+
271
+ if lb_type_name.nil?
272
+ puts "LB Type must be specified...\n#{optparse.banner}"
273
+ exit 1
274
+ end
275
+
276
+ lb_type = find_lb_type_by_name(lb_type_name)
277
+ if lb_type.nil?
278
+ puts "LB Type not found!"
279
+ exit 1
280
+ end
281
+ input_options = Morpheus::Cli::OptionTypes.prompt(lb_type['optionTypes'],options[:options],@api_client, options[:params])
282
+ payload = {task: {name: lb_name, taskOptions: input_options['taskOptions'], type: {code: lb_type['code'], id: lb_type['id']}}}
283
+ begin
284
+ json_response = @load_balancers_interface.create(payload)
285
+ if options[:json]
286
+ print JSON.pretty_generate(json_response)
287
+ else
288
+ print "\n", cyan, "LB #{json_response['loadBalancer']['name']} created successfully", reset, "\n\n"
289
+ end
290
+ rescue RestClient::Exception => e
291
+ ::Morpheus::Cli::ErrorHandler.new.print_rest_exception(e)
292
+ exit 1
293
+ end
294
+ end
295
+
296
+ def remove(args)
297
+ lb_name = args[0]
298
+ options = {}
299
+ optparse = OptionParser.new do|opts|
300
+ opts.banner = "Usage: morpheus load-balancers remove [name]"
301
+ Morpheus::Cli::CliCommand.genericOptions(opts,options)
302
+ end
303
+ if args.count < 1
304
+ puts "\n#{optparse.banner}\n\n"
305
+ exit 1
306
+ end
307
+ optparse.parse(args)
308
+ connect(options)
309
+ begin
310
+ lb = find_lb_by_name_or_code_or_id(lb_name)
311
+ exit 1 if lb.nil?
312
+ exit unless Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the load balancer #{lb['name']}?")
313
+ json_response = @load_balancers_interface.destroy(lb['id'])
314
+ if options[:json]
315
+ print JSON.pretty_generate(json_response)
316
+ else
317
+ print "\n", cyan, "Load Balancer #{lb['name']} removed", reset, "\n\n"
318
+ end
319
+ rescue RestClient::Exception => e
320
+ if e.response.code == 400
321
+ error = JSON.parse(e.response.to_s)
322
+ ::Morpheus::Cli::ErrorHandler.new.print_errors(error,options)
323
+ else
324
+ puts "Error Communicating with the Appliance. Please try again later. #{e}"
325
+ end
326
+ exit 1
327
+ end
328
+ end
329
+
330
+
331
+ private
332
+ def find_lb_by_name(val)
333
+ raise "find_lb_by_name passed a bad name: #{val.inspect}" if val.to_s == ''
334
+ results = @load_balancers_interface.get(val)
335
+ result = nil
336
+ if !results['loadBalancers'].nil? && !results['loadBalancers'].empty?
337
+ result = results['loadBalancers'][0]
338
+ elsif val.to_i.to_s == val
339
+ results = @load_balancers_interface.get(val.to_i)
340
+ result = results['loadBalancer']
341
+ end
342
+ if result.nil?
343
+ print red,bold, "\nLB not found by '#{val}'\n\n",reset
344
+ return nil
345
+ end
346
+ return result
347
+ end
348
+
349
+ def find_lb_type_by_name(val)
350
+ raise "find_lb_type_by_name passed a bad name: #{val.inspect}" if val.to_s == ''
351
+ results = @load_balancers_interface.load_balancer_types(val)
352
+ result = nil
353
+ if !results['loadBalancerTypes'].nil? && !results['loadBalancerTypes'].empty?
354
+ result = results['loadBalancerTypes'][0]
355
+ elsif val.to_i.to_s == val
356
+ results = @load_balancers_interface.load_balancer_types(val.to_i)
357
+ result = results['loadBalancerType']
358
+ end
359
+ if result.nil?
360
+ print red,bold, "\nLB Type not found by '#{val}'\n\n",reset
361
+ return nil
362
+ end
363
+ return result
364
+ end
365
+
366
+ end
@@ -0,0 +1,193 @@
1
+ # Mixin for Morpheus::Cli command classes
2
+ # Provides common methods for fetching and printing accounts, roles, and users.
3
+ # The including class must establish @accounts_interface, @roles_interface, @users_interface
4
+ module Morpheus::Cli::AccountsHelper
5
+
6
+ def find_account_by_id(id)
7
+ raise "#{self.class} has not defined @accounts_interface" if @accounts_interface.nil?
8
+ begin
9
+ json_response = @accounts_interface.get(id.to_i)
10
+ return json_response['account']
11
+ rescue RestClient::Exception => e
12
+ if e.response.code == 404
13
+ print_red_alert "Account not found by id #{id}"
14
+ else
15
+ raise e
16
+ end
17
+ end
18
+ end
19
+
20
+ def find_account_by_name(name)
21
+ raise "#{self.class} has not defined @accounts_interface" if @accounts_interface.nil?
22
+ accounts = @accounts_interface.list({name: name.to_s})['accounts']
23
+ if accounts.empty?
24
+ print_red_alert "Account not found by name #{name}"
25
+ return nil
26
+ elsif accounts.size > 1
27
+ print_red_alert "#{accounts.size} accounts found by name #{name}"
28
+ print_accounts_table(accounts, {color: red})
29
+ print_red_alert "Try using -A ID instead"
30
+ print reset,"\n"
31
+ return nil
32
+ else
33
+ return accounts[0]
34
+ end
35
+ end
36
+
37
+ def find_account_from_options(options)
38
+ account = nil
39
+ if options[:account_name]
40
+ account = find_account_by_name(options[:account_name])
41
+ exit 1 if account.nil?
42
+ elsif options[:account_id]
43
+ account = find_account_by_id(options[:account_id])
44
+ exit 1 if account.nil?
45
+ else
46
+ account = nil # use current account
47
+ end
48
+ return account
49
+ end
50
+
51
+ def find_role_by_id(account_id, id)
52
+ raise "#{self.class} has not defined @roles_interface" if @roles_interface.nil?
53
+ begin
54
+ json_response = @roles_interface.get(account_id, id.to_i)
55
+ return json_response['role']
56
+ rescue RestClient::Exception => e
57
+ if e.response.code == 404
58
+ print_red_alert "Role not found by id #{id}"
59
+ else
60
+ raise e
61
+ end
62
+ end
63
+ end
64
+
65
+ def find_role_by_name(account_id, name)
66
+ raise "#{self.class} has not defined @roles_interface" if @roles_interface.nil?
67
+ roles = @roles_interface.list(account_id, {authority: name.to_s})['roles']
68
+ if roles.empty?
69
+ print_red_alert "Role not found by name #{name}"
70
+ return nil
71
+ elsif roles.size > 1
72
+ print_red_alert "#{roles.size} roles by name #{name}"
73
+ print_roles_table(roles, {color: red})
74
+ print reset,"\n\n"
75
+ return nil
76
+ else
77
+ return roles[0]
78
+ end
79
+ end
80
+
81
+ alias_method :find_role_by_authority, :find_role_by_name
82
+
83
+ def find_user_by_id(account_id, id)
84
+ raise "#{self.class} has not defined @users_interface" if @users_interface.nil?
85
+ begin
86
+ json_response = @users_interface.get(account_id, id.to_i)
87
+ return json_response['user']
88
+ rescue RestClient::Exception => e
89
+ if e.response.code == 404
90
+ print_red_alert "User not found by id #{id}"
91
+ else
92
+ raise e
93
+ end
94
+ end
95
+ end
96
+
97
+ def find_user_by_username(account_id, username)
98
+ raise "#{self.class} has not defined @users_interface" if @users_interface.nil?
99
+ users = @users_interface.list(account_id, {username: username.to_s})['users']
100
+ if users.empty?
101
+ print_red_alert "User not found by username #{username}"
102
+ return nil
103
+ elsif users.size > 1
104
+ print_red_alert "#{users.size} users by username #{username}"
105
+ print_users_table(users, {color: red})
106
+ print reset,"\n\n"
107
+ return nil
108
+ else
109
+ return users[0]
110
+ end
111
+ end
112
+
113
+ def print_accounts_table(accounts, opts={})
114
+ table_color = opts[:color] || cyan
115
+ rows = accounts.collect do |account|
116
+ status_state = nil
117
+ if account['active']
118
+ status_state = "#{green}ACTIVE#{table_color}"
119
+ else
120
+ status_state = "#{red}INACTIVE#{table_color}"
121
+ end
122
+ {
123
+ id: account['id'],
124
+ name: account['name'],
125
+ description: account['description'],
126
+ role: account['role'] ? account['role']['authority'] : nil,
127
+ status: status_state,
128
+ dateCreated: format_local_dt(account['dateCreated'])
129
+ }
130
+ end
131
+
132
+ print table_color
133
+ tp rows, [
134
+ :id,
135
+ :name,
136
+ :description,
137
+ :role,
138
+ {:dateCreated => {:display_name => "Date Created"} },
139
+ :status
140
+ ]
141
+ print reset
142
+ end
143
+
144
+ def print_roles_table(roles, opts={})
145
+ table_color = opts[:color] || cyan
146
+ # tp roles, [
147
+ # 'id',
148
+ # 'name',
149
+ # 'description',
150
+ # 'scope',
151
+ # {'dateCreated' => {:display_name => "Date Created", :display_method => lambda{|it| format_local_dt(it['dateCreated']) } } }
152
+ # ]
153
+ rows = roles.collect do |role|
154
+ {
155
+ id: role['id'],
156
+ name: role['authority'],
157
+ description: role['description'],
158
+ scope: role['scope'],
159
+ owner: role['owner'] ? role['owner']['name'] : nil,
160
+ dateCreated: format_local_dt(role['dateCreated'])
161
+ }
162
+ end
163
+ print table_color
164
+ tp rows, [
165
+ :id,
166
+ :name,
167
+ :description,
168
+ :scope,
169
+ :owner,
170
+ {:dateCreated => {:display_name => "Date Created"} }
171
+ ]
172
+ print reset
173
+ end
174
+
175
+ def print_users_table(users, opts={})
176
+ table_color = opts[:color] || cyan
177
+ rows = users.collect do |user|
178
+ {id: user['id'], username: user['username'], first: user['firstName'], last: user['lastName'], email: user['email'], role: user['role'] ? user['role']['authority'] : nil, account: user['account'] ? user['account']['name'] : nil}
179
+ end
180
+ print table_color
181
+ tp rows, :id, :account, :first, :last, :username, :email, :role
182
+ print reset
183
+ end
184
+
185
+ def print_red_alert(msg)
186
+ print red, bold, "\n#{msg}\n\n", reset
187
+ end
188
+
189
+ def print_green_success(msg)
190
+ print green, bold, "\n#{msg}\n\n", reset
191
+ end
192
+
193
+ end