morpheus-cli 5.0.0 → 5.2.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +16 -0
- data/lib/morpheus/api/billing_interface.rb +1 -0
- data/lib/morpheus/api/deploy_interface.rb +1 -1
- data/lib/morpheus/api/deployments_interface.rb +20 -1
- data/lib/morpheus/api/forgot_password_interface.rb +17 -0
- data/lib/morpheus/api/instances_interface.rb +16 -2
- data/lib/morpheus/api/invoices_interface.rb +12 -3
- data/lib/morpheus/api/search_interface.rb +13 -0
- data/lib/morpheus/api/servers_interface.rb +14 -0
- data/lib/morpheus/api/service_catalog_interface.rb +89 -0
- data/lib/morpheus/api/usage_interface.rb +18 -0
- data/lib/morpheus/cli.rb +6 -2
- data/lib/morpheus/cli/apps.rb +3 -23
- data/lib/morpheus/cli/budgets_command.rb +389 -319
- data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
- data/lib/morpheus/cli/cli_command.rb +51 -10
- data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -13
- data/lib/morpheus/cli/commands/standard/history_command.rb +9 -3
- data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
- data/lib/morpheus/cli/containers_command.rb +0 -24
- data/lib/morpheus/cli/cypher_command.rb +6 -2
- data/lib/morpheus/cli/dashboard_command.rb +260 -20
- data/lib/morpheus/cli/deploy.rb +199 -90
- data/lib/morpheus/cli/deployments.rb +341 -28
- data/lib/morpheus/cli/deploys.rb +206 -41
- data/lib/morpheus/cli/error_handler.rb +7 -0
- data/lib/morpheus/cli/forgot_password.rb +133 -0
- data/lib/morpheus/cli/groups.rb +1 -1
- data/lib/morpheus/cli/health_command.rb +59 -2
- data/lib/morpheus/cli/hosts.rb +271 -39
- data/lib/morpheus/cli/instances.rb +228 -129
- data/lib/morpheus/cli/invoices_command.rb +100 -20
- data/lib/morpheus/cli/jobs_command.rb +94 -92
- data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
- data/lib/morpheus/cli/library_option_types_command.rb +10 -5
- data/lib/morpheus/cli/logs_command.rb +9 -6
- data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
- data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
- data/lib/morpheus/cli/mixins/print_helper.rb +13 -27
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +108 -5
- data/lib/morpheus/cli/option_types.rb +271 -22
- data/lib/morpheus/cli/remote.rb +35 -10
- data/lib/morpheus/cli/reports_command.rb +99 -30
- data/lib/morpheus/cli/roles.rb +193 -155
- data/lib/morpheus/cli/search_command.rb +182 -0
- data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
- data/lib/morpheus/cli/setup.rb +1 -1
- data/lib/morpheus/cli/shell.rb +33 -11
- data/lib/morpheus/cli/tasks.rb +29 -32
- data/lib/morpheus/cli/usage_command.rb +64 -11
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +429 -254
- data/lib/morpheus/cli/whoami.rb +6 -6
- data/lib/morpheus/cli/workflows.rb +33 -40
- data/lib/morpheus/formatters.rb +75 -18
- data/lib/morpheus/terminal.rb +6 -2
- metadata +10 -4
- data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
data/lib/morpheus/cli/setup.rb
CHANGED
data/lib/morpheus/cli/shell.rb
CHANGED
@@ -92,6 +92,13 @@ class Morpheus::Cli::Shell
|
|
92
92
|
def recalculate_auto_complete_commands
|
93
93
|
@morpheus_commands = Morpheus::Cli::CliRegistry.all.keys.reject {|k| [:shell].include?(k) }
|
94
94
|
@shell_commands = [:clear, :history, :reload, :help, :exit]
|
95
|
+
@shell_command_descriptions = {
|
96
|
+
:clear => "Clear terminal output and move cursor to the top",
|
97
|
+
:history => "View morpheus shell command history",
|
98
|
+
:reload => "Reload the shell, can be useful when developing",
|
99
|
+
:help => "Print this help",
|
100
|
+
:exit => "Exit the morpheus shell"
|
101
|
+
}
|
95
102
|
@alias_commands = Morpheus::Cli::CliRegistry.all_aliases.keys
|
96
103
|
@exploded_commands = []
|
97
104
|
Morpheus::Cli::CliRegistry.all.each do |cmd, klass|
|
@@ -336,6 +343,11 @@ class Morpheus::Cli::Shell
|
|
336
343
|
#Morpheus::Logging::DarkPrinter.puts "Shell command: #{input}"
|
337
344
|
input = input.to_s.strip
|
338
345
|
|
346
|
+
# allow pasting in commands that have 'morpheus ' prefix
|
347
|
+
if input[0..(prog_name.size)] == "#{prog_name} "
|
348
|
+
input = input[(prog_name.size + 1)..-1] || ""
|
349
|
+
end
|
350
|
+
|
339
351
|
if !input.empty?
|
340
352
|
|
341
353
|
if input == 'exit'
|
@@ -345,26 +357,36 @@ class Morpheus::Cli::Shell
|
|
345
357
|
return 0
|
346
358
|
#exit 0
|
347
359
|
elsif input == 'help'
|
360
|
+
out = ""
|
348
361
|
if @temporary_shell_mode
|
349
|
-
|
362
|
+
out << "You are in a (temporary) morpheus shell\n"
|
350
363
|
else
|
351
|
-
|
364
|
+
out << "You are in a morpheus shell.\n"
|
352
365
|
end
|
353
|
-
|
366
|
+
out << "See the available commands below.\n"
|
354
367
|
|
355
|
-
|
368
|
+
out << "\nCommands:"
|
356
369
|
# commands = @morpheus_commands + @shell_commands
|
357
|
-
@morpheus_commands.sort.each {|cmd|
|
358
|
-
|
370
|
+
# @morpheus_commands.sort.each {|cmd|
|
371
|
+
# out << "\t#{cmd.to_s}\n"
|
372
|
+
# }
|
373
|
+
sorted_commands = Morpheus::Cli::CliRegistry.all.values.sort { |x,y| x.command_name.to_s <=> y.command_name.to_s }
|
374
|
+
sorted_commands.each {|cmd|
|
375
|
+
# JD: not ready to show description yet, gotta finish filling in every command first
|
376
|
+
# maybe change 'View and manage' to something more concise like 'Manage'
|
377
|
+
# out << "\t#{cmd.command_name.to_s.ljust(28, ' ')} #{cmd.command_description}\n"
|
378
|
+
out << "\t#{cmd.command_name.to_s}\n"
|
359
379
|
}
|
360
380
|
#puts "\n"
|
361
|
-
|
381
|
+
out << "\nShell Commands:\n"
|
362
382
|
@shell_commands.each {|cmd|
|
363
|
-
|
383
|
+
# out << "\t#{cmd.to_s.ljust(28, ' ')} #{@shell_command_descriptions ? @shell_command_descriptions[cmd] : ''}\n"
|
384
|
+
out << "\t#{cmd.to_s}\n"
|
364
385
|
}
|
365
|
-
|
366
|
-
|
367
|
-
|
386
|
+
out << "\n"
|
387
|
+
out << "For more information, see https://github.com/gomorpheus/morpheus-cli/wiki"
|
388
|
+
out << "\n"
|
389
|
+
print out
|
368
390
|
return 0
|
369
391
|
elsif input =~ /^\s*#/
|
370
392
|
Morpheus::Logging::DarkPrinter.puts "ignored comment: #{input}" if Morpheus::Logging.debug?
|
data/lib/morpheus/cli/tasks.rb
CHANGED
@@ -27,34 +27,31 @@ class Morpheus::Cli::Tasks
|
|
27
27
|
params = {}
|
28
28
|
options = {}
|
29
29
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
30
|
-
opts.banner = subcommand_usage()
|
30
|
+
opts.banner = subcommand_usage("[search]")
|
31
31
|
opts.on('-t', '--type x,y,z', Array, "Filter by task type code(s)") do |val|
|
32
32
|
params['taskTypeCodes'] = val
|
33
33
|
end
|
34
|
-
|
34
|
+
build_standard_list_options(opts, options)
|
35
|
+
opts.footer = "List tasks."
|
35
36
|
end
|
36
37
|
optparse.parse!(args)
|
38
|
+
connect(options)
|
37
39
|
if args.count > 0
|
38
|
-
|
40
|
+
options[:phrase] = args.join(" ")
|
39
41
|
end
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
@tasks_interface.
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
render_result = render_with_format(json_response, options, 'tasks')
|
51
|
-
return 0 if render_result
|
52
|
-
|
42
|
+
params.merge!(parse_list_options(options))
|
43
|
+
@tasks_interface.setopts(options)
|
44
|
+
if options[:dry_run]
|
45
|
+
print_dry_run @tasks_interface.dry.list(params)
|
46
|
+
return
|
47
|
+
end
|
48
|
+
json_response = @tasks_interface.list(params)
|
49
|
+
tasks = json_response['tasks']
|
50
|
+
render_response(json_response, options, 'tasks') do
|
53
51
|
title = "Morpheus Tasks"
|
54
52
|
subtitles = []
|
55
53
|
subtitles += parse_list_subtitles(options)
|
56
54
|
print_h1 title, subtitles
|
57
|
-
tasks = json_response['tasks']
|
58
55
|
if tasks.empty?
|
59
56
|
print cyan,"No tasks found.",reset,"\n"
|
60
57
|
else
|
@@ -63,11 +60,13 @@ class Morpheus::Cli::Tasks
|
|
63
60
|
print_results_pagination(json_response)
|
64
61
|
end
|
65
62
|
print reset,"\n"
|
66
|
-
return 0
|
67
|
-
rescue RestClient::Exception => e
|
68
|
-
print_rest_exception(e, options)
|
69
|
-
exit 1
|
70
63
|
end
|
64
|
+
if tasks.empty?
|
65
|
+
return 1, "no tasks found"
|
66
|
+
else
|
67
|
+
return 0, nil
|
68
|
+
end
|
69
|
+
|
71
70
|
end
|
72
71
|
|
73
72
|
def get(args)
|
@@ -334,10 +333,9 @@ class Morpheus::Cli::Tasks
|
|
334
333
|
build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
|
335
334
|
end
|
336
335
|
optparse.parse!(args)
|
337
|
-
|
338
|
-
raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
|
339
|
-
end
|
336
|
+
#verify_args!(args:args, count:1, optparse:optparse)
|
340
337
|
if args[0]
|
338
|
+
# task_name = args[0]
|
341
339
|
task_name = args[0]
|
342
340
|
end
|
343
341
|
# if task_name.nil? || task_type_name.nil?
|
@@ -425,6 +423,9 @@ class Morpheus::Cli::Tasks
|
|
425
423
|
has_file_content = true
|
426
424
|
it['fieldContext'] = nil
|
427
425
|
it['fieldName'] = 'file'
|
426
|
+
# this should be required right!? fix api optionType data plz
|
427
|
+
it['required'] = true
|
428
|
+
it['defaultValue'] = 'local'
|
428
429
|
else
|
429
430
|
if it['fieldContext'].nil? || it['fieldContext'] == ''
|
430
431
|
it['fieldContext'] = 'taskOptions'
|
@@ -658,9 +659,7 @@ class Morpheus::Cli::Tasks
|
|
658
659
|
build_common_options(opts, options, [:options, :payload, :json, :dry_run, :quiet, :remote])
|
659
660
|
end
|
660
661
|
optparse.parse!(args)
|
661
|
-
|
662
|
-
raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
|
663
|
-
end
|
662
|
+
verify_args!(args:args, count:1, optparse:optparse)
|
664
663
|
task_name = args[0]
|
665
664
|
connect(options)
|
666
665
|
begin
|
@@ -734,7 +733,6 @@ class Morpheus::Cli::Tasks
|
|
734
733
|
|
735
734
|
def remove(args)
|
736
735
|
params = {}
|
737
|
-
task_name = args[0]
|
738
736
|
options = {}
|
739
737
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
740
738
|
opts.banner = subcommand_usage("[task]")
|
@@ -744,10 +742,8 @@ class Morpheus::Cli::Tasks
|
|
744
742
|
end
|
745
743
|
end
|
746
744
|
optparse.parse!(args)
|
747
|
-
|
748
|
-
|
749
|
-
exit 1
|
750
|
-
end
|
745
|
+
verify_args!(args:args, count:1, optparse:optparse)
|
746
|
+
task_name = args[0]
|
751
747
|
connect(options)
|
752
748
|
begin
|
753
749
|
task = find_task_by_name_or_id(task_name)
|
@@ -820,6 +816,7 @@ class Morpheus::Cli::Tasks
|
|
820
816
|
if args.count != 1
|
821
817
|
raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
|
822
818
|
end
|
819
|
+
verify_args!(args:args, count:1, optparse:optparse)
|
823
820
|
task_name = args[0]
|
824
821
|
connect(options)
|
825
822
|
begin
|
@@ -2,18 +2,18 @@ require 'morpheus/cli/cli_command'
|
|
2
2
|
|
3
3
|
# CLI command usages
|
4
4
|
# UI is Costing - Usage
|
5
|
-
# API is /
|
5
|
+
# API is /usage and returns usages
|
6
6
|
class Morpheus::Cli::UsageCommand
|
7
7
|
include Morpheus::Cli::CliCommand
|
8
8
|
include Morpheus::Cli::OptionSourceHelper
|
9
9
|
|
10
10
|
set_command_name :'usage'
|
11
11
|
|
12
|
-
register_subcommands :list
|
12
|
+
register_subcommands :list, :get
|
13
13
|
|
14
14
|
def connect(opts)
|
15
15
|
@api_client = establish_remote_appliance_connection(opts)
|
16
|
-
@
|
16
|
+
@usage_interface = @api_client.usage
|
17
17
|
end
|
18
18
|
|
19
19
|
def handle(args)
|
@@ -33,10 +33,10 @@ class Morpheus::Cli::UsageCommand
|
|
33
33
|
options[:cloud] = val
|
34
34
|
end
|
35
35
|
opts.on('--start DATE', String, "Start date in the format YYYY-MM-DD.") do |val|
|
36
|
-
params['startDate'] = val #parse_time(val).utc.iso8601
|
36
|
+
params['startDate'] = val # parse_time(val).utc.iso8601
|
37
37
|
end
|
38
|
-
opts.on('--end DATE', String, "End date in the format YYYY-MM-DD. Default is
|
39
|
-
params['endDate'] = val #parse_time(val).utc.iso8601
|
38
|
+
opts.on('--end DATE', String, "End date in the format YYYY-MM-DD. Default is the current date.") do |val|
|
39
|
+
params['endDate'] = val # parse_time(val).utc.iso8601
|
40
40
|
end
|
41
41
|
opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 5.") do |val|
|
42
42
|
options[:sigdig] = val.to_i
|
@@ -64,12 +64,12 @@ class Morpheus::Cli::UsageCommand
|
|
64
64
|
}
|
65
65
|
end
|
66
66
|
|
67
|
-
@
|
67
|
+
@usage_interface.setopts(options)
|
68
68
|
if options[:dry_run]
|
69
|
-
print_dry_run @
|
69
|
+
print_dry_run @usage_interface.dry.list(params)
|
70
70
|
return
|
71
71
|
end
|
72
|
-
json_response = @
|
72
|
+
json_response = @usage_interface.list(params)
|
73
73
|
usages = json_response[usage_list_key]
|
74
74
|
render_response(json_response, options, usage_list_key) do
|
75
75
|
print_h1 "Morpheus Usages", parse_list_subtitles(options), options
|
@@ -85,6 +85,7 @@ class Morpheus::Cli::UsageCommand
|
|
85
85
|
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
86
86
|
"End Date" => lambda {|it| format_local_dt(it['endDate']) },
|
87
87
|
"Usage Status" => lambda {|it| format_usage_status(it) },
|
88
|
+
"Usage Cost" => lambda {|it| format_money(it['costDetails']['cost'], it['currency'] || 'USD', {sigdig: (options[:sigdig] || 5)}) },
|
88
89
|
"Usage Price" => lambda {|it| format_money(it['price'], it['currency'] || 'USD', {sigdig: (options[:sigdig] || 5)}) },
|
89
90
|
}
|
90
91
|
print as_pretty_table(usages, list_columns.upcase_keys!, options)
|
@@ -99,6 +100,58 @@ class Morpheus::Cli::UsageCommand
|
|
99
100
|
end
|
100
101
|
end
|
101
102
|
|
103
|
+
def get(args)
|
104
|
+
params = {}
|
105
|
+
options = {}
|
106
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
107
|
+
opts.banner = subcommand_usage("[usage]")
|
108
|
+
build_standard_get_options(opts, options)
|
109
|
+
opts.footer = <<-EOT
|
110
|
+
Get details about a specific usage.
|
111
|
+
[usage] is required. This is the id of a usage record.
|
112
|
+
EOT
|
113
|
+
end
|
114
|
+
optparse.parse!(args)
|
115
|
+
verify_args!(args:args, optparse:optparse, min:1)
|
116
|
+
connect(options)
|
117
|
+
id_list = parse_id_list(args)
|
118
|
+
return run_command_for_each_arg(id_list) do |arg|
|
119
|
+
_get(arg, params, options)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def _get(id, params, options)
|
124
|
+
usage = nil
|
125
|
+
@usage_interface.setopts(options)
|
126
|
+
if options[:dry_run]
|
127
|
+
print_dry_run @usage_interface.dry.get(id, params)
|
128
|
+
return
|
129
|
+
end
|
130
|
+
json_response = @usage_interface.get(id, params)
|
131
|
+
usage = json_response[usage_object_key]
|
132
|
+
render_response(json_response, options, usage_object_key) do
|
133
|
+
print_h1 "Usage Details", [], options
|
134
|
+
print cyan
|
135
|
+
show_columns = {
|
136
|
+
"ID" => 'id',
|
137
|
+
"Cloud" => 'zoneName',
|
138
|
+
"Type" => lambda {|it| format_usage_type(it) },
|
139
|
+
"Name" => 'name',
|
140
|
+
"Plan" => 'planName',
|
141
|
+
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
142
|
+
"End Date" => lambda {|it| format_local_dt(it['endDate']) },
|
143
|
+
"Usage Status" => lambda {|it| format_usage_status(it) },
|
144
|
+
"Usage Cost" => lambda {|it| format_money(it['costDetails']['cost'], it['currency'] || 'USD', {sigdig: (options[:sigdig] || 5)}) },
|
145
|
+
"Usage Price" => lambda {|it| format_money(it['price'], it['currency'] || 'USD', {sigdig: (options[:sigdig] || 5)}) },
|
146
|
+
}
|
147
|
+
print_description_list(show_columns, usage)
|
148
|
+
|
149
|
+
# print_h2 "Applicable Prices"
|
150
|
+
|
151
|
+
print reset,"\n"
|
152
|
+
end
|
153
|
+
return 0, nil
|
154
|
+
end
|
102
155
|
|
103
156
|
private
|
104
157
|
|
@@ -141,9 +194,9 @@ class Morpheus::Cli::UsageCommand
|
|
141
194
|
#return usage['status'].to_s.capitalize
|
142
195
|
status_string = usage['status'].to_s
|
143
196
|
if status_string == 'stopped'
|
144
|
-
return "#{
|
197
|
+
return "#{red}#{status_string.upcase}#{return_color}"
|
145
198
|
else
|
146
|
-
return "#{
|
199
|
+
return "#{green}#{status_string.upcase}#{return_color}"
|
147
200
|
end
|
148
201
|
end
|
149
202
|
|
data/lib/morpheus/cli/version.rb
CHANGED
@@ -8,6 +8,7 @@ require 'morpheus/cli/cli_command'
|
|
8
8
|
|
9
9
|
class Morpheus::Cli::VirtualImages
|
10
10
|
include Morpheus::Cli::CliCommand
|
11
|
+
include Morpheus::Cli::ProvisioningHelper
|
11
12
|
|
12
13
|
register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :types => :virtual_image_types
|
13
14
|
alias_subcommand :details, :get
|
@@ -27,6 +28,7 @@ class Morpheus::Cli::VirtualImages
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def list(args)
|
31
|
+
params = {}
|
30
32
|
options = {}
|
31
33
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
32
34
|
opts.banner = subcommand_usage()
|
@@ -42,42 +44,48 @@ class Morpheus::Cli::VirtualImages
|
|
42
44
|
opts.on('--system', "System Images" ) do
|
43
45
|
options[:filterType] = 'System'
|
44
46
|
end
|
45
|
-
|
47
|
+
opts.on('--tags Name=Value',String, "Filter by tags (metadata name value pairs).") do |val|
|
48
|
+
val.split(",").each do |value_pair|
|
49
|
+
k,v = value_pair.strip.split("=")
|
50
|
+
options[:tags] ||= {}
|
51
|
+
options[:tags][k] ||= []
|
52
|
+
options[:tags][k] << (v || '')
|
53
|
+
end
|
54
|
+
end
|
55
|
+
opts.on('-a', '--details', "Show more details." ) do
|
56
|
+
options[:details] = true
|
57
|
+
end
|
58
|
+
build_standard_list_options(opts, options)
|
46
59
|
opts.footer = "List virtual images."
|
47
60
|
end
|
48
61
|
optparse.parse!(args)
|
49
62
|
connect(options)
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
json_response = @virtual_images_interface.get(params)
|
65
|
-
|
66
|
-
if options[:json]
|
67
|
-
puts as_json(json_response, options, "virtualImages")
|
68
|
-
return 0
|
69
|
-
elsif options[:yaml]
|
70
|
-
puts as_yaml(json_response, options, "virtualImages")
|
71
|
-
return 0
|
72
|
-
elsif options[:csv]
|
73
|
-
puts records_as_csv(json_response["virtualImages"], options)
|
74
|
-
return 0
|
63
|
+
# verify_args!(args:args, optparse:optparse, count:0)
|
64
|
+
if args.count > 0
|
65
|
+
options[:phrase] = args.join(" ")
|
66
|
+
end
|
67
|
+
params.merge!(parse_list_options(options))
|
68
|
+
if options[:imageType]
|
69
|
+
params[:imageType] = options[:imageType]
|
70
|
+
end
|
71
|
+
if options[:filterType]
|
72
|
+
params[:filterType] = options[:filterType]
|
73
|
+
end
|
74
|
+
if options[:tags]
|
75
|
+
options[:tags].each do |k,v|
|
76
|
+
params['tags.' + k] = v
|
75
77
|
end
|
76
|
-
|
77
|
-
|
78
|
-
|
78
|
+
end
|
79
|
+
@virtual_images_interface.setopts(options)
|
80
|
+
if options[:dry_run]
|
81
|
+
print_dry_run @virtual_images_interface.dry.list(params)
|
82
|
+
return
|
83
|
+
end
|
84
|
+
json_response = @virtual_images_interface.list(params)
|
85
|
+
images = json_response['virtualImages']
|
86
|
+
render_response(json_response, options, 'virtualImages') do
|
79
87
|
title = "Morpheus Virtual Images"
|
80
|
-
subtitles =
|
88
|
+
subtitles = parse_list_subtitles(options)
|
81
89
|
if options[:imageType]
|
82
90
|
subtitles << "Image Type: #{options[:imageType]}".strip
|
83
91
|
end
|
@@ -91,129 +99,188 @@ class Morpheus::Cli::VirtualImages
|
|
91
99
|
if images.empty?
|
92
100
|
print cyan,"No virtual images found.",reset,"\n"
|
93
101
|
else
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
102
|
+
virtual_image_column_definitions = {
|
103
|
+
"ID" => 'id',
|
104
|
+
"Name" => 'name',
|
105
|
+
"Type" => lambda {|it|
|
106
|
+
# yick, api should return the type with every virtualImage
|
107
|
+
image_type = virtual_image_type_for_name_or_code(it['imageType'])
|
108
|
+
image_type ? "#{image_type['name']}" : it['imageType']
|
109
|
+
},
|
110
|
+
"Operating System" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
|
111
|
+
"Storage" => lambda {|it| !it['storageProvider'].nil? ? it['storageProvider']['name'] : 'Default' },
|
112
|
+
"Size" => lambda {|it| it['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{it['rawSize']} B").pretty}" },
|
113
|
+
"Visibility" => lambda {|it| it['visibility'] },
|
114
|
+
# "Tenant" => lambda {|it| it['account'].instance_of?(Hash) ? it['account']['name'] : it['ownerId'] },
|
115
|
+
"Tenants" => lambda {|it| format_list(it['accounts'].collect {|a| a['name'] }, '', 3) rescue '' },
|
116
|
+
"Source" => lambda {|it| format_virtual_image_source(it) },
|
117
|
+
"Created" => lambda {|it| format_local_dt(it['dateCreated']) },
|
118
|
+
"Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
|
119
|
+
"Tags" => lambda {|it| it['tags'] ? it['tags'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
|
120
|
+
}
|
121
|
+
if json_response['multiTenant'] != true
|
122
|
+
virtual_image_column_definitions.delete("Visibility")
|
123
|
+
virtual_image_column_definitions.delete("Tenants")
|
124
|
+
end
|
125
|
+
if options[:details] != true
|
126
|
+
virtual_image_column_definitions.delete("Tags")
|
127
|
+
virtual_image_column_definitions.delete("Created")
|
128
|
+
virtual_image_column_definitions.delete("Updated")
|
98
129
|
end
|
99
|
-
|
100
|
-
columns = options[:include_fields] if options[:include_fields]
|
101
|
-
print cyan
|
102
|
-
print as_pretty_table(rows, columns, options)
|
130
|
+
print as_pretty_table(images, virtual_image_column_definitions.upcase_keys!, options)
|
103
131
|
print_results_pagination(json_response)
|
104
132
|
end
|
105
133
|
print reset,"\n"
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
134
|
+
end
|
135
|
+
if images.empty?
|
136
|
+
return -1, "no virtual images found"
|
137
|
+
else
|
138
|
+
return 0, nil
|
111
139
|
end
|
112
140
|
end
|
113
141
|
|
114
142
|
def get(args)
|
143
|
+
params = {}
|
115
144
|
options = {}
|
116
|
-
show_details = false
|
117
145
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
118
|
-
opts.banner = subcommand_usage("[
|
119
|
-
opts.on('--details', "Show more details." ) do
|
120
|
-
|
121
|
-
end
|
122
|
-
|
123
|
-
|
124
|
-
|
146
|
+
opts.banner = subcommand_usage("[image]")
|
147
|
+
opts.on('-a', '--details', "Show more details." ) do
|
148
|
+
options[:details] = true
|
149
|
+
end
|
150
|
+
opts.on('--tags LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
|
151
|
+
options[:tags] = val
|
152
|
+
end
|
153
|
+
build_standard_get_options(opts, options)
|
154
|
+
opts.footer = <<-EOT
|
155
|
+
Get details about a virtual image.
|
156
|
+
[image] is required. This is the name or id of a virtual image.
|
157
|
+
EOT
|
125
158
|
end
|
126
159
|
optparse.parse!(args)
|
127
|
-
|
128
|
-
puts optparse
|
129
|
-
exit 1
|
130
|
-
end
|
131
|
-
image_name = args[0]
|
160
|
+
verify_args!(args:args, optparse:optparse, min:1)
|
132
161
|
connect(options)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
162
|
+
id_list = parse_id_list(args)
|
163
|
+
# lookup IDs if names are given
|
164
|
+
id_list = id_list.collect do |id|
|
165
|
+
if id.to_s =~ /\A\d{1,}\Z/
|
166
|
+
id
|
167
|
+
else
|
168
|
+
image = find_virtual_image_by_name_or_id(id)
|
169
|
+
if image
|
170
|
+
image['id']
|
138
171
|
else
|
139
|
-
|
172
|
+
raise_command_error "virtual image not found for name '#{id}'"
|
140
173
|
end
|
141
|
-
return
|
142
|
-
end
|
143
|
-
image = find_virtual_image_by_name_or_id(image_name)
|
144
|
-
return 1 if image.nil?
|
145
|
-
# refetch
|
146
|
-
json_response = @virtual_images_interface.get(image['id'])
|
147
|
-
if options[:json]
|
148
|
-
puts as_json(json_response, options, "virtualImage")
|
149
|
-
return 0
|
150
|
-
elsif options[:yaml]
|
151
|
-
puts as_yaml(json_response, options, "virtualImage")
|
152
|
-
return 0
|
153
|
-
elsif options[:csv]
|
154
|
-
puts records_as_csv([json_response["virtualImage"]], options)
|
155
|
-
return 0
|
156
174
|
end
|
175
|
+
end
|
176
|
+
return run_command_for_each_arg(id_list) do |arg|
|
177
|
+
_get(arg, params, options)
|
178
|
+
end
|
179
|
+
end
|
157
180
|
|
181
|
+
def _get(id, params, options)
|
182
|
+
@virtual_images_interface.setopts(options)
|
183
|
+
if options[:dry_run]
|
184
|
+
print_dry_run @virtual_images_interface.dry.get(id.to_i)
|
185
|
+
return
|
186
|
+
end
|
187
|
+
json_response = @virtual_images_interface.get(id.to_i)
|
158
188
|
image = json_response['virtualImage']
|
189
|
+
image_config = image['config'] || {}
|
190
|
+
image_volumes = image['volumes'] || []
|
159
191
|
image_files = json_response['cloudFiles'] || json_response['files']
|
160
|
-
|
161
|
-
|
162
192
|
image_type = virtual_image_type_for_name_or_code(image['imageType'])
|
163
193
|
image_type_display = image_type ? "#{image_type['name']}" : image['imageType']
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
"
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
194
|
+
render_response(json_response, options, 'virtualImage') do
|
195
|
+
print_h1 "Virtual Image Details", [], options
|
196
|
+
description_cols = {
|
197
|
+
"ID" => 'id',
|
198
|
+
"Name" => 'name',
|
199
|
+
"Type" => lambda {|it| image_type_display },
|
200
|
+
"Operating System" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
|
201
|
+
"Storage" => lambda {|it| !image['storageProvider'].nil? ? image['storageProvider']['name'] : 'Default' },
|
202
|
+
"Size" => lambda {|it| image['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{image['rawSize']} B").pretty}" },
|
203
|
+
"Azure Publisher" => lambda {|it| image_config['publisher'] },
|
204
|
+
"Azure Offer" => lambda {|it| image_config['offer'] },
|
205
|
+
"Azure Sku" => lambda {|it| image_config['sku'] },
|
206
|
+
"Azure Version" => lambda {|it| image_config['version'] },
|
207
|
+
"Source" => lambda {|it| format_virtual_image_source(it) },
|
208
|
+
"Tags" => lambda {|it| it['tags'] ? it['tags'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
|
209
|
+
"Created" => lambda {|it| format_local_dt(it['dateCreated']) },
|
210
|
+
"Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
|
211
|
+
}
|
212
|
+
description_cols.delete("Tags") if image['tags'].nil? || image['tags'].empty?
|
213
|
+
if image['imageType'] == "azure-reference" || image['imageType'] == "azure"
|
214
|
+
description_cols.delete("Size")
|
215
|
+
description_cols.delete("Storage")
|
216
|
+
description_cols["Source"] = lambda {|it| "#{bold}#{cyan}AZURE#{reset}#{cyan}" }
|
217
|
+
else
|
218
|
+
description_cols.delete("Azure Publisher")
|
219
|
+
description_cols.delete("Azure Sku")
|
220
|
+
description_cols.delete("Azure Offer")
|
221
|
+
description_cols.delete("Azure Version")
|
222
|
+
end
|
223
|
+
advanced_description_cols = {
|
224
|
+
#"OS Type" => lambda {|it| it['osType'] ? it['osType']['name'] : "" }, # displayed above as Operating System
|
225
|
+
"Min Memory" => lambda {|it| it['minRam'].to_i != 0 ? Filesize.from("#{it['minRam']} B").pretty : "" },
|
226
|
+
"Min Disk" => lambda {|it| it['minDisk'].to_i != 0 ? Filesize.from("#{it['minDisk']} B").pretty : "" },
|
227
|
+
"Cloud Init?" => lambda {|it| format_boolean it['osType'] },
|
228
|
+
"Install Agent?" => lambda {|it| format_boolean it['osType'] },
|
229
|
+
"SSH Username" => lambda {|it| it['sshUsername'] },
|
230
|
+
"SSH Password" => lambda {|it| it['sshPassword'] },
|
231
|
+
"User Data" => lambda {|it| it['userData'] },
|
232
|
+
"Owner" => lambda {|it| it['tenant'].instance_of?(Hash) ? it['tenant']['name'] : it['ownerId'] },
|
233
|
+
"Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
|
234
|
+
"Tenants" => lambda {|it| format_tenants(it['accounts']) },
|
235
|
+
"Auto Join Domain?" => lambda {|it| format_boolean it['isAutoJoinDomain'] },
|
236
|
+
"VirtIO Drivers Loaded?" => lambda {|it| format_boolean it['virtioSupported'] },
|
237
|
+
"VM Tools Installed?" => lambda {|it| format_boolean it['vmToolsInstalled'] },
|
238
|
+
"Force Guest Customization?" => lambda {|it| format_boolean it['isForceCustomization'] },
|
239
|
+
"Trial Version" => lambda {|it| format_boolean it['trialVersion'] },
|
240
|
+
"Sysprep Enabled?" => lambda {|it| format_boolean it['isSysprep'] },
|
241
|
+
}
|
242
|
+
if options[:details]
|
243
|
+
description_cols.merge!(advanced_description_cols)
|
244
|
+
end
|
245
|
+
print_description_list(description_cols, image)
|
246
|
+
|
247
|
+
if image_volumes && !image_volumes.empty?
|
248
|
+
print_h2 "Volumes", options
|
249
|
+
image_volume_rows = image_volumes.collect do |image_volume|
|
250
|
+
{name: image_volume['name'], size: Filesize.from("#{image_volume['rawSize']} B").pretty}
|
251
|
+
end
|
252
|
+
print cyan
|
253
|
+
print as_pretty_table(image_volume_rows, [:name, :size])
|
254
|
+
print cyan
|
255
|
+
# print "\n", reset
|
207
256
|
end
|
208
|
-
|
209
|
-
|
210
|
-
|
257
|
+
|
258
|
+
if image_files
|
259
|
+
print_h2 "Files (#{image_files.size})"
|
260
|
+
# image_files.each {|image_file|
|
261
|
+
# pretty_filesize = Filesize.from("#{image_file['size']} B").pretty
|
262
|
+
# print cyan," = #{image_file['name']} [#{pretty_filesize}]", "\n"
|
263
|
+
# }
|
264
|
+
# size property changed to GB to match volumes
|
265
|
+
# contentLength is bytes
|
266
|
+
image_file_rows = image_files.collect do |image_file|
|
267
|
+
{filename: image_file['name'], size: Filesize.from("#{image_file['contentLength'] || image_file['size']} B").pretty}
|
268
|
+
end
|
269
|
+
print cyan
|
270
|
+
print as_pretty_table(image_file_rows, [:filename, :size])
|
271
|
+
# print reset,"\n"
|
272
|
+
end
|
273
|
+
|
274
|
+
if options[:details] && image_config && !image_config.empty?
|
275
|
+
print_h2 "Config", options
|
276
|
+
print cyan
|
277
|
+
print as_description_list(image_config, image_config.keys, options)
|
278
|
+
# print "\n", reset
|
279
|
+
end
|
280
|
+
|
281
|
+
print reset,"\n"
|
211
282
|
end
|
212
|
-
|
213
|
-
rescue RestClient::Exception => e
|
214
|
-
print_rest_exception(e, options)
|
215
|
-
exit 1
|
216
|
-
end
|
283
|
+
return 0, nil
|
217
284
|
end
|
218
285
|
|
219
286
|
def update(args)
|
@@ -229,60 +296,68 @@ class Morpheus::Cli::VirtualImages
|
|
229
296
|
tenants_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
|
230
297
|
end
|
231
298
|
end
|
299
|
+
opts.on('--tags LIST', String, "Tags in the format 'name:value, name:value'. This will add and remove tags.") do |val|
|
300
|
+
options[:tags] = val
|
301
|
+
end
|
302
|
+
opts.on('--add-tags TAGS', String, "Add Tags in the format 'name:value, name:value'. This will only add/update tags.") do |val|
|
303
|
+
options[:add_tags] = val
|
304
|
+
end
|
305
|
+
opts.on('--remove-tags TAGS', String, "Remove Tags in the format 'name, name:value'. This removes tags, the :value component is optional and must match if passed.") do |val|
|
306
|
+
options[:remove_tags] = val
|
307
|
+
end
|
232
308
|
build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
|
233
309
|
opts.footer = "Update a virtual image." + "\n" +
|
234
310
|
"[name] is required. This is the name or id of a virtual image."
|
235
311
|
end
|
236
312
|
optparse.parse!(args)
|
237
|
-
|
238
|
-
puts optparse
|
239
|
-
exit 1
|
240
|
-
end
|
313
|
+
verify_args!(args:args, optparse:optparse, count:1)
|
241
314
|
|
242
315
|
connect(options)
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
316
|
+
|
317
|
+
virtual_image = find_virtual_image_by_name_or_id(image_name)
|
318
|
+
return 1 if virtual_image.nil?
|
319
|
+
|
320
|
+
passed_options = parse_passed_options(options)
|
321
|
+
payload = nil
|
322
|
+
if options[:payload]
|
323
|
+
payload = options[:payload]
|
324
|
+
payload.deep_merge!({virtual_image_object_key => passed_options}) unless passed_options.empty?
|
325
|
+
else
|
326
|
+
virtual_image_payload = passed_options
|
327
|
+
if tenants_list
|
328
|
+
virtual_image_payload['accounts'] = tenants_list
|
329
|
+
end
|
330
|
+
# metadata tags
|
331
|
+
if options[:tags]
|
332
|
+
virtual_image_payload['tags'] = parse_metadata(options[:tags])
|
255
333
|
else
|
256
|
-
|
257
|
-
|
258
|
-
puts optparse
|
259
|
-
option_lines = update_virtual_image_option_types().collect {|it| "\t-O #{it['fieldContext'] ? (it['fieldContext'] + '.') : ''}#{it['fieldName']}=\"value\"" }.join("\n")
|
260
|
-
puts "\nAvailable Options:\n#{option_lines}\n\n"
|
261
|
-
exit 1
|
262
|
-
end
|
263
|
-
if tenants_list
|
264
|
-
params['accounts'] = tenants_list
|
265
|
-
end
|
266
|
-
payload = {'virtualImage' => params}
|
334
|
+
# tags = prompt_metadata(options)
|
335
|
+
# payload[virtual_image_object_key]['tags'] = tags of tags
|
267
336
|
end
|
268
|
-
|
269
|
-
if options[:
|
270
|
-
|
271
|
-
return
|
337
|
+
# metadata tags
|
338
|
+
if options[:add_tags]
|
339
|
+
virtual_image_payload['addTags'] = parse_metadata(options[:add_tags])
|
272
340
|
end
|
273
|
-
|
274
|
-
|
275
|
-
print JSON.pretty_generate(json_response)
|
276
|
-
if !response['success']
|
277
|
-
exit 1
|
278
|
-
end
|
279
|
-
else
|
280
|
-
print "\n", cyan, "Virtual Image #{image['name']} updated", reset, "\n\n"
|
341
|
+
if options[:remove_tags]
|
342
|
+
virtual_image_payload['removeTags'] = parse_metadata(options[:remove_tags])
|
281
343
|
end
|
282
|
-
|
283
|
-
|
284
|
-
|
344
|
+
if virtual_image_payload.empty?
|
345
|
+
raise_command_error "Specify at least one option to update.\n#{optparse}"
|
346
|
+
end
|
347
|
+
payload = {'virtualImage' => virtual_image_payload}
|
348
|
+
end
|
349
|
+
@virtual_images_interface.setopts(options)
|
350
|
+
if options[:dry_run]
|
351
|
+
print_dry_run @virtual_images_interface.dry.update(virtual_image['id'], payload)
|
352
|
+
return
|
353
|
+
end
|
354
|
+
json_response = @virtual_images_interface.update(virtual_image['id'], payload)
|
355
|
+
render_response(json_response, options, 'virtualImage') do
|
356
|
+
print_green_success "Updated virtual image #{virtual_image['name']}"
|
357
|
+
_get(virtual_image["id"], {}, options)
|
285
358
|
end
|
359
|
+
return 0, nil
|
360
|
+
|
286
361
|
end
|
287
362
|
|
288
363
|
def virtual_image_types(args)
|
@@ -339,6 +414,19 @@ class Morpheus::Cli::VirtualImages
|
|
339
414
|
opts.on( '-U', '--url URL', "Image File URL. This can be used instead of uploading local files." ) do |val|
|
340
415
|
file_url = val
|
341
416
|
end
|
417
|
+
opts.on( '-c', '--cloud CLOUD', "Cloud to scope image to, certain types require a cloud to be selected, eg. Azure Reference" ) do |val|
|
418
|
+
# options[:cloud] = val
|
419
|
+
options[:options]['cloud'] = val
|
420
|
+
end
|
421
|
+
opts.on( '--azure-offer OFFER', String, "Azure Reference offer value, only applies to Azure Reference" ) do |val|
|
422
|
+
options[:options]['offer'] = val
|
423
|
+
end
|
424
|
+
opts.on( '--azure-sku SKU', String, "Azure SKU value, only applies to Azure Reference" ) do |val|
|
425
|
+
options[:options]['sku'] = val
|
426
|
+
end
|
427
|
+
opts.on( '--azure-version VERSION', String, "Azure Version value, only applies to Azure Reference" ) do |val|
|
428
|
+
options[:options]['version'] = val
|
429
|
+
end
|
342
430
|
opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
|
343
431
|
if list.size == 1 && list[0] == 'null' # hacky way to clear it
|
344
432
|
tenants_list = []
|
@@ -346,7 +434,13 @@ class Morpheus::Cli::VirtualImages
|
|
346
434
|
tenants_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
|
347
435
|
end
|
348
436
|
end
|
349
|
-
|
437
|
+
opts.on('--tags LIST', String, "Metadata tags in the format 'name:value, name:value'") do |val|
|
438
|
+
options[:tags] = val
|
439
|
+
end
|
440
|
+
# build_option_type_options(opts, options, add_virtual_image_option_types)
|
441
|
+
# build_option_type_options(opts, options, add_virtual_image_advanced_option_types)
|
442
|
+
build_standard_add_options(opts, options)
|
443
|
+
|
350
444
|
opts.footer = "Create a virtual image."
|
351
445
|
end
|
352
446
|
optparse.parse!(args)
|
@@ -368,26 +462,43 @@ class Morpheus::Cli::VirtualImages
|
|
368
462
|
options[:options]['name'] ||= image_name
|
369
463
|
end
|
370
464
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
465
|
+
payload = {}
|
466
|
+
if options[:payload]
|
467
|
+
payload = options[:payload]
|
468
|
+
payload.deep_merge!({'virtualImage' => parse_passed_options(options)})
|
469
|
+
else
|
470
|
+
payload.deep_merge!({'virtualImage' => parse_passed_options(options)})
|
471
|
+
virtual_image_payload = {}
|
472
|
+
# v_prompt = Morpheus::Cli::OptionTypes.prompt(add_virtual_image_option_types, options[:options], @api_client, options[:params])
|
473
|
+
if image_type_name
|
474
|
+
image_type = virtual_image_type_for_name_or_code(image_type_name)
|
475
|
+
# fix issue with api returning imageType vmware instead of vmdk
|
476
|
+
if image_type.nil? && image_type_name == 'vmware'
|
477
|
+
image_type = virtual_image_type_for_name_or_code('vmdk')
|
478
|
+
elsif image_type.nil? && image_type_name == 'vmdk'
|
479
|
+
image_type = virtual_image_type_for_name_or_code('vmware')
|
480
|
+
end
|
481
|
+
if image_type.nil?
|
482
|
+
print_red_alert "Virtual Image Type not found by code '#{image_type_name}'"
|
483
|
+
return 1
|
484
|
+
end
|
485
|
+
# options[:options] ||= {}
|
486
|
+
# options[:options]['imageType'] ||= image_type['code']
|
487
|
+
else
|
488
|
+
image_type_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'imageType', 'fieldLabel' => 'Image Type', 'type' => 'select', 'optionSource' => 'virtualImageTypes', 'required' => true, 'description' => 'Select Virtual Image Type.', 'displayOrder' => 2}],options[:options],@api_client,{})
|
489
|
+
image_type = virtual_image_type_for_name_or_code(image_type_prompt['imageType'])
|
378
490
|
end
|
379
|
-
|
380
|
-
|
381
|
-
|
491
|
+
|
492
|
+
# azure requires us to search the marketplace to select publisher, cloud, offerm sku
|
493
|
+
if image_type['code'] == "azure-reference" || image_type['code'] == "azure"
|
494
|
+
cloud_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'cloud', 'fieldLabel' => 'Cloud', 'type' => 'select', 'optionSource' => 'clouds', 'required' => true, 'description' => 'Select Azure Cloud.', :fmt=>:natural}],options[:options],@api_client, {zoneTypeWhiteList: 'azure'})
|
495
|
+
cloud_id = cloud_prompt['cloud'].to_i
|
496
|
+
|
497
|
+
marketplace_config = prompt_azure_marketplace(cloud_id, options)
|
498
|
+
virtual_image_payload['config'] ||= {}
|
499
|
+
virtual_image_payload['config'].deep_merge!(marketplace_config)
|
382
500
|
end
|
383
|
-
# options[:options] ||= {}
|
384
|
-
# options[:options]['imageType'] ||= image_type['code']
|
385
|
-
else
|
386
|
-
image_type_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'imageType', 'fieldLabel' => 'Image Type', 'type' => 'select', 'optionSource' => 'virtualImageTypes', 'required' => true, 'description' => 'Select Virtual Image Type.', 'displayOrder' => 2}],options[:options],@api_client,{})
|
387
|
-
image_type = virtual_image_type_for_name_or_code(image_type_prompt['imageType'])
|
388
|
-
end
|
389
501
|
|
390
|
-
begin
|
391
502
|
my_option_types = add_virtual_image_option_types(image_type, !file_url)
|
392
503
|
# if options[:no_prompt]
|
393
504
|
# my_option_types.each do |it|
|
@@ -396,9 +507,9 @@ class Morpheus::Cli::VirtualImages
|
|
396
507
|
# end
|
397
508
|
# end
|
398
509
|
# end
|
399
|
-
|
400
|
-
|
401
|
-
virtual_image_payload
|
510
|
+
v_prompt = Morpheus::Cli::OptionTypes.prompt(my_option_types, options[:options], @api_client, options[:params])
|
511
|
+
v_prompt.deep_compact!
|
512
|
+
virtual_image_payload.deep_merge!(v_prompt)
|
402
513
|
virtual_image_files = virtual_image_payload.delete('virtualImageFiles')
|
403
514
|
virtual_image_payload['imageType'] = image_type['code']
|
404
515
|
storage_provider_id = virtual_image_payload.delete('storageProviderId')
|
@@ -408,65 +519,74 @@ class Morpheus::Cli::VirtualImages
|
|
408
519
|
if tenants_list
|
409
520
|
virtual_image_payload['accounts'] = tenants_list
|
410
521
|
end
|
522
|
+
# metadata tags
|
523
|
+
if options[:tags]
|
524
|
+
tags = parse_metadata(options[:tags])
|
525
|
+
virtual_image_payload['tags'] = tags if tags
|
526
|
+
else
|
527
|
+
# tags = prompt_metadata(options)
|
528
|
+
# virtual_image_payload['tags'] = tags of tags
|
529
|
+
end
|
411
530
|
# fix issue with api returning imageType vmware instead of vmdk
|
412
531
|
if virtual_image_payload && virtual_image_payload['imageType'] == 'vmware'
|
413
532
|
virtual_image_payload['imageType'] == 'vmdk'
|
414
533
|
end
|
415
|
-
payload = {virtualImage
|
416
|
-
|
417
|
-
|
418
|
-
print_dry_run @virtual_images_interface.dry.create(payload)
|
419
|
-
if file_url
|
420
|
-
print_dry_run @virtual_images_interface.dry.upload_by_url(":id", file_url, file_name)
|
421
|
-
elsif virtual_image_files && !virtual_image_files.empty?
|
422
|
-
virtual_image_files.each do |key, filepath|
|
423
|
-
print_dry_run @virtual_images_interface.dry.upload(":id", "(Contents of file #{filepath})")
|
424
|
-
end
|
425
|
-
end
|
426
|
-
return
|
427
|
-
end
|
428
|
-
|
429
|
-
json_response = @virtual_images_interface.create(payload)
|
430
|
-
virtual_image = json_response['virtualImage']
|
431
|
-
|
432
|
-
if options[:json]
|
433
|
-
print JSON.pretty_generate(json_response)
|
434
|
-
elsif !options[:quiet]
|
435
|
-
print "\n", cyan, "Virtual Image #{virtual_image['name']} created successfully", reset, "\n\n"
|
436
|
-
end
|
534
|
+
#payload = {'virtualImage' => virtual_image_payload}
|
535
|
+
payload.deep_merge!({'virtualImage' => virtual_image_payload})
|
536
|
+
end
|
437
537
|
|
438
|
-
|
538
|
+
@virtual_images_interface.setopts(options)
|
539
|
+
if options[:dry_run]
|
540
|
+
print_dry_run @virtual_images_interface.dry.create(payload)
|
439
541
|
if file_url
|
440
|
-
|
441
|
-
print cyan, "Uploading file by url #{file_url} ...", reset, "\n"
|
442
|
-
end
|
443
|
-
upload_json_response = @virtual_images_interface.upload_by_url(virtual_image['id'], file_url, file_name)
|
444
|
-
if options[:json]
|
445
|
-
print JSON.pretty_generate(upload_json_response)
|
446
|
-
end
|
542
|
+
print_dry_run @virtual_images_interface.dry.upload_by_url(":id", file_url, file_name)
|
447
543
|
elsif virtual_image_files && !virtual_image_files.empty?
|
448
544
|
virtual_image_files.each do |key, filepath|
|
449
|
-
|
450
|
-
print cyan, "Uploading file (#{key}) #{filepath} ...", reset, "\n"
|
451
|
-
end
|
452
|
-
image_file = File.new(filepath, 'rb')
|
453
|
-
upload_json_response = @virtual_images_interface.upload(virtual_image['id'], image_file, file_name)
|
454
|
-
if options[:json]
|
455
|
-
print JSON.pretty_generate(upload_json_response)
|
456
|
-
end
|
545
|
+
print_dry_run @virtual_images_interface.dry.upload(":id", "(Contents of file #{filepath})")
|
457
546
|
end
|
458
|
-
else
|
459
|
-
puts cyan, "No files uploaded.", reset
|
460
547
|
end
|
548
|
+
return
|
549
|
+
end
|
550
|
+
|
551
|
+
json_response = @virtual_images_interface.create(payload)
|
552
|
+
virtual_image = json_response['virtualImage']
|
553
|
+
|
554
|
+
# if options[:json]
|
555
|
+
# print JSON.pretty_generate(json_response)
|
556
|
+
# elsif !options[:quiet]
|
557
|
+
# print "\n", cyan, "Virtual Image #{virtual_image['name']} created successfully", reset, "\n\n"
|
558
|
+
# end
|
461
559
|
|
462
|
-
|
463
|
-
|
560
|
+
# now upload the file, do this in the background maybe?
|
561
|
+
if file_url
|
562
|
+
unless options[:quiet]
|
563
|
+
print cyan, "Uploading file by url #{file_url} ...", reset, "\n"
|
564
|
+
end
|
565
|
+
upload_json_response = @virtual_images_interface.upload_by_url(virtual_image['id'], file_url, file_name)
|
566
|
+
# if options[:json]
|
567
|
+
# print JSON.pretty_generate(upload_json_response)
|
568
|
+
# end
|
569
|
+
elsif virtual_image_files && !virtual_image_files.empty?
|
570
|
+
virtual_image_files.each do |key, filepath|
|
571
|
+
unless options[:quiet]
|
572
|
+
print cyan, "Uploading file (#{key}) #{filepath} ...", reset, "\n"
|
573
|
+
end
|
574
|
+
image_file = File.new(filepath, 'rb')
|
575
|
+
upload_json_response = @virtual_images_interface.upload(virtual_image['id'], image_file, file_name)
|
576
|
+
# if options[:json]
|
577
|
+
# print JSON.pretty_generate(upload_json_response)
|
578
|
+
# end
|
464
579
|
end
|
580
|
+
else
|
581
|
+
# puts cyan, "No files uploaded.", reset
|
582
|
+
end
|
465
583
|
|
466
|
-
|
467
|
-
|
468
|
-
|
584
|
+
render_response(json_response, options, 'virtualImage') do
|
585
|
+
print_green_success "Added virtual image #{virtual_image['name']}"
|
586
|
+
return _get(virtual_image["id"], {}, options)
|
469
587
|
end
|
588
|
+
return 0, nil
|
589
|
+
|
470
590
|
end
|
471
591
|
|
472
592
|
def add_file(args)
|
@@ -676,16 +796,17 @@ class Morpheus::Cli::VirtualImages
|
|
676
796
|
tmp_option_types = [
|
677
797
|
{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
|
678
798
|
#{'fieldName' => 'imageType', 'fieldLabel' => 'Image Type', 'type' => 'select', 'optionSource' => 'virtualImageTypes', 'required' => true, 'description' => 'Select Virtual Image Type.', 'displayOrder' => 2},
|
679
|
-
{'fieldName' => 'osType', 'fieldLabel' => '
|
680
|
-
{'fieldName' => '
|
681
|
-
{'fieldName' => '
|
682
|
-
{'fieldName' => '
|
683
|
-
{'fieldName' => '
|
684
|
-
{'fieldName' => '
|
685
|
-
{'fieldName' => '
|
799
|
+
{'fieldName' => 'osType', 'fieldLabel' => 'Operating System', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false, 'description' => 'Select Operating System.', 'displayOrder' => 3},
|
800
|
+
{'fieldName' => 'minRamGB', 'fieldLabel' => 'Minimum Memory (GB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (GB)', 'displayOrder' => 4},
|
801
|
+
# {'fieldName' => 'minDiskGB', 'fieldLabel' => 'Minimum Disk (GB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (GB)', 'displayOrder' => 4},
|
802
|
+
{'fieldName' => 'isCloudInit', 'fieldLabel' => 'Cloud Init Enabled?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Cloud Init Enabled?', 'displayOrder' => 5},
|
803
|
+
{'fieldName' => 'installAgent', 'fieldLabel' => 'Install Agent?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Install Agent?', 'displayOrder' => 6},
|
804
|
+
{'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text', 'required' => false, 'description' => 'Enter an SSH Username', 'displayOrder' => 7},
|
805
|
+
{'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'type' => 'password', 'required' => false, 'description' => 'Enter an SSH Password', 'displayOrder' => 8},
|
806
|
+
{'fieldName' => 'storageProviderId', 'type' => 'select', 'fieldLabel' => 'Storage Provider', 'optionSource' => 'storageProviders', 'required' => false, 'description' => 'Select Storage Provider.', 'displayOrder' => 9},
|
686
807
|
{'fieldName' => 'userData', 'fieldLabel' => 'Cloud-Init User Data', 'type' => 'textarea', 'required' => false, 'displayOrder' => 10},
|
687
808
|
{'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}], 'required' => false, 'description' => 'Visibility', 'category' => 'permissions', 'defaultValue' => 'private', 'displayOrder' => 40},
|
688
|
-
{'fieldName' => 'isAutoJoinDomain', 'fieldLabel' => 'Auto Join Domain?', 'type' => 'checkbox', 'required' => false, 'description' => 'Auto Join Domain?', 'category' => 'advanced', 'displayOrder' => 40},
|
809
|
+
{'fieldName' => 'isAutoJoinDomain', 'fieldLabel' => 'Auto Join Domain?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Auto Join Domain?', 'category' => 'advanced', 'displayOrder' => 40},
|
689
810
|
{'fieldName' => 'virtioSupported', 'fieldLabel' => 'VirtIO Drivers Loaded?', 'type' => 'checkbox', 'defaultValue' => 'on', 'required' => false, 'description' => 'VirtIO Drivers Loaded?', 'category' => 'advanced', 'displayOrder' => 40},
|
690
811
|
{'fieldName' => 'vmToolsInstalled', 'fieldLabel' => 'VM Tools Installed?', 'type' => 'checkbox', 'defaultValue' => 'on', 'required' => false, 'description' => 'VM Tools Installed?', 'category' => 'advanced', 'displayOrder' => 40},
|
691
812
|
{'fieldName' => 'isForceCustomization', 'fieldLabel' => 'Force Guest Customization?', 'type' => 'checkbox', 'defaultValue' => 'off', 'required' => false, 'description' => 'Force Guest Customization?', 'category' => 'advanced', 'displayOrder' => 40},
|
@@ -696,22 +817,25 @@ class Morpheus::Cli::VirtualImages
|
|
696
817
|
image_type_code = image_type ? image_type['code'] : nil
|
697
818
|
if image_type_code
|
698
819
|
if image_type_code == 'ami'
|
699
|
-
tmp_option_types << {'fieldName' => 'externalId', 'fieldLabel' => 'AMI id', 'type' => 'text', 'required' => false, 'displayOrder' =>
|
820
|
+
tmp_option_types << {'fieldName' => 'externalId', 'fieldLabel' => 'AMI id', 'type' => 'text', 'required' => false, 'displayOrder' => 11}
|
700
821
|
if include_file_selection
|
701
|
-
tmp_option_types << {'fieldName' => 'imageFile', 'fieldLabel' => 'Image File', 'type' => 'file', 'required' => false, 'displayOrder' =>
|
822
|
+
tmp_option_types << {'fieldName' => 'imageFile', 'fieldLabel' => 'Image File', 'type' => 'file', 'required' => false, 'displayOrder' => 12}
|
702
823
|
end
|
703
824
|
elsif image_type_code == 'vmware' || image_type_code == 'vmdk'
|
704
825
|
if include_file_selection
|
705
|
-
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageFile', 'fieldLabel' => 'OVF File', 'type' => 'file', 'required' => false, 'displayOrder' =>
|
706
|
-
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageDescriptorFile', 'fieldLabel' => 'VMDK File', 'type' => 'file', 'required' => false, 'displayOrder' =>
|
826
|
+
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageFile', 'fieldLabel' => 'OVF File', 'type' => 'file', 'required' => false, 'displayOrder' => 11}
|
827
|
+
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageDescriptorFile', 'fieldLabel' => 'VMDK File', 'type' => 'file', 'required' => false, 'displayOrder' => 12}
|
707
828
|
end
|
708
829
|
elsif image_type_code == 'pxe'
|
709
|
-
tmp_option_types << {'fieldName' => 'config.menu', 'fieldLabel' => 'Menu', 'type' => 'text', 'required' => false, 'displayOrder' =>
|
710
|
-
tmp_option_types << {'fieldName' => 'imagePath', 'fieldLabel' => 'Image Path', 'type' => 'text', 'required' => true, 'displayOrder' =>
|
830
|
+
tmp_option_types << {'fieldName' => 'config.menu', 'fieldLabel' => 'Menu', 'type' => 'text', 'required' => false, 'displayOrder' => 11}
|
831
|
+
tmp_option_types << {'fieldName' => 'imagePath', 'fieldLabel' => 'Image Path', 'type' => 'text', 'required' => true, 'displayOrder' => 12}
|
711
832
|
tmp_option_types.reject! {|opt| ['isCloudInit', 'installAgent', 'sshUsername', 'sshPassword'].include?(opt['fieldName'])}
|
833
|
+
elsif image_type_code == 'azure' || image_type_code == 'azure-reference'
|
834
|
+
# Azure Marketplace Prompt happens elsewhere
|
835
|
+
tmp_option_types.reject! {|opt| ['storageProviderId', 'userData', 'sshUsername', 'sshPassword'].include?(opt['fieldName'])}
|
712
836
|
else
|
713
837
|
if include_file_selection
|
714
|
-
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageFile', 'fieldLabel' => 'Image File', 'type' => 'file', 'required' => false, 'description' => 'Choose an image file to upload', 'displayOrder' =>
|
838
|
+
tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageFile', 'fieldLabel' => 'Image File', 'type' => 'file', 'required' => false, 'description' => 'Choose an image file to upload', 'displayOrder' => 11}
|
715
839
|
end
|
716
840
|
end
|
717
841
|
end
|
@@ -721,7 +845,10 @@ class Morpheus::Cli::VirtualImages
|
|
721
845
|
|
722
846
|
def update_virtual_image_option_types(image_type = nil)
|
723
847
|
list = add_virtual_image_option_types(image_type)
|
724
|
-
list.each {|it|
|
848
|
+
list.each {|it|
|
849
|
+
it.delete('required')
|
850
|
+
it.delete('defaultValue')
|
851
|
+
}
|
725
852
|
list
|
726
853
|
end
|
727
854
|
|
@@ -735,5 +862,53 @@ class Morpheus::Cli::VirtualImages
|
|
735
862
|
""
|
736
863
|
end
|
737
864
|
end
|
865
|
+
|
866
|
+
def prompt_azure_marketplace(cloud_id, options)
|
867
|
+
rtn = {}
|
868
|
+
publisher_value, offer_value, sku_value, version_value = nil, nil, nil, nil
|
869
|
+
|
870
|
+
# Marketplace Publisher & Offer
|
871
|
+
marketplace_api_params = {'zoneId' => cloud_id}
|
872
|
+
v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'offer', 'fieldLabel' => 'Azure Marketplace Offer', 'type' => 'typeahead', 'optionSource' => 'searchAzureMarketplace', 'required' => true, 'description' => "Select Azure Marketplace Offer."}], options[:options],@api_client, marketplace_api_params)
|
873
|
+
# offer_value = v_prompt['marketplace']
|
874
|
+
# actually need both offer and publisher of these to query correctly..sigh
|
875
|
+
marketplace_option = Morpheus::Cli::OptionTypes.get_last_select()
|
876
|
+
offer_value = marketplace_option['offer']
|
877
|
+
publisher_value = marketplace_option['publisher']
|
878
|
+
|
879
|
+
# SKU & VERSION
|
880
|
+
if options[:options] && options[:options]['sku'] && options[:options]['version']
|
881
|
+
# the value to match on is actually sku|version
|
882
|
+
options[:options]['sku'] = options[:options]['sku'] + '|' + options[:options]['version']
|
883
|
+
end
|
884
|
+
sku_api_params = {'zoneId' => cloud_id, publisher: publisher_value, offer: offer_value}
|
885
|
+
v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'sku', 'fieldLabel' => 'Azure Marketplace SKU', 'type' => 'select', 'optionSource' => 'searchAzureMarketplaceSkus', 'required' => true, 'description' => "Select Azure Marketplace SKU and Version, the format is SKU|Version"}], options[:options],@api_client, sku_api_params)
|
886
|
+
# marketplace_option = Morpheus::Cli::OptionTypes.get_last_select()
|
887
|
+
# sku_value = marketplace_option['sku']
|
888
|
+
# version_value = marketplace_option['version']
|
889
|
+
sku_value = v_prompt['sku']
|
890
|
+
if sku_value && sku_value.include?("|")
|
891
|
+
sku_value, version_value = sku_value.split("|")
|
892
|
+
end
|
893
|
+
|
894
|
+
rtn['publisher'] = publisher_value
|
895
|
+
rtn['offer'] = offer_value
|
896
|
+
rtn['sku'] = sku_value
|
897
|
+
rtn['version'] = version_value
|
898
|
+
return rtn
|
899
|
+
end
|
900
|
+
|
901
|
+
def format_virtual_image_source(virtual_image, return_color=cyan)
|
902
|
+
out = ""
|
903
|
+
if virtual_image['userUploaded']
|
904
|
+
# out << "#{green}UPLOADED#{return_color}"
|
905
|
+
out << "#{cyan}UPLOADED#{return_color}"
|
906
|
+
elsif virtual_image['systemImage']
|
907
|
+
out << "#{cyan}SYSTEM#{return_color}"
|
908
|
+
else
|
909
|
+
out << "#{cyan}SYNCED#{return_color}"
|
910
|
+
end
|
911
|
+
out
|
912
|
+
end
|
738
913
|
|
739
914
|
end
|