morpheus-cli 5.3.2 → 5.3.3

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/accounts_interface.rb +4 -30
  4. data/lib/morpheus/api/api_client.rb +12 -0
  5. data/lib/morpheus/api/instances_interface.rb +18 -5
  6. data/lib/morpheus/api/load_balancer_pools_interface.rb +9 -0
  7. data/lib/morpheus/api/load_balancer_types_interface.rb +9 -0
  8. data/lib/morpheus/api/load_balancer_virtual_servers_interface.rb +9 -0
  9. data/lib/morpheus/api/load_balancers_interface.rb +4 -53
  10. data/lib/morpheus/api/network_routers_interface.rb +56 -0
  11. data/lib/morpheus/api/secondary_read_interface.rb +25 -0
  12. data/lib/morpheus/api/secondary_rest_interface.rb +42 -0
  13. data/lib/morpheus/api/virtual_images_interface.rb +23 -2
  14. data/lib/morpheus/cli/apps.rb +3 -2
  15. data/lib/morpheus/cli/cli_command.rb +21 -14
  16. data/lib/morpheus/cli/cli_registry.rb +55 -2
  17. data/lib/morpheus/cli/cloud_resource_pools_command.rb +169 -133
  18. data/lib/morpheus/cli/clusters.rb +51 -33
  19. data/lib/morpheus/cli/instances.rb +292 -174
  20. data/lib/morpheus/cli/invoices_command.rb +79 -99
  21. data/lib/morpheus/cli/library_cluster_layouts_command.rb +20 -0
  22. data/lib/morpheus/cli/load_balancer_types.rb +37 -0
  23. data/lib/morpheus/cli/load_balancers.rb +149 -314
  24. data/lib/morpheus/cli/log_settings_command.rb +7 -3
  25. data/lib/morpheus/cli/mixins/load_balancers_helper.rb +156 -0
  26. data/lib/morpheus/cli/mixins/print_helper.rb +11 -0
  27. data/lib/morpheus/cli/mixins/provisioning_helper.rb +123 -101
  28. data/lib/morpheus/cli/mixins/rest_command.rb +657 -0
  29. data/lib/morpheus/cli/monitoring_checks_command.rb +2 -0
  30. data/lib/morpheus/cli/network_routers_command.rb +1183 -185
  31. data/lib/morpheus/cli/networks_command.rb +194 -101
  32. data/lib/morpheus/cli/option_parser.rb +25 -17
  33. data/lib/morpheus/cli/option_types.rb +42 -45
  34. data/lib/morpheus/cli/tenants_command.rb +18 -20
  35. data/lib/morpheus/cli/vdi_pools_command.rb +4 -1
  36. data/lib/morpheus/cli/version.rb +1 -1
  37. data/lib/morpheus/cli/virtual_images.rb +249 -29
  38. data/lib/morpheus/cli.rb +1 -0
  39. data/lib/morpheus/ext/string.rb +41 -0
  40. data/lib/morpheus/formatters.rb +4 -0
  41. data/morpheus-cli.gemspec +1 -1
  42. metadata +13 -4
@@ -32,27 +32,35 @@ module Morpheus
32
32
  #out << "Options:\n"
33
33
  # the default way..
34
34
  # out << summarize().join("")
35
-
36
35
  # super hacky, should be examining the option, not the fully formatted description
37
36
  my_summaries = summarize()
38
- summarize().each do |opt_description|
39
- is_hidden = (@hidden_options || []).find { |hidden_switch|
40
- # opt_description.include?("--#{hidden_switch}")
41
- if hidden_switch.start_with?("-")
42
- opt_description.to_s.strip.start_with?("#{hidden_switch} ")
43
- else
44
- opt_description.to_s.strip.start_with?("--#{hidden_switch} ")
45
- end
46
- }
47
- if is_hidden
48
- if opts[:show_hidden_options]
49
- # out << opt_description + " (hidden)"
50
- out << opt_description
37
+ if opts[:show_hidden_options]
38
+ my_summaries.each do |full_line|
39
+ out << full_line
40
+ end
41
+ else
42
+ on_hidden_option = false
43
+ my_summaries.each do |full_line|
44
+ opt_description = full_line.to_s.strip
45
+ if opt_description.start_with?("-")
46
+ is_hidden = (@hidden_options || []).find { |hidden_switch|
47
+ if hidden_switch.start_with?("-")
48
+ opt_description.start_with?("#{hidden_switch} ")
49
+ else
50
+ opt_description.start_with?("--#{hidden_switch} ")
51
+ end
52
+ }
53
+ if is_hidden
54
+ on_hidden_option = true
55
+ else
56
+ on_hidden_option = false
57
+ out << full_line
58
+ end
51
59
  else
