morpheus-cli 4.2.12 → 4.2.17

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/README.md +8 -6
  4. data/lib/morpheus/api/api_client.rb +32 -14
  5. data/lib/morpheus/api/auth_interface.rb +4 -2
  6. data/lib/morpheus/api/backup_jobs_interface.rb +9 -0
  7. data/lib/morpheus/api/backups_interface.rb +16 -0
  8. data/lib/morpheus/api/deploy_interface.rb +25 -56
  9. data/lib/morpheus/api/deployments_interface.rb +43 -54
  10. data/lib/morpheus/api/doc_interface.rb +57 -0
  11. data/lib/morpheus/api/instances_interface.rb +5 -0
  12. data/lib/morpheus/api/rest_interface.rb +40 -0
  13. data/lib/morpheus/api/user_sources_interface.rb +0 -15
  14. data/lib/morpheus/api/users_interface.rb +2 -3
  15. data/lib/morpheus/benchmarking.rb +2 -2
  16. data/lib/morpheus/cli.rb +3 -1
  17. data/lib/morpheus/cli/access_token_command.rb +27 -10
  18. data/lib/morpheus/cli/apps.rb +23 -16
  19. data/lib/morpheus/cli/backup_jobs_command.rb +276 -0
  20. data/lib/morpheus/cli/backups_command.rb +271 -0
  21. data/lib/morpheus/cli/boot_scripts_command.rb +1 -1
  22. data/lib/morpheus/cli/cli_command.rb +176 -45
  23. data/lib/morpheus/cli/cli_registry.rb +10 -1
  24. data/lib/morpheus/cli/clusters.rb +2 -19
  25. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +23 -20
  26. data/lib/morpheus/cli/commands/standard/man_command.rb +1 -1
  27. data/lib/morpheus/cli/containers_command.rb +2 -1
  28. data/lib/morpheus/cli/credentials.rb +14 -10
  29. data/lib/morpheus/cli/deploy.rb +374 -0
  30. data/lib/morpheus/cli/deployments.rb +521 -197
  31. data/lib/morpheus/cli/deploys.rb +271 -126
  32. data/lib/morpheus/cli/doc.rb +182 -0
  33. data/lib/morpheus/cli/error_handler.rb +23 -8
  34. data/lib/morpheus/cli/errors.rb +3 -2
  35. data/lib/morpheus/cli/health_command.rb +4 -3
  36. data/lib/morpheus/cli/hosts.rb +2 -1
  37. data/lib/morpheus/cli/image_builder_command.rb +2 -2
  38. data/lib/morpheus/cli/instances.rb +138 -18
  39. data/lib/morpheus/cli/invoices_command.rb +338 -223
  40. data/lib/morpheus/cli/library_layouts_command.rb +1 -1
  41. data/lib/morpheus/cli/library_option_lists_command.rb +61 -125
  42. data/lib/morpheus/cli/library_option_types_command.rb +32 -37
  43. data/lib/morpheus/cli/login.rb +9 -3
  44. data/lib/morpheus/cli/logs_command.rb +3 -2
  45. data/lib/morpheus/cli/mixins/accounts_helper.rb +158 -100
  46. data/lib/morpheus/cli/mixins/backups_helper.rb +115 -0
  47. data/lib/morpheus/cli/mixins/deployments_helper.rb +135 -0
  48. data/lib/morpheus/cli/mixins/library_helper.rb +32 -0
  49. data/lib/morpheus/cli/mixins/logs_helper.rb +18 -9
  50. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  51. data/lib/morpheus/cli/mixins/print_helper.rb +149 -84
  52. data/lib/morpheus/cli/mixins/provisioning_helper.rb +2 -2
  53. data/lib/morpheus/cli/mixins/whoami_helper.rb +19 -6
  54. data/lib/morpheus/cli/network_routers_command.rb +1 -1
  55. data/lib/morpheus/cli/option_parser.rb +48 -5
  56. data/lib/morpheus/cli/option_types.rb +46 -10
  57. data/lib/morpheus/cli/price_sets_command.rb +1 -1
  58. data/lib/morpheus/cli/remote.rb +8 -10
  59. data/lib/morpheus/cli/roles.rb +49 -92
  60. data/lib/morpheus/cli/security_groups.rb +7 -1
  61. data/lib/morpheus/cli/service_plans_command.rb +10 -10
  62. data/lib/morpheus/cli/setup.rb +1 -1
  63. data/lib/morpheus/cli/shell.rb +7 -6
  64. data/lib/morpheus/cli/subnets_command.rb +1 -1
  65. data/lib/morpheus/cli/tenants_command.rb +133 -163
  66. data/lib/morpheus/cli/user_groups_command.rb +20 -65
  67. data/lib/morpheus/cli/user_settings_command.rb +115 -13
  68. data/lib/morpheus/cli/user_sources_command.rb +57 -24
  69. data/lib/morpheus/cli/users.rb +210 -186
  70. data/lib/morpheus/cli/version.rb +1 -1
  71. data/lib/morpheus/cli/whitelabel_settings_command.rb +29 -5
  72. data/lib/morpheus/cli/whoami.rb +113 -6
  73. data/lib/morpheus/cli/workflows.rb +1 -1
  74. data/lib/morpheus/ext/hash.rb +21 -0
  75. data/lib/morpheus/formatters.rb +7 -19
  76. data/lib/morpheus/terminal.rb +1 -0
  77. metadata +12 -3
  78. data/lib/morpheus/cli/auth_command.rb +0 -105
@@ -31,18 +31,31 @@ class Morpheus::Cli::ErrorHandler
31
31
  # raise err
32
32
  # @stderr.puts "#{red}#{err.message}#{reset}"
33
33
  puts_angry_error err.message
34
- @stderr.puts "Use -h to get help with this command."
34
+ @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner
35
+ @stderr.puts "Try --help for more usage information"
35
36
  do_print_stacktrace = false
36
37
  # exit_code = 127
37
- # when Morpheus::Cli::CommandArgumentsError
38
+ when Morpheus::Cli::CommandArgumentsError
39
+ puts_angry_error err.message
40
+ @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner
41
+ @stderr.puts "Try --help for more usage information"
42
+ do_print_stacktrace = false
43
+ if err.exit_code
44
+ exit_code = err.exit_code
45
+ end
46
+
38
47
  when Morpheus::Cli::CommandError
39
- # @stderr.puts "#{red}#{err.message}#{reset}"
40
- # this should probably print the whole thing as red, but just does the first line for now.
48
+ # this should probably always print the whole thing as red, but just does the first line for now.
49
+ # until verify_args! replaces raise_command_error where the full parser help is in the error message..
41
50
  message_lines = err.message.split(/\r?\n/)
42
51
  first_line = message_lines.shift
43
52
  puts_angry_error first_line
44
- @stderr.puts message_lines.join("\n") unless message_lines.empty?
45
- @stderr.puts "Use -h to get help with this command."
53
+ if !message_lines.empty?
54
+ @stderr.puts message_lines.join("\n") unless message_lines.empty?
55
+ else
56
+ @stderr.puts err.optparse.banner if err.optparse && err.optparse.banner && message_lines.empty?
57
+ @stderr.puts "Try --help for more usage information"
58
+ end
46
59
  do_print_stacktrace = false
47
60
  if err.exit_code
48
61
  exit_code = err.exit_code
@@ -82,7 +95,7 @@ class Morpheus::Cli::ErrorHandler
82
95
  @stderr.puts err.to_s
83
96
  end
84
97
  else
85
- @stderr.puts "Use --debug for more information."
98
+ @stderr.puts "Use -V or --debug for more verbose debugging information."
86
99
  end
87
100
  end
88
101
 
@@ -127,6 +140,8 @@ class Morpheus::Cli::ErrorHandler
127
140
  begin
128
141
  print_rest_errors(JSON.parse(err.response.to_s), options)
129
142
  rescue TypeError, JSON::ParserError => ex
143
+ # not json, just 404
144
+ @stderr.print red, "Error Communicating with the remote appliance. #{e}", reset, "\n"
130
145
  end
131
146
  else
132
147
  @stderr.print red, "Error Communicating with the remote appliance. #{e}", reset, "\n"
@@ -145,7 +160,7 @@ class Morpheus::Cli::ErrorHandler
145
160
  @stderr.print reset
146
161
  end
147
162
  else
148
- @stderr.puts "Use --debug for more information."
163
+ @stderr.puts "Use -V or --debug for more verbose debugging information."
149
164
  end
150
165
  end
151
166
  else
@@ -3,7 +3,7 @@ module Morpheus::Cli
3
3
  # A standard error to raise in your CliCommand classes.
4
4
  class CommandError < StandardError
5
5
 
6
- attr_reader :args, :exit_code
6
+ attr_reader :args, :optparse, :exit_code
7
7
 
8
8
  def initialize(msg, args=[], optparse=nil, exit_code=nil)
9
9
  @args = args
@@ -11,6 +11,7 @@ module Morpheus::Cli
11
11
  @exit_code = exit_code # || 1
12
12
  super(msg)
13
13
  end
14
+
14
15
  end
15
16
 
16
17
  # An error indicating the command was not recoginzed
@@ -24,7 +25,7 @@ module Morpheus::Cli
24
25
 
25
26
  # An error for wrong number of arguments
26
27
  # could use ::OptionParser::MissingArgument, ::OptionParser::NeedlessArgument
27
- # maybe return an error code niftier than 1?
28
+ # maybe return an error code other than 1?
28
29
  class CommandArgumentsError < CommandError
29
30
 
30
31
  def initialize(msg, args=[], optparse=nil, exit_code=nil)
