morpheus-cli 4.1.14 → 4.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +4 -0
  4. data/lib/morpheus/api/library_container_types_interface.rb +1 -1
  5. data/lib/morpheus/api/library_instance_types_interface.rb +7 -7
  6. data/lib/morpheus/api/library_layouts_interface.rb +1 -1
  7. data/lib/morpheus/api/network_routers_interface.rb +101 -0
  8. data/lib/morpheus/api/tasks_interface.rb +12 -14
  9. data/lib/morpheus/cli.rb +1 -0
  10. data/lib/morpheus/cli/apps.rb +15 -12
  11. data/lib/morpheus/cli/cli_command.rb +40 -2
  12. data/lib/morpheus/cli/clusters.rb +13 -7
  13. data/lib/morpheus/cli/cypher_command.rb +5 -2
  14. data/lib/morpheus/cli/hosts.rb +1 -1
  15. data/lib/morpheus/cli/instances.rb +21 -5
  16. data/lib/morpheus/cli/jobs_command.rb +83 -27
  17. data/lib/morpheus/cli/library_cluster_layouts_command.rb +12 -12
  18. data/lib/morpheus/cli/library_container_scripts_command.rb +52 -40
  19. data/lib/morpheus/cli/library_container_types_command.rb +2 -60
  20. data/lib/morpheus/cli/library_instance_types_command.rb +22 -1
  21. data/lib/morpheus/cli/library_layouts_command.rb +65 -65
  22. data/lib/morpheus/cli/library_option_lists_command.rb +72 -59
  23. data/lib/morpheus/cli/library_option_types_command.rb +30 -186
  24. data/lib/morpheus/cli/library_spec_templates_command.rb +39 -64
  25. data/lib/morpheus/cli/mixins/library_helper.rb +213 -0
  26. data/lib/morpheus/cli/mixins/provisioning_helper.rb +89 -37
  27. data/lib/morpheus/cli/mixins/whoami_helper.rb +16 -1
  28. data/lib/morpheus/cli/network_routers_command.rb +1281 -0
  29. data/lib/morpheus/cli/networks_command.rb +164 -72
  30. data/lib/morpheus/cli/option_types.rb +187 -73
  31. data/lib/morpheus/cli/price_sets_command.rb +4 -4
  32. data/lib/morpheus/cli/prices_command.rb +15 -15
  33. data/lib/morpheus/cli/remote.rb +3 -3
  34. data/lib/morpheus/cli/service_plans_command.rb +17 -8
  35. data/lib/morpheus/cli/tasks.rb +437 -169
  36. data/lib/morpheus/cli/version.rb +1 -1
  37. data/lib/morpheus/formatters.rb +8 -0
  38. metadata +6 -3
@@ -6,6 +6,7 @@ require 'morpheus/cli/mixins/infrastructure_helper'
6
6
 
7
7
  class Morpheus::Cli::NetworksCommand
8
8
  include Morpheus::Cli::CliCommand
9
+ include Morpheus::Cli::WhoamiHelper
9
10
  include Morpheus::Cli::InfrastructureHelper
10
11
 
11
12
  set_command_name :networks
@@ -26,6 +27,7 @@ class Morpheus::Cli::NetworksCommand
26
27
  @network_types_interface = @api_client.network_types
27
28
  @subnets_interface = @api_client.subnets
28
29
  @subnet_types_interface = @api_client.subnet_types
30
+ @groups_interface = @api_client.groups
29
31
  @clouds_interface = @api_client.clouds
30
32
  @options_interface = @api_client.options
31
33
  end
@@ -97,6 +99,7 @@ class Morpheus::Cli::NetworksCommand
97
99
  id: network['id'],
98
100
  name: network['name'],
99
101
  type: network['type'] ? network['type']['name'] : '',
102
+ group: network['group'] ? network['group']['name'] : 'Shared',
100
103
  cloud: network['zone'] ? network['zone']['name'] : '',
101
104
  cidr: network['cidr'],
102
105
  pool: network['pool'] ? network['pool']['name'] : '',
@@ -114,6 +117,7 @@ class Morpheus::Cli::NetworksCommand
114
117
  name: " #{subnet['name']}",
115
118
  # type: subnet['type'] ? subnet['type']['name'] : '',
116
119
  type: "Subnet",
120
+ group: network['group'] ? network['group']['name'] : 'Shared',
117
121
  cloud: network['zone'] ? network['zone']['name'] : '',
118
122
  cidr: subnet['cidr'],
119
123
  pool: subnet['pool'] ? subnet['pool']['name'] : '',
@@ -126,7 +130,7 @@ class Morpheus::Cli::NetworksCommand
126
130
  end
127
131
  end
128
132
  end
129
- columns = [:id, :name, :type, :cloud, :cidr, :pool, :dhcp, :subnets, :active, :visibility, :tenants]
133
+ columns = [:id, :name, :type, :group, :cloud, :cidr, :pool, :dhcp, :subnets, :active, :visibility, :tenants]
130
134
  if options[:include_fields]
131
135
  columns = options[:include_fields]
132
136
  end