52
- # hidden
60
+ if on_hidden_option == false
61
+ out << full_line
62
+ end
53
63
  end
54
- else
55
- out << opt_description
56
64
  end
57
65
  end
58
66
  end
@@ -61,11 +61,11 @@ module Morpheus
61
61
  ).each do |option_type|
62
62
  context_map = results
63
63
  value = nil
64
- value_found=false
64
+ value_found = false
65
+ field_group = (option_type['fieldGroup'] || 'default').to_s.sub(/options\Z/i, "").strip # avoid "ADVANCED OPTION OPTIONS"
65
66
 
66
- if cur_field_group != (option_type['fieldGroup'] || 'default')
67
- cur_field_group = option_type['fieldGroup']
68
- cur_field_group = cur_field_group.to_s.sub(/options\Z/i, "").strip # avoid "ADVANCED OPTION OPTIONS"
67
+ if cur_field_group != field_group
68
+ cur_field_group = field_group
69
69
  print "\n#{cur_field_group.upcase} OPTIONS\n#{"=" * ("#{cur_field_group} OPTIONS".length)}\n\n"
70
70
  end
71
71
 
@@ -94,46 +94,42 @@ module Morpheus
94
94
  end
95
95
 
96
96
  if !visible_option_check_value.to_s.empty?
97
- # support formats code=value or code:value OR code:(value|value2|value3)
98
- # OR fieldContext.fieldName=value
99
- parts = visible_option_check_value.include?("=") ? visible_option_check_value.split("=") : visible_option_check_value.split(":")
100
- depends_on_code = parts[0]
101
- depends_on_value = parts[1].to_s.strip
102
- depends_on_values = []
103
- if depends_on_value.size > 0
104
- # strip parenthesis
105
- if depends_on_value[0] && depends_on_value[0].chr == "("
106
- depends_on_value = depends_on_value[1..-1]
107
- end
108
- depends_on_value.chomp(")")
109
- depends_on_values = depends_on_value.split("|").collect { |it| it.strip }
110
- end
111
- depends_on_option_type = option_types.find {|it| it["code"] == depends_on_code }
112
- if !depends_on_option_type
113
- depends_on_option_type = option_types.find {|it|
114
- (it['fieldContext'] ? "#{it['fieldContext']}.#{it['fieldName']}" : it['fieldName']) == depends_on_code
115
- }
97
+ match_type = 'any'
98
+
99
+ if visible_option_check_value.include?('::')
100
+ match_type = 'all' if visible_option_check_value.start_with?('matchAll')
101
+ visible_option_check_value = visible_option_check_value[visible_option_check_value.index('::') + 2..-1]
116
102
  end