@@ -457,7 +457,7 @@ class Morpheus::Cli::HealthCommand
457
457
  optparse = Morpheus::Cli::OptionParser.new do |opts|
458
458
  opts.banner = subcommand_usage()
459
459
  opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
460
- params['level'] = params['level'] ? [params['level'], val].flatten : val
460
+ params['level'] = params['level'] ? [params['level'], val].flatten : [val]
461
461
  end
462
462
  opts.on('--start TIMESTAMP','--start TIMESTAMP', "Start timestamp. Default is 30 days ago.") do |val|
463
463
  start_date = parse_time(val) #.utc.iso8601
@@ -465,7 +465,7 @@ class Morpheus::Cli::HealthCommand
465
465
  opts.on('--end TIMESTAMP','--end TIMESTAMP', "End timestamp. Default is now.") do |val|
466
466
  end_date = parse_time(val) #.utc.iso8601
467
467
  end
468
- opts.on('--table', '--table', "Format output as a table.") do
468
+ opts.on('-t', '--table', "Format output as a table.") do
469
469
  options[:table] = true
470
470
  end
471
471
  opts.on('-a', '--all', "Display all details: entire message." ) do
@@ -484,6 +484,7 @@ class Morpheus::Cli::HealthCommand
484
484
  # params['endDate'] = end_date.utc.iso8601 if end_date
485
485
  params['startMs'] = (start_date.to_i * 1000) if start_date
486
486
  params['endMs'] = (end_date.to_i * 1000) if end_date
487
+ params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
487
488
  params.merge!(parse_list_options(options))
488
489
  @health_interface.setopts(options)
489
490
  if options[:dry_run]
@@ -497,7 +498,7 @@ class Morpheus::Cli::HealthCommand
497
498
  title = "Morpheus Health Logs"
498
499
  subtitles = []
499
500
  if params['level']
500
- subtitles << "Level: #{params['level']}"
501
+ subtitles << "Level: #{[params['level']].flatten.join(',')}"
501
502
  end
502
503
  if start_date
503
504
  subtitles << "Start: #{start_date}"
@@ -582,7 +582,7 @@ class Morpheus::Cli::Hosts
582
582
  options[:end] = parse_time(val) #.utc.iso8601
583
583
  end
584
584
  opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
585
- params['level'] = params['level'] ? [params['level'], val].flatten : val
585
+ params['level'] = params['level'] ? [params['level'], val].flatten : [val]
586
586
  end
587
587
  opts.on('--table', '--table', "Format ouput as a table.") do
588
588
  options[:table] = true
@@ -600,6 +600,7 @@ class Morpheus::Cli::Hosts
600
600
  connect(options)
601
601
  begin
602
602
  server = find_host_by_name_or_id(args[0])
603
+ params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
603
604
  params.merge!(parse_list_options(options))
604
605
  params['query'] = params.delete('phrase') if params['phrase']
605
606
  params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
@@ -518,7 +518,7 @@ class Morpheus::Cli::ImageBuilderCommand
518
518
  opts.on( '-K', '--keep-virtual-images', "Preserve associated virtual images" ) do
519
519
  query_params['keepVirtualImages'] = 'on'
520
520
  end
521
- build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run, :remote])
521
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
522
522
  end
523
523
  optparse.parse!(args)
524
524
 
@@ -565,7 +565,7 @@ class Morpheus::Cli::ImageBuilderCommand
565
565
  query_params = {}
566
566
  optparse = Morpheus::Cli::OptionParser.new do |opts|
567
567
  opts.banner = subcommand_usage("[image-build]")
568
- build_common_options(opts, options, [:account, :auto_confirm, :json, :dry_run, :remote])
568
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
569
569
  end
570
570
  optparse.parse!(args)
571
571
 
@@ -9,14 +9,22 @@ class Morpheus::Cli::Instances
9
9
  include Morpheus::Cli::AccountsHelper # needed? replace with OptionSourceHelper
10
10
  include Morpheus::Cli::OptionSourceHelper
11
11
  include Morpheus::Cli::ProvisioningHelper
12
+ include Morpheus::Cli::DeploymentsHelper
12
13
  include Morpheus::Cli::ProcessesHelper
13
14
  include Morpheus::Cli::LogsHelper
14
15
 
15
16
  set_command_name :instances
16
17
  set_command_description "View and manage instances."
17
- register_subcommands :list, :count, :get, :view, :add, :update, :remove, :cancel_removal, :logs, :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details}, :stats, :stop, :start, :restart, :actions, :action, :suspend, :eject, :backup, :backups, :stop_service, :start_service, :restart_service, :resize, :clone, :envs, :setenv, :delenv, :security_groups, :apply_security_groups, :run_workflow, :import_snapshot, :console, :status_check, {:containers => :list_containers}, :scaling, {:'scaling-update' => :scaling_update}
18
- register_subcommands :wiki, :update_wiki
19
- register_subcommands :exec => :execution_request
18
+ register_subcommands :list, :count, :get, :view, :add, :update, :remove, :cancel_removal, :logs,
19
+ :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details},
20
+ :stats, :stop, :start, :restart, :actions, :action, :suspend, :eject, :stop_service, :start_service, :restart_service,
21
+ :backup, :backups, :resize, :clone, :envs, :setenv, :delenv,
22
+ :security_groups, :apply_security_groups, :run_workflow, :import_snapshot,
23
+ :console, :status_check, {:containers => :list_containers},
24
+ :scaling, {:'scaling-update' => :scaling_update},
25
+ :wiki, :update_wiki,
26
+ {:exec => :execution_request},
27
+ :deploys
20
28
  #register_subcommands :firewall_disable, :firewall_enable
21
29
  # register_subcommands {:'lb-update' => :load_balancer_update}
22
30
  alias_subcommand :details, :get
@@ -43,6 +51,8 @@ class Morpheus::Cli::Instances
43
51
  @options_interface = @api_client.options
44
52
  @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
45
53
  @execution_request_interface = @api_client.execution_request
54
+ @deploy_interface = @api_client.deploy
55
+ @deployments_interface = @api_client.deployments
46
56
  end
47
57
 
48
58
  def handle(args)
@@ -546,18 +556,12 @@ class Morpheus::Cli::Instances
546
556
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
547
557
  end
548
558
  optparse.parse!(args)
549
- if args.count < 1
550
- puts optparse
551
- exit 1
552
- end
559
+ verify_args!(args:args, optparse:optparse, count:1)
553
560
  connect(options)
554
561
 
555
562
  begin
556
563
  instance = find_instance_by_name_or_id(args[0])
557
564
  return 1 if instance.nil?
558
- new_group = nil
559
-
560
-
561
565
  if options[:payload]
562
566
  payload = options[:payload]
563
567
  end
@@ -579,10 +583,40 @@ class Morpheus::Cli::Instances
579
583
  params['ownerId'] = owner_id
580
584
  #payload['createdById'] = options[:owner].to_i # pre 4.2.1 api
581
585
  end
582
- if params.empty? && options[:owner].nil?
583
- print_red_alert "Specify at least one option to update"
584
- puts optparse
585
- exit 1
586
+ if options[:group]
587
+ group = find_group_by_name_or_id_for_provisioning(options[:group])
588
+ if group.nil?
589
+ return 1, "group not found"
590
+ end
591
+ payload['instance']['site'] = {'id' => group['id']}
592
+ end
593
+ # metadata tags
594
+ # if options[:options]['metadata'].is_a?(Array) && !options[:metadata]
595
+ # options[:metadata] = options[:options]['metadata']
596
+ # end
597
+ if options[:metadata]
598
+ metadata = []
599
+ if options[:metadata] == "[]" || options[:metadata] == "null"
600
+ payload['instance']['metadata'] = []
601
+ elsif options[:metadata].is_a?(Array)
602
+ payload['instance']['metadata'] = options[:metadata]
603
+ else
604
+ # parse string into format name:value, name:value
605
+ # merge IDs from current metadata
606
+ # todo: should allow quoted semicolons..
607
+ metadata_list = options[:metadata].split(",").select {|it| !it.to_s.empty? }
608
+ metadata_list = metadata_list.collect do |it|
609
+ metadata_pair = it.split(":")
610
+ row = {}
611
+ row['name'] = metadata_pair[0].to_s.strip
612
+ row['value'] = metadata_pair[1].to_s.strip
613
+ row
614
+ end
615
+ payload['instance']['metadata'] = metadata_list
616
+ end
617
+ end
618
+ if payload['instance'].empty? && params.empty? && options[:owner].nil?
619
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
586
620
  end
587
621
  if !params.empty?
588
622
  payload['instance'].deep_merge!(params)
@@ -1009,7 +1043,7 @@ class Morpheus::Cli::Instances
1009
1043
  # options[:interval] = parse_time(val).utc.iso8601
1010
1044
  # end
1011
1045
  opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
1012
- params['level'] = params['level'] ? [params['level'], val].flatten : val
1046
+ params['level'] = params['level'] ? [params['level'], val].flatten : [val]
1013
1047
  end
1014
1048
  opts.on('--table', '--table', "Format ouput as a table.") do
1015
1049
  options[:table] = true
@@ -1036,6 +1070,7 @@ class Morpheus::Cli::Instances
1036
1070
  return 1
1037
1071
  end
1038
1072
  end
1073
+ params['level'] = params['level'].collect {|it| it.to_s.upcase }.join('|') if params['level'] # api works with INFO|WARN
1039
1074
  params.merge!(parse_list_options(options))
1040
1075
  params['query'] = params.delete('phrase') if params['phrase']
1041
1076
  params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