@@ -190,6 +194,7 @@ class Morpheus::Cli::NetworksCommand
190
194
  "Name" => 'name',
191
195
  "Description" => 'description',
192
196
  "Type" => lambda {|it| it['type'] ? it['type']['name'] : '' },
197
+ "Group" => lambda {|it| it['group'] ? it['group']['name'] : 'Shared' },
193
198
  "Cloud" => lambda {|it| it['zone'] ? it['zone']['name'] : '' },
194
199
  "CIDR" => 'cidr',
195
200
  "Gateway" => 'gateway',
@@ -207,7 +212,9 @@ class Morpheus::Cli::NetworksCommand
207
212
  }
208
213
  print_description_list(description_cols, network)
209
214
 
210
- if network['resourcePermission'].nil?
215
+ if network["group"]
216
+ # Group Access is n/a unless network is_a? Shared (no group)
217
+ elsif network['resourcePermission'].nil?
211
218
  print "\n", "No group access found", "\n"
212
219
  else
213
220
  print_h2 "Group Access"
@@ -272,6 +279,9 @@ class Morpheus::Cli::NetworksCommand
272
279
  group_defaults_list = nil
273
280
  optparse = Morpheus::Cli::OptionParser.new do |opts|
274
281
  opts.banner = subcommand_usage("-t TYPE")
282
+ opts.on( '-g', '--group GROUP', "Group Name or ID. Default is Shared." ) do |val|
283
+ options[:group] = val
284
+ end
275
285
  opts.on( '-c', '--cloud CLOUD', "Cloud Name or ID" ) do |val|
276
286
  options[:cloud] = val
277
287
  end
@@ -411,19 +421,79 @@ class Morpheus::Cli::NetworksCommand
411
421
  payload['network']['description'] = v_prompt['description']
412
422
  end
413
423
 
424
+ # Group
425
+ # ok, networks list needs to know if they have full or groups permission
426
+ group = nil
427
+ groups_dropdown = nil
428
+ group_is_required = true
429
+ network_perm = (current_user_permissions || []).find {|perm| perm['code'] == 'infrastructure-networks'}
430
+ if network_perm && ['full','read'].include?(network_perm['access'])
431
+ group_is_required = false
432
+ groups_dropdown = get_available_groups_with_shared
433
+ else
434
+ # they have group access, shared cannot be selected.
435
+ groups_dropdown = get_available_groups
436
+ end
437
+ if options[:group]
438
+ group_id = options[:group]
439
+ group = groups_dropdown.find {|it| it["value"].to_s == group_id.to_s || it["name"].to_s == group_id}
440
+ if group.nil?
441
+ print_red_alert "Group not found by id #{group_id}"
442
+ return 1
443
+ end
444
+ if group_id.to_s == 'shared'
445
+ group_id = nil
446
+ group = nil
447
+ end
448
+ else
449
+ group_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'group', 'type' => 'select', 'fieldLabel' => 'Group', 'selectOptions' => groups_dropdown, 'required' => group_is_required, 'description' => 'Select Group.'}],options,@api_client,{})
450
+ group_id = group_prompt['group']
451
+ if group_id.to_s == '' || group_id.to_s == 'shared'
452
+ group_id = nil
453
+ group = nil
454
+ else
455
+ group = groups_dropdown.find {|it| it["value"].to_s == group_id.to_s || it["name"].to_s == group_id}
456
+ if group.nil?
457
+ print_red_alert "Group not found by id #{group_id}"
458
+ return 1
459
+ end
460
+ end
461
+ end
462
+ if group
463
+ payload['network']['site'] = {'id' => group['id']}
464
+ else
465
+ # shared
466
+ end
467
+
414
468
  # Cloud
415
469
  cloud = nil
416
- if options[:cloud]
417
- cloud = find_cloud_by_name_or_id(options[:cloud])
418
- # meh, should validate cloud is in the cloudsForNetworks dropdown..
419
- return 1 if cloud.nil?
470
+ if group
471
+ if options[:cloud]
472
+ cloud_id = options[:cloud]
473
+ cloud = group["clouds"].find {|it| it["id"].to_s == cloud_id.to_s || it["name"].to_s == cloud_id}
474
+ if cloud.nil?
475
+ print_red_alert "Cloud not found by id #{cloud_id}"
476
+ return 1
477
+ end
478
+ else
479
+ api_params = {groupId:group['id']}
480
+ cloud_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'type' => 'select', 'fieldLabel' => 'Cloud', 'optionSource' => 'cloudsForNetworks', 'required' => true, 'description' => 'Select Cloud.'}],options,@api_client,api_params)
481
+ cloud_id = cloud_prompt['cloud']
482
+ cloud = find_cloud_by_name_or_id(cloud_id) if cloud_id
483
+ return 1 if cloud.nil?
484
+ end
420
485
  else
