morpheus-cli 5.4.5 → 5.5.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.
Files changed (44) 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/clusters_interface.rb +12 -0
  5. data/lib/morpheus/api/network_pool_servers_interface.rb +7 -0
  6. data/lib/morpheus/api/prices_interface.rb +6 -0
  7. data/lib/morpheus/api/scale_thresholds_interface.rb +9 -0
  8. data/lib/morpheus/cli/cli_command.rb +55 -23
  9. data/lib/morpheus/cli/commands/apps.rb +2 -2
  10. data/lib/morpheus/cli/commands/cloud_resource_pools_command.rb +33 -2
  11. data/lib/morpheus/cli/commands/clouds.rb +61 -31
  12. data/lib/morpheus/cli/commands/clusters.rb +66 -5
  13. data/lib/morpheus/cli/commands/cypher_command.rb +22 -23
  14. data/lib/morpheus/cli/commands/hosts.rb +5 -1
  15. data/lib/morpheus/cli/commands/instances.rb +12 -12
  16. data/lib/morpheus/cli/commands/integrations_command.rb +1 -1
  17. data/lib/morpheus/cli/commands/invoices_command.rb +8 -1
  18. data/lib/morpheus/cli/commands/jobs_command.rb +45 -225
  19. data/lib/morpheus/cli/commands/library_container_types_command.rb +52 -3
  20. data/lib/morpheus/cli/commands/library_option_lists_command.rb +18 -8
  21. data/lib/morpheus/cli/commands/library_option_types_command.rb +56 -62
  22. data/lib/morpheus/cli/commands/load_balancers.rb +11 -19
  23. data/lib/morpheus/cli/commands/network_pool_servers_command.rb +5 -2
  24. data/lib/morpheus/cli/commands/prices_command.rb +25 -11
  25. data/lib/morpheus/cli/commands/roles.rb +475 -70
  26. data/lib/morpheus/cli/commands/scale_thresholds.rb +103 -0
  27. data/lib/morpheus/cli/commands/tasks.rb +64 -22
  28. data/lib/morpheus/cli/commands/user_sources_command.rb +107 -39
  29. data/lib/morpheus/cli/commands/users.rb +10 -10
  30. data/lib/morpheus/cli/commands/view.rb +1 -0
  31. data/lib/morpheus/cli/commands/workflows.rb +21 -14
  32. data/lib/morpheus/cli/error_handler.rb +13 -4
  33. data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
  34. data/lib/morpheus/cli/mixins/execution_request_helper.rb +1 -1
  35. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +3 -3
  36. data/lib/morpheus/cli/mixins/jobs_helper.rb +173 -0
  37. data/lib/morpheus/cli/mixins/print_helper.rb +120 -38
  38. data/lib/morpheus/cli/mixins/provisioning_helper.rb +2 -3
  39. data/lib/morpheus/cli/mixins/rest_command.rb +41 -14
  40. data/lib/morpheus/cli/option_types.rb +69 -13
  41. data/lib/morpheus/cli/version.rb +1 -1
  42. data/lib/morpheus/logging.rb +6 -8
  43. data/lib/morpheus/routes.rb +3 -5
  44. metadata +6 -4
@@ -2,6 +2,7 @@ require 'morpheus/cli/cli_command'
2
2
 
3
3
  class Morpheus::Cli::Workflows
4
4
  include Morpheus::Cli::CliCommand
5
+ include Morpheus::Cli::JobsHelper
5
6
 
6
7
  register_subcommands :list, :get, :add, :update, :remove, :execute
7
8
  set_default_subcommand :list
@@ -538,6 +539,7 @@ class Morpheus::Cli::Workflows
538
539
  instances = []
539
540
  server_ids = []
540
541
  servers = []
542
+ default_refresh_interval = 10
541
543
  optparse = Morpheus::Cli::OptionParser.new do |opts|
542
544
  opts.banner = subcommand_usage("[workflow] --instance [instance] [options]")
543
545
  opts.on('--instance INSTANCE', String, "Instance name or id to execute the workflow on. This option can be passed more than once.") do |val|
@@ -572,6 +574,12 @@ class Morpheus::Cli::Workflows
572
574
  opts.on('--config [TEXT]', String, "Custom config") do |val|
573
575
  params['customConfig'] = val.to_s
574
576
  end
577
+ opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
578
+ options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
579
+ end
580
+ opts.on(nil, '--no-refresh', "Do not refresh" ) do
581
+ options[:no_refresh] = true
582
+ end
575
583
  build_common_options(opts, options, [:payload, :options, :json, :dry_run, :remote])
576
584
  end
577
585
  optparse.parse!(args)
