morpheus-cli 4.2.14 → 4.2.19

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 +44 -55
  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 +4 -1
  17. data/lib/morpheus/cli/access_token_command.rb +27 -10
  18. data/lib/morpheus/cli/apps.rb +21 -15
  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/blueprints_command.rb +27 -61
  22. data/lib/morpheus/cli/boot_scripts_command.rb +1 -1
  23. data/lib/morpheus/cli/cli_command.rb +183 -45
  24. data/lib/morpheus/cli/cli_registry.rb +3 -0
  25. data/lib/morpheus/cli/clouds.rb +7 -10
  26. data/lib/morpheus/cli/clusters.rb +0 -18
  27. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +23 -20
  28. data/lib/morpheus/cli/commands/standard/man_command.rb +1 -1
  29. data/lib/morpheus/cli/credentials.rb +13 -9
  30. data/lib/morpheus/cli/deploy.rb +374 -0
  31. data/lib/morpheus/cli/deployments.rb +521 -197
  32. data/lib/morpheus/cli/deploys.rb +271 -126
  33. data/lib/morpheus/cli/doc.rb +182 -0
  34. data/lib/morpheus/cli/error_handler.rb +23 -8
  35. data/lib/morpheus/cli/errors.rb +3 -2
  36. data/lib/morpheus/cli/image_builder_command.rb +2 -2
  37. data/lib/morpheus/cli/instances.rb +136 -17
  38. data/lib/morpheus/cli/invoices_command.rb +339 -225
  39. data/lib/morpheus/cli/jobs_command.rb +2 -2
  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/mixins/accounts_helper.rb +158 -100
  45. data/lib/morpheus/cli/mixins/backups_helper.rb +115 -0
  46. data/lib/morpheus/cli/mixins/deployments_helper.rb +135 -0
  47. data/lib/morpheus/cli/mixins/library_helper.rb +32 -0
  48. data/lib/morpheus/cli/mixins/option_source_helper.rb +1 -1
  49. data/lib/morpheus/cli/mixins/print_helper.rb +149 -84
  50. data/lib/morpheus/cli/mixins/provisioning_helper.rb +2 -2
  51. data/lib/morpheus/cli/mixins/whoami_helper.rb +19 -6
  52. data/lib/morpheus/cli/network_routers_command.rb +1 -1
  53. data/lib/morpheus/cli/option_parser.rb +48 -5
  54. data/lib/morpheus/cli/option_types.rb +46 -10
  55. data/lib/morpheus/cli/price_sets_command.rb +1 -1
  56. data/lib/morpheus/cli/projects_command.rb +7 -7
  57. data/lib/morpheus/cli/remote.rb +3 -2
  58. data/lib/morpheus/cli/roles.rb +49 -92
  59. data/lib/morpheus/cli/security_groups.rb +7 -1
  60. data/lib/morpheus/cli/service_plans_command.rb +10 -10
  61. data/lib/morpheus/cli/setup.rb +1 -1
  62. data/lib/morpheus/cli/shell.rb +7 -6
  63. data/lib/morpheus/cli/subnets_command.rb +1 -1
  64. data/lib/morpheus/cli/tasks.rb +24 -10
  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 +11 -8
  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)
@@ -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)
@@ -1218,7 +1252,6 @@ class Morpheus::Cli::Instances
1218
1252
  "Environment" => 'instanceContext',
1219
1253
  "Labels" => lambda {|it| it['tags'] ? it['tags'].join(',') : '' },
1220
1254
  "Metadata" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
1221
- "Power Schedule" => lambda {|it| (it['powerSchedule'] && it['powerSchedule']['type']) ? it['powerSchedule']['type']['name'] : '' },
1222
1255
  "Owner" => lambda {|it|
1223
1256
  if it['owner']
1224
1257
  (it['owner']['username'] || it['owner']['id'])
@@ -1228,13 +1261,20 @@ class Morpheus::Cli::Instances
1228
1261
  },
1229
1262
  #"Tenant" => lambda {|it| it['tenant'] ? it['tenant']['name'] : '' },
1230
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']) : '' },
1231
1269
  "Nodes" => lambda {|it| it['containers'] ? it['containers'].count : 0 },
1232
1270
  "Connection" => lambda {|it| format_instance_connection_string(it) },
1233
1271
  "Status" => lambda {|it| format_instance_status(it) }
1234
1272
  }
1235
-
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?
1236
1276
  description_cols["Removal Date"] = lambda {|it| format_local_dt(it['removalDate'])} if instance['status'] == 'pendingRemoval'
1237
-
1277
+ description_cols.delete("Last Deployment") if instance['lastDeploy'].nil?
1238
1278
  print_description_list(description_cols, instance)
1239
1279
 