421
- # print_red_alert "Cloud not specified!"
422
- # exit 1
423
- cloud_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'type' => 'select', 'fieldLabel' => 'Cloud', 'optionSource' => 'cloudsForNetworks', 'required' => true, 'description' => 'Select Cloud.'}],options,@api_client,{})
424
- cloud_id = cloud_prompt['cloud']
425
- cloud = find_cloud_by_name_or_id(cloud_id) if cloud_id
426
- return 1 if cloud.nil?
486
+ if options[:cloud]
487
+ cloud = find_cloud_by_name_or_id(options[:cloud])
488
+ # meh, should validate cloud is in the cloudsForNetworks dropdown..
489
+ return 1 if cloud.nil?
490
+ else
491
+ api_params = {}
492
+ cloud_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'type' => 'select', 'fieldLabel' => 'Cloud', 'optionSource' => 'cloudsForNetworks', 'required' => true, 'description' => 'Select Cloud.'}],options,@api_client,api_params)
493
+ cloud_id = cloud_prompt['cloud']
494
+ cloud = find_cloud_by_name_or_id(cloud_id) if cloud_id
495
+ return 1 if cloud.nil?
496
+ end
427
497
  end
428
498
  payload['network']['zone'] = {'id' => cloud['id']}
429
499
 
@@ -446,83 +516,84 @@ class Morpheus::Cli::NetworksCommand
446
516
  print_red_alert "Network Type not found by id '#{network_type_id}'"
447
517
  return 1
448
518
  end
449
- network_type_option_types = network_type['optionTypes']
450
- if network_type_option_types && network_type_option_types.size > 0
451
- # prompt for option types
452
- # JD: 3.6.2 has fieldContext: 'domain' , which is wrong
453
- network_type_option_types.each do |option_type|
454
- # if option_type['fieldContext'] == 'domain'
455
- # option_type['fieldContext'] = 'network'
456
- # end
457
- #option_type['fieldContext'] = nil
458
- end
459
- network_type_params = Morpheus::Cli::OptionTypes.prompt(network_type_option_types,options[:options],@api_client, {zoneId: cloud['id']})
460
- # network context options belong at network level and not network.network
461
- network_context_params = network_type_params.delete('network')
462
- payload['network'].deep_merge!(network_context_params) if network_context_params
463
- payload['network'].deep_merge!(network_type_params)
464
-
465
- #todo: special handling of type: 'aciVxlan'
466
519
 
520
+ # CIDR
521
+ if options['cidr']
522
+ payload['network']['cidr'] = options['cidr']
467
523
  else
468
- # DEFAULT INPUTS
524
+ #if network_type['cidrEditable']
525
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cidr', 'fieldLabel' => 'CIDR', 'type' => 'text', 'required' => network_type['cidrRequired'], 'description' => ''}], options)
526
+ payload['network']['cidr'] = v_prompt['cidr']
527
+ #end
528
+ end
469
529
 
470
- # Gateway
471
- if options['gateway']
472
- payload['network']['gateway'] = options['gateway']
473
- else
530
+ # Gateway
531
+ if options['gateway']
532
+ payload['network']['gateway'] = options['gateway']
533
+ else
534
+ if network_type['gatewayEditable']
474
535
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'gateway', 'fieldLabel' => 'Gateway', 'type' => 'text', 'required' => false, 'description' => ''}], options)
475
536
  payload['network']['gateway'] = v_prompt['gateway']
476
537
  end
538
+ end
477
539
 
478
- # DNS Primary
479
- if options['dnsPrimary']
480
- payload['network']['dnsPrimary'] = options['dnsPrimary']
481
- else
540
+ # DNS Primary
541
+ if options['dnsPrimary']
542
+ payload['network']['dnsPrimary'] = options['dnsPrimary']
543
+ else
544
+ if network_type['dnsEditable']
482
545
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'dnsPrimary', 'fieldLabel' => 'DNS Primary', 'type' => 'text', 'required' => false, 'description' => ''}], options)
483
546
  payload['network']['dnsPrimary'] = v_prompt['dnsPrimary']
484
547
  end
548
+ end
485
549
 
486
- # DNS Secondary
487
- if options['dnsSecondary']
488
- payload['network']['dnsSecondary'] = options['dnsSecondary']
489
- else
550
+ # DNS Secondary
551
+ if options['dnsSecondary']
552
+ payload['network']['dnsSecondary'] = options['dnsSecondary']
553
+ else
554
+ if network_type['dnsEditable']
490
555
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'dnsSecondary', 'fieldLabel' => 'DNS Secondary', 'type' => 'text', 'required' => false, 'description' => ''}], options)
491
556
  payload['network']['dnsSecondary'] = v_prompt['dnsSecondary']
492
557
  end
558
+ end
493
559
 
494
- # CIDR
495
- if options['cidr']
496
- payload['network']['cidr'] = options['cidr']
497
- else
498
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cidr', 'fieldLabel' => 'CIDR', 'type' => 'text', 'required' => false, 'description' => ''}], options)
499
- payload['network']['cidr'] = v_prompt['cidr']
500
- end
501
-
502
- # VLAN ID
503
- if options['vlanId']
504
- payload['network']['vlanId'] = options['vlanId']
505
- else
560
+ # VLAN ID
561
+ if options['vlanId']
562
+ payload['network']['vlanId'] = options['vlanId']
563
+ else
564
+ if network_type['vlanEditable']
506
565
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'vlanId', 'fieldLabel' => 'VLAN ID', 'type' => 'number', 'required' => false, 'description' => ''}], options)
507
566
  payload['network']['vlanId'] = v_prompt['vlanId']