@@ -1217,7 +1252,6 @@ class Morpheus::Cli::Instances
1217
1252
  "Environment" => 'instanceContext',
1218
1253
  "Labels" => lambda {|it| it['tags'] ? it['tags'].join(',') : '' },
1219
1254
  "Metadata" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
1220
- "Power Schedule" => lambda {|it| (it['powerSchedule'] && it['powerSchedule']['type']) ? it['powerSchedule']['type']['name'] : '' },
1221
1255
  "Owner" => lambda {|it|
1222
1256
  if it['owner']
1223
1257
  (it['owner']['username'] || it['owner']['id'])
@@ -1227,13 +1261,20 @@ class Morpheus::Cli::Instances
1227
1261
  },
1228
1262
  #"Tenant" => lambda {|it| it['tenant'] ? it['tenant']['name'] : '' },
1229
1263
  "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1264
+ # "Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
1265
+ "Power Schedule" => lambda {|it| (it['powerSchedule'] && it['powerSchedule']['type']) ? it['powerSchedule']['type']['name'] : '' },
1266
+ "Last Deployment" => lambda {|it| (it['lastDeploy'] ? "#{it['lastDeploy']['deployment']['name']} #{it['lastDeploy']['deploymentVersion']['userVersion']} at #{format_local_dt it['lastDeploy']['deployDate']}" : nil) rescue "" },
1267
+ "Expire Date" => lambda {|it| it['expireDate'] ? format_local_dt(it['expireDate']) : '' },
1268
+ "Shutdown Date" => lambda {|it| it['shutdownDate'] ? format_local_dt(it['shutdownDate']) : '' },
1230
1269
  "Nodes" => lambda {|it| it['containers'] ? it['containers'].count : 0 },
1231
1270
  "Connection" => lambda {|it| format_instance_connection_string(it) },
1232
1271
  "Status" => lambda {|it| format_instance_status(it) }
1233
1272
  }
1234
-
1273
+ description_cols.delete("Power Schedule") if instance['powerSchedule'].nil?
1274
+ description_cols.delete("Expire Date") if instance['expireDate'].nil?
1275
+ description_cols.delete("Shutdown Date") if instance['shutdownDate'].nil?
1235
1276
  description_cols["Removal Date"] = lambda {|it| format_local_dt(it['removalDate'])} if instance['status'] == 'pendingRemoval'
1236
-
1277
+ description_cols.delete("Last Deployment") if instance['lastDeploy'].nil?
1237
1278
  print_description_list(description_cols, instance)
1238
1279
 
1239
1280
  if instance['statusMessage']
@@ -3605,6 +3646,60 @@ class Morpheus::Cli::Instances
3605
3646
  end
3606
3647
  end
3607
3648
 
3649
+ def deploys(args)
3650
+ params = {}
3651
+ options = {}
3652
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
3653
+ opts.banner = subcommand_usage("[instance] [search]")
3654
+ build_standard_list_options(opts, options)
3655
+ opts.footer = <<-EOT
3656
+ List deployments for an instance.
3657
+ [instance] is required. This is the name or id of an instance
3658
+ [search] is optional. Filters on deployment version identifier
3659
+ EOT
3660
+ end
3661
+ optparse.parse!(args)
3662
+ verify_args!(args:args, optparse:optparse, min:1)
3663
+ connect(options)
3664
+ if args.count > 1
3665
+ options[:phrase] = args.join(" ")
3666
+ end
3667
+ params.merge!(parse_list_options(options))
3668
+ instance = find_instance_by_name_or_id(args[0])
3669
+ return 1 if instance.nil?
3670
+ # @deploy_interface.setopts(options)
3671
+ # if options[:dry_run]
3672
+ # print_dry_run @deploy_interface.dry.list(instance['id'], params)
3673
+ # return
3674
+ # end
3675
+ # json_response = @deploy_interface.list(instance['id'], params)
3676
+
3677
+ @instances_interface.setopts(options)
3678
+ if options[:dry_run]
3679
+ print_dry_run @instances_interface.dry.deploys(instance['id'], params)
3680
+ return
3681
+ end
3682
+ json_response = @instances_interface.deploys(instance['id'], params)
3683
+
3684
+ app_deploys = json_response['appDeploys']
3685
+ render_response(json_response, options, 'appDeploys') do
3686
+ print_h1 "Instance Deploys", ["#{instance['name']}"] + parse_list_subtitles(options), options
3687
+ if app_deploys.empty?
3688
+ print cyan,"No deployments found.",reset,"\n"
3689
+ else
3690
+ print as_pretty_table(app_deploys, app_deploy_column_definitions.upcase_keys!, options)
3691
+ if json_response['meta']
3692
+ print_results_pagination(json_response)
3693
+ else
3694
+ print_results_pagination({size:app_deploys.size,total:app_deploys.size.to_i})
3695
+ end
3696
+
3697
+ end
3698
+ print reset,"\n"
3699
+ end
3700
+ return 0
3701
+ end
3702
+
3608
3703
  private
3609
3704
 
3610
3705
  def find_zone_by_name_or_id(group_id, val)
@@ -3901,4 +3996,29 @@ private
3901
3996
  ]
3902
3997
  end
3903
3998
 
3999
+ def app_deploy_column_definitions
4000
+ {
4001
+ "ID" => 'id',
4002
+ "Deployment" => lambda {|it| it['deployment']['name'] rescue '' },
4003
+ "Version" => lambda {|it| (it['deploymentVersion']['userVersion'] || it['deploymentVersion']['version']) rescue '' },
4004
+ "Deploy Date" => lambda {|it| format_local_dt(it['deployDate']) },
4005
+ "Status" => lambda {|it| format_app_deploy_status(it['status']) },
4006
+ }
4007
+ end
4008
+
4009
+ def format_app_deploy_status(status, return_color=cyan)
4010
+ out = ""
4011
+ s = status.to_s.downcase
4012
+ if s == 'deployed'
4013
+ out << "#{green}#{s.upcase}#{return_color}"
4014
+ elsif s == 'open' || s == 'archived' || s == 'committed'
4015
+ out << "#{cyan}#{s.upcase}#{return_color}"
4016
+ elsif s == 'failed'
4017
+ out << "#{red}#{s.upcase}#{return_color}"
4018
+ else
4019
+ out << "#{yellow}#{s.upcase}#{return_color}"
4020
+ end
4021
+ out
4022
+ end
4023
+
3904
4024
  end
@@ -25,39 +25,29 @@ class Morpheus::Cli::InvoicesCommand
25
25
  options = {}
26
26
  params = {}
27
27
  ref_ids = []
28
+ query_tags = {}
28
29
  optparse = Morpheus::Cli::OptionParser.new do |opts|
29
30
  opts.banner = subcommand_usage()
30
- opts.on('-a', '--all', "Display all costs, prices and raw data" ) do
31
+ opts.on('-a', '--all', "Display all details, costs and prices." ) do
32
+ options[:show_all] = true
31
33
  options[:show_estimates] = true
32
34
  # options[:show_costs] = true
33
35
  options[:show_prices] = true
34
- options[:show_raw_data] = true
36
+ # options[:show_raw_data] = true
35
37
  end
36
- opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Memory, Storage, etc." ) do
38
+ opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Storage, Network, Extra" ) do
37
39
  options[:show_estimates] = true
38
40
  end
39
- # opts.on('--costs', '--costs', "Display all costs: Compute, Memory, Storage, etc." ) do
41
+ # opts.on('--costs', '--costs', "Display all costs: Compute, Storage, Network, Extra" ) do
40
42
  # options[:show_costs] = true
41
43
  # end
42
- opts.on('--prices', '--prices', "Display prices: Total, Compute, Memory, Storage, etc." ) do
44
+ opts.on('--prices', '--prices', "Display prices: Total, Compute, Storage, Network, Extra" ) do
43
45
  options[:show_prices] = true
44
46
  end
45
47
  opts.on('--type TYPE', String, "Filter by Ref Type eg. ComputeSite (Group), ComputeZone (Cloud), ComputeServer (Host), Instance, Container, User") do |val|
46
- if val.to_s.downcase == 'cloud' || val.to_s.downcase == 'zone'
47
- params['refType'] = 'ComputeZone'
48
- elsif val.to_s.downcase == 'instance'
49
- params['refType'] = 'Instance'
50
- elsif val.to_s.downcase == 'server' || val.to_s.downcase == 'host'
51
- params['refType'] = 'ComputeServer'
52
- elsif val.to_s.downcase == 'cluster'
53
- params['refType'] = 'ComputeServerGroup'
54
- elsif val.to_s.downcase == 'group'
55
- params['refType'] = 'ComputeSite'
56
- elsif val.to_s.downcase == 'user'
57
- params['refType'] = 'User'
58
- else
59
- params['refType'] = val
60
- end
48
+ params['refType'] ||= []
49
+ values = val.split(",").collect {|it| it.strip }.select {|it| it != "" }
50
+ values.each { |it| params['refType'] << parse_invoice_ref_type(it) }
61
51
  end
62
52
  opts.on('--id ID', String, "Filter by Ref ID") do |val|
63
53
  ref_ids << val
@@ -116,6 +106,11 @@ class Morpheus::Cli::InvoicesCommand
116
106
  opts.on('--tenant ID', String, "View invoices for a tenant. Default is your own account.") do |val|
117
107
  params['accountId'] = val
118
108
  end
109
+ opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
110
+ k,v = val.split("=")
111
+ query_tags[k] ||= []
112
+ query_tags[k] << v
113
+ end
119
114
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
120
115
  options[:show_raw_data] = true
121
116
  end
@@ -123,6 +118,14 @@ class Morpheus::Cli::InvoicesCommand
123
118
  params['includeTotals'] = true