1240
1280
  if instance['statusMessage']
@@ -3606,6 +3646,60 @@ class Morpheus::Cli::Instances
3606
3646
  end
3607
3647
  end
3608
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
+
3609
3703
  private
3610
3704
 
3611
3705
  def find_zone_by_name_or_id(group_id, val)
@@ -3902,4 +3996,29 @@ private
3902
3996
  ]
3903
3997
  end
3904
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
+
3905
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
- 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
47
+ opts.on('-t', '--type TYPE', "Filter by Ref Type eg. ComputeSite (Group), ComputeZone (Cloud), ComputeServer (Host), Instance, Container, User") do |val|
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)
@@ -186,6 +194,7 @@ class Morpheus::Cli::InvoicesCommand
186
194
  print_h1 title, subtitles
187
195
  if invoices.empty?
188
196
  print cyan,"No invoices found.",reset,"\n"
197
+ print reset,"\n"
189
198
  else
190
199
  # current_date = Time.now
191
200
  # current_period = "#{current_date.year}#{current_date.month.to_s.rjust(2, '0')}"
@@ -194,105 +203,137 @@ class Morpheus::Cli::InvoicesCommand
194
203
  {"INVOICE ID" => lambda {|it| it['id'] } },
195
204
  {"TYPE" => lambda {|it| format_invoice_ref_type(it) } },
196
205
  {"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
- ] : []) + [
206
+ {"REF NAME" => lambda {|it|
207
+ if options[:show_all]
208
+ it['refName']
209
+ else
210
+ truncate_string_right(it['refName'], 100)
211
+ end
212
+ } },
203
213
  #{"INTERVAL" => lambda {|it| it['interval'] } },
204
214
  {"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']) } },
215
+ #{"TENANT" => lambda {|it| it['account'] ? it['account']['name'] : '' } },
216
+
217
+ #{"COST TYPE" => lambda {|it| it['costType'].to_s.capitalize } },
208
218
  {"PERIOD" => lambda {|it| format_invoice_period(it) } },
209
219
  {"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']) } },
220
+ {"END" => lambda {|it| format_date(it['endDate']) } },
221
+ ] + (options[:show_all] ? [
222
+ {"REF START" => lambda {|it| format_dt(it['refStart']) } },
223
+ {"REF END" => lambda {|it| format_dt(it['refEnd']) } },
224
+ ] : []) + [
225
+ {"COMPUTE" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) } },
226
+ # {"MEMORY" => lambda {|it| format_money(it['memoryCost']) } },
227
+ {"STORAGE" => lambda {|it| format_money(it['storageCost'], 'usd', {sigdig:options[:sigdig]}) } },
228
+ {"NETWORK" => lambda {|it| format_money(it['networkCost'], 'usd', {sigdig:options[:sigdig]}) } },
229
+ {"EXTRA" => lambda {|it| format_money(it['extraCost'], 'usd', {sigdig:options[:sigdig]}) } },
230
+ {"MTD" => lambda {|it| format_money(it['runningCost'], 'usd', {sigdig:options[:sigdig]}) } },
212
231
  {"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
232
+ format_money(it['totalCost'], 'usd', {sigdig:options[:sigdig]}) + ((it['totalCost'].to_f > 0 && it['totalCost'] != it['runningCost']) ? " (Projected)" : "")
219
233
  } }
220
234
  ]
221
235
 
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
236
  if options[:show_prices]
230
237
  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']) } },
238
+ {"COMPUTE PRICE" => lambda {|it| format_money(it['computePrice'], 'usd', {sigdig:options[:sigdig]}) } },
239
+ # {"MEMORY PRICE" => lambda {|it| format_money(it['memoryPrice'], 'usd', {sigdig:options[:sigdig]}) } },
240
+ {"STORAGE PRICE" => lambda {|it| format_money(it['storagePrice'], 'usd', {sigdig:options[:sigdig]}) } },
241
+ {"NETWORK PRICE" => lambda {|it| format_money(it['networkPrice'], 'usd', {sigdig:options[:sigdig]}) } },
242
+ {"EXTRA PRICE" => lambda {|it| format_money(it['extraPrice'], 'usd', {sigdig:options[:sigdig]}) } },
243
+ {"MTD PRICE" => lambda {|it| format_money(it['runningPrice'], 'usd', {sigdig:options[:sigdig]}) } },
237
244
  {"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
245
+ format_money(it['totalPrice'], 'usd', {sigdig:options[:sigdig]}) + ((it['totalCost'].to_f > 0 && it['totalCost'] != it['runningCost']) ? " (Projected)" : "")
243
246
  } }
244
247
  ]
245
248
  end
246
249
  if options[:show_estimates]