117
- if depends_on_option_type
118
- # dependent option type has a different value
119
- depends_on_field_key = depends_on_option_type['fieldContext'].nil? || depends_on_option_type['fieldContext'].empty? ? "#{depends_on_option_type['fieldName']}" : "#{depends_on_option_type['fieldContext']}.#{depends_on_option_type['fieldName']}"
120
- found_dep_value = get_object_value(results, depends_on_field_key) || get_object_value(options, depends_on_field_key)
121
-
122
- if depends_on_values.size > 0
123
- # must be in the specified values
124
- # todo: uhh this actually needs to change to parse regex
125
- if !depends_on_values.include?(found_dep_value)
126
- next
127
- end
103
+
104
+ found_dep_value = match_type == 'all' ? true : false
105
+ visible_option_check_value.sub(',', ' ').split(' ').each do |value|
106
+ parts = value.split(':')
107
+ depends_on_code = parts[0]
108
+ depends_on_value = parts.count > 1 ? parts[1].to_s.strip : nil
109
+ depends_on_option_type = option_types.find {|it| it["code"] == depends_on_code }
110
+ if !depends_on_option_type
111
+ depends_on_option_type = option_types.find {|it|
112
+ (it['fieldContext'] ? "#{it['fieldContext']}.#{it['fieldName']}" : it['fieldName']) == depends_on_code
113
+ }
114
+ end
115
+
116
+ if depends_on_option_type
117
+ depends_on_field_key = depends_on_option_type['fieldContext'].nil? || depends_on_option_type['fieldContext'].empty? ? "#{depends_on_option_type['fieldName']}" : "#{depends_on_option_type['fieldContext']}.#{depends_on_option_type['fieldName']}"
128
118
  else
129
- # no value found
130
- if found_dep_value.to_s.empty?
131
- next
132
- end
119
+ depends_on_field_key = depends_on_code
120
+ end
121
+
122
+ field_value = get_object_value(results, depends_on_field_key) ||
123
+ get_object_value(options, depends_on_field_key) ||
124
+ get_object_value(api_params, depends_on_field_key)
125
+
126
+ if !field_value.nil? && (depends_on_value.nil? || depends_on_value.empty? || field_value.match?(depends_on_value))
127
+ found_dep_value = true if match_type != 'all'
128
+ else
129
+ found_dep_value = false if match_type == 'all'
133
130
  end
134
- else
135
- # could not find the dependent option type, proceed and prompt
136
131
  end
132
+ next if !found_dep_value
137
133
  end
138
134
 
139
135
  cur_namespace = options
@@ -246,7 +242,7 @@ module Morpheus
246
242
  # I suppose the entered value should take precedence
247
243
  # api_params = api_params.merge(options) # this might be good enough
248
244
  # dup it
249
- value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).merge(results)), options[:no_prompt], nil, paging_enabled)
245
+ value = select_prompt(option_type, api_client, (option_type['noParams'] ? {} : (api_params || {}).deep_merge(results)), options[:no_prompt], nil, paging_enabled)
250
246
  if value && option_type['type'] == 'multiSelect'
251
247
  value = [value]
252
248
  while self.confirm("Add another #{option_type['fieldLabel']}?", {:default => false}) do
@@ -283,10 +279,10 @@ module Morpheus
283
279
 
284
280
  if option_type['type'] == 'multiSelect'
285
281
  value = [value] if !value.nil? && !value.is_a?(Array)
286
- parent_context_map[parent_ns] = value
287
- else
288
- context_map[field_name] = value
282
+ # parent_context_map[parent_ns] = value
289
283
  end
284
+ context_map[field_name] = value if !(value.nil? || (value.is_a?(Hash) && value.empty?))
285
+ parent_context_map.reject! {|k,v| k == parent_ns && (v.nil? || (v.is_a?(Hash) && v.empty?))}
290
286
  end
291
287
  results
292
288
  end
@@ -957,8 +953,9 @@ module Morpheus
957
953
  out << "\n"
958
954
  out << "#{header}\n"
959
955
  out << "#{'=' * header.length}\n"
956
+
960
957
  select_options.each do |option|
961
- out << " * #{option['name']} [#{option['value']}]\n"
958
+ out << (option['isGroup'] ? "- #{option['name']}\n" : " * #{option['name']} [#{option['value']}]\n")
962
959
  end
963
960
  return out
964
961
  end
@@ -248,41 +248,39 @@ EOT
248
248
 
249
249
 
250
250
  def remove(args)
251
+ params = {}
251
252
  options = {}
252
253
  optparse = Morpheus::Cli::OptionParser.new do |opts|
253
- opts.banner = subcommand_usage("[name]")
254
- build_common_options(opts, options, [:auto_confirm, :json, :remote, :dry_run])
254
+ opts.banner = subcommand_usage("[tenant]")
255
+ opts.on('--remove-resources [on|off]', ['on','off'], "Remove Infrastructure. Default is off.") do |val|
256
+ params[:removeResources] = val.nil? ? 'on' : val
257
+ end
258
+ build_standard_remove_options(opts, options)
259
+ opts.footer = <<-EOT
260
+ Delete a tenant.
261
+ [tenant] is required. This is the name or id of a tenant.
262
+ EOT
255
263
  end
