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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +4 -0
- data/lib/morpheus/api/clusters_interface.rb +12 -0
- data/lib/morpheus/api/network_pool_servers_interface.rb +7 -0
- data/lib/morpheus/api/prices_interface.rb +6 -0
- data/lib/morpheus/api/scale_thresholds_interface.rb +9 -0
- data/lib/morpheus/cli/cli_command.rb +55 -23
- data/lib/morpheus/cli/commands/apps.rb +2 -2
- data/lib/morpheus/cli/commands/cloud_resource_pools_command.rb +33 -2
- data/lib/morpheus/cli/commands/clouds.rb +61 -31
- data/lib/morpheus/cli/commands/clusters.rb +66 -5
- data/lib/morpheus/cli/commands/cypher_command.rb +22 -23
- data/lib/morpheus/cli/commands/hosts.rb +5 -1
- data/lib/morpheus/cli/commands/instances.rb +12 -12
- data/lib/morpheus/cli/commands/integrations_command.rb +1 -1
- data/lib/morpheus/cli/commands/invoices_command.rb +8 -1
- data/lib/morpheus/cli/commands/jobs_command.rb +45 -225
- data/lib/morpheus/cli/commands/library_container_types_command.rb +52 -3
- data/lib/morpheus/cli/commands/library_option_lists_command.rb +18 -8
- data/lib/morpheus/cli/commands/library_option_types_command.rb +56 -62
- data/lib/morpheus/cli/commands/load_balancers.rb +11 -19
- data/lib/morpheus/cli/commands/network_pool_servers_command.rb +5 -2
- data/lib/morpheus/cli/commands/prices_command.rb +25 -11
- data/lib/morpheus/cli/commands/roles.rb +475 -70
- data/lib/morpheus/cli/commands/scale_thresholds.rb +103 -0
- data/lib/morpheus/cli/commands/tasks.rb +64 -22
- data/lib/morpheus/cli/commands/user_sources_command.rb +107 -39
- data/lib/morpheus/cli/commands/users.rb +10 -10
- data/lib/morpheus/cli/commands/view.rb +1 -0
- data/lib/morpheus/cli/commands/workflows.rb +21 -14
- data/lib/morpheus/cli/error_handler.rb +13 -4
- data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -1
- data/lib/morpheus/cli/mixins/execution_request_helper.rb +1 -1
- data/lib/morpheus/cli/mixins/infrastructure_helper.rb +3 -3
- data/lib/morpheus/cli/mixins/jobs_helper.rb +173 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +120 -38
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +2 -3
- data/lib/morpheus/cli/mixins/rest_command.rb +41 -14
- data/lib/morpheus/cli/option_types.rb +69 -13
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/logging.rb +6 -8
- data/lib/morpheus/routes.rb +3 -5
- 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
|
-
|
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
|
-
|
646
|
-
|
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
|
-
|
663
|
-
|
664
|
-
|
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
|
-
|
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
|
-
|
48
|
-
|
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
|
-
|
64
|
-
|
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'] : '
|
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
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|