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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Dockerfile +1 -1
  4. data/lib/morpheus/api/api_client.rb +16 -0
  5. data/lib/morpheus/api/billing_interface.rb +1 -0
  6. data/lib/morpheus/api/deploy_interface.rb +1 -1
  7. data/lib/morpheus/api/deployments_interface.rb +20 -1
  8. data/lib/morpheus/api/forgot_password_interface.rb +17 -0
  9. data/lib/morpheus/api/instances_interface.rb +16 -2
  10. data/lib/morpheus/api/invoices_interface.rb +12 -3
  11. data/lib/morpheus/api/search_interface.rb +13 -0
  12. data/lib/morpheus/api/servers_interface.rb +14 -0
  13. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  14. data/lib/morpheus/api/usage_interface.rb +18 -0
  15. data/lib/morpheus/cli.rb +6 -2
  16. data/lib/morpheus/cli/apps.rb +3 -23
  17. data/lib/morpheus/cli/budgets_command.rb +389 -319
  18. data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
  19. data/lib/morpheus/cli/cli_command.rb +51 -10
  20. data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -13
  21. data/lib/morpheus/cli/commands/standard/history_command.rb +9 -3
  22. data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
  23. data/lib/morpheus/cli/containers_command.rb +0 -24
  24. data/lib/morpheus/cli/cypher_command.rb +6 -2
  25. data/lib/morpheus/cli/dashboard_command.rb +260 -20
  26. data/lib/morpheus/cli/deploy.rb +199 -90
  27. data/lib/morpheus/cli/deployments.rb +341 -28
  28. data/lib/morpheus/cli/deploys.rb +206 -41
  29. data/lib/morpheus/cli/error_handler.rb +7 -0
  30. data/lib/morpheus/cli/forgot_password.rb +133 -0
  31. data/lib/morpheus/cli/groups.rb +1 -1
  32. data/lib/morpheus/cli/health_command.rb +59 -2
  33. data/lib/morpheus/cli/hosts.rb +271 -39
  34. data/lib/morpheus/cli/instances.rb +228 -129
  35. data/lib/morpheus/cli/invoices_command.rb +100 -20
  36. data/lib/morpheus/cli/jobs_command.rb +94 -92
  37. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  38. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  39. data/lib/morpheus/cli/logs_command.rb +9 -6
  40. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  41. data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
  42. data/lib/morpheus/cli/mixins/print_helper.rb +13 -27
  43. data/lib/morpheus/cli/mixins/provisioning_helper.rb +108 -5
  44. data/lib/morpheus/cli/option_types.rb +271 -22
  45. data/lib/morpheus/cli/remote.rb +35 -10
  46. data/lib/morpheus/cli/reports_command.rb +99 -30
  47. data/lib/morpheus/cli/roles.rb +193 -155
  48. data/lib/morpheus/cli/search_command.rb +182 -0
  49. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  50. data/lib/morpheus/cli/setup.rb +1 -1
  51. data/lib/morpheus/cli/shell.rb +33 -11
  52. data/lib/morpheus/cli/tasks.rb +29 -32
  53. data/lib/morpheus/cli/usage_command.rb +64 -11
  54. data/lib/morpheus/cli/version.rb +1 -1
  55. data/lib/morpheus/cli/virtual_images.rb +429 -254
  56. data/lib/morpheus/cli/whoami.rb +6 -6
  57. data/lib/morpheus/cli/workflows.rb +33 -40
  58. data/lib/morpheus/formatters.rb +75 -18
  59. data/lib/morpheus/terminal.rb +6 -2
  60. metadata +10 -4
  61. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
@@ -9,7 +9,7 @@ class Morpheus::Cli::Setup
9
9
 
10
10
  set_command_name :setup
11
11
 
12
- register_subcommands :init
12
+ # register_subcommands :init
13
13
 
14
14
  # no authorization needed
15
15
  def connect(options={})