508
567
  end
568
+ end
509
569
 
510
- # DHCP Server
511
- if options['dhcpServer'] != nil
512
- payload['network']['dhcpServer'] = options['dhcpServer']
513
- else
570
+ # prompt for option types
571
+ network_type_option_types = network_type['optionTypes']
572
+ if network_type_option_types && network_type_option_types.size > 0
573
+ network_type_params = Morpheus::Cli::OptionTypes.prompt(network_type_option_types,options[:options],@api_client, {zoneId: cloud['id']})
574
+ # network context options belong at network level and not network.network
575
+ network_context_params = network_type_params.delete('network')
576
+ payload['network'].deep_merge!(network_context_params) if network_context_params
577
+ payload['network'].deep_merge!(network_type_params)
578
+
579
+ end
580
+
581
+ # DHCP Server
582
+ if options['dhcpServer'] != nil
583
+ payload['network']['dhcpServer'] = options['dhcpServer']
584
+ else
585
+ if network_type['dhcpServerEditable']
514
586
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'dhcpServer', 'fieldLabel' => 'DHCP Server', 'type' => 'checkbox', 'required' => false, 'description' => ''}], options)
515
587
  payload['network']['dhcpServer'] = v_prompt['dhcpServer']
516
588
  end
589
+ end
517
590
 
518
- # Allow IP Override
519
- if options['allowStaticOverride'] != nil
520
- payload['network']['allowStaticOverride'] = options['allowStaticOverride']
521
- else
522
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'allowStaticOverride', 'fieldLabel' => 'Allow IP Override', 'type' => 'checkbox', 'required' => false, 'description' => ''}], options)
523
- payload['network']['allowStaticOverride'] = v_prompt['allowStaticOverride']
524
- end
525
-
591
+ # Allow IP Override
592
+ if options['allowStaticOverride'] != nil
593
+ payload['network']['allowStaticOverride'] = options['allowStaticOverride']
594
+ else
595
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'allowStaticOverride', 'fieldLabel' => 'Allow IP Override', 'type' => 'checkbox', 'required' => false, 'description' => ''}], options)
596
+ payload['network']['allowStaticOverride'] = v_prompt['allowStaticOverride']
526
597
  end
527
598
 
528
599
  ## IPAM Options
@@ -531,10 +602,12 @@ class Morpheus::Cli::NetworksCommand
531
602
  if options['pool']
532
603
  payload['network']['pool'] = options['pool'].to_i
533
604
  else
534
- # todo: select dropdown
535
- # v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'pool', 'fieldLabel' => 'Network Pool', 'type' => 'select', 'optionSource' => 'networkPools', 'required' => false, 'description' => ''}], options, @api_client, {zoneId: cloud['id']})
536
- v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'pool', 'fieldLabel' => 'Network Pool', 'type' => 'text', 'required' => false, 'description' => ''}], options)
537
- payload['network']['pool'] = v_prompt['pool'].to_i if v_prompt['pool']
605
+ if network_type['canAssignPool']
606
+ # todo: select dropdown
607
+ # v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'pool', 'fieldLabel' => 'Network Pool', 'type' => 'select', 'optionSource' => 'networkPools', 'required' => false, 'description' => ''}], options, @api_client, {zoneId: cloud['id']})
608
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'pool', 'fieldLabel' => 'Network Pool', 'type' => 'text', 'required' => false, 'description' => ''}], options)
609
+ payload['network']['pool'] = v_prompt['pool'].to_i if v_prompt['pool']
610
+ end
538
611
  end
539
612
 
540
613
  ## Advanced Options
@@ -976,9 +1049,13 @@ class Morpheus::Cli::NetworksCommand
976
1049
 
977
1050
  def remove(args)
978
1051
  options = {}
1052
+ params = {}
979
1053
  optparse = Morpheus::Cli::OptionParser.new do |opts|
980
1054
  opts.banner = subcommand_usage("[network]")
981
1055
  build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run, :remote])
1056
+ opts.on( '-f', '--force', "Force Delete" ) do
1057
+ params[:force] = 'true'
1058
+ end
982
1059
  opts.footer = "Delete a network." + "\n" +
983
1060
  "[network] is required. This is the name or id of a network."
984
1061
  end
@@ -1000,10 +1077,10 @@ class Morpheus::Cli::NetworksCommand
1000
1077
  end
1001
1078
  @networks_interface.setopts(options)
1002
1079
  if options[:dry_run]
1003
- print_dry_run @networks_interface.dry.destroy(network['id'])
1080
+ print_dry_run @networks_interface.dry.destroy(network['id'], params)
1004
1081
  return 0
1005
1082
  end
1006
- json_response = @networks_interface.destroy(network['id'])
1083
+ json_response = @networks_interface.destroy(network['id'], params)
1007
1084
  if options[:json]