124
119
  options[:show_invoice_totals] = true
125
120
  end
121
+ opts.on('--totals-only', "View totals only") do |val|
122
+ params['includeTotals'] = true
123
+ options[:show_invoice_totals] = true
124
+ options[:totals_only] = true
125
+ end
126
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
127
+ options[:sigdig] = val.to_i
128
+ end
126
129
  build_standard_list_options(opts, options)
127
130
  opts.footer = "List invoices."
128
131
  end
@@ -166,6 +169,11 @@ class Morpheus::Cli::InvoicesCommand
166
169
  end
167
170
  params['rawData'] = true if options[:show_raw_data]
168
171
  params['refId'] = ref_ids unless ref_ids.empty?
172
+ if query_tags && !query_tags.empty?
173
+ query_tags.each do |k,v|
174
+ params['tags.' + k] = v
175
+ end
176
+ end
169
177
  @invoices_interface.setopts(options)
170
178
  if options[:dry_run]
171
179
  print_dry_run @invoices_interface.dry.list(params)
@@ -185,7 +193,9 @@ class Morpheus::Cli::InvoicesCommand
185
193
  subtitles += parse_list_subtitles(options)
186
194
  print_h1 title, subtitles
187
195
  if invoices.empty?
188
- print cyan,"No invoices found.",reset,"\n"
196
+ unless options[:totals_only]
197
+ print cyan,"No invoices found.",reset,"\n"
198
+ end
189
199
  else
190
200
  # current_date = Time.now
191
201
  # current_period = "#{current_date.year}#{current_date.month.to_s.rjust(2, '0')}"
@@ -194,105 +204,137 @@ class Morpheus::Cli::InvoicesCommand
194
204
  {"INVOICE ID" => lambda {|it| it['id'] } },
195
205
  {"TYPE" => lambda {|it| format_invoice_ref_type(it) } },
196
206
  {"REF ID" => lambda {|it| it['refId'] } },
197
- {"REF NAME" => lambda {|it| it['refName'] } }
198
- ] + (show_projects ? [
199
- {"PROJECT ID" => lambda {|it| it['project'] ? it['project']['id'] : '' } },
200
- {"PROJECT NAME" => lambda {|it| it['project'] ? it['project']['name'] : '' } },
201
- {"PROJECT TAGS" => lambda {|it| it['project'] ? truncate_string(format_metadata(it['project']['tags']), 50) : '' } }
202
- ] : []) + [
207
+ {"REF NAME" => lambda {|it|
208
+ if options[:show_all]
209
+ it['refName']
210
+ else
211
+ truncate_string_right(it['refName'], 100)
212
+ end
213
+ } },
203
214
  #{"INTERVAL" => lambda {|it| it['interval'] } },
204
215
  {"CLOUD" => lambda {|it| it['cloud'] ? it['cloud']['name'] : '' } },
205
- {"ACCOUNT" => lambda {|it| it['account'] ? it['account']['name'] : '' } },
206
- {"ACTIVE" => lambda {|it| format_boolean(it['active']) } },
207
- #{"ESTIMATE" => lambda {|it| format_boolean(it['estimate']) } },
216
+ #{"TENANT" => lambda {|it| it['account'] ? it['account']['name'] : '' } },
217
+
218
+ #{"COST TYPE" => lambda {|it| it['costType'].to_s.capitalize } },
208
219
  {"PERIOD" => lambda {|it| format_invoice_period(it) } },
209
220
  {"START" => lambda {|it| format_date(it['startDate']) } },
210
- {"END" => lambda {|it| it['endDate'] ? format_date(it['endDate']) : '' } },
211
- {"MTD" => lambda {|it| format_money(it['runningCost']) } },
221
+ {"END" => lambda {|it| format_date(it['endDate']) } },
222
+ ] + (options[:show_all] ? [
223
+ {"REF START" => lambda {|it| format_dt(it['refStart']) } },
224
+ {"REF END" => lambda {|it| format_dt(it['refEnd']) } },
225
+ ] : []) + [
226
+ {"COMPUTE" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) } },
227
+ # {"MEMORY" => lambda {|it| format_money(it['memoryCost']) } },
228
+ {"STORAGE" => lambda {|it| format_money(it['storageCost'], 'usd', {sigdig:options[:sigdig]}) } },
229
+ {"NETWORK" => lambda {|it| format_money(it['networkCost'], 'usd', {sigdig:options[:sigdig]}) } },
230
+ {"EXTRA" => lambda {|it| format_money(it['extraCost'], 'usd', {sigdig:options[:sigdig]}) } },
231
+ {"MTD" => lambda {|it| format_money(it['runningCost'], 'usd', {sigdig:options[:sigdig]}) } },
212
232
  {"TOTAL" => lambda {|it|
213
-
214
- if it['runningMultiplier'] && it['runningMultiplier'].to_i != 1 && it['totalCost'].to_f > 0
215
- format_money(it['totalCost']) + " (Projected)"
216
- else
217
- format_money(it['totalCost'])
218
- end
233
+ format_money(it['totalCost'], 'usd', {sigdig:options[:sigdig]}) + ((it['totalCost'].to_f > 0 && it['totalCost'] != it['runningCost']) ? " (Projected)" : "")
219
234
  } }
220
235
  ]
221
236
 
222
- columns += [
223
- {"COMPUTE" => lambda {|it| format_money(it['computeCost']) } },
224
- # {"MEMORY" => lambda {|it| format_money(it['memoryCost']) } },
225
- {"STORAGE" => lambda {|it| format_money(it['storageCost']) } },
226
- {"NETWORK" => lambda {|it| format_money(it['networkCost']) } },
227
- {"OTHER" => lambda {|it| format_money(it['extraCost']) } },
228
- ]
229
237
  if options[:show_prices]
230
238
  columns += [
231
- {"COMPUTE PRICE" => lambda {|it| format_money(it['computePrice']) } },
232
- # {"MEMORY PRICE" => lambda {|it| format_money(it['memoryPrice']) } },
233
- {"STORAGE PRICE" => lambda {|it| format_money(it['storagePrice']) } },
234
- {"NETWORK PRICE" => lambda {|it| format_money(it['networkPrice']) } },
235
- {"OTHER PRICE" => lambda {|it| format_money(it['extraPrice']) } },
236
- {"MTD PRICE" => lambda {|it| format_money(it['runningPrice']) } },
239
+ {"COMPUTE PRICE" => lambda {|it| format_money(it['computePrice'], 'usd', {sigdig:options[:sigdig]}) } },
240
+ # {"MEMORY PRICE" => lambda {|it| format_money(it['memoryPrice'], 'usd', {sigdig:options[:sigdig]}) } },
241
+ {"STORAGE PRICE" => lambda {|it| format_money(it['storagePrice'], 'usd', {sigdig:options[:sigdig]}) } },
242
+ {"NETWORK PRICE" => lambda {|it| format_money(it['networkPrice'], 'usd', {sigdig:options[:sigdig]}) } },
243
+ {"EXTRA PRICE" => lambda {|it| format_money(it['extraPrice'], 'usd', {sigdig:options[:sigdig]}) } },
244
+ {"MTD PRICE" => lambda {|it| format_money(it['runningPrice'], 'usd', {sigdig:options[:sigdig]}) } },
237
245
  {"TOTAL PRICE" => lambda {|it|
238
- if it['runningMultiplier'] && it['runningMultiplier'].to_i != 1 && it['totalPrice'].to_f > 0
239
- format_money(it['totalPrice']) + " (Projected)"
240
- else
241
- format_money(it['totalPrice'])
242
- end
246
+ format_money(it['totalPrice'], 'usd', {sigdig:options[:sigdig]}) + ((it['totalCost'].to_f > 0 && it['totalCost'] != it['runningCost']) ? " (Projected)" : "")
243
247
  } }
244
248
  ]
245
249
  end
246
250
  if options[:show_estimates]
247
251
  columns += [
248
- {"MTD EST." => lambda {|it| format_money(it['estimatedRunningCost']) } },
252
+ {"COMPUTE EST." => lambda {|it| format_money(it['estimatedComputeCost'], 'usd', {sigdig:options[:sigdig]}) } },
253
+ # {"MEMORY EST." => lambda {|it| format_money(it['estimatedMemoryCost'], 'usd', {sigdig:options[:sigdig]}) } },
254
+ {"STORAGE EST." => lambda {|it| format_money(it['estimatedStorageCost'], 'usd', {sigdig:options[:sigdig]}) } },
255
+ {"NETWORK EST." => lambda {|it| format_money(it['estimatedNetworkCost'], 'usd', {sigdig:options[:sigdig]}) } },
256
+ {"EXTRA EST." => lambda {|it| format_money(it['estimatedExtraCost'], 'usd', {sigdig:options[:sigdig]}) } },
257
+ {"MTD EST." => lambda {|it| format_money(it['estimatedRunningCost'], 'usd', {sigdig:options[:sigdig]}) } },
249
258
  {"TOTAL EST." => lambda {|it|
250
- if it['runningMultiplier'] && it['runningMultiplier'].to_i != 1 && it['estimatedTotalCost'].to_f > 0
251
- format_money(it['estimatedTotalCost']) + " (Projected)"
252
- else
253
- format_money(it['estimatedTotalCost'])
254
- end
259
+ format_money(it['estimatedTotalCost'], 'usd', {sigdig:options[:sigdig]}) + ((it['estimatedTotalCost'].to_f > 0 && it['estimatedTotalCost'] != it['estimatedRunningCost']) ? " (Projected)" : "")
255
260
  } },
256
- {"COMPUTE EST." => lambda {|it| format_money(it['estimatedComputeCost']) } },
257
- # {"MEMORY EST." => lambda {|it| format_money(it['estimatedMemoryCost']) } },
258
- {"STORAGE EST." => lambda {|it| format_money(it['estimatedStorageCost']) } },
259
- {"NETWORK EST." => lambda {|it| format_money(it['estimatedNetworkCost']) } },
260
- {"OTHER EST." => lambda {|it| format_money(it['estimatedExtraCost']) } },
261
261
  ]