256
264
  optparse.parse!(args)
257
- if args.count < 1
258
- puts optparse
259
- exit 1
260
- end
265
+ verify_args!(args:args, optparse:optparse, count:1)
266
+ optparse.parse!(args)
261
267
  connect(options)
262
268
  begin
263
269
  # allow finding by ID since name is not unique!
264
270
  account = find_account_by_name_or_id(args[0])
265
- exit 1 if account.nil?
266
- unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the account #{account['name']}?")
267
- exit
268
- end
269
- if options[:dry_run] && options[:json]
270
- puts as_json(payload, options)
271
- return 0
271
+ return 1, "tenant not found" if account.nil?
272
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the tenant #{account['name']}?")
273
+ return 9, "aborted command"
272
274
  end
273
275
  @accounts_interface.setopts(options)
274
276
  if options[:dry_run]
275
- print_dry_run @accounts_interface.dry.destroy(account['id'])
277
+ print_dry_run @accounts_interface.dry.destroy(account['id'], params)
276
278
  return
277
279
  end
278
- json_response = @accounts_interface.destroy(account['id'])
279
- if options[:json]
280
- print JSON.pretty_generate(json_response)
281
- print "\n"
282
- else
280
+ json_response = @accounts_interface.destroy(account['id'], params)
281
+ render_response(json_response, options) do
283
282
  print_green_success "Tenant #{account['name']} removed"
284
283
  end
285
-
286
284
  rescue RestClient::Exception => e
287
285
  print_rest_exception(e, options)
288
286
  exit 1
@@ -418,6 +418,7 @@ EOT
418
418
  "Name" => 'name',
419
419
  "Description" => 'description',
420
420
  "Persistent" => lambda {|it| format_boolean(it['persistentUser']) },
421
+ "Recyclable" => lambda {|it| it['recyclable'].nil? ? nil : format_boolean(it['recyclable']) },
421
422
  "Enabled" => lambda {|it| format_boolean(it['enabled']) },