@@ -580,7 +588,7 @@ class Morpheus::Cli::Workflows
580
588
  end
581
589
  workflow_name = args[0]
582
590
  connect(options)
583
- begin
591
+
584
592
  workflow = find_workflow_by_name_or_id(workflow_name)
585
593
  return 1 if workflow.nil?
586
594
 
@@ -642,10 +650,8 @@ class Morpheus::Cli::Workflows
642
650
  return 0
643
651
  end
644
652
  json_response = @task_sets_interface.run(workflow['id'], payload)
645
- if options[:json]
646
- puts as_json(json_response, options)
647
- return json_response['success'] ? 0 : 1
648
- else
653
+
654
+ render_response(json_response, options) do
649
655
  target_desc = nil
650
656
  if instances.size() > 0
651
657
  target_desc = (instances.size() == 1) ? "instance #{instances[0]['name']}" : "#{instances.size()} instances"
@@ -657,18 +663,19 @@ class Morpheus::Cli::Workflows
657
663
  else
658
664
  print_green_success "Executing workflow #{workflow['name']}"
659
665
  end
660
- # todo: refresh, use get processId and load process record isntead? err
661
666
  if json_response["jobExecution"] && json_response["jobExecution"]["id"]
662
- get_args = [json_response["jobExecution"]["id"], "--details"] + (options[:remote] ? ["-r",options[:remote]] : [])
663
- Morpheus::Logging::DarkPrinter.puts((['jobs', 'get-execution'] + get_args).join(' ')) if Morpheus::Logging.debug?
664
- return ::Morpheus::Cli::JobsCommand.new.handle(['get-execution'] + get_args)
667
+ job_execution_id = json_response["jobExecution"]["id"]
668
+ if options[:no_refresh]
669
+ get_args = [json_response["jobExecution"]["id"], "--details"] + (options[:remote] ? ["-r",options[:remote]] : [])
670
+ Morpheus::Logging::DarkPrinter.puts((['jobs', 'get-execution'] + get_args).join(' ')) if Morpheus::Logging.debug?
671
+ ::Morpheus::Cli::JobsCommand.new.handle(['get-execution'] + get_args)
672
+ else
673
+ #Morpheus::Cli::JobsCommand.new.handle(["get-execution", job_execution_id, "--refresh", options[:refresh_interval].to_s]+ (options[:remote] ? ["-r",options[:remote]] : []))
674
+ job_execution_results = wait_for_job_execution(job_execution_id, options.merge({:details => true}))
675
+ end
665
676
  end
666
- return json_response['success'] ? 0 : 1
667
677
  end
668
- rescue RestClient::Exception => e
669
- print_rest_exception(e, options)
670
- return 1
671
- end
678
+ return 0, nil
672
679
  end
673
680
 
674
681
  private
@@ -44,8 +44,13 @@ class Morpheus::Cli::ErrorHandler
44
44
  # exit_code = 127
45
45
  when Morpheus::Cli::CommandArgumentsError
46
46
  puts_angry_error err.message
47
- @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner
48
- @stderr.puts "Try --help for more usage information"
47
+ if err.args.include?("--help") || err.args.include?("--help")
48
+ @stderr.puts err.optparse
49
+ else
50
+ @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner
51
+ @stderr.puts "Try --help for more usage information"
52
+ end
53
+
49
54
  do_print_stacktrace = false
50
55
  if err.exit_code
51
56
  exit_code = err.exit_code
@@ -60,8 +65,12 @@ class Morpheus::Cli::ErrorHandler
60
65
  if !message_lines.empty?
61
66
  @stderr.puts message_lines.join("\n") unless message_lines.empty?
62
67
  else
63
- @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner && message_lines.empty?
64
- @stderr.puts "Try --help for more usage information"
68
+ if err.args.include?("--help") || err.args.include?("--help")
69
+ @stderr.puts err.optparse
70
+ else
71
+ @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner && message_lines.empty?
72
+ @stderr.puts "Try --help for more usage information"
73
+ end
65
74
  end
66
75
  do_print_stacktrace = false
67
76
  if err.exit_code
@@ -136,7 +136,7 @@ module Morpheus::Cli::AccountsHelper
136
136
  "Multitenant" => lambda {|it|
137
137
  format_boolean(it['multitenant']).to_s + (it['multitenantLocked'] ? " (LOCKED)" : "")
138
138
  },
139
- "Default Persona" => lambda {|it| it['defaultPersona'] ? it['defaultPersona']['name'] : '(standard)' },
139
+ "Default Persona" => lambda {|it| it['defaultPersona'] ? it['defaultPersona']['name'] : '' },
140
140
  "Owner" => lambda {|it| it['owner'] ? it['owner']['name'] : '' },