247
250
  columns += [
248
- {"MTD EST." => lambda {|it| format_money(it['estimatedRunningCost']) } },
251
+ {"COMPUTE EST." => lambda {|it| format_money(it['estimatedComputeCost'], 'usd', {sigdig:options[:sigdig]}) } },
252
+ # {"MEMORY EST." => lambda {|it| format_money(it['estimatedMemoryCost'], 'usd', {sigdig:options[:sigdig]}) } },
253
+ {"STORAGE EST." => lambda {|it| format_money(it['estimatedStorageCost'], 'usd', {sigdig:options[:sigdig]}) } },
254
+ {"NETWORK EST." => lambda {|it| format_money(it['estimatedNetworkCost'], 'usd', {sigdig:options[:sigdig]}) } },
255
+ {"EXTRA EST." => lambda {|it| format_money(it['estimatedExtraCost'], 'usd', {sigdig:options[:sigdig]}) } },
256
+ {"MTD EST." => lambda {|it| format_money(it['estimatedRunningCost'], 'usd', {sigdig:options[:sigdig]}) } },
249
257
  {"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
258
+ format_money(it['estimatedTotalCost'], 'usd', {sigdig:options[:sigdig]}) + ((it['estimatedTotalCost'].to_f > 0 && it['estimatedTotalCost'] != it['estimatedRunningCost']) ? " (Projected)" : "")
255
259
  } },
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
260
  ]
262
261
  end