422
423
  "Pool Usage" => lambda {|it|
423
424
  # todo: [== ] 2/8 would be neat generate_usage_bar(...)
@@ -447,12 +448,13 @@ EOT
447
448
  "Max Size" => lambda {|it| format_number(it['maxPoolSize']) rescue '' },
448
449
  "Lease Timeout" => lambda {|it| format_number(it['allocationTimeoutMinutes']) rescue '' },
449
450
  "Persistent" => lambda {|it| format_boolean(it['persistentUser']) },
451
+ "Recyclable" => lambda {|it| it['recyclable'].nil? ? nil : format_boolean(it['recyclable']) },
452
+ "Enabled" => lambda {|it| format_boolean(it['enabled']) },
450
453
  "Allow Copy" => lambda {|it| format_boolean(it['allowCopy']) },
451
454
  "Allow Printer" => lambda {|it| format_boolean(it['allowPrinter']) },
452
455
  "Allow File Share" => lambda {|it| format_boolean(it['allowFileshare']) },
453
456
  "Allow Hypervisor Console" => lambda {|it| format_boolean(it['allowHypervisorConsole']) },
454
457
  "Auto Create User" => lambda {|it| format_boolean(it['autoCreateLocalUserOnReservation']) },
455
- "Enabled" => lambda {|it| format_boolean(it['enabled']) },
456
458
  "Logo" => lambda {|it| it['logo'] || it['imagePath'] },
457
459
  #"Config" => lambda {|it| it['config'] },
458
460
  "Group" => lambda {|it| it['group'] ? it['group']['name'] : nil },
@@ -484,6 +486,7 @@ EOT
484
486
  {'fieldName' => 'maxPoolSize', 'fieldLabel' => 'Max Size', 'type' => 'number', 'required' => true, 'description' => 'Max limit on number of allocations and instances within the pool.'},
485
487
  {'fieldName' => 'allocationTimeoutMinutes', 'fieldLabel' => 'Lease Timeout', 'type' => 'number', 'description' => 'Time (in minutes) after a user disconnects before an allocation is recycled or shutdown depending on persistence.'},
486
488
  {'fieldName' => 'persistentUser', 'fieldLabel' => 'Persistent', 'type' => 'checkbox', 'defaultValue' => false},
489
+ {'fieldName' => 'recyclable', 'fieldLabel' => 'Recyclable', 'type' => 'checkbox', 'defaultValue' => false, 'description' => 'Recyclable VDI Pools only work with cloud types that support snapshot management (i.e. Vmware, Nutanix, VCD)'},
487
490
  {'fieldName' => 'allowCopy', 'fieldLabel' => 'Allow Copy', 'type' => 'checkbox', 'defaultValue' => false},
488
491
  {'fieldName' => 'allowPrinter', 'fieldLabel' => 'Allow Printer', 'type' => 'checkbox', 'defaultValue' => false},
489
492
  {'fieldName' => 'allowFileshare', 'fieldLabel' => 'Allow File Share', 'type' => 'checkbox', 'defaultValue' => false},
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "5.3.2"
4
+ VERSION = "5.3.3"
5
5
  end
6
6
  end
@@ -11,8 +11,7 @@ class Morpheus::Cli::VirtualImages
11
11
  include Morpheus::Cli::ProvisioningHelper
12
12
 
13
13
  register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :types => :virtual_image_types
14
- alias_subcommand :details, :get
15
- set_default_subcommand :list
14
+ register_subcommands :list_locations, :get_location, :remove_location
16
15
 
17
16
  # def initialize()
18
17
  # # @appliance_name, @appliance_url = Morpheus::Cli::Remote.active_appliance
@@ -188,6 +187,7 @@ EOT
188
187
  image = json_response['virtualImage']
189
188
  image_config = image['config'] || {}
190
189
  image_volumes = image['volumes'] || []
190
+ image_locations = image['locations'] || []
191
191
  image_files = json_response['cloudFiles'] || json_response['files']
192
192
  image_type = virtual_image_type_for_name_or_code(image['imageType'])
193
193
  image_type_display = image_type ? "#{image_type['name']}" : image['imageType']
@@ -255,7 +255,7 @@ EOT
255
255
  # print "\n", reset
256
256
  end
257
257
 
258
- if image_files
258
+ if image_files && !image_files.empty?
259
259
  print_h2 "Files (#{image_files.size})"
260
260
  # image_files.each {|image_file|
261
261
  # pretty_filesize = Filesize.from("#{image_file['size']} B").pretty
@@ -270,6 +270,11 @@ EOT
270
270
  print as_pretty_table(image_file_rows, [:filename, :size])
271
271
  # print reset,"\n"
272
272
  end
273
+
274
+ if image_locations && !image_locations.empty?
275
+ print_h2 "Locations", options
276
+ print as_pretty_table(image_locations, virtual_image_location_list_column_definitions.upcase_keys!, options)
277
+ end
273
278
 
274
279
  if options[:details] && image_config && !image_config.empty?
275
280
  print_h2 "Config", options
@@ -689,7 +694,7 @@ EOT
689
694
  image = find_virtual_image_by_name_or_id(image_name)
690
695
  return 1 if image.nil?
691
696
  unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the virtual image filename #{filename}?")
692
- exit
697
+ return 9, "aborted"
693
698
  end
694
699
  @virtual_images_interface.setopts(options)
695
700
  if options[:dry_run]
@@ -709,44 +714,188 @@ EOT
709
714
  end
710
715
 
711
716
  def remove(args)
717
+ params = {}
712
718
  options = {}
713
719
  optparse = Morpheus::Cli::OptionParser.new do |opts|
714
- opts.banner = subcommand_usage("[name]")
715
- build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
720
+ opts.banner = subcommand_usage("[image] [location]")
721
+ opts.on('--remove-from-cloud [true|false]', String, "Remove from all clouds. Default is true.") do |val|
722
+ options[:options]['removeFromCloud'] = ['','true','on'].include?(val.to_s)
723
+ end
724
+ build_standard_remove_options(opts, options)
725
+ opts.footer = <<-EOT
726
+ Delete a virtual image.
727
+ [image] is required. This is the name or id of a virtual image.
728
+ EOT
716
729
  end
717
730
  optparse.parse!(args)
718
- if args.count < 1
719
- puts optparse
720
- exit 1
731
+ verify_args!(args:args, optparse:optparse, count:1)
732
+ connect(options)
733
+ image = find_virtual_image_by_name_or_id(args[0])
734
+ return 1, "virtual image not found for '#{args[0]}'" if image.nil?
735
+ params.merge!(parse_query_options(options))
736
+ # Delete prompt
737
+ # [ X ] Remove from all clouds
738
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'removeFromCloud', 'fieldLabel' => 'Remove from all clouds', 'type' => 'checkbox', 'defaultValue' => true, 'required' => true, 'description' => "Remove from all clouds"}], options[:options], @api_client)
739
+ remove_from_cloud = v_prompt['removeFromCloud'].to_s == 'true' || v_prompt['removeFromCloud'].to_s == 'on'
740
+ params['removeFromCloud'] = remove_from_cloud
741
+
742
+ # Delete confirmation
743
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the virtual image #{image['name']}?")
744
+ return 9, "aborted"
745
+ end
746
+
747
+ @virtual_images_interface.setopts(options)
748
+ if options[:dry_run]
749
+ print_dry_run @virtual_images_interface.dry.destroy(image['id'], params)
750
+ return
751
+ end
752
+ json_response = @virtual_images_interface.destroy(image['id'], params)
753
+ render_response(json_response, options) do
754
+ print_green_success "Removed virtual image #{image['name']}"
755
+ end
756
+ return 0, nil
757
+ end
758
+
759
+ def list_locations(args)
760
+ params = {}
761
+ options = {}
762
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
763
+ opts.banner = subcommand_usage("[image]")
764
+ build_standard_list_options(opts, options)
765
+ opts.footer = <<-EOT
766
+ List virtual image locations for a specific virtual image.
767
+ [image] is required. This is the name or id of a virtual image.
768
+ EOT
769
+ end
770
+ optparse.parse!(args)
771
+ verify_args!(args:args, optparse:optparse, min:1)
772
+ if args.count > 1
773
+ options[:phrase] = args[1..-1].join(" ")
721
774
  end