141
141
  #"Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
142
142
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
@@ -1,6 +1,6 @@
1
1
  require 'morpheus/cli/mixins/print_helper'
2
2
  # Mixin for Morpheus::Cli command classes
3
- # Provides common methods for provisioning instances
3
+ # Provides refreshing execution-request records by unique id (uuid)
4
4
  module Morpheus::Cli::ExecutionRequestHelper
5
5
 
6
6
  def self.included(klass)
@@ -111,9 +111,9 @@ module Morpheus::Cli::InfrastructureHelper
111
111
  return cloud
112
112
  end
113
113
 
114
- def get_available_cloud_types(refresh=false)
114
+ def get_available_cloud_types(refresh=false, params = {})
115
115
  if !@available_cloud_types || refresh
116
- @available_cloud_types = clouds_interface.cloud_types({max:1000})['zoneTypes']
116
+ @available_cloud_types = clouds_interface.cloud_types({max:1000}.deep_merge(params))['zoneTypes']
117
117
  end
118
118
  return @available_cloud_types
119
119
  end
@@ -130,7 +130,7 @@ module Morpheus::Cli::InfrastructureHelper
130
130
  end
131
131
 
132
132
  def cloud_type_for_name(name)
133
- return get_available_cloud_types().find { |z| z['name'].downcase == name.downcase || z['code'].downcase == name.downcase}
133
+ return get_available_cloud_types(true, {'name' => name}).find { |z| z['name'].downcase == name.downcase || z['code'].downcase == name.downcase}
134
134
  end
135
135
 
136
136
 