262
+ columns += [
263
+ {"ESTIMATE" => lambda {|it| format_boolean(it['estimate']) } },
264
+ {"ACTIVE" => lambda {|it| format_boolean(it['active']) } },
265
+ {"ITEMS" => lambda {|it| it['lineItems'].size rescue '' } },
266
+ {"TAGS" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' } },
267
+ ]
268
+ if show_projects
269
+ columns += [
270
+ {"PROJECT ID" => lambda {|it| it['project'] ? it['project']['id'] : '' } },
271
+ {"PROJECT NAME" => lambda {|it| it['project'] ? it['project']['name'] : '' } },
272
+ {"PROJECT TAGS" => lambda {|it| it['project'] ? truncate_string(format_metadata(it['project']['tags']), 50) : '' } },
273
+ ]
274
+ end
275
+ columns += [
276
+ {"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
277
+ {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
278
+ ]
263
279
  if options[:show_raw_data]
264
280
  columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
265
281
  end
266
- print as_pretty_table(invoices, columns, options)
267
- print_results_pagination(json_response, {:label => "invoice", :n_label => "invoices"})
282
+ unless options[:totals_only]
283
+ print as_pretty_table(invoices, columns, options)
284
+ print_results_pagination(json_response, {:label => "invoice", :n_label => "invoices"})
285
+ end
268
286
 
269
287
  if options[:show_invoice_totals]
270
288
  invoice_totals = json_response['invoiceTotals']
289
+ print_h2 "Invoice Totals (#{format_number(json_response['meta']['total']) rescue ''})"
290
+
271
291
  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']) },
292
+ cost_rows = [
293
+ {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']},
294
+ ]
295
+ if options[:show_prices]
296
+ cost_rows += [
297
+ {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']},
298
+ ]
299
+ end
300
+ if options[:show_estimates]
301
+ cost_rows += [
302
+ {label: 'Metered 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']},
303
+ {label: 'Metered 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']},
304
+ ]
305
+ end
306
+ cost_columns = {
307
+ "" => lambda {|it| it[:label] },
308
+ "Compute".upcase => lambda {|it| format_money(it[:compute], 'usd', {sigdig:options[:sigdig]}) },
309
+ "Memory".upcase => lambda {|it| format_money(it[:memory], 'usd', {sigdig:options[:sigdig]}) },
310
+ "Storage".upcase => lambda {|it| format_money(it[:storage], 'usd', {sigdig:options[:sigdig]}) },
311
+ "Network".upcase => lambda {|it| format_money(it[:network], 'usd', {sigdig:options[:sigdig]}) },
312
+ "License".upcase => lambda {|it| format_money(it[:license], 'usd', {sigdig:options[:sigdig]}) },
313
+ "Extra".upcase => lambda {|it| format_money(it[:extra], 'usd', {sigdig:options[:sigdig]}) },
314
+ "MTD".upcase => lambda {|it| format_money(it[:running], 'usd', {sigdig:options[:sigdig]}) },
315
+ "Total".upcase => lambda {|it|
316
+ format_money(it[:total], 'usd', {sigdig:options[:sigdig]}) + ((it[:total].to_f > 0 && it[:total] != it[:running]) ? " (Projected)" : "")
317
+ },
287
318
  }
288
- print_description_list(invoice_totals_columns, invoice_totals)
319
+ # remove columns that rarely have data...
320
+ if cost_rows.sum { |it| it[:memory].to_f } == 0
321
+ cost_columns.delete("Memory".upcase)
322
+ end
323
+ if cost_rows.sum { |it| it[:license].to_f } == 0
324
+ cost_columns.delete("License".upcase)
325
+ end
326
+ if cost_rows.sum { |it| it[:extra].to_f } == 0
327
+ cost_columns.delete("Extra".upcase)
328
+ end
329
+ print as_pretty_table(cost_rows, cost_columns, options)
289
330
  else
290
331
  print "\n"
291
332
  print yellow, "No invoice totals data", reset, "\n"
292
333
  end
293
334
  end
335
+ print reset,"\n"
294
336
  end
295
- print reset,"\n"
296
337
  return 0, nil
297
338
  end
298
339
  end
@@ -301,14 +342,17 @@ class Morpheus::Cli::InvoicesCommand
301
342
  options = {}
302
343
  optparse = Morpheus::Cli::OptionParser.new do |opts|
303
344
  opts.banner = subcommand_usage("[id]")
304
- opts.on('-a', '--all', "Display all costs, prices and raw data" ) do
345
+ opts.on('-a', '--all', "Display all details, costs and prices." ) do
305
346
  options[:show_estimates] = true
306
347
  # options[:show_costs] = true
307
348
  options[:show_prices] = true
308
- options[:show_raw_data] = true
349
+ # options[:show_raw_data] = true
309
350
  options[:max_line_items] = 10000
310
351
  end
311
- opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Memory, Storage, etc." ) do
352
+ opts.on('--prices', '--prices', "Display prices: Total, Compute, Storage, Network, Extra" ) do
353
+ options[:show_prices] = true
354
+ end
355
+ opts.on('--estimates', '--estimates', "Display all estimated costs, from usage info: Compute, Storage, Network, Extra" ) do
312
356
  options[:show_estimates] = true
313
357
  end
314
358
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
@@ -318,12 +362,12 @@ class Morpheus::Cli::InvoicesCommand
318
362
  options[:show_raw_data] = true
319
363
  options[:pretty_json] = true
320
364
  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
365
  opts.on('--no-line-items', '--no-line-items', "Do not display line items.") do |val|
325
366
  options[:hide_line_items] = true
326
367
  end
368
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
369
+ options[:sigdig] = val.to_i
370
+ end
327
371
  build_standard_get_options(opts, options)
328
372
  opts.footer = "Get details about a specific invoice."
329
373
  opts.footer = <<-EOT
@@ -365,20 +409,24 @@ EOT
365
409
  "Type" => lambda {|it| format_invoice_ref_type(it) },
366
410
  "Ref ID" => lambda {|it| it['refId'] },
367
411
  "Ref Name" => lambda {|it| it['refName'] },
412
+ "Cloud" => lambda {|it| it['cloud'] ? it['cloud']['name'] : '' },
368
413
  "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
414
  "Power State" => lambda {|it| format_server_power_state(it) },
373
- "Account" => lambda {|it| it['account'] ? it['account']['name'] : '' },
415
+ "Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
374
416
  "Active" => lambda {|it| format_boolean(it['active']) },
375
- "Period" => lambda {|it| format_invoice_period(it) },
376
417
  "Estimate" => lambda {|it| format_boolean(it['estimate']) },
418
+ #"Cost Type" => lambda {|it| it['costType'].to_s.capitalize },
419
+ "Period" => lambda {|it| format_invoice_period(it) },
377
420
  #"Interval" => lambda {|it| it['interval'] },
378
421
  "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']) : '' },
422
+ "End" => lambda {|it| format_date(it['endDate']) },
423
+ "Ref Start" => lambda {|it| format_dt(it['refStart']) },
424
+ "Ref End" => lambda {|it| format_dt(it['refEnd']) },
425
+ "Items" => lambda {|it| it['lineItems'].size rescue '' },
426
+ "Tags" => lambda {|it| it['metadata'] ? it['metadata'].collect {|m| "#{m['name']}: #{m['value']}" }.join(', ') : '' },
427
+ "Project ID" => lambda {|it| it['project'] ? it['project']['id'] : '' },
428
+ "Project Name" => lambda {|it| it['project'] ? it['project']['name'] : '' },
429
+ "Project Tags" => lambda {|it| it['project'] ? format_metadata(it['project']['tags']) : '' },
382
430
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
383
431
  "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
384
432
  }
@@ -391,6 +439,9 @@ EOT
391
439
  description_cols.delete("Project Name")
392
440
  description_cols.delete("Project Tags")
393
441
  end
442
+ if invoice['metadata'].nil? || invoice['metadata'].empty?
443
+ description_cols.delete("Tags")
444
+ end
394
445
  if !['ComputeServer','Instance','Container'].include?(invoice['refType'])
395
446
  description_cols.delete("Power State")
396
447
  end
@@ -398,27 +449,27 @@ EOT
398
449
  =begin
399
450
  print_h2 "Costs"
400
451
  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']) },
452
+ "Compute" => lambda {|it| format_money(it['computeCost'], 'usd', {sigdig:options[:sigdig]}) },
453
+ "Memory" => lambda {|it| format_money(it['memoryCost'], 'usd', {sigdig:options[:sigdig]}) },
454
+ "Storage" => lambda {|it| format_money(it['storageCost'], 'usd', {sigdig:options[:sigdig]}) },
455
+ "Network" => lambda {|it| format_money(it['networkCost'], 'usd', {sigdig:options[:sigdig]}) },
456
+ "License" => lambda {|it| format_money(it['licenseCost'], 'usd', {sigdig:options[:sigdig]}) },
457
+ "Extra" => lambda {|it| format_money(it['extraCost'], 'usd', {sigdig:options[:sigdig]}) },
458
+ "Running" => lambda {|it| format_money(it['runningCost'], 'usd', {sigdig:options[:sigdig]}) },
459
+ "Total Cost" => lambda {|it| format_money(it['totalCost'], 'usd', {sigdig:options[:sigdig]}) },
409
460
  }
410
461
  print as_pretty_table([invoice], cost_columns, options)
411
462
 
412
463
  print_h2 "Prices"
413
464
  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']) },
465
+ "Compute" => lambda {|it| format_money(it['computePrice'], 'usd', {sigdig:options[:sigdig]}) },
466
+ "Memory" => lambda {|it| format_money(it['memoryPrice'], 'usd', {sigdig:options[:sigdig]}) },
467
+ "Storage" => lambda {|it| format_money(it['storagePrice'], 'usd', {sigdig:options[:sigdig]}) },
468
+ "Network" => lambda {|it| format_money(it['networkPrice'], 'usd', {sigdig:options[:sigdig]}) },
469
+ "License" => lambda {|it| format_money(it['licensePrice'], 'usd', {sigdig:options[:sigdig]}) },
470
+ "Extra" => lambda {|it| format_money(it['extraPrice'], 'usd', {sigdig:options[:sigdig]}) },
471
+ "Running" => lambda {|it| format_money(it['runningPrice'], 'usd', {sigdig:options[:sigdig]}) },
472
+ "Total Price" => lambda {|it| format_money(it['totalPrice'], 'usd', {sigdig:options[:sigdig]}) },
422
473
  }