722
- image_name = args[0]
723
775
  connect(options)
724
- begin
725
- image = find_virtual_image_by_name_or_id(image_name)
726
- return 1 if image.nil?
727
- unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the virtual image #{image['name']}?")
728
- exit
729
- end
730
- @virtual_images_interface.setopts(options)
731
- if options[:dry_run]
732
- print_dry_run @virtual_images_interface.dry.destroy(image['id'])
733
- return
734
- end
735
- json_response = @virtual_images_interface.destroy(image['id'])
736
- if options[:json]
737
- print JSON.pretty_generate(json_response)
776
+ image = find_virtual_image_by_name_or_id(args[0])
777
+ return 1, "virtual image not found for '#{args[0]}'" if image.nil?
778
+ params.merge!(parse_list_options(options))
779
+ @virtual_images_interface.setopts(options)
780
+ if options[:dry_run]
781
+ print_dry_run @virtual_images_interface.dry.list_locations(image['id'], params)
782
+ return
783
+ end
784
+ json_response = @virtual_images_interface.list_locations(image['id'], params)
785
+ records = json_response['locations']
786
+ render_response(json_response, options, 'virtualImages') do
787
+ title = "Virtual Image Locations"
788
+ subtitles = parse_list_subtitles(options)
789
+ print_h1 title, subtitles
790
+ if records.empty?
791
+ print cyan,"No virtual image locations found.",reset,"\n"
738
792
  else
739
- print "\n", cyan, "Virtual Image #{image['name']} removed", reset, "\n\n"
793
+ print as_pretty_table(records, virtual_image_location_list_column_definitions.upcase_keys!, options)
794
+ print_results_pagination(json_response)
740
795
  end