1008
1085
  print JSON.pretty_generate(json_response)
1009
1086
  print "\n"
@@ -1223,4 +1300,19 @@ class Morpheus::Cli::NetworksCommand
1223
1300
 
1224
1301
  private
1225
1302
 
1303
+ def get_available_groups(refresh=false)
1304
+ if !@available_groups || refresh
1305
+ option_results = @options_interface.options_for_source('groups',{})
1306
+ @available_groups = option_results['data'].collect {|it|
1307
+ {"id" => it["value"], "name" => it["name"], "value" => it["value"]}
1308
+ }
1309
+ end
1310
+ #puts "get_available_groups() rtn: #{@available_groups.inspect}"
1311
+ return @available_groups
1312
+ end
1313
+
1314
+ def get_available_groups_with_shared(refresh=false)
1315
+ [{"id" => 'shared', "name" => 'Shared', "value" => 'shared'}] + get_available_groups(refresh)
1316
+ end
1317
+
1226
1318
  end
@@ -39,13 +39,18 @@ module Morpheus
39
39
  paging_enabled = false if Morpheus::Cli.windows?
40
40
  results = {}
41
41
  options = options || {}
42
+ # inject cli only stuff into option_types (should clone() here)
43
+ option_types.each do |option_type|
44
+ if options[:help_field_prefix]
45
+ option_type[:help_field_prefix] = options[:help_field_prefix]
46
+ end
47
+ end
42
48
  # puts "Options Prompt #{options}"
43
49
  option_types.sort { |x,y| x['displayOrder'].to_i <=> y['displayOrder'].to_i }.each do |option_type|
44
50
  context_map = results
45
51
  value = nil
46
52
  value_found=false
47
53
 
48
-
49
54
  # How about this instead?
50
55
  # option_type = option_type.clone
51
56
  # field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
@@ -56,7 +61,10 @@ module Morpheus
56
61
  # end
57
62
  # end
58
63
 
64
+ # allow for mapping of domain to relevant type: domain.zone => router.zone
65
+ option_type['fieldContext'] = (options[:context_map] || {})[option_type['fieldContext']] || option_type['fieldContext']
59
66
  field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
67
+ help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
60
68
  namespaces = field_key.split(".")
61
69
  field_name = namespaces.pop
62
70
 
@@ -78,33 +86,38 @@ module Morpheus
78
86
  end
79
87
  end
80
88
 
81
-
82
89
  cur_namespace = options
90
+ parent_context_map = context_map
91
+ parent_ns = field_name
83
92
 
84
93
  namespaces.each do |ns|
85
94
  next if ns.empty?
95
+ parent_context_map = context_map
96
+ parent_ns = ns
86
97
  cur_namespace[ns.to_s] ||= {}
87
98
  cur_namespace = cur_namespace[ns.to_s]
88
99
  context_map[ns.to_s] ||= {}
89
100
  context_map = context_map[ns.to_s]
90
101
  end
102
+
91
103
  # use the value passed in the options map
92
- if cur_namespace.key?(field_name)
104
+ if cur_namespace.respond_to?('key?') && cur_namespace.key?(field_name)
93
105
  value = cur_namespace[field_name]
106
+ input_value = ['select', 'multiSelect'].include?(option_type['type']) && option_type['fieldInput'] ? cur_namespace[option_type['fieldInput']] : nil
94
107
  if option_type['type'] == 'number'
95
108
  value = value.to_s.include?('.') ? value.to_f : value.to_i
96
- elsif option_type['type'] == 'select'
109
+ elsif ['select', 'multiSelect'].include?(option_type['type'])
97
110
  # this should just fall down through below, with the extra params no_prompt, use_value
98
- value = select_prompt(option_type.merge({'defaultValue' => value}), api_client, (api_params || {}).merge(results), true)
111
+ value = select_prompt(option_type.merge({'defaultValue' => value, 'defaultInputValue' => input_value}), api_client, (api_params || {}).merge(results), true)
99
112
  end
100
113
  if options[:always_prompt] != true
101
114
  value_found = true
102
115
  end
103
116
  end
104
-
105
- # set the value that has been passed to the option type default value: options[fieldContext.fieldName]
117
+
118
+ # set the value that has been passed to the option type default value
106
119
  if value != nil # && value != ''
107
- option_type = option_type.clone
120
+ option_type = option_type.clone
108
121
  option_type['defaultValue'] = value
109
122
  end
110
123
  # no_prompt means skip prompting and instead
@@ -119,14 +132,14 @@ module Morpheus
119
132
  if !value_found
120
133
  # select type is special because it supports skipSingleOption
121
134
  # and prints the available options on error
122
- if option_type['type'] == 'select'
123
- value = select_prompt(option_type.merge({'defaultValue' => value}), api_client, (api_params || {}).merge(results), true)
135
+ if ['select', 'multiSelect'].include?(option_type['type'])
136
+ value = select_prompt(option_type, api_client, (api_params || {}).merge(results), true)
124
137
  value_found = !!value
125
138
  end
126
139
  if !value_found
127
140
  if option_type['required']
