morpheus-cli 3.1.1 → 3.1.1.1

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.
@@ -0,0 +1,602 @@
1
+ require 'rest_client'
2
+ require 'optparse'
3
+ require 'filesize'
4
+ require 'table_print'
5
+ require 'morpheus/cli/cli_command'
6
+ require 'morpheus/cli/mixins/infrastructure_helper'
7
+
8
+ class Morpheus::Cli::NetworkGroupsCommand
9
+ include Morpheus::Cli::CliCommand
10
+ include Morpheus::Cli::InfrastructureHelper
11
+
12
+ set_command_name :'network-groups'
13
+
14
+ register_subcommands :list, :get, :add, :update, :remove
15
+
16
+ # set_default_subcommand :list
17
+
18
+ def initialize()
19
+ # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
20
+ end
21
+
22
+ def connect(opts)
23
+ @api_client = establish_remote_appliance_connection(opts)
24
+ @network_groups_interface = @api_client.network_groups
25
+ @clouds_interface = @api_client.clouds
26
+ @options_interface = @api_client.options
27
+ end
28
+
29
+ def handle(args)
30
+ handle_subcommand(args)
31
+ end
32
+
33
+ def list(args)
34
+ options = {}
35
+ params = {}
36
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
37
+ opts.banner = subcommand_usage()
38
+ build_common_options(opts, options, [:list, :json, :yaml, :csv, :fields, :json, :dry_run, :remote])
39
+ opts.footer = "List network groups."
40
+ end
41
+ optparse.parse!(args)
42
+ connect(options)
43
+ begin
44
+ [:phrase, :offset, :max, :sort, :direction].each do |k|
45
+ params[k] = options[k] unless options[k].nil?
46
+ end
47
+ if options[:dry_run]
48
+ print_dry_run @network_groups_interface.dry.list(params)
49
+ return
50
+ end
51
+ json_response = @network_groups_interface.list(params)
52
+ network_groups = json_response["networkGroups"]
53
+ if options[:include_fields]
54
+ json_response = {"networkGroups" => filter_data(network_groups, options[:include_fields]) }
55
+ end
56
+ if options[:json]
57
+ puts as_json(json_response, options)
58
+ return 0
59
+ elsif options[:yaml]
60
+ puts as_yaml(json_response, options)
61
+ return 0
62
+ elsif options[:csv]
63
+ puts records_as_csv(network_groups, options)
64
+ return 0
65
+ end
66
+ title = "Morpheus Network Groups"
67
+ subtitles = []
68
+ if params[:phrase]
69
+ subtitles << "Search: #{params[:phrase]}".strip
70
+ end
71
+ print_h1 title, subtitles
72
+ if network_groups.empty?
73
+ print cyan,"No network groups found.",reset,"\n"
74
+ else
75
+ rows = network_groups.collect {|network_group|
76
+ row = {
77
+ id: network_group['id'],
78
+ name: network_group['name'],
79
+ description: network_group['description'],
80
+ # networks: network_group['networks'] ? network_group['networks'].collect {|it| it['name'] }.uniq.join(', ') : '',
81
+ networks: network_group['networks'] ? network_group['networks'].size : 0,
82
+ visibility: network_group['visibility'].to_s.capitalize,
83
+ tenants: network_group['tenants'] ? network_group['tenants'].collect {|it| it['name'] }.uniq.join(', ') : ''
84
+ }
85
+ row
86
+ }
87
+ columns = [:id, :name, :description, :networks, :visibility, :tenants]
88
+ if options[:include_fields]
89
+ columns = options[:include_fields]
90
+ end
91
+ print cyan
92
+ print as_pretty_table(rows, columns, options)
93
+ print reset
94
+ print_results_pagination(json_response, {:label => "network group", :n_label => "network groups"})
95
+ end
96
+ print reset,"\n"
97
+ return 0
98
+ rescue RestClient::Exception => e
99
+ print_rest_exception(e, options)
100
+ exit 1
101
+ end
102
+ end
103
+
104
+ def get(args)
105
+ options = {}
106
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
107
+ opts.banner = subcommand_usage("[network-group]")
108
+ build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
109
+ opts.footer = "Get details about a network group." + "\n" +
110
+ "[network-group] is required. This is the name or id of a network group."
111
+ end
112
+ optparse.parse!(args)
113
+ if args.count != 1
114
+ print_error Morpheus::Terminal.angry_prompt
115
+ puts_error "#{command_name} missing argument: [network-group]\n#{optparse}"
116
+ return 1
117
+ end
118
+ connect(options)
119
+ begin
120
+ if options[:dry_run]
121
+ if args[0].to_s =~ /\A\d{1,}\Z/
122
+ print_dry_run @network_groups_interface.dry.get(args[0].to_i)
123
+ else
124
+ print_dry_run @network_groups_interface.dry.list({name:args[0]})
125
+ end
126
+ return
127
+ end
128
+ network_group = find_network_group_by_name_or_id(args[0])
129
+ return 1 if network_group.nil?
130
+ json_response = {'networkGroup' => network_group} # skip redundant request
131
+ # json_response = @network_groups_interface.get(network_group['id'])
132
+ network_group = json_response['networkGroup']
133
+ if options[:include_fields]
134
+ json_response = {'networkGroup' => filter_data(network_group, options[:include_fields]) }
135
+ end
136
+ if options[:json]
137
+ puts as_json(json_response, options)
138
+ return 0
139
+ elsif options[:yaml]
140
+ puts as_yaml(json_response, options)
141
+ return 0
142
+ elsif options[:csv]
143
+ puts records_as_csv([network_group], options)
144
+ return 0
145
+ end
146
+ print_h1 "Network Group Details"
147
+ print cyan
148
+ description_cols = {
149
+ "ID" => 'id',
150
+ "Name" => 'name',
151
+ "Description" => 'description',
152
+ "Networks" => lambda {|it| it['networks'] ? it['networks'].collect {|it| it['name'] }.uniq.join(', ') : '' },
153
+ "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
154
+ "Tenants" => lambda {|it| it['tenants'] ? it['tenants'].collect {|it| it['name'] }.uniq.join(', ') : '' },
155
+ # "Owner" => lambda {|it| it['owner'] ? it['owner']['name'] : '' },
156
+ }
157
+ print_description_list(description_cols, network_group)
158
+
159
+
160
+ if network_group['resourcePermission'].nil?
161
+ print "\n", "No group access found", "\n"
162
+ else
163
+ print_h2 "Group Access"
164
+ rows = []
165
+ if network_group['resourcePermission']['all']
166
+ rows.push({"name" => 'All'})
167
+ end
168
+ if network_group['resourcePermission']['sites']
169
+ network_group['resourcePermission']['sites'].each do |site|
170
+ rows.push(site)
171
+ end
172
+ end
173
+ rows = rows.collect do |site|
174
+ {group: site['name'], default: site['default'] ? 'Yes' : ''}
175
+ end
176
+ columns = [:group, :default]
177
+ print cyan
178
+ print as_pretty_table(rows, columns)
179
+ end
180
+
181
+ print reset,"\n"
182
+ return 0
183
+ rescue RestClient::Exception => e
184
+ print_rest_exception(e, options)
185
+ return 1
186
+ end
187
+ end
188
+
189
+ def add(args)
190
+ options = {}
191
+ tenants = nil
192
+ group_access_all = nil
193
+ group_access_list = nil
194
+ group_defaults_list = nil
195
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
196
+ opts.banner = subcommand_usage("--networks [id,id,id]")
197
+ opts.on('--name VALUE', String, "Name for this network group") do |val|
198
+ options['name'] = val
199
+ end
200
+ opts.on('--description VALUE', String, "Description of network group") do |val|
201
+ options['description'] = val
202
+ end
203
+ opts.on('--networks LIST', Array, "Networks in the group, comma separated list of network IDs") do |list|
204
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
205
+ options['networks'] = []
206
+ else
207
+ options['networks'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
208
+ end
209
+ end
210
+ opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
211
+ group_access_all = val.to_s == 'on' || val.to_s == 'true'
212
+ end
213
+ opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
214
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
215
+ group_access_list = []
216
+ else
217
+ group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
218
+ end
219
+ end
220
+ opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
221
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
222
+ group_defaults_list = []
223
+ else
224
+ group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
225
+ end
226
+ end
227
+ opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
228
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
229
+ options['tenants'] = []
230
+ else
231
+ options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
232
+ end
233
+ end
234
+ opts.on('--accounts LIST', Array, "alias for --tenants") do |list|
235
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
236
+ options['tenants'] = []
237
+ else
238
+ options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
239
+ end
240
+ end
241
+ opts.on('--visibility [private|public]', String, "Visibility") do |val|
242
+ options['visibility'] = val
243
+ end
244
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
245
+ opts.footer = "Create a new network group." + "\n" +
246
+ "[name] is required and can be passed as --name instead."
247
+ end
248
+ optparse.parse!(args)
249
+ if args.count > 1
250
+ print_error Morpheus::Terminal.angry_prompt
251
+ puts_error "wrong number of arguments, expected 0-1 and got #{args.count}\n#{optparse}"
252
+ return 1
253
+ end
254
+ connect(options)
255
+ begin
256
+ # merge -O options into normally parsed options
257
+ options.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
258
+
259
+ # support [name] as first argument
260
+ if args[0]
261
+ options['name'] = args[0]
262
+ end
263
+
264
+ # construct payload
265
+ payload = nil
266
+ if options[:payload]
267
+ payload = options[:payload]
268
+ else
269
+ # prompt for network options
270
+ payload = {
271
+ 'networkGroup' => {
272
+ # 'config' => {}
273
+ }
274
+ }
275
+
276
+ # allow arbitrary -O options
277
+ payload['networkGroup'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
278
+
279
+ # Name
280
+ if options['name']
281
+ payload['networkGroup']['name'] = options['name']
282
+ else
283
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Name for this network group.'}], options)
284
+ payload['networkGroup']['name'] = v_prompt['name']
285
+ end
286
+
287
+ # Description
288
+ if options['description']
289
+ payload['networkGroup']['description'] = options['description']
290
+ else
291
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false, 'description' => 'Description of network group.'}], options)
292
+ payload['networkGroup']['description'] = v_prompt['description']
293
+ end
294
+
295
+ # Networks
296
+ if options['networks']
297
+ payload['networkGroup']['networks'] = options['networks'].collect {|it| {id: it} }
298
+ else
299
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'networks', 'fieldLabel' => 'Networks', 'type' => 'text', 'required' => true, 'description' => 'Networks in the group, comma separated list of network IDs.'}], options)
300
+ payload['networkGroup']['networks'] = v_prompt['networks'].to_s.split(",").collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq.collect {|it| {id: it} }
301
+ end
302
+
303
+ # Group Access
304
+ if group_access_all != nil
305
+ payload['resourcePermissions'] ||= {}
306
+ payload['resourcePermissions']['all'] = group_access_all
307
+ end
308
+ if group_access_list != nil
309
+ payload['resourcePermissions'] ||= {}
310
+ payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
311
+ site = {"id" => site_id.to_i}
312
+ if group_defaults_list && group_defaults_list.include?(site_id)
313
+ site["default"] = true
314
+ end
315
+ site
316
+ end
317
+ end
318
+
319
+ # Tenants
320
+ if options['tenants']
321
+ payload['tenantPermissions'] = {}
322
+ payload['tenantPermissions']['accounts'] = options['tenants']
323
+ end
324
+
325
+ # Visibility
326
+ if options['visibility'] != nil
327
+ payload['networkGroup']['visibility'] = options['visibility']
328
+ end
329
+
330
+ end
331
+
332
+
333
+ if options[:dry_run]
334
+ print_dry_run @network_groups_interface.dry.create(payload)
335
+ return
336
+ end
337
+ json_response = @network_groups_interface.create(payload)
338
+ if options[:json]
339
+ print JSON.pretty_generate(json_response)
340
+ print "\n"
341
+ elsif !options[:quiet]
342
+ network_group = json_response['networkGroup']
343
+ print_green_success "Added network group #{network_group['name']}"
344
+ get([network_group['id']])
345
+ end
346
+ return 0
347
+ rescue RestClient::Exception => e
348
+ print_rest_exception(e, options)
349
+ exit 1
350
+ end
351
+ end
352
+
353
+ def update(args)
354
+ options = {}
355
+ tenants = nil
356
+ group_access_all = nil
357
+ group_access_list = nil
358
+ group_defaults_list = nil
359
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
360
+ opts.banner = subcommand_usage("[network-group] [options]")
361
+ opts.on('--name VALUE', String, "Name for this network group") do |val|
362
+ options['name'] = val
363
+ end
364
+ opts.on('--description VALUE', String, "Description of network group") do |val|
365
+ options['description'] = val
366
+ end
367
+ opts.on('--networks LIST', Array, "Networks in the group, comma separated list of network IDs") do |list|
368
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
369
+ options['networks'] = []
370
+ else
371
+ options['networks'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
372
+ end
373
+ end
374
+ opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
375
+ group_access_all = val.to_s == 'on' || val.to_s == 'true'
376
+ end
377
+ opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
378
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
379
+ group_access_list = []
380
+ else
381
+ group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
382
+ end
383
+ end
384
+ opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
385
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
386
+ group_defaults_list = []
387
+ else
388
+ group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
389
+ end
390
+ end
391
+ opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
392
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
393
+ options['tenants'] = []
394
+ else
395
+ options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
396
+ end
397
+ end
398
+ opts.on('--accounts LIST', Array, "alias for --tenants") do |list|
399
+ if list.size == 1 && list[0] == 'null' # hacky way to clear it
400
+ options['tenants'] = []
401
+ else
402
+ options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
403
+ end
404
+ end
405
+ opts.on('--visibility [private|public]', String, "Visibility") do |val|
406
+ options['visibility'] = val
407
+ end
408
+ build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
409
+ opts.footer = "Update a network group." + "\n" +
410
+ "[network-group] is required. This is the id of a network group."
411
+ end
412
+ optparse.parse!(args)
413
+ if args.count != 1
414
+ print_error Morpheus::Terminal.angry_prompt
415
+ puts_error "wrong number of arguments, expected 1 and got #{args.count}\n#{optparse}"
416
+ return 1
417
+ end
418
+ connect(options)
419
+
420
+ begin
421
+ network_group = find_network_group_by_name_or_id(args[0])
422
+ return 1 if network_group.nil?
423
+
424
+ # merge -O options into normally parsed options
425
+ options.deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
426
+
427
+ # construct payload
428
+ payload = nil
429
+ if options[:payload]
430
+ payload = options[:payload]
431
+ else
432
+ # prompt for network options
433
+ payload = {
434
+ 'networkGroup' => {
435
+ }
436
+ }
437
+
438
+ # allow arbitrary -O options
439
+ payload['networkGroup'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) }) if options[:options]
440
+
441
+ # Name
442
+ if options['name']
443
+ payload['networkGroup']['name'] = options['name']
444
+ else
445
+ # v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Name for this network group.'}], options)
446
+ # payload['networkGroup']['name'] = v_prompt['name']
447
+ end
448
+
449
+ # Description
450
+ if options['description']
451
+ payload['networkGroup']['description'] = options['description']
452
+ else
453
+ # v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false, 'description' => 'Description of network group.'}], options)
454
+ # payload['networkGroup']['description'] = v_prompt['description']
455
+ end
456
+
457
+ # Networks
458
+ if options['networks']
459
+ payload['networkGroup']['networks'] = options['networks'].collect {|it| {id: it} }
460
+ else
461
+ # v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'networks', 'fieldLabel' => 'Networks', 'type' => 'text', 'required' => true, 'description' => 'Networks in the group, comma separated list of network IDs.'}], options)
462
+ # payload['networkGroup']['networks'] = v_prompt['networks'].to_s.split(",").collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq.collect {|it| {id: it} }
463
+ end
464
+
465
+ # Group Access
466
+ if group_access_all != nil
467
+ payload['resourcePermissions'] ||= {}
468
+ payload['resourcePermissions']['all'] = group_access_all
469
+ end
470
+ if group_access_list != nil
471
+ payload['resourcePermissions'] ||= {}
472
+ payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
473
+ site = {"id" => site_id.to_i}
474
+ if group_defaults_list && group_defaults_list.include?(site_id)
475
+ site["default"] = true
476
+ end
477
+ site
478
+ end
479
+ end
480
+
481
+ # Tenants
482
+ if options['tenants']
483
+ payload['tenantPermissions'] = {}
484
+ payload['tenantPermissions']['accounts'] = options['tenants']
485
+ end
486
+
487
+ # Visibility
488
+ if options['visibility'] != nil
489
+ payload['networkGroup']['visibility'] = options['visibility']
490
+ end
491
+
492
+ end
493
+
494
+ if options[:dry_run]
495
+ print_dry_run @network_groups_interface.dry.update(network_group["id"], payload)
496
+ return
497
+ end
498
+ json_response = @network_groups_interface.update(network_group["id"], payload)
499
+ if options[:json]
500
+ puts as_json(json_response)
501
+ else
502
+ network_group = json_response['networkGroup']
503
+ print_green_success "Updated network group #{network_group['name']}"
504
+ get([network_group['id']])
505
+ end
506
+ return 0
507
+ rescue RestClient::Exception => e
508
+ print_rest_exception(e, options)
509
+ return 1
510
+ end
511
+ end
512
+
513
+ def remove(args)
514
+ options = {}
515
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
516
+ opts.banner = subcommand_usage("[network-group]")
517
+ build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run, :remote])
518
+ opts.footer = "Delete a network group." + "\n" +
519
+ "[network-group] is required. This is the name or id of a network group."
520
+ end
521
+ optparse.parse!(args)
522
+
523
+ if args.count < 1
524
+ print_error Morpheus::Terminal.angry_prompt
525
+ puts_error "#{command_name} missing argument: [network-group]\n#{optparse}"
526
+ return 1
527
+ end
528
+
529
+ connect(options)
530
+ begin
531
+ network_group = find_network_group_by_name_or_id(args[0])
532
+ return 1 if network_group.nil?
533
+
534
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the network group: #{network_group['name']}?")
535
+ return 9, "aborted command"
536
+ end
537
+ if options[:dry_run]
538
+ print_dry_run @network_groups_interface.dry.destroy(network_group['id'])
539
+ return 0
540
+ end
541
+ json_response = @network_groups_interface.destroy(network_group['id'])
542
+ if options[:json]
543
+ print JSON.pretty_generate(json_response)
544
+ print "\n"
545
+ else
546
+ print_green_success "Removed network group #{network_group['name']}"
547
+ # list([])
548
+ end
549
+ return 0
550
+ rescue RestClient::Exception => e
551
+ print_rest_exception(e, options)
552
+ return 1
553
+ end
554
+ end
555
+
556
+ private
557
+
558
+
559
+ def find_network_group_by_name_or_id(val)
560
+ if val.to_s =~ /\A\d{1,}\Z/
561
+ return find_network_group_by_id(val)
562
+ else
563
+ return find_network_group_by_name(val)
564
+ end
565
+ end
566
+
567
+ def find_network_group_by_id(id)
568
+ begin
569
+ json_response = @network_groups_interface.get(id.to_i)
570
+ return json_response['networkGroup']
571
+ rescue RestClient::Exception => e
572
+ if e.response && e.response.code == 404
573
+ print_red_alert "Network Group not found by id #{id}"
574
+ return nil
575
+ else
576
+ raise e
577
+ end
578
+ end
579
+ end
580
+
581
+ def find_network_group_by_name(name)
582
+ json_response = @network_groups_interface.list({name: name.to_s})
583
+ network_groups = json_response['networkGroups']
584
+ if network_groups.empty?
585
+ print_red_alert "Network Group not found by name #{name}"
586
+ return nil
587
+ elsif network_groups.size > 1
588
+ print_red_alert "#{network_groups.size} network groups found by name #{name}"
589
+ # print_networks_table(networks, {color: red})
590
+ rows = network_groups.collect do |network_group|
591
+ {id: it['id'], name: it['name']}
592
+ end
593
+ print red
594
+ tp rows, [:id, :name]
595
+ print reset,"\n"
596
+ return nil
597
+ else
598
+ return network_groups[0]
599
+ end
600
+ end
601
+
602
+ end