741
- rescue RestClient::Exception => e
742
- print_rest_exception(e, options)
743
- exit 1
796
+ print reset,"\n"
797
+ end
798
+ return 0, nil
799
+ end
800
+
801
+ def get_location(args)
802
+ params = {}
803
+ options = {}
804
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
805
+ opts.banner = subcommand_usage("[image] [location]")
806
+ build_standard_remove_options(opts, options)
807
+ opts.footer = <<-EOT
808
+ Get details about a virtual image location.
809
+ [image] is required. This is the name or id of a virtual image.
810
+ [location] is required. This is the name or id of a virtual image location.
811
+ EOT
744
812
  end
813
+ optparse.parse!(args)
814
+ verify_args!(args:args, optparse:optparse, count:2)
815
+ connect(options)
816
+ image = find_virtual_image_by_name_or_id(args[0])
817
+ return 1, "virtual image not found for '#{args[0]}'" if image.nil?
818
+ location = find_virtual_image_location_by_name_or_id(image['id'], args[1])
819
+ return 1, "location not found for '#{args[1]}'" if location.nil?
820
+ params.merge!(parse_query_options(options))
821
+ @virtual_images_interface.setopts(options)
822
+ if options[:dry_run]
823
+ print_dry_run @virtual_images_interface.dry.get_location(image['id'], location['id'])
824
+ return 0, nil
825
+ end
826
+ # json_response = @virtual_images_interface.get(image['id'], location['id'])
827
+ json_response = {'location' => location} # skip redundant request
828
+ render_response(json_response, options, 'location') do
829
+ location = json_response['location']
830
+ volumes = location['volumes'] || []
831
+ print_h1 "Virtual Image Location Details", [], options
832
+ print_description_list(virtual_image_location_column_definitions, location, options)
833
+ if volumes && !volumes.empty?
834
+ print_h2 "Volumes", options
835
+ volume_rows = location_volumes.collect do |volume|
836
+ {name: volume['name'], size: Filesize.from("#{volume['rawSize']} B").pretty}
837
+ end
838
+ print cyan
839
+ print as_pretty_table(volume_rows, [:name, :size], options)
840
+ print cyan
841
+ # print "\n", reset
842
+ end
843
+ print reset,"\n"
844
+ end
845
+ return 0, nil
745
846
  end
746
847
 
848
+ def remove_location(args)
849
+ params = {}
850
+ options = {}
851
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
852
+ opts.banner = subcommand_usage("[image] [location]")
853
+ opts.on('--remove-from-cloud [true|false]', String, "Remove from cloud. Default is true.") do |val|
854
+ options[:options]['removeFromCloud'] = ['','true','on'].include?(val.to_s)
855
+ end
856
+ build_standard_remove_options(opts, options)
857
+ opts.footer = <<-EOT
858
+ Delete a virtual image location.
859
+ [image] is required. This is the name or id of a virtual image.
860
+ [location] is required. This is the name or id of a virtual image location.
861
+ EOT
862
+ end
863
+ optparse.parse!(args)
864
+ verify_args!(args:args, optparse:optparse, count:2)
865
+ connect(options)
866
+ image = find_virtual_image_by_name_or_id(args[0])
867
+ return 1, "virtual image not found for '#{args[0]}'" if image.nil?
868
+ location = find_virtual_image_location_by_name_or_id(image['id'], args[1])
869
+ return 1, "location not found for '#{args[1]}'" if location.nil?
870
+
871
+ params.merge!(parse_query_options(options))
872
+
873
+ # Delete prompt
874
+ # [ X ] Remove from cloud
875
+ v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'removeFromCloud', 'fieldLabel' => 'Remove from cloud', 'type' => 'checkbox', 'defaultValue' => true, 'required' => true, 'description' => "Remove from cloud"}], options[:options], @api_client)
876
+ remove_from_cloud = v_prompt['removeFromCloud'].to_s == 'true' || v_prompt['removeFromCloud'].to_s == 'on'
877
+ params['removeFromCloud'] = remove_from_cloud
878
+
879
+ # Delete confirmation
880
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the virtual image location #{location['id']}?")
881
+ return 9, "aborted"
882
+ end
883
+
884
+ @virtual_images_interface.setopts(options)
885
+ if options[:dry_run]
886
+ print_dry_run @virtual_images_interface.dry.destroy_location(image['id'], location['id'], params)
887
+ return
888
+ end
889
+ json_response = @virtual_images_interface.destroy_location(image['id'], location['id'], params)
890
+ render_response(json_response, options) do
891
+ print_green_success "Removed virtual image location #{location['id']}"
892
+ end
893
+ return 0, nil
894
+ end
747
895
 
