morpheus-cli 4.2.12 → 4.2.17

Sign up to get free protection for your applications and to get access to all the features.
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'