@@ -0,0 +1,173 @@
1
+ require 'morpheus/cli/mixins/print_helper'
2
+ # Mixin for Morpheus::Cli command classes
3
+ # Provides refreshing job execution records by id
4
+ module Morpheus::Cli::JobsHelper
5
+
6
+ def self.included(klass)
7
+ klass.send :include, Morpheus::Cli::PrintHelper
8
+ # klass.send :include, Morpheus::Cli::ProcessesHelper
9
+ end
10
+
11
+ def api_client
12
+ raise "#{self.class} has not defined @api_client" if @api_client.nil?
13
+ @api_client
14
+ end
15
+
16
+ def jobs_interface
17
+ # get_interface('jobs')
18
+ api_client.jobs
19
+ end
20
+
21
+ def get_process_event_data(process_or_event)
22
+ {
23
+ id: process_or_event['id'],
24
+ description: process_or_event['description'] || (process_or_event['refType'] == 'instance' ? process_or_event['displayName'] : (process_or_event['processTypeName'] || '').capitalize),
25
+ start_date: format_local_dt(process_or_event['startDate']),
26
+ created_by: process_or_event['createdBy'] ? process_or_event['createdBy']['displayName'] : '',
27
+ duration: format_human_duration((process_or_event['duration'] || process_or_event['statusEta'] || 0) / 1000.0),
28
+ status: format_job_status(process_or_event['status']),
29
+ error: process_or_event['message'] || process_or_event['error'],
30
+ output: process_or_event['output'],
31
+ }
32
+ end
33
+
34
+ # both process and process events
35
+ def print_process_events(events, options={})
36
+ # event_columns = [:id, :description, :start_date, :created_by, :duration, :status, :error, :output]
37
+ event_columns = {
38
+ "ID" => lambda {|it| it[:id]},
39
+ "Description" => lambda {|it| it[:description]},
40
+ "Start Date" => lambda {|it| it[:start_date]},
41
+ "Created By" => lambda {|it| it[:created_by]},
42
+ "Duration" => lambda {|it| it[:duration]},
43
+ "Status" => lambda {|it| it[:status]},
44
+ "Error" => lambda {|it| options[:details] ? it[:error] : truncate_string(it[:error], 32) },
45
+ "Output" => lambda {|it| options[:details] ? it[:output] : truncate_string(it[:output], 32) }
46
+ }
47
+ print as_pretty_table(events.collect {|it| get_process_event_data(it)}, event_columns.upcase_keys!, options)
48
+ end
49
+
50
+ def print_job_execution(job_execution, options)
51
+ process = job_execution['process']
52
+ print cyan
53
+ description_cols = {
54
+ "ID" => lambda {|it| it['id'] },
55
+ "Job" => lambda {|it| it['job'] ? it['job']['name'] : ''},
56
+ "Job Type" => lambda {|it| it['job'] && it['job']['type'] ? (it['job']['type']['code'] == 'morpheus.workflow' ? 'Workflow' : 'Task') : ''},
57
+ # "Description" => lambda {|it| it['description'] || (it['job'] ? it['job']['description'] : '') },
58
+ "Start Date" => lambda {|it| format_local_dt(it['startDate'])},
59
+ "ETA/Time" => lambda {|it| it['duration'] ? format_human_duration(it['duration'] / 1000.0) : ''},
60
+ "Status" => lambda {|it| format_job_status(it['status'])},
61
+ #"Output" => lambda {|it| it['process'] && (it['process']['output']) ?(it['process']['output']).to_s.strip : ''},
62
+ #"Error" => lambda {|it| it['process'] && (it['process']['message'] || it['process']['error']) ? red + (it['process']['message'] || it['process']['error']).to_s.strip + cyan : ''},
63
+ "Created By" => lambda {|it| it['createdBy'].nil? ? '' : it['createdBy']['displayName'] || it['createdBy']['username']}
64
+ }
65
+ print_description_list(description_cols, job_execution, options)
66
+
67
+ if process
68
+ process_data = get_process_event_data(process)
69
+ print_h2 "Process Details"
70
+ process_description_cols = {
71
+ "Process ID" => lambda {|it| it[:id]},
72
+ "Description" => lambda {|it| it[:description]},
73
+ "Start Date" => lambda {|it| it[:start_date]},
74
+ "Created By" => lambda {|it| it[:created_by]},
75
+ "Duration" => lambda {|it| it[:duration]},
76
+ "Status" => lambda {|it| it[:status]}
77
+ }
78
+ print_description_list(process_description_cols, process_data, options)
79
+
80
+ if process_data[:output] && process_data[:output].strip.length > 0
81
+ print_h2 "Output"
82
+ print process['output']
83
+ end
84
+ if process_data[:error] && process_data[:error].strip.length > 0
85
+ print_h2 "Error"
86
+ print process['message'] || process['error']
87
+ print reset,"\n"
88
+ end
89
+
90
+
91
+ if process['events'] && !process['events'].empty?
92
+ print_h2 "Process Events", options
93
+ print_process_events(process['events'], options)
94
+ end
95
+ else
96
+ print reset,"\n"
97
+ end
98
+ return 0, nil
99
+ end
100
+
101
+ def print_job_executions(execs, options={})
102
+ if execs.empty?
103
+ print cyan,"No job executions found.",reset,"\n"
104
+ else
105
+ rows = execs.collect do |ex|
106
+ {
107
+ id: ex['id'],
108
+ job: ex['job'] ? ex['job']['name'] : '',
109
+ description: ex['description'] || ex['job'] ? ex['job']['description'] : '',
110
+ type: ex['job'] && ex['job']['type'] ? (ex['job']['type']['code'] == 'morpheus.workflow' ? 'Workflow' : 'Task') : '',
111
+ start: format_local_dt(ex['startDate']),
112
+ duration: ex['duration'] ? format_human_duration(ex['duration'] / 1000.0) : '',
113
+ status: format_job_status(ex['status']),
114
+ error: truncate_string(ex['process'] && (ex['process']['message'] || ex['process']['error']) ? ex['process']['message'] || ex['process']['error'] : '', options[:details] ? nil : 32),
115
+ output: truncate_string(ex['process'] && ex['process']['output'] ? ex['process']['output'] : '', options[:details] ? nil : 32),
116
+ }
117
+ end
118
+
119
+ columns = [
120
+ :id, :job, :type, {'START DATE' => :start}, {'ETA/TIME' => :duration}, :status, :error, :output
121
+ ]
122
+ print as_pretty_table(rows, columns, options)
123
+ end
124
+ end
125
+
126
+ def format_job_status(status_string, return_color=cyan)
127
+ out = ""
128
+ if status_string
129
+ if ['complete','success', 'successful', 'ok'].include?(status_string)
130
+ out << "#{green}#{status_string.upcase}"
131
+ elsif ['error', 'offline', 'failed', 'failure'].include?(status_string)
132
+ out << "#{red}#{status_string.upcase}"
133
+ else
134
+ out << "#{yellow}#{status_string.upcase}"
135
+ end
136
+ end
137
+ out + return_color
138
+ end
139
+
140
+ # this is built into CliCommand now...
141
+ # def find_by_name_or_id(type, val)
142
+ # interface = instance_variable_get "@#{type}s_interface"
143
+ # typeCamelCase = type.gsub(/(?:^|_)([a-z])/) do $1.upcase end
144
+ # typeCamelCase = typeCamelCase[0, 1].downcase + typeCamelCase[1..-1]
145
+ # (val.to_s =~ /\A\d{1,}\Z/) ? interface.get(val.to_i)[typeCamelCase] : interface.list({'name' => val})["#{typeCamelCase}s"].first
146
+ # end
147
+
148
+ # refresh execution request until it is finished
149
+ # returns json response data of the last execution request when status reached 'completed' or 'failed'
150
+ def wait_for_job_execution(job_execution_id, options={}, print_output = true)
151
+ refresh_interval = 10
152
+ if options[:refresh_interval].to_i > 0
153
+ refresh_interval = options[:refresh_interval]
154
+ end
155
+ refresh_display_seconds = refresh_interval % 1.0 == 0 ? refresh_interval.to_i : refresh_interval
156
+ unless options[:quiet]
157
+ print cyan, "Refreshing every #{refresh_display_seconds} seconds until execution is complete...", "\n", reset
158
+ end
159
+ job_execution = jobs_interface.get_execution(job_execution_id)['jobExecution']
160
+ while ['new','queued','pending','running'].include?(job_execution['status']) do
161
+ sleep(refresh_interval)
162
+ job_execution = jobs_interface.get_execution(job_execution_id)['jobExecution']
163
+ end
164
+ if print_output && options[:quiet] != true
165
+ print_h1 "Morpheus Job Execution", [], options
166
+ print_job_execution(job_execution, options)
167
+ end
168
+ return job_execution
169
+ end
170
+
171
+
172
+
173
+ end
@@ -608,6 +608,9 @@ module Morpheus::Cli::PrintHelper
608
608
  # @param suffix [String] the character to pad right side with. Default is '...'