423
474
  print as_pretty_table([invoice], price_columns, options)
424
475
  =end
@@ -426,33 +477,77 @@ EOT
426
477
  # current_date = Time.now
427
478
  # current_period = "#{current_date.year}#{current_date.month.to_s.rjust(2, '0')}"
428
479
 
429
- print "\n"
430
- # print_h2 "Costs"
480
+
481
+
482
+ # Line Items
483
+ line_items = invoice['lineItems']
484
+ if line_items && line_items.size > 0 && options[:hide_line_items] != true
485
+ line_items_columns = [
486
+ {"ID" => lambda {|it| it['id'] } },
487
+ #{"REF TYPE" => lambda {|it| format_invoice_ref_type(it) } },
488
+ #{"REF ID" => lambda {|it| it['refId'] } },
489
+ #{"REF NAME" => lambda {|it| it['refName'] } },
490
+ #{"REF CATEGORY" => lambda {|it| it['refCategory'] } },
491
+ {"START" => lambda {|it| format_dt(it['startDate']) } },
492
+ {"END" => lambda {|it| format_dt(it['endDate']) } },
493
+ {"USAGE TYPE" => lambda {|it| it['usageType'] } },
494
+ {"USAGE CATEGORY" => lambda {|it| it['usageCategory'] } },
495
+ {"USAGE" => lambda {|it| it['itemUsage'] } },
496
+ {"RATE" => lambda {|it| it['itemRate'] } },
497
+ {"UNIT" => lambda {|it| it['rateUnit'] } },
498
+ {"COST" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
499
+ {"PRICE" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
500
+ #{"TAX" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
501
+ # {"TERM" => lambda {|it| it['itemTerm'] } },
502
+ {"ITEM ID" => lambda {|it| truncate_string_right(it['itemId'], 65) } },
503
+ {"ITEM NAME" => lambda {|it| it['itemName'] } },
504
+ {"ITEM TYPE" => lambda {|it| it['itemType'] } },
505
+ {"ITEM DESCRIPTION" => lambda {|it| it['itemDescription'] } },
506
+ {"PRODUCT CODE" => lambda {|it| it['productCode'] } },
507
+ {"CREATED" => lambda {|it| format_local_dt(it['dateCreated']) } },
508
+ {"UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) } }
509
+ ]
510
+ if options[:show_raw_data]
511
+ line_items_columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
512
+ end
513
+ print_h2 "Line Items"
514
+ #max_line_items = options[:max_line_items] ? options[:max_line_items].to_i : 5
515
+ paged_line_items = line_items #.first(max_line_items)
516
+ print as_pretty_table(paged_line_items, line_items_columns, options)
517
+ print_results_pagination({total: line_items.size, size: paged_line_items.size}, {:label => "line item", :n_label => "line items"})
518
+ end
519
+
520
+ # cost_types = ["Costs"]
521
+ # cost_types << "Prices" if options[:show_prices]
522
+ # cost_types << "Estimates" if options[:show_estimates]
523
+ # print_h2 cost_types.size == 1 ? "Totals" : "Total #{anded_list(cost_types)}"
524
+ print_h2 "Invoice Totals"
525
+
431
526
  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
527
  {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
528
  ]
529
+ if options[:show_prices]
530
+ cost_rows += [
531
+ {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']},
532
+ ]
533
+ end
435
534
  if options[:show_estimates]
436
535
  cost_rows += [
437
- {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']},
536
+ {label: 'Metered 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']},
537
+ {label: 'Metered 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
538
  ]
440
539
  end
441
540
  cost_columns = {
442
541
  "" => 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]) },
542
+ "Compute".upcase => lambda {|it| format_money(it[:compute], 'usd', {sigdig:options[:sigdig]}) },
543
+ "Memory".upcase => lambda {|it| format_money(it[:memory], 'usd', {sigdig:options[:sigdig]}) },
544
+ "Storage".upcase => lambda {|it| format_money(it[:storage], 'usd', {sigdig:options[:sigdig]}) },
545
+ "Network".upcase => lambda {|it| format_money(it[:network], 'usd', {sigdig:options[:sigdig]}) },
546
+ "License".upcase => lambda {|it| format_money(it[:license], 'usd', {sigdig:options[:sigdig]}) },
547
+ "Extra".upcase => lambda {|it| format_money(it[:extra], 'usd', {sigdig:options[:sigdig]}) },
548
+ "MTD" => lambda {|it| format_money(it[:running], 'usd', {sigdig:options[:sigdig]}) },
450
549
  "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
550
+ format_money(it[:total], 'usd', {sigdig:options[:sigdig]}) + ((it[:total].to_f > 0 && it[:total] != it[:running]) ? " (Projected)" : "")
456
551
  },
457
552
  }
458
553
  # remove columns that rarely have data...
@@ -463,7 +558,7 @@ EOT
463
558
  cost_columns.delete("License".upcase)
464
559
  end
465
560
  if cost_rows.sum { |it| it[:extra].to_f } == 0
466
- cost_columns.delete("Other".upcase)
561
+ cost_columns.delete("Extra".upcase)
467
562
  end
468
563
  print as_pretty_table(cost_rows, cost_columns, options)
469
564
 
@@ -471,41 +566,6 @@ EOT
471
566
  print_h2 "Raw Data"
472
567
  puts as_json(invoice['rawData'], {pretty_json:false}.merge(options))
473
568
  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
569
 
510
570
  print reset,"\n"
511
571
  return 0
@@ -596,43 +656,36 @@ EOT
596
656
  options = {}
597
657
  params = {}
598
658
  ref_ids = []
659
+ query_tags = {}
599
660
  optparse = Morpheus::Cli::OptionParser.new do |opts|
600
661
  opts.banner = subcommand_usage()
601
- opts.on('-a', '--all', "Display all costs, prices and raw data" ) do
662
+ opts.on('-a', '--all', "Display all details, costs and prices." ) do
602
663
  options[:show_actual_costs] = true
603
664
  options[:show_costs] = true
604
665
  options[:show_prices] = true
605
- options[:show_raw_data] = true
666
+ # options[:show_raw_data] = true
606
667
  end
607
- # opts.on('--actuals', '--actuals', "Display all actual costs: Compute, Memory, Storage, etc." ) do
668
+ # opts.on('--actuals', '--actuals', "Display all actual costs: Compute, Storage, Network, Extra" ) do
608
669
  # options[:show_actual_costs] = true
609
670
  # end
610
- # opts.on('--costs', '--costs', "Display all costs: Compute, Memory, Storage, etc." ) do
671
+ # opts.on('--costs', '--costs', "Display all costs: Compute, Storage, Network, Extra" ) do
611
672
  # options[:show_costs] = true
612
673
  # end
613
- # opts.on('--prices', '--prices', "Display prices: Total, Compute, Memory, Storage, etc." ) do
614
- # options[:show_prices] = true
615
- # end
674
+ opts.on('--prices', '--prices', "Display prices: Total, Compute, Storage, Network, Extra" ) do
675
+ options[:show_prices] = true
676
+ end
616
677
  opts.on('--invoice-id ID', String, "Filter by Invoice ID") do |val|
617
678
  params['invoiceId'] ||= []
618
679
  params['invoiceId'] << val
619
680
  end
620
- 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
681
+ opts.on('--external-id ID', String, "Filter by External ID") do |val|
682
+ params['externalId'] ||= []
683
+ params['externalId'] << val
684
+ end
685
+ opts.on('-t', '--type TYPE', "Filter by Ref Type eg. ComputeSite (Group), ComputeZone (Cloud), ComputeServer (Host), Instance, Container, User") do |val|
686
+ params['refType'] ||= []
687
+ values = val.split(",").collect {|it| it.strip }.select {|it| it != "" }
688
+ values.each { |it| params['refType'] << parse_invoice_ref_type(it) }
636
689
  end
637
690
  opts.on('--id ID', String, "Filter by Ref ID") do |val|
638
691
  ref_ids << val
@@ -691,6 +744,11 @@ EOT
691
744
  opts.on('--tenant ID', String, "View invoice line items for a tenant. Default is your own account.") do |val|
692
745
  params['accountId'] = val
693
746
  end
747
+ # opts.on('--tags Name=Value',String, "Filter by tags.") do |val|
748
+ # k,v = val.split("=")
749
+ # query_tags[k] ||= []
750
+ # query_tags[k] << v
751
+ # end
694
752
  opts.on('--raw-data', '--raw-data', "Display Raw Data, the cost data from the cloud provider's API.") do |val|
695
753
  options[:show_raw_data] = true
696
754
  end
@@ -698,6 +756,14 @@ EOT
698
756
  params['includeTotals'] = true
699
757
  options[:show_invoice_totals] = true
700
758
  end
759
+ opts.on('--totals-only', "View totals only") do |val|
760
+ params['includeTotals'] = true
761
+ options[:show_invoice_totals] = true
762
+ options[:totals_only] = true
763
+ end
764
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
765
+ options[:sigdig] = val.to_i
766
+ end
701
767
  build_standard_list_options(opts, options)
702
768
  opts.footer = "List invoice line items."
703
769
  end
@@ -742,6 +808,11 @@ EOT
742
808
  end
743
809
  params['rawData'] = true if options[:show_raw_data]
744
810
  params['refId'] = ref_ids unless ref_ids.empty?
811
+ if query_tags && !query_tags.empty?
812
+ query_tags.each do |k,v|
813
+ params['tags.' + k] = v
814
+ end
815
+ end
745
816
  @invoice_line_items_interface.setopts(options)
746
817
  if options[:dry_run]
747
818
  print_dry_run @invoice_line_items_interface.dry.list(params)
@@ -773,15 +844,22 @@ EOT
773
844
  {"REF NAME" => lambda {|it| it['refName'] } },
774
845
  #{"REF CATEGORY" => lambda {|it| it['refCategory'] } },
775
846
  {"START" => lambda {|it| format_date(it['startDate']) } },
776
- {"END" => lambda {|it| it['endDate'] ? format_date(it['endDate']) : '' } },
847
+ {"END" => lambda {|it| format_date(it['endDate']) } },
777
848
  {"USAGE TYPE" => lambda {|it| it['usageType'] } },
778
849
  {"USAGE CATEGORY" => lambda {|it| it['usageCategory'] } },
779
850
  {"USAGE" => lambda {|it| it['itemUsage'] } },
780
851
  {"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'] } },
852
+ {"UNIT" => lambda {|it| it['rateUnit'] } },
853
+ {"COST" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
854
+ ] + (options[:show_prices] ? [
855
+ {"PRICE" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
856
+ {"TAX" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
857
+ ] : []) + [
858
+ {"ITEM ID" => lambda {|it| truncate_string_right(it['itemId'], 65) } },
859
+ {"ITEM NAME" => lambda {|it| it['itemName'] } },
860
+ {"ITEM TYPE" => lambda {|it| it['itemType'] } },
861
+ {"ITEM DESCRIPTION" => lambda {|it| it['itemDescription'] } },
862
+ {"PRODUCT CODE" => lambda {|it| it['productCode'] } },
785
863
  "CREATED" => lambda {|it| format_local_dt(it['dateCreated']) },
786
864
  "UPDATED" => lambda {|it| format_local_dt(it['lastUpdated']) }
787
865
  ]
@@ -789,35 +867,39 @@ EOT
789
867
  if options[:show_raw_data]
790
868
  columns += [{"RAW DATA" => lambda {|it| truncate_string(it['rawData'].to_s, 10) } }]
791
869
  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
870
  # if options[:show_invoice_totals]
805
871
  # line_item_totals = json_response['lineItemTotals']
806
872
  # 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"
873
+ # totals_row = line_item_totals.clone
874
+ # totals_row['id'] = 'TOTAL:'
875
+ # #totals_row['usageCategory'] = 'TOTAL:'
876
+ # line_items = line_items + [totals_row]
819
877
  # end
820
878
  # end
879
+ unless options[:totals_only]
880
+ print as_pretty_table(line_items, columns, options)
881
+ print_results_pagination(json_response, {:label => "line item", :n_label => "line items"})
882
+ end
883
+
884
+ if options[:show_invoice_totals]
885
+ line_item_totals = json_response['lineItemTotals']
886
+ if line_item_totals
887
+ print_h2 "Line Item Totals" unless options[:totals_only]
888
+ invoice_totals_columns = [
889
+ {"Items" => lambda {|it| format_number(json_response['meta']['total']) rescue '' } },
890
+ #{"Usage" => lambda {|it| it['itemUsage'] } },
891
+ {"Cost" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) } },
892
+ ] + (options[:show_prices] ? [
893
+ {"Price" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) } },
894
+ #{"Tax" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) } },
895
+
896
+ ] : [])
897
+ print_description_list(invoice_totals_columns, line_item_totals)
898
+ else
899
+ print "\n"
900
+ print yellow, "No line item totals data", reset, "\n"
901
+ end
902
+ end
821
903
 