128
141
  print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
129
- print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
142
+ print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
130
143
  print "\n"
131
144
  exit 1
132
145
  else
@@ -150,7 +163,7 @@ module Morpheus
150
163
  value = multiline_prompt(option_type)
151
164
  elsif option_type['type'] == 'code-editor'
152
165
  value = multiline_prompt(option_type)
153
- elsif option_type['type'] == 'select'
166
+ elsif ['select', 'multiSelect'].include?(option_type['type'])
154
167
  # so, the /api/options/source is may need ALL the previously
155
168
  # selected values that are being accumulated in options
156
169
  # api_params is just extra params to always send
@@ -158,70 +171,87 @@ module Morpheus
158
171
  # api_params = api_params.merge(options) # this might be good enough
159
172
  # dup it
160
173
  value = select_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
161
- elsif option_type['type'] == 'hidden'
162
- value = option_type['defaultValue']
163
- input = value
164
- elsif option_type['type'] == 'file'
165
- value = file_prompt(option_type)
174
+ if value && option_type['type'] == 'multiSelect'
175
+ value = [value]
176
+ while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
177
+ if addn_value = select_prompt(option_type, api_client, (api_params || {}).merge(results), options[:no_prompt], nil, paging_enabled)
178
+ value << addn_value
179
+ else
180
+ break
181
+ end
182
+ end
183
+ end
184
+ elsif option_type['type'] == 'hidden'
185
+ value = option_type['defaultValue']
186
+ input = value
187
+ elsif option_type['type'] == 'file'
188
+ value = file_prompt(option_type)
189
+ elsif option_type['type'] == 'file-content'
190
+ value = file_content_prompt(option_type, options, api_client, {})
191
+ else
192
+ value = generic_prompt(option_type)
193
+ end
194
+ end
195
+
196
+ if option_type['type'] == 'multiSelect'
197
+ value = [value] if !value.nil? && !value.is_a?(Array)
198
+ parent_context_map[parent_ns] = value
166
199
  else
167
- value = generic_prompt(option_type)
200
+ context_map[field_name] = value
168
201
  end
169
202
  end
170
- context_map[field_name] = value
203
+ results
171
204
  end
172
205
 
173
- return results
174
- end
175
-
176
- def self.grails_params(data, context=nil)
177
- params = {}
178
- data.each do |k,v|
179
- if v.is_a?(Hash)
180
- params.merge!(grails_params(v, context ? "#{context}.#{k.to_s}" : k))
181
- else
182
- if context
183
- params["#{context}.#{k.to_s}"] = v
206
+ def self.grails_params(data, context=nil)
207
+ params = {}
208
+ data.each do |k,v|
209
+ if v.is_a?(Hash)
210
+ params.merge!(grails_params(v, context ? "#{context}.#{k.to_s}" : k))
184
211
  else
185
- params[k.to_s] = v
212
+ if context
213
+ params["#{context}.#{k.to_s}"] = v
214
+ else
215
+ params[k.to_s] = v
216
+ end
186
217
  end
187
218
  end
219
+ return params
188
220
  end
189
- return params
190
- end
191
221
 