609
609
  def truncate_string(value, width, suffix="...")
610
610
  value = value.to_s
611
+ if !width
612
+ return value
613
+ end
611
614
  # JD: hack alerty.. this sux, but it's a best effort to preserve values containing ascii coloring codes
612
615
  # it stops working when there are words separated by ascii codes, eg. two diff colors
613
616
  # plus this is probably pretty slow...
@@ -714,7 +717,11 @@ module Morpheus::Cli::PrintHelper
714
717
 
715
718
  # support --fields x,y,z and --all-fields or --fields all
716
719
  all_fields = data.first ? data.first.keys : []
717
-
720
+ #todo: support --raw-fields meh, not really needed..
721
+ # if options[:include_fields] && options[:raw_fields]
722
+ # data = transform_data_for_field_options(data, options)
723
+ # data = data[options[:include_fields_context]] if options[:include_fields_context]
724
+ # elsif options[:include_fields]
718
725
  if options[:include_fields]
719
726
  if (options[:include_fields].is_a?(Array) && options[:include_fields].size == 1 && options[:include_fields][0] == 'all') || options[:include_fields] == 'all'
720
727
  columns = all_fields
@@ -1093,16 +1100,78 @@ module Morpheus::Cli::PrintHelper
1093
1100
  '"' + v.to_s.gsub('"', '""') + '"'
1094
1101
  end
1095
1102
 
1096
- def as_csv(data, columns, options={})
1103
+ def as_csv(data, default_columns=nil, options={}, object_key=nil)
1097
1104
  out = ""
1098
1105
  delim = options[:csv_delim] || options[:delim] || ","
1099
1106
  newline = options[:csv_newline] || options[:newline] || "\n"
1100
1107
  include_header = options[:csv_no_header] ? false : true
1101
1108
  do_quotes = options[:csv_quotes] || options[:quotes]
1102
1109
 
1110
+ if options[:include_fields]
1111
+ data = transform_data_for_field_options(data, options, object_key)
1112
+ if data.is_a?(Hash)
1113
+ if options[:raw_fields]
1114
+ if options[:include_fields_context]
1115
+ data = data[options[:include_fields_context]]
1116
+ else
1117
+ # todo: could use a dynamic object_key, first Array or Hash in the data, can probably always do this...
1118
+ # if object_key.nil?
1119
+ # object_key = data.keys.find {|k| data[k].is_a?(Array) || data[k].is_a?(Hash) }
1120
+ # end
1121
+ # if object_key && data[object_key]
1122
+ # data = data[object_key]
1123
+ # end
1124
+ end
1125
+ else
1126
+ if object_key
1127
+ data = data[object_key]
1128
+ end
1129
+ end
1130
+ end
1131
+ else
1132
+ # need array of records, so always select the object/array here
1133
+ if object_key
1134
+ if data.is_a?(Hash)
1135
+ data = data[object_key]
1136
+ else
1137
+ Morpheus::Logging::DarkPrinter.puts "as_csv() expects data as an to fetch object key '#{object_key}' from, #{records.class}." if Morpheus::Logging.debug?
1138
+ end
1139
+ end
1140
+ end
1141
+
1142
+
1143
+ records = data
1144
+
1145
+ # allow records as Array and Hash only..
1146
+ if records.is_a?(Array)
1147
+ # records = records
1148
+ elsif records.is_a?(Hash)
1149
+ records = [records]
1150
+ else
1151
+ #raise "records_as_csv expects records as an Array of objects to render"
1152
+ Morpheus::Logging::DarkPrinter.puts "as_csv() expects data as an Array of objects to render, got a #{records.class} instead" if Morpheus::Logging.debug?
1153
+ # return ""
1154
+ return out
1155
+ end
1156
+
1157
+ # build column definitions, by default use all properties for the first record (Hash) in the array
1158
+ columns = []
1159
+ all_fields = records.first.is_a?(Hash) ? records.first.keys : []
1160
+ if options[:include_fields]
1161
+ if (options[:include_fields].is_a?(Array) && options[:include_fields].size == 1 && options[:include_fields][0] == 'all') || options[:include_fields] == 'all'
1162
+ columns = all_fields
1163
+ else
1164
+ columns = options[:include_fields]
1165
+ end
1166
+ elsif options[:all_fields]
1167
+ columns = all_fields
1168
+ elsif default_columns
1169
+ columns = default_columns
1170
+ else
1171
+ columns = all_fields
1172
+ end
1173
+
1103
1174
  column_defs = build_column_definitions(columns)