822
904
  end
823
905
  print reset,"\n"
@@ -840,6 +922,9 @@ EOT
840
922
  options[:show_raw_data] = true
841
923
  options[:pretty_json] = true
842
924
  end
925
+ opts.on('--sigdig DIGITS', "Significant digits when rounding cost values for display as currency. Default is 2. eg. $3.50") do |val|
926
+ options[:sigdig] = val.to_i
927
+ end
843
928
  build_standard_get_options(opts, options)
844
929
  opts.footer = "Get details about a specific invoice line item."
845
930
  opts.footer = <<-EOT
@@ -883,11 +968,16 @@ EOT
883
968
  "Usage Category" => lambda {|it| it['usageCategory'] },
884
969
  "Item Usage" => lambda {|it| it['itemUsage'] },
885
970
  "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'] },
971
+ "Item Cost" => lambda {|it| format_money(it['itemCost'], 'usd', {sigdig:options[:sigdig]}) },
972
+ "Item Price" => lambda {|it| format_money(it['itemPrice'], 'usd', {sigdig:options[:sigdig]}) },
973
+ #"Item Tax" => lambda {|it| format_money(it['itemTax'], 'usd', {sigdig:options[:sigdig]}) },
890
974
  #"Tax Type" => lambda {|it| it['taxType'] },