262
262
  end
263
+ columns += [
264
+ {"ESTIMATE" => lambda {|it| format_boolean(it['estimate']) } },
265
+ {"ACTIVE" => lambda {|it| format_boolean(it['active']) } },
266
+ {"ITEMS" => lambda {|it| it['lineItems'].size rescue '' } },
267
+ {"TAGS" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' } },
268
+ ]
269
+ if show_projects
270
+ columns += [
271
+ {"PROJECT ID" => lambda {|it| it['project'] ? it['project']['id'] : '' } },
272
+ {"PROJECT NAME" => lambda {|it| it['project'] ? it['project']['name'] : '' } },
273
+ {"PROJECT TAGS" => lambda {|it| it['project'] ? truncate_string(format_metadata(it['project']['tags']), 50) : '' } },
274
+ ]
275
+ end
276
+ columns += [
277
+ {"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
278
+ {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
279
+ ]
263
280
  if options[:show_raw_data]
264
281
  columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
265
282
  end
266
- print as_pretty_table(invoices, columns, options)
267
- print_results_pagination(json_response, {:label => "invoice", :n_label => "invoices"})
283
+ unless options[:totals_only]
284
+ print as_pretty_table(invoices, columns, options)
285
+ print_results_pagination(json_response, {:label => "invoice", :n_label => "invoices"})
286
+ end
268
287
 
269
288
  if options[:show_invoice_totals]
270
289
  invoice_totals = json_response['invoiceTotals']
290
+ print_h2 "Invoice Totals (#{format_number(json_response['meta']['total']) rescue ''})"
291
+
271
292
  if invoice_totals
272
- print_h2 "Invoice Totals"
273
- invoice_totals_columns = {
274
- "# Invoices" => lambda {|it| format_number(json_response['meta']['total']) rescue '' },
275
- "Total Price" => lambda {|it| format_money(it['actualTotalPrice']) },
276
- "Total Cost" => lambda {|it| format_money(it['actualTotalCost']) },
277
- "Running Price" => lambda {|it| format_money(it['actualRunningPrice']) },
278
- "Running Cost" => lambda {|it| format_money(it['actualRunningCost']) },
279
- # "Invoice Total Price" => lambda {|it| format_money(it['invoiceTotalPrice']) },
280
- # "Invoice Total Cost" => lambda {|it| format_money(it['invoiceTotalCost']) },
281
- # "Invoice Running Price" => lambda {|it| format_money(it['invoiceRunningPrice']) },
282
- # "Invoice Running Cost" => lambda {|it| format_money(it['invoiceRunningCost']) },
283
- # "Estimated Total Price" => lambda {|it| format_money(it['estimatedTotalPrice']) },
284
- # "Estimated Total Cost" => lambda {|it| format_money(it['estimatedTotalCost']) },
285
- # "Compute Price" => lambda {|it| format_money(it['computePrice']) },
286
- # "Compute Cost" => lambda {|it| format_money(it['computeCost']) },
293
+ cost_rows = [
294
+ {label: 'Cost'.upcase, compute: invoice_totals['actualComputeCost'], memory: invoice_totals['actualMemoryCost'], storage: invoice_totals['actualStorageCost'], network: invoice_totals['actualNetworkCost'], license: invoice_totals['actualLicenseCost'], extra: invoice_totals['actualExtraCost'], running: invoice_totals['actualRunningCost'], total: invoice_totals['actualTotalCost']},
295
+ ]
296
+ if options[:show_prices]
297
+ cost_rows += [
298
+ {label: 'Price'.upcase, compute: invoice_totals['actualComputePrice'], memory: invoice_totals['actualMemoryPrice'], storage: invoice_totals['actualStoragePrice'], network: invoice_totals['actualNetworkPrice'], license: invoice_totals['actualLicensePrice'], extra: invoice_totals['actualExtraPrice'], running: invoice_totals['actualRunningPrice'], total: invoice_totals['actualTotalPrice']},
299
+ ]
300
+ end
301
+ if options[:show_estimates]
302
+ cost_rows += [
303
+ {label: 'Estimated Cost'.upcase, compute: invoice_totals['estimatedComputeCost'], memory: invoice_totals['estimatedMemoryCost'], storage: invoice_totals['estimatedStorageCost'], network: invoice_totals['estimatedNetworkCost'], license: invoice_totals['estimatedLicenseCost'], extra: invoice_totals['estimatedExtraCost'], running: invoice_totals['estimatedRunningCost'], total: invoice_totals['estimatedTotalCost']},
304
+ {label: 'Estimated Price'.upcase, compute: invoice_totals['estimatedComputePrice'], memory: invoice_totals['estimatedMemoryPrice'], storage: invoice_totals['estimatedStoragePrice'], network: invoice_totals['estimatedNetworkPrice'], license: invoice_totals['estimatedLicensePrice'], extra: invoice_totals['estimatedExtraPrice'], running: invoice_totals['estimatedRunningPrice'], total: invoice_totals['estimatedTotalPrice']},
305
+ ]
306
+ end
307
+ cost_columns = {
308
+ "" => lambda {|it| it[:label] },
309
+ "Compute".upcase => lambda {|it| format_money(it[:compute], 'usd', {sigdig:options[:sigdig]}) },
310
+ "Memory".upcase => lambda {|it| format_money(it[:memory], 'usd', {sigdig:options[:sigdig]}) },
311
+ "Storage".upcase => lambda {|it| format_money(it[:storage], 'usd', {sigdig:options[:sigdig]}) },
312
+ "Network".upcase => lambda {|it| format_money(it[:network], 'usd', {sigdig:options[:sigdig]}) },
313
+ "License".upcase => lambda {|it| format_money(it[:license], 'usd', {sigdig:options[:sigdig]}) },
314
+ "Extra".upcase => lambda {|it| format_money(it[:extra], 'usd', {sigdig:options[:sigdig]}) },
315
+ "MTD".upcase => lambda {|it| format_money(it[:running], 'usd', {sigdig:options[:sigdig]}) },
316
+ "Total".upcase => lambda {|it|
317
+ format_money(it[:total], 'usd', {sigdig:options[:sigdig]}) + ((it[:total].to_f > 0 && it[:total] != it[:running]) ? " (Projected)" : "")
318
+ },
287
319
  }
288
- print_description_list(invoice_totals_columns, invoice_totals)
320
+ # remove columns that rarely have data...
321
+ if cost_rows.sum { |it| it[:memory].to_f } == 0
322
+ cost_columns.delete("Memory".upcase)
323
+ end
324
+ if cost_rows.sum { |it| it[:license].to_f } == 0
325
+ cost_columns.delete("License".upcase)
326
+ end
327
+ if cost_rows.sum { |it| it[:extra].to_f } == 0
328
+ cost_columns.delete("Extra".upcase)
329
+ end
330
+ print as_pretty_table(cost_rows, cost_columns, options)
289
331
  else
290
332
  print "\n"
291
333
  print yellow, "No invoice totals data", reset, "\n"
292
334
  end
293
335
  end
336
+ print reset,"\n"
294
337
  end
295
- print reset,"\n"
296
338
  return 0, nil
297
339
  end
298
340
  end
@@ -301,14 +343,17 @@ class Morpheus::Cli::InvoicesCommand
301
343
  options = {}
302
344
  optparse = Morpheus::Cli::OptionParser.new do |opts|
303
345
  opts.banner = subcommand_usage("[id]")
304
- opts.on('-a', '--all', "Display all costs, prices and raw data" ) do
346
+ opts.on('-a', '--all', "Display all details, costs and prices." ) do
305
347
  options[:show_estimates] = true
306
348
  # options[:show_costs] = true
307
349
  options[:show_prices] = true
308
- options[:show_raw_data] = true
350
+ # options[:show_raw_data] = true
309
351
  options[:max_line_items] = 10000
310
352
  end
311
- opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Memory, Storage, etc." ) do
353
+ opts.on('--prices', '--prices', "Display prices: Total, Compute, Storage, Network, Extra" ) do
354
+ options[:show_prices] = true
355
+ end
356
+ opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Storage, Network, Extra" ) do
312
357
  options[:show_estimates] = true
313
358
  end
314
359
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
@@ -318,12 +363,12 @@ class Morpheus::Cli::InvoicesCommand
318
363
  options[:show_raw_data] = true
319
364
  options[:pretty_json] = true
320
365
  end
321
- opts.on('-m', '--max-line-items NUMBER', "Maximum number of line items to display. Default is 5.") do |val|
322
- options[:max_line_items] = val.to_i
323
- end
324
366
  opts.on('--no-line-items', '--no-line-items', "Do not display line items.") do |val|
325
367
  options[:hide_line_items] = true
326
368
  end
369
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
370
+ options[:sigdig] = val.to_i
371
+ end
327
372
  build_standard_get_options(opts, options)
328
373
  opts.footer = "Get details about a specific invoice."
329
374
  opts.footer = <<-EOT
@@ -365,20 +410,24 @@ EOT
365
410
  "Type" => lambda {|it| format_invoice_ref_type(it) },
366
411
  "Ref ID" => lambda {|it| it['refId'] },
367
412
  "Ref Name" => lambda {|it| it['refName'] },
413
+ "Cloud" => lambda {|it| it['cloud'] ? it['cloud']['name'] : '' },
368
414
  "Plan" => lambda {|it| it['plan'] ? it['plan']['name'] : '' },
369
- "Project ID" => lambda {|it| it['project'] ? it['project']['id'] : '' },
370
- "Project Name" => lambda {|it| it['project'] ? it['project']['name'] : '' },
371
- "Project Tags" => lambda {|it| it['project'] ? format_metadata(it['project']['tags']) : '' },
372
415
  "Power State" => lambda {|it| format_server_power_state(it) },
373
- "Account" => lambda {|it| it['account'] ? it['account']['name'] : '' },
416
+ "Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
374
417
  "Active" => lambda {|it| format_boolean(it['active']) },
375
- "Period" => lambda {|it| format_invoice_period(it) },
376
418
  "Estimate" => lambda {|it| format_boolean(it['estimate']) },
419
+ #"Cost Type" => lambda {|it| it['costType'].to_s.capitalize },
420
+ "Period" => lambda {|it| format_invoice_period(it) },
377
421
  #"Interval" => lambda {|it| it['interval'] },
378
422
  "Start" => lambda {|it| format_date(it['startDate']) },
379
- "End" => lambda {|it| it['endDate'] ? format_date(it['endDate']) : '' },
380
- "Ref Start" => lambda {|it| format_local_dt(it['refStart']) },
381
- "Ref End" => lambda {|it| it['refEnd'] ? format_local_dt(it['refEnd']) : '' },
423
+ "End" => lambda {|it| format_date(it['endDate']) },
424
+ "Ref Start" => lambda {|it| format_dt(it['refStart']) },
425
+ "Ref End" => lambda {|it| format_dt(it['refEnd']) },
426
+ "Items" => lambda {|it| it['lineItems'].size rescue '' },
427
+ "Tags" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
428
+ "Project ID" => lambda {|it| it['project'] ? it['project']['id'] : '' },
429
+ "Project Name" => lambda {|it| it['project'] ? it['project']['name'] : '' },
430
+ "Project Tags" => lambda {|it| it['project'] ? format_metadata(it['project']['tags']) : '' },
382
431
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
383
432
  "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
384
433
  }
@@ -391,6 +440,9 @@ EOT
391
440
  description_cols.delete("Project Name")
392
441
  description_cols.delete("Project Tags")
393
442
  end
443
+ if invoice['metadata'].nil? || invoice['metadata'].empty?
444
+ description_cols.delete("Tags")
445
+ end
394
446
  if !['ComputeServer','Instance','Container'].include?(invoice['refType'])
395
447
  description_cols.delete("Power State")
396
448
  end
@@ -398,27 +450,27 @@ EOT
398
450
  =begin
399
451
  print_h2 "Costs"
400
452
  cost_columns = {
401
- "Compute" => lambda {|it| format_money(it['computeCost']) },
402
- "Memory" => lambda {|it| format_money(it['memoryCost']) },
403
- "Storage" => lambda {|it| format_money(it['storageCost']) },
404
- "Network" => lambda {|it| format_money(it['networkCost']) },
405
- "License" => lambda {|it| format_money(it['licenseCost']) },
406
- "Other" => lambda {|it| format_money(it['extraCost']) },
407
- "Running" => lambda {|it| format_money(it['runningCost']) },
408
- "Total Cost" => lambda {|it| format_money(it['totalCost']) },
453
+ "Compute" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) },
454
+ "Memory" => lambda {|it| format_money(it['memoryCost'], 'usd', {sigdig:options[:sigdig]}) },
455
+ "Storage" => lambda {|it| format_money(it['storageCost'], 'usd', {sigdig:options[:sigdig]}) },
456
+ "Network" => lambda {|it| format_money(it['networkCost'], 'usd', {sigdig:options[:sigdig]}) },
457
+ "License" => lambda {|it| format_money(it['licenseCost'], 'usd', {sigdig:options[:sigdig]}) },
458
+ "Extra" => lambda {|it| format_money(it['extraCost'], 'usd', {sigdig:options[:sigdig]}) },
459
+ "Running" => lambda {|it| format_money(it['runningCost'], 'usd', {sigdig:options[:sigdig]}) },
460
+ "Total Cost" => lambda {|it| format_money(it['totalCost'], 'usd', {sigdig:options[:sigdig]}) },
409
461
  }
410
462
  print as_pretty_table([invoice], cost_columns, options)
411
463
 
412
464
  print_h2 "Prices"
413
465
  price_columns = {
414
- "Compute" => lambda {|it| format_money(it['computePrice']) },
415
- "Memory" => lambda {|it| format_money(it['memoryPrice']) },
416
- "Storage" => lambda {|it| format_money(it['storagePrice']) },
417
- "Network" => lambda {|it| format_money(it['networkPrice']) },
418
- "License" => lambda {|it| format_money(it['licensePrice']) },
419
- "Other" => lambda {|it| format_money(it['extraPrice']) },
420
- "Running" => lambda {|it| format_money(it['runningPrice']) },
421
- "Total Price" => lambda {|it| format_money(it['totalPrice']) },
466
+ "Compute" => lambda {|it| format_money(it['computePrice'], 'usd', {sigdig:options[:sigdig]}) },
467
+ "Memory" => lambda {|it| format_money(it['memoryPrice'], 'usd', {sigdig:options[:sigdig]}) },
468
+ "Storage" => lambda {|it| format_money(it['storagePrice'], 'usd', {sigdig:options[:sigdig]}) },
469
+ "Network" => lambda {|it| format_money(it['networkPrice'], 'usd', {sigdig:options[:sigdig]}) },
470
+ "License" => lambda {|it| format_money(it['licensePrice'], 'usd', {sigdig:options[:sigdig]}) },
471
+ "Extra" => lambda {|it| format_money(it['extraPrice'], 'usd', {sigdig:options[:sigdig]}) },
472
+ "Running" => lambda {|it| format_money(it['runningPrice'], 'usd', {sigdig:options[:sigdig]}) },
473
+ "Total Price" => lambda {|it| format_money(it['totalPrice'], 'usd', {sigdig:options[:sigdig]}) },
422
474
  }
423
475
  print as_pretty_table([invoice], price_columns, options)
424
476
  =end
@@ -426,33 +478,77 @@ EOT
426
478
  # current_date = Time.now
427
479
  # current_period = "#{current_date.year}#{current_date.month.to_s.rjust(2, '0')}"
428
480
 
429
- print "\n"
430
- # print_h2 "Costs"
481
+
482
+
483
+ # Line Items
484
+ line_items = invoice['lineItems']
485
+ if line_items && line_items.size > 0 && options[:hide_line_items] != true
486
+ line_items_columns = [
487
+ {"ID" => lambda {|it| it['id'] } },
488
+ #{"REF TYPE" => lambda {|it| format_invoice_ref_type(it) } },
489
+ #{"REF ID" => lambda {|it| it['refId'] } },
490
+ #{"REF NAME" => lambda {|it| it['refName'] } },
491
+ #{"REF CATEGORY" => lambda {|it| it['refCategory'] } },
492
+ {"START" => lambda {|it| format_dt(it['startDate']) } },
493
+ {"END" => lambda {|it| format_dt(it['endDate']) } },
494
+ {"USAGE TYPE" => lambda {|it| it['usageType'] } },
495
+ {"USAGE CATEGORY" => lambda {|it| it['usageCategory'] } },
496
+ {"USAGE" => lambda {|it| it['itemUsage'] } },
497
+ {"RATE" => lambda {|it| it['itemRate'] } },
498
+ {"UNIT" => lambda {|it| it['rateUnit'] } },
499
+ {"COST" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
500
+ {"PRICE" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
501
+ #{"TAX" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
502
+ # {"TERM" => lambda {|it| it['itemTerm'] } },
503
+ {"ITEM ID" => lambda {|it| truncate_string_right(it['itemId'], 65) } },
504
+ {"ITEM NAME" => lambda {|it| it['itemName'] } },
505
+ {"ITEM TYPE" => lambda {|it| it['itemType'] } },
506
+ {"ITEM DESCRIPTION" => lambda {|it| it['itemDescription'] } },
507
+ {"PRODUCT CODE" => lambda {|it| it['productCode'] } },
508
+ {"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
509
+ {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
510
+ ]
511
+ if options[:show_raw_data]
512
+ line_items_columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
513
+ end
514
+ print_h2 "Line Items"
515
+ #max_line_items = options[:max_line_items] ? options[:max_line_items].to_i : 5
516
+ paged_line_items = line_items #.first(max_line_items)
517
+ print as_pretty_table(paged_line_items, line_items_columns, options)
518
+ print_results_pagination({total: line_items.size, size: paged_line_items.size}, {:label => "line item", :n_label => "line items"})
519
+ end
520
+
521
+ # cost_types = ["Costs"]
522
+ # cost_types << "Prices" if options[:show_prices]
523
+ # cost_types << "Estimates" if options[:show_estimates]
524
+ # print_h2 cost_types.size == 1 ? "Totals" : "Total #{anded_list(cost_types)}"
525
+ print_h2 "Invoice Totals"
526
+
431
527
  cost_rows = [
432
- {label: 'Price'.upcase, compute: invoice['computePrice'], memory: invoice['memoryPrice'], storage: invoice['storagePrice'], network: invoice['networkPrice'], license: invoice['licensePrice'], extra: invoice['extraPrice'], running: invoice['runningPrice'], total: invoice['totalPrice']},
433
528
  {label: 'Cost'.upcase, compute: invoice['computeCost'], memory: invoice['memoryCost'], storage: invoice['storageCost'], network: invoice['networkCost'], license: invoice['licenseCost'], extra: invoice['extraCost'], running: invoice['runningCost'], total: invoice['totalCost']},
434
529
  ]
530
+ if options[:show_prices]
531
+ cost_rows += [
532
+ {label: 'Price'.upcase, compute: invoice['computePrice'], memory: invoice['memoryPrice'], storage: invoice['storagePrice'], network: invoice['networkPrice'], license: invoice['licensePrice'], extra: invoice['extraPrice'], running: invoice['runningPrice'], total: invoice['totalPrice']},
533
+ ]
534
+ end
435
535
  if options[:show_estimates]
436
536
  cost_rows += [
437
537
  {label: 'Estimated Cost'.upcase, compute: invoice['estimatedComputeCost'], memory: invoice['estimatedMemoryCost'], storage: invoice['estimatedStorageCost'], network: invoice['estimatedNetworkCost'], license: invoice['estimatedLicenseCost'], extra: invoice['estimatedExtraCost'], running: invoice['estimatedRunningCost'], total: invoice['estimatedTotalCost']},
438
- {label: 'Estimated Price'.upcase, compute: invoice['estimatedComputeCost'], memory: invoice['estimatedMemoryCost'], storage: invoice['estimatedStorageCost'], network: invoice['estimatedNetworkCost'], license: invoice['estimatedLicenseCost'], extra: invoice['estimatedExtraCost'], running: invoice['estimatedRunningCost'], total: invoice['estimatedTotalCost']},
538
+ {label: 'Estimated Price'.upcase, compute: invoice['estimatedComputePrice'], memory: invoice['estimatedMemoryPrice'], storage: invoice['estimatedStoragePrice'], network: invoice['estimatedNetworkPrice'], license: invoice['estimatedLicensePrice'], extra: invoice['estimatedExtraPrice'], running: invoice['estimatedRunningPrice'], total: invoice['estimatedTotalPrice']},
439
539
  ]
440
540
  end
441
541
  cost_columns = {
442
542
  "" => lambda {|it| it[:label] },
443
- "Compute".upcase => lambda {|it| format_money(it[:compute]) },
444
- "Memory".upcase => lambda {|it| format_money(it[:memory]) },
445
- "Storage".upcase => lambda {|it| format_money(it[:storage]) },
446
- "Network".upcase => lambda {|it| format_money(it[:network]) },
447
- "License".upcase => lambda {|it| format_money(it[:license]) },
448
- "Other".upcase => lambda {|it| format_money(it[:extra]) },
449
- "MTD" => lambda {|it| format_money(it[:running]) },
543
+ "Compute".upcase => lambda {|it| format_money(it[:compute], 'usd', {sigdig:options[:sigdig]}) },
544
+ "Memory".upcase => lambda {|it| format_money(it[:memory], 'usd', {sigdig:options[:sigdig]}) },
545
+ "Storage".upcase => lambda {|it| format_money(it[:storage], 'usd', {sigdig:options[:sigdig]}) },
546
+ "Network".upcase => lambda {|it| format_money(it[:network], 'usd', {sigdig:options[:sigdig]}) },
547
+ "License".upcase => lambda {|it| format_money(it[:license], 'usd', {sigdig:options[:sigdig]}) },
548
+ "Extra".upcase => lambda {|it| format_money(it[:extra], 'usd', {sigdig:options[:sigdig]}) },
549
+ "MTD" => lambda {|it| format_money(it[:running], 'usd', {sigdig:options[:sigdig]}) },
450
550
  "Total".upcase => lambda {|it|
451
- if invoice['runningMultiplier'] && invoice['runningMultiplier'].to_i != 1 && it[:total].to_f.to_f > 0
452
- format_money(it[:total]) + " (Projected)"
453
- else
454
- format_money(it[:total])
455
- end
551
+ format_money(it[:total], 'usd', {sigdig:options[:sigdig]}) + ((it[:total].to_f > 0 && it[:total] != it[:running]) ? " (Projected)" : "")
456
552
  },
457
553
  }
458
554
  # remove columns that rarely have data...
@@ -463,7 +559,7 @@ EOT
463
559
  cost_columns.delete("License".upcase)
464
560
  end
465
561
  if cost_rows.sum { |it| it[:extra].to_f } == 0
466
- cost_columns.delete("Other".upcase)
562
+ cost_columns.delete("Extra".upcase)
467
563
  end
468
564
  print as_pretty_table(cost_rows, cost_columns, options)
469
565
 
@@ -471,41 +567,6 @@ EOT
471
567
  print_h2 "Raw Data"
472
568
  puts as_json(invoice['rawData'], {pretty_json:false}.merge(options))
473
569
  end
474
-
475
- # Line Items
476
- line_items = invoice['lineItems']
477
- if line_items && line_items.size > 0 && options[:hide_line_items] != true
478
-
479
- line_items_columns = [
480
- {"ID" => lambda {|it| it['id'] } },
481
- {"TYPE" => lambda {|it| format_invoice_ref_type(it) } },
482
- {"REF ID" => lambda {|it| it['refId'] } },
483
- {"REF NAME" => lambda {|it| it['refName'] } },
484
- #{"REF CATEGORY" => lambda {|it| it['refCategory'] } },
485
- {"START" => lambda {|it| format_date(it['startDate']) } },
486
- {"END" => lambda {|it| it['endDate'] ? format_date(it['endDate']) : '' } },
487
- {"USAGE TYPE" => lambda {|it| it['usageType'] } },
488
- {"USAGE CATEGORY" => lambda {|it| it['usageCategory'] } },
489
- {"USAGE" => lambda {|it| it['itemUsage'] } },
490
- {"RATE" => lambda {|it| it['itemRate'] } },
491
- {"COST" => lambda {|it| format_money(it['itemCost']) } },
492
- {"PRICE" => lambda {|it| format_money(it['itemPrice']) } },
493
- {"TAX" => lambda {|it| format_money(it['itemTax']) } },
494
- # {"TERM" => lambda {|it| it['itemTerm'] } },
495
- "CREATED" => lambda {|it| format_local_dt(it['dateCreated']) },
496
- "UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) }
497
- ]
498
-
499
- if options[:show_raw_data]
500
- line_items_columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
501
- end
502
-
503
- print_h2 "Line Items"
504
- max_line_items = options[:max_line_items] ? options[:max_line_items].to_i : 5
505
- paged_line_items = line_items.first(max_line_items)
506
- print as_pretty_table(paged_line_items, line_items_columns, options)
507
- print_results_pagination({total: line_items.size, size: paged_line_items.size}, {:label => "line item", :n_label => "line items"})
508
- end
509
570
 
510
571
  print reset,"\n"
511
572
  return 0
@@ -596,43 +657,36 @@ EOT
596
657
  options = {}
597
658
  params = {}
598
659
  ref_ids = []
660
+ query_tags = {}
599
661
  optparse = Morpheus::Cli::OptionParser.new do |opts|
600
662
  opts.banner = subcommand_usage()
601
- opts.on('-a', '--all', "Display all costs, prices and raw data" ) do
663
+ opts.on('-a', '--all', "Display all details, costs and prices." ) do
602
664
  options[:show_actual_costs] = true
603
665
  options[:show_costs] = true
604
666
  options[:show_prices] = true
605
- options[:show_raw_data] = true
667
+ # options[:show_raw_data] = true
606
668
  end
607
- # opts.on('--actuals', '--actuals', "Display all actual costs: Compute, Memory, Storage, etc." ) do
669
+ # opts.on('--actuals', '--actuals', "Display all actual costs: Compute, Storage, Network, Extra" ) do
608
670
  # options[:show_actual_costs] = true
609
671
  # end
610
- # opts.on('--costs', '--costs', "Display all costs: Compute, Memory, Storage, etc." ) do
672
+ # opts.on('--costs', '--costs', "Display all costs: Compute, Storage, Network, Extra" ) do
611
673
  # options[:show_costs] = true
612
674
  # end
613
- # opts.on('--prices', '--prices', "Display prices: Total, Compute, Memory, Storage, etc." ) do
614
- # options[:show_prices] = true
615
- # end
675
+ opts.on('--prices', '--prices', "Display prices: Total, Compute, Storage, Network, Extra" ) do
676
+ options[:show_prices] = true
677
+ end
616
678
  opts.on('--invoice-id ID', String, "Filter by Invoice ID") do |val|
617
679
  params['invoiceId'] ||= []
618
680
  params['invoiceId'] << val
619
681
  end
682
+ opts.on('--external-id ID', String, "Filter by External ID") do |val|
683
+ params['externalId'] ||= []
684
+ params['externalId'] << val
685
+ end
620
686
  opts.on('--type TYPE', String, "Filter by Ref Type eg. ComputeSite (Group), ComputeZone (Cloud), ComputeServer (Host), Instance, Container, User") do |val|
621
- if val.to_s.downcase == 'cloud' || val.to_s.downcase == 'zone'
622
- params['refType'] = 'ComputeZone'
623
- elsif val.to_s.downcase == 'instance'
624
- params['refType'] = 'Instance'
625
- elsif val.to_s.downcase == 'server' || val.to_s.downcase == 'host'
626
- params['refType'] = 'ComputeServer'
627
- elsif val.to_s.downcase == 'cluster'
628
- params['refType'] = 'ComputeServerGroup'
629
- elsif val.to_s.downcase == 'group'
630
- params['refType'] = 'ComputeSite'
631
- elsif val.to_s.downcase == 'user'
632
- params['refType'] = 'User'
633
- else
634
- params['refType'] = val
635
- end
687
+ params['refType'] ||= []
688
+ values = val.split(",").collect {|it| it.strip }.select {|it| it != "" }
689
+ values.each { |it| params['refType'] << parse_invoice_ref_type(it) }
636
690
  end
637
691
  opts.on('--id ID', String, "Filter by Ref ID") do |val|
638
692
  ref_ids << val
@@ -691,6 +745,11 @@ EOT
691
745
  opts.on('--tenant ID', String, "View invoice line items for a tenant. Default is your own account.") do |val|
692
746
  params['accountId'] = val
693
747
  end
748
+ # opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
749
+ # k,v = val.split("=")
750
+ # query_tags[k] ||= []
751
+ # query_tags[k] << v
752
+ # end
694
753
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
695
754
  options[:show_raw_data] = true
696
755
  end
@@ -698,6 +757,14 @@ EOT
698
757
  params['includeTotals'] = true
699
758
  options[:show_invoice_totals] = true
700
759
  end
760
+ opts.on('--totals-only', "View totals only") do |val|
761
+ params['includeTotals'] = true
762
+ options[:show_invoice_totals] = true
763
+ options[:totals_only] = true
764
+ end
765
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
766
+ options[:sigdig] = val.to_i
767
+ end
701
768
  build_standard_list_options(opts, options)
702
769
  opts.footer = "List invoice line items."
703
770
  end
@@ -742,6 +809,11 @@ EOT
742
809
  end
743
810
  params['rawData'] = true if options[:show_raw_data]
744
811
  params['refId'] = ref_ids unless ref_ids.empty?
812
+ if query_tags && !query_tags.empty?
813
+ query_tags.each do |k,v|
814
+ params['tags.' + k] = v
815
+ end
816
+ end
745
817
  @invoice_line_items_interface.setopts(options)
746
818
  if options[:dry_run]
747
819
  print_dry_run @invoice_line_items_interface.dry.list(params)
@@ -773,15 +845,22 @@ EOT
773
845
  {"REF NAME" => lambda {|it| it['refName'] } },
774
846
  #{"REF CATEGORY" => lambda {|it| it['refCategory'] } },
775
847
  {"START" => lambda {|it| format_date(it['startDate']) } },
776
- {"END" => lambda {|it| it['endDate'] ? format_date(it['endDate']) : '' } },
848
+ {"END" => lambda {|it| format_date(it['endDate']) } },
777
849
  {"USAGE TYPE" => lambda {|it| it['usageType'] } },
778
850
  {"USAGE CATEGORY" => lambda {|it| it['usageCategory'] } },
779
851
  {"USAGE" => lambda {|it| it['itemUsage'] } },
780
852
  {"RATE" => lambda {|it| it['itemRate'] } },
781
- {"COST" => lambda {|it| format_money(it['itemCost']) } },
782
- {"PRICE" => lambda {|it| format_money(it['itemPrice']) } },
783
- {"TAX" => lambda {|it| format_money(it['itemTax']) } },
784
- # {"TERM" => lambda {|it| it['itemTerm'] } },
853
+ {"UNIT" => lambda {|it| it['rateUnit'] } },
854
+ {"COST" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
855
+ ] + (options[:show_prices] ? [
856
+ {"PRICE" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
857
+ {"TAX" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
858
+ ] : []) + [
859
+ {"ITEM ID" => lambda {|it| truncate_string_right(it['itemId'], 65) } },
860
+ {"ITEM NAME" => lambda {|it| it['itemName'] } },
861
+ {"ITEM TYPE" => lambda {|it| it['itemType'] } },
862
+ {"ITEM DESCRIPTION" => lambda {|it| it['itemDescription'] } },
863
+ {"PRODUCT CODE" => lambda {|it| it['productCode'] } },
785
864
  "CREATED" => lambda {|it| format_local_dt(it['dateCreated']) },
786
865
  "UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) }
787
866
  ]
@@ -789,35 +868,39 @@ EOT
789
868
  if options[:show_raw_data]
790
869
  columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
791
870
  end
792
- if options[:show_invoice_totals]
793
- line_item_totals = json_response['lineItemTotals']
794
- if line_item_totals
795
- totals_row = line_item_totals.clone
796
- totals_row['id'] = 'TOTAL:'
797
- #totals_row['usageCategory'] = 'TOTAL:'
798
- line_items = line_items + [totals_row]
799
- end
800
- end
801
- print as_pretty_table(line_items, columns, options)
802
- print_results_pagination(json_response, {:label => "line item", :n_label => "line items"})
803
-
804
871
  # if options[:show_invoice_totals]
805
872
  # line_item_totals = json_response['lineItemTotals']
806
873
  # if line_item_totals
807
- # print_h2 "Line Items Totals"
808
- # invoice_totals_columns = {
809
- # "# Line Items" => lambda {|it| format_number(json_response['meta']['total']) rescue '' },
810
- # "Cost" => lambda {|it| format_money(it['itemCost']) },
811
- # "Price" => lambda {|it| format_money(it['itemPrice']) },
812
- # "Tax" => lambda {|it| format_money(it['itemTax']) },
813
- # "Usage" => lambda {|it| it['itemUsage'] },
814
- # }
815
- # print_description_list(invoice_totals_columns, line_item_totals)
816
- # else
817
- # print "\n"
818
- # print yellow, "No line item totals data", reset, "\n"
874
+ # totals_row = line_item_totals.clone
875
+ # totals_row['id'] = 'TOTAL:'
876
+ # #totals_row['usageCategory'] = 'TOTAL:'
877
+ # line_items = line_items + [totals_row]
819
878
  # end
820
879
  # end
880
+ unless options[:totals_only]
881
+ print as_pretty_table(line_items, columns, options)
882
+ print_results_pagination(json_response, {:label => "line item", :n_label => "line items"})
883
+ end
884
+
885
+ if options[:show_invoice_totals]
886
+ line_item_totals = json_response['lineItemTotals']
887
+ if line_item_totals
888
+ print_h2 "Line Item Totals" unless options[:totals_only]
889
+ invoice_totals_columns = [
890
+ {"Items" => lambda {|it| format_number(json_response['meta']['total']) rescue '' } },
891
+ #{"Usage" => lambda {|it| it['itemUsage'] } },
892
+ {"Cost" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
893
+ ] + (options[:show_prices] ? [
894
+ {"Price" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
895
+ #{"Tax" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
896
+
897
+ ] : [])
898
+ print_description_list(invoice_totals_columns, line_item_totals)
899
+ else
900
+ print "\n"
901
+ print yellow, "No line item totals data", reset, "\n"
902
+ end
903
+ end
821
904
 
822
905
  end
823
906
  print reset,"\n"
@@ -840,6 +923,9 @@ EOT
840
923
  options[:show_raw_data] = true
841
924
  options[:pretty_json] = true
842
925
  end
926
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
927
+ options[:sigdig] = val.to_i
928
+ end
843
929
  build_standard_get_options(opts, options)
844
930
  opts.footer = "Get details about a specific invoice line item."
845
931
  opts.footer = <<-EOT
@@ -883,11 +969,16 @@ EOT
883
969
  "Usage Category" => lambda {|it| it['usageCategory'] },
884
970
  "Item Usage" => lambda {|it| it['itemUsage'] },
885
971
  "Item Rate" => lambda {|it| it['itemRate'] },
886
- "Item Cost" => lambda {|it| format_money(it['itemCost']) },
887
- "Item Price" => lambda {|it| format_money(it['itemrPrice']) },
888
- "Item Tax" => lambda {|it| format_money(it['itemTax']) },
889
- "Item Term" => lambda {|it| it['itemTerm'] },
972
+ "Item Cost" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) },
973
+ "Item Price" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) },
974
+ #"Item Tax" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) },
890
975
  #"Tax Type" => lambda {|it| it['taxType'] },
976
+ "Item Term" => lambda {|it| it['itemTerm'] },
977
+ "Item ID" => lambda {|it| it['itemId'] },
978
+ "Item Name" => lambda {|it| it['itemName'] },
979
+ "Item Type" => lambda {|it| it['itemType'] },
980
+ "Item Description" => lambda {|it| it['itemDescription'] },
981
+ "Product Code" => lambda {|it| it['productCode'] },
891
982
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
892
983
  "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
893
984
  }
@@ -956,6 +1047,25 @@ EOT
956
1047
  end
957
1048
  end
958
1049
 
1050
+ def parse_invoice_ref_type(ref_type)
1051
+ val = ref_type.to_s.downcase
1052
+ if val == 'cloud' || val == 'zone'
1053
+ 'ComputeZone'
1054
+ elsif val == 'instance'
1055
+ 'Instance'
1056
+ elsif val == 'server' || val == 'host'
1057
+ 'ComputeServer'
1058
+ elsif val == 'cluster'
1059
+ 'ComputeServerGroup'
1060
+ elsif val == 'group' || val == 'site'
1061
+ 'ComputeSite'
1062
+ elsif val == 'user'
1063
+ 'User'
1064
+ else
1065
+ ref_type
1066
+ end
1067
+ end
1068
+
959
1069
  # convert "202003" to "March 2020"
960
1070
  def format_invoice_period(it)
961
1071
  interval = it['interval']
@@ -1002,6 +1112,11 @@ EOT
1002
1112
  end
1003
1113
  end
1004
1114
 
1115
+ def get_current_period()
1116
+ now = Time.now.utc
1117
+ now.year.to_s + now.month.to_s.rjust(2,'0')
1118
+ end
1119
+
1005
1120
  def format_server_power_state(server, return_color=cyan)
1006
1121
  out = ""
1007
1122
  if server['powerState'] == 'on'