1104
- #columns = columns.flatten.compact
1105
- data_array = [data].flatten.compact
1106
1175
 
1107
1176
  if include_header
1108
1177
  headers = column_defs.collect {|column_def| column_def.label }
@@ -1113,7 +1182,7 @@ module Morpheus::Cli::PrintHelper
1113
1182
  out << newline
1114
1183
  end
1115
1184
  lines = []
1116
- data_array.each do |obj|
1185
+ records.each do |obj|
1117
1186
  if obj
1118
1187
  cells = []
1119
1188
  column_defs.each do |column_def|
@@ -1137,29 +1206,9 @@ module Morpheus::Cli::PrintHelper
1137
1206
 
1138
1207
  end
1139
1208
 
1209
+ # deprecated, replaced by as_csv(records, columns, options, object_key)
1140
1210
  def records_as_csv(records, options={}, default_columns=nil)
1141
- out = ""
1142
- if !records
1143
- #raise "records_as_csv expects records as an Array of objects to render"
1144
- return out
1145
- end
1146
- cols = []
1147
- all_fields = records.first ? records.first.keys : []
1148
- if options[:include_fields]
1149
- if (options[:include_fields].is_a?(Array) && options[:include_fields].size == 1 && options[:include_fields][0] == 'all') || options[:include_fields] == 'all'
1150
- cols = all_fields
1151
- else
1152
- cols = options[:include_fields]
1153
- end
1154
- elsif options[:all_fields]
1155
- cols = all_fields
1156
- elsif default_columns
1157
- cols = default_columns
1158
- else
1159
- cols = all_fields
1160
- end
1161
- out << as_csv(records, cols, options)
1162
- out
1211
+ as_csv(records, default_columns, options)
1163
1212
  end
1164
1213
 
1165
1214
  def as_json(data, options={}, object_key=nil)
@@ -1169,12 +1218,7 @@ module Morpheus::Cli::PrintHelper
1169
1218
  end
1170
1219
 
1171
1220
  if options[:include_fields]
1172
- if object_key
1173
- # data[object_key] = filter_data(data[object_key], options[:include_fields])
1174
- data = {(object_key) => filter_data(data[object_key], options[:include_fields]) }
1175
- else
1176
- data = filter_data(data, options[:include_fields])
1177
- end
1221
+ data = transform_data_for_field_options(data, options, object_key)
1178
1222
  end
1179
1223
 
1180
1224
  do_pretty = options.key?(:pretty_json) ? options[:pretty_json] : true
@@ -1193,11 +1237,7 @@ module Morpheus::Cli::PrintHelper
1193
1237
  return "null" # "No data"
1194
1238
  end
1195
1239
  if options[:include_fields]
1196
- if object_key
1197
- data[object_key] = filter_data(data[object_key], options[:include_fields])
1198
- else
1199
- data = filter_data(data, options[:include_fields])
1200
- end
1240
+ data = transform_data_for_field_options(data, options, object_key)
1201
1241
  end
1202
1242
  begin
1203
1243
  out << data.to_yaml
@@ -1209,6 +1249,48 @@ module Morpheus::Cli::PrintHelper
1209
1249
  out
1210
1250
  end
1211
1251
 