975
+ "Item Term" => lambda {|it| it['itemTerm'] },
976
+ "Item ID" => lambda {|it| it['itemId'] },
977
+ "Item Name" => lambda {|it| it['itemName'] },
978
+ "Item Type" => lambda {|it| it['itemType'] },
979
+ "Item Description" => lambda {|it| it['itemDescription'] },
980
+ "Product Code" => lambda {|it| it['productCode'] },
891
981
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
892
982
  "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
893
983
  }
@@ -956,6 +1046,25 @@ EOT
956
1046
  end
957
1047
  end
958
1048
 
1049
+ def parse_invoice_ref_type(ref_type)
1050
+ val = ref_type.to_s.downcase
1051
+ if val == 'cloud' || val == 'zone'
1052
+ 'ComputeZone'
1053
+ elsif val == 'instance'
1054
+ 'Instance'
1055
+ elsif val == 'server' || val == 'host'
1056
+ 'ComputeServer'
1057
+ elsif val == 'cluster'
1058
+ 'ComputeServerGroup'
1059
+ elsif val == 'group' || val == 'site'
1060
+ 'ComputeSite'
1061
+ elsif val == 'user'
1062
+ 'User'
1063
+ else
1064
+ ref_type
1065
+ end
1066
+ end
1067
+
959
1068
  # convert "202003" to "March 2020"
960
1069
  def format_invoice_period(it)
961
1070
  interval = it['interval']
@@ -1002,6 +1111,11 @@ EOT
1002
1111
  end
1003
1112
  end
1004
1113
 
1114
+ def get_current_period()
1115
+ now = Time.now.utc
1116
+ now.year.to_s + now.month.to_s.rjust(2,'0')
1117
+ end
1118
+
1005
1119
  def format_server_power_state(server, return_color=cyan)
1006
1120
  out = ""
1007
1121
  if server['powerState'] == 'on'