192
- def self.radio_prompt(option_type)
193
- value_found = false
194
- value = nil
195
- options = []
196
- if option_type['config'] and option_type['config']['radioOptions']
197
- option_type['config']['radioOptions'].each do |radio_option|
198
- options << {key: radio_option['key'], checked: radio_option['checked']}
199
- end
200
- end
201
- optionString = options.collect{ |b| b[:checked] ? "(#{b[:key]})" : b[:key]}.join(', ')
202
- while !value_found do
203
- print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }[#{optionString}]: "
204
- input = $stdin.gets.chomp!
205
- if input == '?'
206
- help_prompt(option_type)
207
- else
208
- if input.nil? || input.empty?
209
- selectedOption = options.find{|o| o[:checked] == true}
210
- else
211
- selectedOption = options.find{|o| o[:key].downcase == input.downcase}
222
+ def self.radio_prompt(option_type)
223
+ value_found = false
224
+ value = nil
225
+ options = []
226
+ if option_type['config'] and option_type['config']['radioOptions']
227
+ option_type['config']['radioOptions'].each do |radio_option|
228
+ options << {key: radio_option['key'], checked: radio_option['checked']}
212
229
  end
213
- if selectedOption
214
- value = selectedOption[:key]
230
+ end
231
+ optionString = options.collect{ |b| b[:checked] ? "(#{b[:key]})" : b[:key]}.join(', ')
232
+ while !value_found do
233
+ print "#{option_type['fieldLabel']}#{option_type['fieldAddOn'] ? ('(' + option_type['fieldAddOn'] + ') ') : '' }[#{optionString}]: "
234
+ input = $stdin.gets.chomp!
235
+ if input == '?'
236
+ help_prompt(option_type)
215
237
  else
216
- puts "Invalid Option. Please select from #{optionString}."
217
- end
218
- if !value.nil? || option_type['required'] != true
219
- value_found = true
238
+ if input.nil? || input.empty?
239
+ selectedOption = options.find{|o| o[:checked] == true}
240
+ else
241
+ selectedOption = options.find{|o| o[:key].downcase == input.downcase}
242
+ end
243
+ if selectedOption
244
+ value = selectedOption[:key]
245
+ else
246
+ puts "Invalid Option. Please select from #{optionString}."
247
+ end
248
+ if !value.nil? || option_type['required'] != true
249
+ value_found = true
250
+ end
220
251
  end
221
252
  end
253
+ return value
222
254
  end
223
- return value
224
- end
225
255
 
226
256
  def self.number_prompt(option_type)
227
257
  value_found = false
@@ -251,10 +281,13 @@ module Morpheus
251
281
 
252
282
  def self.select_prompt(option_type,api_client, api_params={}, no_prompt=false, use_value=nil, paging_enabled=false)
253
283
  paging_enabled = false if Morpheus::Cli.windows?
284
+ field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
285
+ help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
254
286
  value_found = false
255
287
  value = nil
256
288
  value_field = (option_type['config'] ? option_type['config']['valueField'] : nil) || 'value'
257
289
  default_value = option_type['defaultValue']
290
+ default_value = default_value['id'] if default_value && default_value.is_a?(Hash) && !default_value['id'].nil?
258
291
  # local array of options
259
292
  if option_type['selectOptions']
260
293
  # calculate from inline lambda
@@ -330,7 +363,7 @@ module Morpheus
330
363
  value = select_options[0][value_field]
331
364
  elsif option_type['required']
332
365
  print Term::ANSIColor.red, "\nMissing Required Option\n\n", Term::ANSIColor.reset
333
- print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{option_type['fieldContext'] ? (option_type['fieldContext']+'.') : ''}#{option_type['fieldName']}=] - #{option_type['description']}\n", Term::ANSIColor.reset
366
+ print Term::ANSIColor.red, " * #{option_type['fieldLabel']} [-O #{help_field_key}=] - #{option_type['description']}\n", Term::ANSIColor.reset
334
367
  if select_options && select_options.size > 10
335
368
  display_select_options(option_type, select_options.first(10))
336
369
  puts " (#{select_options.size-1} more)"
@@ -395,7 +428,12 @@ module Morpheus
395
428
  value_found = true
396
429
  end
397
430
  end
398
- return value
431
+
432
+ # wrap in object when using fieldInput
433
+ if value && !option_type['fieldInput'].nil?
434
+ value = {option_type['fieldName'].split('.').last => value, option_type['fieldInput'] => (no_prompt ? option_type['defaultInputValue'] : field_input_prompt(option_type))}
435
+ end
436
+ value
399
437
  end
400
438
 
401
439
  # this is a funky one, the user is prompted for yes/no
@@ -436,6 +474,27 @@ module Morpheus
436
474
  return value
437
475
  end
438
476
 
477
+ def self.field_input_prompt(option_type)
478
+ value_found = false
479
+ value = nil
480
+
481
+ input_field_label = option_type['fieldInput'].gsub(/[A-Z]/, ' \0').split(' ').collect {|it| it.capitalize}.join(' ')
482
+ input_field_name = option_type['fieldName'].split('.').reverse.drop(1).reverse.push(option_type['fieldInput']).join('.')
483
+ input_option_type = option_type.merge({'fieldName' => input_field_name, 'fieldLabel' => input_field_label, 'required' => true, 'type' => 'text'})
484
+
485
+ while !value_found do
486
+ print "#{input_field_label}#{option_type['defaultInputValue'] ? " [#{option_type['defaultInputValue']}]" : ''}: "
487
+ input = $stdin.gets.chomp!
488
+ value = input.empty? ? option_type['defaultInputValue'] : input
489
+ if input == '?'
490
+ help_prompt(input_option_type)
491
+ elsif !value.nil?
492
+ value_found = true
493
+ end
494
+ end
495
+ return value
496
+ end
497
+
439
498
  def self.generic_prompt(option_type)
440
499
  value_found = false
441
500
  value = nil
@@ -528,14 +587,62 @@ module Morpheus
528
587
  return value
529
588
  end
530
589
 
590
+ # file_content_prompt() prompts for source (local,repository,url) and then content or repo or.
591
+ # returns a Hash like {sourceType:"local",content:"yadda",contentPath:null,contentRef:null}
592
+ def self.file_content_prompt(option_type, options={}, api_client=nil, api_params={})
593
+ file_params = {}
594
+ options ||= {}
595
+ full_field_key = option_type['fieldContext'] ? "#{option_type['fieldContext']}.#{option_type['fieldName']}" : "#{option_type['fieldName']}"
596
+ passed_file_params = get_object_value(options, full_field_key)
597
+ if passed_file_params.is_a?(Hash)
598
+ file_params = passed_file_params
599
+ end
600
+ is_required = option_type['required']
601
+ if file_params['source']
602
+ file_params['sourceType'] = file_params.delete('source')
603
+ end
604
+ source_type = file_params['sourceType']
605
+ # source
606
+ if source_type.nil?
607
+ source_type = select_prompt({'fieldContext' => full_field_key, 'fieldName' => 'source', 'fieldLabel' => 'Source', 'type' => 'select', 'optionSource' => 'fileContentSource', 'required' => is_required, 'defaultValue' => (is_required ? 'local' : nil)}, api_client, {}, options[:no_prompt])
608
+ file_params['sourceType'] = source_type
609
+ end
610
+ # source type options
611
+ if source_type == "local"
612
+ # prompt for content
613
+ if file_params['content'].nil?
614
+ file_params['content'] = multiline_prompt({'fieldContext' => full_field_key, 'fieldName' => 'content', 'type' => 'code-editor', 'fieldLabel' => 'Content', 'required' => true})
615
+ end
616
+ elsif source_type == "url"
617
+ if file_params['url']
618
+ file_params['contentPath'] = file_params.delete('url')
619
+ end
620
+ if file_params['contentPath'].nil?
621
+ file_params['contentPath'] = generic_prompt({'fieldContext' => full_field_key, 'fieldName' => 'url', 'fieldLabel' => 'URL', 'type' => 'text', 'required' => true})
622
+ end
623
+ elsif source_type == "repository"
624
+ if file_params['repository'].nil?
625
+ repository_id = select_prompt({'fieldContext' => full_field_key, 'fieldName' => 'repositoryId', 'fieldLabel' => 'Repository', 'type' => 'select', 'optionSource' => 'codeRepositories', 'required' => true}, api_client, {}, options[:no_prompt])
626
+ file_params['repository'] = {'id' => repository_id}
627
+ end
628
+ if file_params['contentPath'].nil?
629
+ file_params['contentPath'] = generic_prompt({'fieldContext' => full_field_key, 'fieldName' => 'path', 'fieldLabel' => 'File Path', 'type' => 'text', 'required' => true})
630
+ end
631
+ if file_params['contentRef'].nil?
632
+ file_params['contentRef'] = generic_prompt({'fieldContext' => full_field_key, 'fieldName' => 'ref', 'fieldLabel' => 'Version Ref', 'type' => 'text'})
633
+ end
634
+ end
635
+ return file_params
636
+ end
637
+
531
638
  def self.help_prompt(option_type)
532
- full_field_name = option_type['fieldContext'] ? (option_type['fieldContext']+'.') : ''
533
- full_field_name << option_type['fieldName'].to_s
639
+ field_key = [option_type['fieldContext'], option_type['fieldName']].select {|it| it && it != '' }.join('.')
640
+ help_field_key = option_type[:help_field_prefix] ? "#{option_type[:help_field_prefix]}.#{field_key}" : field_key
534
641
  # an attempt at prompting help for natural options without the -O switch
535
642
  if option_type[:fmt] == :natural
536
- print Term::ANSIColor.green," * #{option_type['fieldLabel']} [--#{full_field_name}=] ", Term::ANSIColor.reset , "#{option_type['description']}\n"
643
+ print Term::ANSIColor.green," * #{option_type['fieldLabel']} [--#{help_field_key}=] ", Term::ANSIColor.reset , "#{option_type['description']}\n"
537
644
  else
538
- print Term::ANSIColor.green," * #{option_type['fieldLabel']} [-O #{full_field_name}=] - ", Term::ANSIColor.reset , "#{option_type['description']}\n"
645
+ print Term::ANSIColor.green," * #{option_type['fieldLabel']} [-O #{help_field_key}=] - ", Term::ANSIColor.reset , "#{option_type['description']}\n"
539
646
  end
540
647
  end
541
648
 
@@ -559,17 +666,24 @@ module Morpheus
559
666
  end
560
667
  end
561
668
 
562
- def self.format_option_types_help(option_types)
669
+ def self.format_option_types_help(option_types, opts={})
563
670
  if option_types.empty?
564
- "Available Options:\nNone\n\n"
671
+ "#{opts[:color]}#{opts[:title] || "Available Options:"}\nNone\n\n"
565
672
  else
566
- option_lines = option_types.collect {|it| " -O #{it['fieldName']}=\"value\"" }.join("\n")
567
- "Available Options:\n#{option_lines}\n\n"
673
+ if opts[:include_context]
674
+ option_lines = option_types.sort {|it| it['displayOrder']}.collect {|it|
675
+ field_context = (opts[:context_map] || {})[it['fieldContext']] || it['fieldContext']
676
+ " -O #{field_context && field_context != '' ? "#{field_context}." : ''}#{it['fieldName']}=\"value\""
677
+ }
678
+ else
679
+ option_lines = option_types.sort {|it| it['displayOrder']}.collect {|it| " -O #{it['fieldName']}=\"value\"" }
680
+ end
681
+ "#{opts[:color]}#{opts[:title] || "Available Options:"}\n#{option_lines.join("\n")}\n\n"
568
682
  end
569
683
  end
570
684
 
571
- def self.display_option_types_help(option_types)
572
- puts self.format_option_types_help(option_types)
685
+ def self.display_option_types_help(option_types, opts={})
686
+ puts self.format_option_types_help(option_types, opts)
573
687
  end
574
688
 
575
689
  def self.optional_label(option_type)