1252
+
1253
+ # transform data for options --fields id,authority and --select id,authority
1254
+ # support traversing records with --raw-fields and the list command. Example: roles list --raw-fields roles.id,roles.authority
1255
+ def transform_data_for_field_options(data, options, object_key=nil)
1256
+ if options[:include_fields] && data.is_a?(Hash)
1257
+ if options[:raw_fields]
1258
+ row = (object_key && !options[:raw_fields]) ? data[object_key] : data
1259
+ records = [row].flatten()
1260
+ # look for an array in the first field only now...
1261
+ field_parts = options[:include_fields][0].to_s.split(".")
1262
+ field_context = field_parts[0]
1263
+ context_data = data[field_context]
1264
+ if field_parts.size > 1 && context_data.is_a?(Array)
1265
+ # inject all the root level properties to be selectable too..
1266
+ context_data = data.delete(field_context)
1267
+ # records = context_data
1268
+ records = context_data.collect {|it| it.is_a?(Hash) ? data.merge(it) : data }
1269
+ # hacky modifying options in place
1270
+ options[:include_fields_context] = field_context
1271
+ options[:include_fields] = options[:include_fields].collect {|it| it.sub(field_context+'.', '')}
1272
+ # data = filter_data(records, options[:include_fields])
1273
+ # data[field_context] = filter_data(records, options[:include_fields])
1274
+ data = {(field_context) => filter_data(records, options[:include_fields])}
1275
+ else
1276
+ data = filter_data(data, options[:include_fields])
1277
+ end
1278
+ else
1279
+ # By default, fields are relative to the object_key, so you can use -F id instead of requiring -F instance.id
1280
+ # So ironically it is the 'raw' options (:raw_fields == true) that has to do all this funny stuff to filter intuitively.
1281
+ if object_key
1282
+ # this removes everything but the object, makes sense when using --fields
1283
+ data = {(object_key) => filter_data(data[object_key], options[:include_fields])}
1284
+ # this preserves other fields eg. meta...
1285
+ # data[object_key] = filter_data(data[object_key], options[:include_fields])
1286
+ else
1287
+ data = filter_data(data, options[:include_fields])
1288
+ end
1289
+ end
1290
+ end
1291
+ return data
1292
+ end
1293
+
1212
1294
  def sleep_with_dots(sleep_seconds, dots=3, dot_chr=".")
1213
1295
  dot_interval = (sleep_seconds.to_f / dots.to_i)
1214
1296
  dots.to_i.times do |dot_index|
@@ -1160,6 +1160,7 @@ module Morpheus::Cli::ProvisioningHelper
1160
1160
  #volume['size'] = plan_size
1161
1161
  #volume['sizeId'] = nil #volume.delete('sizeId')
1162
1162
  end
1163
+
1163
1164
  if !datastore_options.empty?
1164
1165
  default_datastore = datastore_options.find {|ds| ds['value'].to_s == volume['datastoreId'].to_s}
1165
1166
  v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldContext' => field_context, 'fieldName' => 'datastoreId', 'type' => 'select', 'fieldLabel' => 'Root Datastore', 'selectOptions' => datastore_options, 'required' => true, 'description' => 'Choose a datastore.', 'defaultValue' => default_datastore ? default_datastore['name'] : volume['datastoreId']}], options[:options])
@@ -2081,7 +2082,6 @@ module Morpheus::Cli::ProvisioningHelper
2081
2082
  if !visibility && !options[:no_prompt]
2082
2083
  visibility = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'visibility', 'fieldLabel' => 'Tenant Permissions Visibility', 'type' => 'select', 'defaultValue' => 'private', 'required' => true, 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}]}], options[:options], @api_client, {})['visibility']
2083
2084
  end
2084
-
2085
2085
  permissions['resourcePool'] = {'visibility' => visibility} if visibility
2086
2086
  end
2087
2087
 
@@ -2113,8 +2113,7 @@ module Morpheus::Cli::ProvisioningHelper
2113
2113
  def prompt_permissions_v2(options, excludes = [])
2114
2114
  perms = prompt_permissions(options, excludes)
2115
2115
  rtn = {}