@@ -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
- puts "You are in a (temporary) morpheus shell"
362
+ out << "You are in a (temporary) morpheus shell\n"
350
363
  else
351
- puts "You are in a morpheus shell."
364
+ out << "You are in a morpheus shell.\n"
352
365
  end
353
- puts "See the available commands below."
366
+ out << "See the available commands below.\n"
354
367
 
355
- puts "\nCommands:"
368
+ out << "\nCommands:"
356
369
  # commands = @morpheus_commands + @shell_commands
357
- @morpheus_commands.sort.each {|cmd|
358
- puts "\t#{cmd.to_s}"
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
- puts "\nShell Commands:"
381
+ out << "\nShell Commands:\n"
362
382
  @shell_commands.each {|cmd|
363
- puts "\t#{cmd.to_s}"
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
- puts "\n"
366
- puts "For more information, see https://github.com/gomorpheus/morpheus-cli/wiki"
367
- #print "\n"
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?
@@ -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
- build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
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
- raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
40
+ options[:phrase] = args.join(" ")
39
41
  end
40
- connect(options)
41
- begin
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
-
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
- if args.count > 1
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
- if args.count != 1
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
- if args.count < 1
748
- puts optparse
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 /billing and returns usages
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 #, :list_tenant, :list_clouds, :list_zones, :list_zones, :list_zones, :list_zones
12
+ register_subcommands :list, :get
13
13
 
14
14
  def connect(opts)
15
15
  @api_client = establish_remote_appliance_connection(opts)
16
- @billing_interface = @api_client.billing
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 now.") do |val|
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
- @billing_interface.setopts(options)
67
+ @usage_interface.setopts(options)
68
68
  if options[:dry_run]
69
- print_dry_run @billing_interface.dry.list(params)
69
+ print_dry_run @usage_interface.dry.list(params)
70
70
  return
71
71
  end
72
- json_response = @billing_interface.list(params)
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 "#{cyan}#{status_string.upcase}#{return_color}"
197
+ return "#{red}#{status_string.upcase}#{return_color}"
145
198
  else
146
- return "#{cyan}#{status_string.upcase}#{return_color}"
199
+ return "#{green}#{status_string.upcase}#{return_color}"
147
200
  end
148
201
  end
149
202
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "5.0.0"
4
+ VERSION = "5.2.2"
5
5
  end
6
6
  end
@@ -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
- build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
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
- begin
51
- params = {}
52
- params.merge!(parse_list_options(options))
53
- if options[:imageType]
54
- params[:imageType] = options[:imageType]
55
- end
56
- if options[:filterType]
57
- params[:filterType] = options[:filterType]
58
- end
59
- @virtual_images_interface.setopts(options)
60
- if options[:dry_run]
61
- print_dry_run @virtual_images_interface.dry.get(params)
62
- return
63
- end
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
- images = json_response['virtualImages']
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
- rows = images.collect do |image|
95
- image_type = virtual_image_type_for_name_or_code(image['imageType'])
96
- image_type_display = image_type ? "#{image_type['name']}" : image['imageType']
97
- {name: image['name'], id: image['id'], type: image_type_display, source: image['userUploaded'] ? "#{green}UPLOADED#{cyan}" : (image['systemImage'] ? 'SYSTEM' : "#{white}SYNCED#{cyan}"), storage: !image['storageProvider'].nil? ? image['storageProvider']['name'] : 'Default', size: image['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{image['rawSize']} B").pretty}"}
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
- columns = [:id, :name, :type, :storage, :size, :source]
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
- return 0
108
- rescue RestClient::Exception => e
109
- print_rest_exception(e, options)
110
- exit 1
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("[name]")
119
- opts.on('--details', "Show more details." ) do
120
- show_details = true
121
- end
122
- build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
123
- opts.footer = "Get details about a virtual image." + "\n" +
124
- "[name] is required. This is the name or id of a virtual image."
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
- if args.count < 1
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
- begin
134
- @virtual_images_interface.setopts(options)
135
- if options[:dry_run]
136
- if args[0].to_s =~ /\A\d{1,}\Z/
137
- print_dry_run @virtual_images_interface.dry.get(args[0].to_i)
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
- print_dry_run @virtual_images_interface.dry.get({name:args[0]})
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
- print_h1 "Virtual Image Details"
165
- print cyan
166
- description_cols = {
167
- "ID" => 'id',
168
- "Name" => 'name',
169
- "Type" => lambda {|it| image_type_display },
170
- "Storage" => lambda {|it| !image['storageProvider'].nil? ? image['storageProvider']['name'] : 'Default' },
171
- "Size" => lambda {|it| image['rawSize'].nil? ? 'Unknown' : "#{Filesize.from("#{image['rawSize']} B").pretty}" },
172
- "Source" => lambda {|it| image['userUploaded'] ? "#{green}UPLOADED#{cyan}" : (image['systemImage'] ? 'SYSTEM' : "#{white}SYNCED#{cyan}") },
173
- # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
174
- # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
175
- }
176
- advanced_description_cols = {
177
- "OS Type" => lambda {|it| it['osType'] ? it['osType']['name'] : "" },
178
- "Min Memory" => lambda {|it| it['minRam'].to_i != 0 ? Filesize.from("#{it['minRam']} B").pretty : "" },
179
- "Cloud Init?" => lambda {|it| format_boolean it['osType'] },
180
- "Install Agent?" => lambda {|it| format_boolean it['osType'] },
181
- "SSH Username" => lambda {|it| it['sshUsername'] },
182
- "SSH Password" => lambda {|it| it['sshPassword'] },
183
- "User Data" => lambda {|it| it['userData'] },
184
- "Visibility" => lambda {|it| it['visibility'].to_s.capitalize },
185
- "Tenants" => lambda {|it| format_tenants(it['accounts']) },
186
- "Auto Join Domain?" => lambda {|it| format_boolean it['isAutoJoinDomain'] },
187
- "VirtIO Drivers Loaded?" => lambda {|it| format_boolean it['virtioSupported'] },
188
- "VM Tools Installed?" => lambda {|it| format_boolean it['vmToolsInstalled'] },
189
- "Force Guest Customization?" => lambda {|it| format_boolean it['isForceCustomization'] },
190
- "Trial Version" => lambda {|it| format_boolean it['trialVersion'] },
191
- "Sysprep Enabled?" => lambda {|it| format_boolean it['isSysprep'] },
192
- }
193
- if show_details
194
- description_cols.merge!(advanced_description_cols)
195
- end
196
- print_description_list(description_cols, image)
197
-
198
- if image_files
199
- print_h2 "Files (#{image_files.size})"
200
- # image_files.each {|image_file|
201
- # pretty_filesize = Filesize.from("#{image_file['size']} B").pretty
202
- # print cyan," = #{image_file['name']} [#{pretty_filesize}]", "\n"
203
- # }
204
- image_file_rows = image_files.collect do |image_file|
205
-
206
- {filename: image_file['name'], size: Filesize.from("#{image_file['size']} B").pretty}
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
- print cyan
209
- print as_pretty_table(image_file_rows, [:filename, :size])
210
- # print reset,"\n"
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
- print reset,"\n"
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
- if args.count < 1
238
- puts optparse
239
- exit 1
240
- end
313
+ verify_args!(args:args, optparse:optparse, count:1)
241
314
 
242
315
  connect(options)
243
- begin
244
- image = find_virtual_image_by_name_or_id(image_name)
245
- return 1 if image.nil?
246
-
247
- payload = nil
248
- if options[:payload]
249
- payload = options[:payload]
250
- # support -O OPTION switch on top of --payload
251
- if options[:options]
252
- payload['virtualImage'] ||= {}
253
- payload['virtualImage'].deep_merge!(options[:options].reject {|k,v| k.is_a?(Symbol) })
254
- end
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
- params = options[:options] || {}
257
- if params.empty? && tenants_list.nil?
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
- @virtual_images_interface.setopts(options)
269
- if options[:dry_run]
270
- print_dry_run @virtual_images_interface.dry.update(image['id'], payload)
271
- return
337
+ # metadata tags
338
+ if options[:add_tags]
339
+ virtual_image_payload['addTags'] = parse_metadata(options[:add_tags])
272
340
  end
273
- response = @virtual_images_interface.update(image['id'], payload)
274
- if options[:json]
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
- rescue RestClient::Exception => e
283
- print_rest_exception(e, options)
284
- exit 1
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
- build_common_options(opts, options, [:options, :json, :dry_run, :remote])
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
- if image_type_name
372
- image_type = virtual_image_type_for_name_or_code(image_type_name)
373
- # fix issue with api returning imageType vmware instead of vmdk
374
- if image_type.nil? && image_type_name == 'vmware'
375
- image_type = virtual_image_type_for_name_or_code('vmdk')
376
- elsif image_type.nil? && image_type_name == 'vmdk'
377
- image_type = virtual_image_type_for_name_or_code('vmware')
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
- if image_type.nil?
380
- print_red_alert "Virtual Image Type not found by code '#{image_type_name}'"
381
- return 1
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
- params = Morpheus::Cli::OptionTypes.prompt(my_option_types, options[:options], @api_client, options[:params])
400
- params.deep_compact!
401
- virtual_image_payload = {}.merge(params)
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: virtual_image_payload}
416
- @virtual_images_interface.setopts(options)
417
- if options[:dry_run]
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
- # now upload the file, do this in the background maybe?
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
- unless options[:quiet]
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
- unless options[:quiet]
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
- if !options[:json]
463
- get([virtual_image['id']])
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
- rescue RestClient::Exception => e
467
- print_rest_exception(e, options)
468
- exit 1
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' => 'OS Type', 'type' => 'select', 'optionSource' => 'osTypes', 'required' => false, 'description' => 'Select OS Type.', 'displayOrder' => 3},
680
- {'fieldName' => 'minRam', 'fieldLabel' => 'Minimum Memory (MB)', 'type' => 'number', 'required' => false, 'description' => 'Minimum Memory (MB)', 'displayOrder' => 4},
681
- {'fieldName' => 'isCloudInit', 'fieldLabel' => 'Cloud Init Enabled?', 'type' => 'checkbox', 'required' => false, 'description' => 'Cloud Init Enabled?', 'displayOrder' => 4},
682
- {'fieldName' => 'installAgent', 'fieldLabel' => 'Install Agent?', 'type' => 'checkbox', 'required' => false, 'description' => 'Install Agent?', 'displayOrder' => 4},
683
- {'fieldName' => 'sshUsername', 'fieldLabel' => 'SSH Username', 'type' => 'text', 'required' => false, 'description' => 'Enter an SSH Username', 'displayOrder' => 5},
684
- {'fieldName' => 'sshPassword', 'fieldLabel' => 'SSH Password', 'type' => 'password', 'required' => false, 'description' => 'Enter an SSH Password', 'displayOrder' => 6},
685
- {'fieldName' => 'storageProviderId', 'type' => 'select', 'fieldLabel' => 'Storage Provider', 'optionSource' => 'storageProviders', 'required' => false, 'description' => 'Select Storage Provider.', 'displayOrder' => 7},
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' => 10}
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' => 10}
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' => 10}
706
- tmp_option_types << {'fieldContext' => 'virtualImageFiles', 'fieldName' => 'imageDescriptorFile', 'fieldLabel' => 'VMDK File', 'type' => 'file', 'required' => false, 'displayOrder' => 10}
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' => 10}
710
- tmp_option_types << {'fieldName' => 'imagePath', 'fieldLabel' => 'Image Path', 'type' => 'text', 'required' => true, 'displayOrder' => 10}
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' => 10}
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| it['required'] = false }
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