748
896
  private
749
- def find_virtual_image_by_name_or_id(val)
897
+
898
+ def find_virtual_image_by_name_or_id(val)
750
899
  if val.to_s =~ /\A\d{1,}\Z/
751
900
  return find_virtual_image_by_id(val)
752
901
  else
@@ -911,4 +1060,75 @@ EOT
911
1060
  out
912
1061
  end
913
1062
 
1063
+
1064
+ ## Virtual Image Locations
1065
+
1066
+ def virtual_image_location_object_key
1067
+ "location"
1068
+ end
1069
+
1070
+ def virtual_image_location_list_key
1071
+ "locations"
1072
+ end
1073
+
1074
+ def find_virtual_image_location_by_name_or_id(virtual_image_id, val)
1075
+ if val.to_s =~ /\A\d{1,}\Z/
1076
+ return find_virtual_image_location_by_id(virtual_image_id, val)
1077
+ else
1078
+ return find_virtual_image_location_by_name(virtual_image_id, val)
1079
+ end
1080
+ end
1081
+
1082
+ def virtual_image_location_list_column_definitions
1083
+ virtual_image_location_column_definitions
1084
+ end
1085
+
1086
+ def virtual_image_location_column_definitions
1087
+ {
1088
+ "ID" => 'id',
1089
+ "Name" => 'imageName',
1090
+ "Cloud" => lambda {|it| it['cloud']['name'] rescue '' },
1091
+ "Public" => lambda {|it| format_boolean(it['isPublic']) },
1092
+ "Region" => lambda {|it| it['imageRegion'] },
1093
+ "External ID" => lambda {|it| it['externalId'] },
1094
+ "Price Plan" => lambda {|it| it['pricePlan'] ? it['pricePlan']['name'] : nil },
1095
+ # "Virtual Image" => lambda {|it| it['virtualImage']['name'] rescue '' },
1096
+ # "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1097
+ # "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
1098
+ }
1099
+ end
1100
+
1101
+
1102
+ def find_virtual_image_location_by_id(virtual_image_id, id)
1103
+ begin
1104
+ json_response = @virtual_images_interface.get_location(virtual_image_id, id.to_i)
1105
+ return json_response[virtual_image_location_object_key]
1106
+ rescue RestClient::Exception => e
1107
+ if e.response && e.response.code == 404
1108
+ print_red_alert "Virtual Image Location not found by id '#{id}'"
1109
+ else
1110
+ raise e
1111
+ end
1112
+ end
1113
+ end
1114
+
1115
+ def find_virtual_image_location_by_name(virtual_image_id, name)
1116
+ json_response = @virtual_images_interface.list_locations(virtual_image_id, {imageName: name.to_s})
1117
+ virtual_image_locations = json_response[virtual_image_location_list_key]
1118
+ if virtual_image_locations.empty?
1119
+ print_red_alert "Virtual Image Location not found by name '#{name}'"
1120
+ return nil
1121
+ elsif virtual_image_locations.size > 1
1122
+ print_red_alert "#{virtual_image_locations.size} Virtual Image Locations found by name '#{name}'"
1123
+ print_error "\n"
1124
+ puts_error as_pretty_table(virtual_image_locations, {"ID" => 'id', "NAME" => 'imageName'}, {color:red})
1125
+ print_red_alert "Try using ID instead"
1126
+ print_error reset,"\n"
1127
+ return nil
1128
+ else
1129
+ return virtual_image_locations[0]
1130
+ end
1131
+ end
1132
+
1133
+
914
1134
  end