2116
-
2117
- rtn['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil?
2116
+ rtn['visibility'] = perms['resourcePool']['visibility'] if !perms['resourcePool'].nil? && !excludes.include?('visibility')
2118
2117
  rtn['tenants'] = ((perms['tenantPermissions'] || {})['accounts'] || []).collect {|it| {'id' => it}}
2119
2118
  rtn
2120
2119
  end
@@ -148,6 +148,17 @@ module Morpheus::Cli::RestCommand
148
148
 
149
149
  alias :set_rest_interface_name :rest_interface_name=
150
150
 
151
+ # rest_perms_config enables and configures permissions prompt
152
+ def rest_perms_config
153
+ @rest_perms_config || {}
154
+ end
155
+
156
+ def rest_perms_config=(v)
157
+ @rest_perms_config = v
158
+ end
159
+
160
+ alias :set_rest_perms_config :rest_perms_config=
161
+
151
162
  # rest_has_type indicates a resource has a type. default is false
152
163
  def rest_has_type
153
164
  @rest_has_type == true
@@ -329,6 +340,10 @@ module Morpheus::Cli::RestCommand
329
340
  self.class.rest_interface_name
330
341
  end
331
342
 
343
+ def rest_perms_config
344
+ self.class.rest_perms_config
345
+ end
346
+
332
347
  # returns the default rest interface, allows using rest_interface_name = "your"
333
348
  # or override this method to return @your_interface if needed
334
349
  def rest_interface
@@ -600,25 +615,25 @@ EOT
600
615
  verify_args!(args:args, optparse:optparse, count: 0)
601
616
  end
602
617
  connect(options)
603
- # load or prompt for type
604
- if rest_has_type && type_option_type.nil?
605
- if record_type_id.nil?
606
- #raise_command_error "#{rest_type_label} is required.\n#{optparse}"
607
- type_list = rest_type_interface.list({max:10000, creatable:true})[rest_type_list_key]
608
- type_dropdown_options = respond_to?("#{rest_key}_type_list_to_options", true) ? send("#{rest_key}_type_list_to_options", type_list) : type_list.collect {|it| {'name' => it['name'], 'value' => it['code']} }
609
- record_type_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'fieldLabel' => rest_type_label, 'type' => 'select', 'selectOptions' => type_dropdown_options, 'required' => true}], options[:options], @api_client)['type']
610
- end
611
- record_type = rest_type_find_by_name_or_id(record_type_id)
612
- if record_type.nil?
613
- return 1, "#{rest_type_label} not found for '#{record_type_id}"
614
- end
615
- end
616
618
  passed_options = parse_passed_options(options)
617
619
  payload = {}
618
620
  if options[:payload]
619
621
  payload = options[:payload]
620
622
  payload.deep_merge!({rest_object_key => passed_options})
621
623
  else
624
+ # load or prompt for type
625
+ if rest_has_type && type_option_type.nil?
626
+ if record_type_id.nil?
627
+ #raise_command_error "#{rest_type_label} is required.\n#{optparse}"
628
+ type_list = rest_type_interface.list({max:10000, creatable:true})[rest_type_list_key]
629
+ type_dropdown_options = respond_to?("#{rest_key}_type_list_to_options", true) ? send("#{rest_key}_type_list_to_options", type_list) : type_list.collect {|it| {'name' => it['name'], 'value' => it['code']} }
630
+ record_type_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'fieldLabel' => rest_type_label, 'type' => 'select', 'selectOptions' => type_dropdown_options, 'required' => true}], options[:options], @api_client)['type']
631
+ end
632
+ record_type = rest_type_find_by_name_or_id(record_type_id)
633
+ if record_type.nil?
634
+ return 1, "#{rest_type_label} not found for '#{record_type_id}"
635
+ end
636
+ end
622
637
  record_payload = {}
623
638
  if record_name
624
639
  record_payload['name'] = record_name
@@ -667,7 +682,7 @@ EOT
667
682
  end
668
683
  end
669
684
  api_params = (options[:params] || {}).merge(record_payload)
670
- v_prompt = Morpheus::Cli::OptionTypes.prompt(my_option_types, options[:options], @api_client, api_params)
685
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(my_option_types, options[:options], @api_client, api_params, false, true)
671
686
  v_prompt.deep_compact!
672
687
  v_prompt.booleanize! # 'on' => true
673
688
  record_payload.deep_merge!(v_prompt)
@@ -679,6 +694,18 @@ EOT
679
694
  v_prompt.booleanize! # 'on' => true
680
695
  record_payload.deep_merge!(v_prompt)
681
696
  end
697
+ # permissions
698
+ if rest_perms_config[:enabled]
699
+ if rest_perms_config[:version] == 2
700
+ perms = prompt_permissions_v2(options.deep_merge(rest_perms_config[:options] || {}), rest_perms_config[:excludes] || [])
701
+ else
702
+ perms = prompt_permissions(options.deep_merge(rest_perms_config[:options] || {}), rest_perms_config[:excludes] || [])
703
+ end
704
+ if !rest_perms_config[:name].nil?
705
+ perms.transform_keys! {|k| k == 'resourcePermissions' ? rest_perms_config[:name] : k}
706
+ end
707
+ record_payload.merge!(perms)
708
+ end
682
709
  payload[rest_object_key] = record_payload
683
710
  end
684
711
  rest_interface.setopts(options)