morpheus-cli 8.0.7 → 8.0.9

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.
@@ -2915,12 +2915,18 @@ class Morpheus::Cli::Instances
2915
2915
  options = {}
2916
2916
  optparse = Morpheus::Cli::OptionParser.new do |opts|
2917
2917
  opts.banner = subcommand_usage("[instance]")
2918
- opts.on( '--name VALUE', String, "Snapshot Name. Default is server name + timestamp" ) do |val|
2918
+ opts.on( '--name VALUE', String, "Snapshot Name. Default is \"{name}.{timestamp}\"" ) do |val|
2919
2919
  options[:options]['name'] = val
2920
2920
  end
2921
2921
  opts.on( '--description VALUE', String, "Snapshot Description." ) do |val|
2922
2922
  options[:options]['description'] = val
2923
2923
  end
2924
+ opts.on('--memory-snapshot [on|off]', String, "Memory Snapshot? Whether to include the memory state in the snapshot.") do |val|
2925
+ options[:options]['memorySnapshot'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2926
+ end
2927
+ opts.on('--for-export [on|off]', String, "For Export? Indicates the snapshot is intended for export to storage.") do |val|
2928
+ options[:options]['forExport'] = val.to_s == '' || val.to_s == 'on' || val.to_s == 'true'
2929
+ end
2924
2930
  opts.on('--refresh [SECONDS]', String, "Refresh until execution is complete. Default interval is #{default_refresh_interval} seconds.") do |val|
2925
2931
  options[:refresh_interval] = val.to_s.empty? ? default_refresh_interval : val.to_f
2926
2932
  end
@@ -2937,15 +2943,36 @@ EOT
2937
2943
  verify_args!(args:args, optparse:optparse, count:1)
2938
2944
  connect(options)
2939
2945
  instance = find_instance_by_name_or_id(args[0])
2940
- unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2941
- exit 1
2942
- end
2946
+
2943
2947
  payload = {}
2944
2948
  if options[:payload]
2945
2949
  payload = options[:payload]
2946
2950
  payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2947
2951
  else
2948
2952
  payload.deep_merge!({'snapshot' => parse_passed_options(options)})
2953
+ snapshot = payload['snapshot']
2954
+ # prompt for name and description
2955
+ name = prompt_value({'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Snapshot Name', 'description' => "Snapshot Name. Default is \"{name}.{timestamp}\""}, options)
2956
+ snapshot['name'] = name if !name.to_s.empty?
2957
+ description = prompt_value({'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'description' => "Snapshot Description."}, options)
2958
+ snapshot['description'] = description if !description.to_s.empty?
2959
+ # need to GET provision type for some settings...
2960
+ provision_type = nil
2961
+ begin
2962
+ provision_type = @provision_types_interface.get(instance['layout']['provisionTypeId'])['provisionType']
2963
+ rescue => ex
2964
+ Morpheus::Logging::DarkPrinter.puts "Failed to load provision type!" if Morpheus::Logging.debug?
2965
+ end
2966
+ if provision_type && provision_type['hasMemorySnapshots']
2967
+ # prompt for memorySnapshot
2968
+ memory_snapshot = prompt_value({'fieldName' => 'memorySnapshot', 'type' => 'checkbox', 'fieldLabel' => 'Memory Snapshot?', 'description' => "Snapshot Description."}, options)
2969
+ snapshot['memorySnapshot'] = memory_snapshot if !memory_snapshot.to_s.empty?
2970
+ end
2971
+ # convert "on" and "off" to true/false
2972
+ payload.booleanize!
2973
+ end
2974
+ unless options[:yes] || ::Morpheus::Cli::OptionTypes::confirm("Are you sure you would like to snapshot the instance '#{instance['name']}'?", options)
2975
+ exit 1
2949
2976
  end
2950
2977
  @instances_interface.setopts(options)
2951
2978
  if options[:dry_run]
@@ -3470,17 +3497,18 @@ EOT
3470
3497
  def snapshots(args)
3471
3498
  options = {}
3472
3499
  optparse = Morpheus::Cli::OptionParser.new do |opts|
3473
- opts.banner = subcommand_usage("[instance]")
3500
+ opts.banner = subcommand_usage("[instance] [snapshot]")
3474
3501
  # no pagination yet
3475
3502
  # build_standard_list_options(opts, options)
3476
- build_standard_get_options(opts, options)
3503
+ build_standard_list_options(opts, options, [:details])
3477
3504
  opts.footer = <<-EOT
3478
3505
  List snapshots for an instance.
3479
3506
  [instance] is required. This is the name or id of an instance
3507
+ [snapshot] is optional. this is the name or id a snapshot to filter by.
3480
3508
  EOT
3481
3509
  end
3482
3510
  optparse.parse!(args)
3483
- verify_args!(args:args, optparse:optparse, count:1)
3511
+ verify_args!(args:args, optparse:optparse, min:1, max: 2)
3484
3512
  connect(options)
3485
3513
  begin
3486
3514
  instance = find_instance_by_name_or_id(args[0])
@@ -3491,25 +3519,52 @@ EOT
3491
3519
  return
3492
3520
  end
3493
3521
  json_response = @instances_interface.snapshots(instance['id'], params)
3494
- snapshots = json_response['snapshots']
3522
+ snapshots = json_response['snapshots']
3523
+ # [snapshots] is done with post api filtering by id or name or externalId
3524
+ if args[1]
3525
+ if args[1] =~ /\A\d{1,}\Z/
3526
+ snapshots = snapshots.select {|it| it['id'].to_s == args[1] }
3527
+ else
3528
+ # snapshots = snapshots.select {|it| it['name'] == args[1] || it['externalId'] == args[1] }
3529
+ # match beginning of name of externalId
3530
+ snapshots = snapshots.select {|it| it['name'].to_s.index(args[1]) == 0 || it['externalId'].to_s.index(args[1]) == 0 }
3531
+ end
3532
+ json_response['snapshots'] = snapshots # update response for -j filtering too
3533
+ end
3495
3534
  render_response(json_response, options, 'snapshots') do
3496
3535
  print_h1 "Snapshots: #{instance['name']} (#{instance['instanceType']['name']})", [], options
3497
3536
  if snapshots.empty?
3498
- print cyan,"No snapshots found",reset,"\n"
3537
+ if args[1]
3538
+ print cyan,"No snapshots found for '#{args[1]}'",reset,"\n"
3539
+ elsif
3540
+ print cyan,"No snapshots found",reset,"\n"
3541
+ end
3542
+ print reset, "\n"
3499
3543
  else
3500
- snapshot_column_definitions = {
3501
- "ID" => lambda {|it| it['id'] },
3502
- "Name" => lambda {|it| it['name'] },
3503
- "Description" => lambda {|it| it['description'] },
3504
- # "Type" => lambda {|it| it['snapshotType'] },
3505
- "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
3506
- "Status" => lambda {|it| format_snapshot_status(it) }
3507
- }
3508
- print cyan
3509
- print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
3510
- print_results_pagination({size: snapshots.size, total: snapshots.size})
3544
+ if options[:details]
3545
+ if snapshots.size > 3
3546
+ print cyan, "Showing first 3 snapshots. Use the ID to get more details.", reset, "\n"
3547
+ snapshots = snapshots.first(3) # this actually makes a request for each one here so don't go crazy...
3548
+ end
3549
+ snapshots.each do |snapshot|
3550
+ Morpheus::Cli::Snapshots.new.handle(["get", snapshot['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
3551
+ end
3552
+ else
3553
+ # Snapshots List
3554
+ snapshot_column_definitions = {
3555
+ "ID" => lambda {|it| it['id'] },
3556
+ "Name" => lambda {|it| it['name'] },
3557
+ "Description" => lambda {|it| it['description'] },
3558
+ # "Type" => lambda {|it| it['snapshotType'] },
3559
+ "Date Created" => lambda {|it| format_local_dt(it['snapshotCreated']) },
3560
+ "Status" => lambda {|it| format_snapshot_status(it) }
3561
+ }
3562
+ print cyan
3563
+ print as_pretty_table(snapshots, snapshot_column_definitions.upcase_keys!, options)
3564
+ print_results_pagination({size: snapshots.size, total: snapshots.size})
3565
+ print reset, "\n"
3566
+ end
3511
3567
  end
3512
- print reset, "\n"
3513
3568
  end
3514
3569
  return 0
3515
3570
  rescue RestClient::Exception => e
@@ -77,11 +77,15 @@ class Morpheus::Cli::Snapshots
77
77
  "Snapshot Type" => 'snapshotType',
78
78
  "Cloud" => lambda {|it| format_name_and_id(it['zone']) },
79
79
  "Datastore" => lambda {|it| format_name_and_id(it['datastore']) },
80
+ "Memory Snapshot" => lambda {|it| format_boolean(it['memorySnapshot']) },
81
+ "For Export" => lambda {|it| format_boolean(it['forExport']) },
80
82
  "Parent Snapshot" => lambda {|it| format_name_and_id(it['parentSnapshot']) },
81
83
  "Active" => lambda {|it| format_boolean(it['currentlyActive']) },
82
84
  "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
83
85
  "Status" => lambda {|it| format_snapshot_status(it) }
84
86
  }
87
+ description_cols.delete("Memory Snapshot") if !snapshot['memorySnapshot']
88
+ description_cols.delete("For Export") if !snapshot['forExport']
85
89
  print_description_list(description_cols, snapshot)
86
90
 
87
91
  print reset, "\n"
@@ -6,7 +6,8 @@ class Morpheus::Cli::VirtualImages
6
6
  include Morpheus::Cli::CliCommand
7
7
  include Morpheus::Cli::ProvisioningHelper
8
8
 
9
- register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove, :convert, :types => :virtual_image_types
9
+ register_subcommands :list, :get, :add, :add_file, :remove_file, :update, :remove,
10
+ :convert, {:types => :virtual_image_types}, :download
10
11
  register_subcommands :list_locations, :get_location, :remove_location
11
12
 
12
13
  # def initialize()
@@ -973,6 +974,94 @@ EOT
973
974
  return 0, nil
974
975
  end
975
976
 
977
+ def download(args)
978
+ full_command_string = "#{command_name} download #{args.join(' ')}".strip
979
+ options = {}
980
+ outfile = nil
981
+ do_overwrite = false
982
+ do_mkdir = false
983
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
984
+ opts.banner = subcommand_usage("[image] [local-file]")
985
+ opts.on( '-f', '--force', "Overwrite existing [local-file] if it exists." ) do
986
+ do_overwrite = true
987
+ # do_mkdir = true
988
+ end
989
+ opts.on( '-p', '--mkdir', "Create missing directories for [local-file] if they do not exist." ) do
990
+ do_mkdir = true
991
+ end
992
+ build_common_options(opts, options, [:dry_run, :quiet])
993
+ opts.footer = "Download a virtual image as a .zip file.\n" +
994
+ "[image] is required. This is the name or id of a virtual image.\n" +
995
+ "[local-file] is required. This is the full local filepath for the downloaded zip file."
996
+ end
997
+ optparse.parse!(args)
998
+ verify_args!(args:args, optparse:optparse, count:2)
999
+ connect(options)
1000
+
1001
+ virtual_image = find_virtual_image_by_name_or_id(args[0])
1002
+ return 1 if virtual_image.nil?
1003
+ outfile = args[1]
1004
+ # if outfile.end_with?(".zip")
1005
+ # print_red_alert "[local-file] is invalid. It must use extension .zip: #{outfile}"
1006
+ # return 1
1007
+ # end
1008
+ outfile = File.expand_path(outfile)
1009
+ if Dir.exist?(outfile)
1010
+ raise_command_error "[local-file] is invalid. It is the name of an existing directory: #{outfile}", args, optparse
1011
+ end
1012
+ destination_dir = File.dirname(outfile)
1013
+ if !Dir.exist?(destination_dir)
1014
+ if do_mkdir
1015
+ print cyan,"Creating local directory #{destination_dir}",reset,"\n"
1016
+ FileUtils.mkdir_p(destination_dir)
1017
+ else
1018
+ raise_command_error "[local-file] is invalid. Directory not found: #{destination_dir}", args, optparse
1019
+ end
1020
+ end
1021
+ if File.exist?(outfile)
1022
+ if do_overwrite
1023
+ # uhh need to be careful wih the passed filepath here..
1024
+ # don't delete, just overwrite.
1025
+ # File.delete(outfile)
1026
+ else
1027
+ raise_command_error "[local-file] is invalid. File already exists: #{outfile}", "Use -f to overwrite the existing file.", args, optparse
1028
+ end
1029
+ end
1030
+
1031
+ @virtual_images_interface.setopts(options)
1032
+ if options[:dry_run]
1033
+ print_dry_run @virtual_images_interface.dry.download_chunked(virtual_image['id'], outfile), full_command_string
1034
+ return 0, nil
1035
+ end
1036
+ if !options[:quiet]
1037
+ print cyan + "Downloading archive file #{virtual_image['name']} to #{outfile} ... "
1038
+ end
1039
+
1040
+ http_response = @virtual_images_interface.download_chunked(virtual_image['id'], outfile)
1041
+
1042
+ # FileUtils.chmod(0600, outfile)
1043
+ success = http_response.code.to_i == 200
1044
+ if success
1045
+ if !options[:quiet]
1046
+ print green + "SUCCESS" + reset + "\n"
1047
+ end
1048
+ return 0, nil
1049
+ else
1050
+ if !options[:quiet]
1051
+ print red + "ERROR" + reset + " HTTP #{http_response.code}" + "\n"
1052
+ end
1053
+ # F it, just remove a bad result
1054
+ if File.exist?(outfile) && File.file?(outfile)
1055
+ Morpheus::Logging::DarkPrinter.puts "Deleting bad file download: #{outfile}" if Morpheus::Logging.debug?
1056
+ File.delete(outfile)
1057
+ end
1058
+ if options[:debug]
1059
+ puts_error http_response.inspect
1060
+ end
1061
+ return 1, "Error downloading file"
1062
+ end
1063
+ end
1064
+
976
1065
  private
977
1066
 
978
1067
  def find_virtual_image_by_name_or_id(val)
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "8.0.7"
4
+ VERSION = "8.0.9"
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morpheus-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.7
4
+ version: 8.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Estes
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2025-06-13 00:00:00.000000000 Z
14
+ date: 2025-08-25 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: public_suffix