morpheus-cli 4.1.8 → 4.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/api_client.rb +24 -0
  4. data/lib/morpheus/api/{old_cypher_interface.rb → budgets_interface.rb} +10 -11
  5. data/lib/morpheus/api/cloud_datastores_interface.rb +7 -0
  6. data/lib/morpheus/api/cloud_resource_pools_interface.rb +2 -2
  7. data/lib/morpheus/api/cypher_interface.rb +18 -12
  8. data/lib/morpheus/api/health_interface.rb +72 -0
  9. data/lib/morpheus/api/instances_interface.rb +1 -1
  10. data/lib/morpheus/api/library_instance_types_interface.rb +7 -0
  11. data/lib/morpheus/api/log_settings_interface.rb +6 -0
  12. data/lib/morpheus/api/network_security_servers_interface.rb +30 -0
  13. data/lib/morpheus/api/price_sets_interface.rb +42 -0
  14. data/lib/morpheus/api/prices_interface.rb +68 -0
  15. data/lib/morpheus/api/provisioning_settings_interface.rb +29 -0
  16. data/lib/morpheus/api/servers_interface.rb +1 -1
  17. data/lib/morpheus/api/service_plans_interface.rb +34 -11
  18. data/lib/morpheus/api/task_sets_interface.rb +8 -0
  19. data/lib/morpheus/api/tasks_interface.rb +8 -0
  20. data/lib/morpheus/cli.rb +6 -3
  21. data/lib/morpheus/cli/appliance_settings_command.rb +13 -5
  22. data/lib/morpheus/cli/approvals_command.rb +1 -1
  23. data/lib/morpheus/cli/apps.rb +88 -28
  24. data/lib/morpheus/cli/backup_settings_command.rb +1 -1
  25. data/lib/morpheus/cli/blueprints_command.rb +2 -0
  26. data/lib/morpheus/cli/budgets_command.rb +672 -0
  27. data/lib/morpheus/cli/cli_command.rb +13 -2
  28. data/lib/morpheus/cli/cli_registry.rb +1 -0
  29. data/lib/morpheus/cli/clusters.rb +40 -274
  30. data/lib/morpheus/cli/commands/standard/benchmark_command.rb +114 -66
  31. data/lib/morpheus/cli/commands/standard/coloring_command.rb +12 -0
  32. data/lib/morpheus/cli/commands/standard/curl_command.rb +31 -6
  33. data/lib/morpheus/cli/commands/standard/echo_command.rb +8 -3
  34. data/lib/morpheus/cli/commands/standard/set_prompt_command.rb +1 -1
  35. data/lib/morpheus/cli/containers_command.rb +37 -24
  36. data/lib/morpheus/cli/cypher_command.rb +191 -150
  37. data/lib/morpheus/cli/health_command.rb +903 -0
  38. data/lib/morpheus/cli/hosts.rb +43 -32
  39. data/lib/morpheus/cli/instances.rb +119 -68
  40. data/lib/morpheus/cli/jobs_command.rb +1 -1
  41. data/lib/morpheus/cli/library_instance_types_command.rb +61 -11
  42. data/lib/morpheus/cli/library_option_types_command.rb +2 -2
  43. data/lib/morpheus/cli/log_settings_command.rb +46 -3
  44. data/lib/morpheus/cli/logs_command.rb +24 -17
  45. data/lib/morpheus/cli/mixins/accounts_helper.rb +2 -0
  46. data/lib/morpheus/cli/mixins/logs_helper.rb +73 -19
  47. data/lib/morpheus/cli/mixins/print_helper.rb +29 -1
  48. data/lib/morpheus/cli/mixins/provisioning_helper.rb +554 -96
  49. data/lib/morpheus/cli/mixins/whoami_helper.rb +13 -1
  50. data/lib/morpheus/cli/networks_command.rb +3 -0
  51. data/lib/morpheus/cli/option_types.rb +83 -53
  52. data/lib/morpheus/cli/price_sets_command.rb +543 -0
  53. data/lib/morpheus/cli/prices_command.rb +669 -0
  54. data/lib/morpheus/cli/processes_command.rb +0 -2
  55. data/lib/morpheus/cli/provisioning_settings_command.rb +237 -0
  56. data/lib/morpheus/cli/remote.rb +9 -4
  57. data/lib/morpheus/cli/reports_command.rb +10 -4
  58. data/lib/morpheus/cli/roles.rb +93 -38
  59. data/lib/morpheus/cli/security_groups.rb +10 -0
  60. data/lib/morpheus/cli/service_plans_command.rb +736 -0
  61. data/lib/morpheus/cli/tasks.rb +220 -8
  62. data/lib/morpheus/cli/tenants_command.rb +3 -16
  63. data/lib/morpheus/cli/users.rb +2 -25
  64. data/lib/morpheus/cli/version.rb +1 -1
  65. data/lib/morpheus/cli/whitelabel_settings_command.rb +18 -18
  66. data/lib/morpheus/cli/whoami.rb +28 -10
  67. data/lib/morpheus/cli/workflows.rb +488 -36
  68. data/lib/morpheus/formatters.rb +22 -0
  69. data/morpheus-cli.gemspec +1 -0
  70. metadata +28 -5
  71. data/lib/morpheus/cli/accounts.rb +0 -335
  72. data/lib/morpheus/cli/old_cypher_command.rb +0 -412
@@ -0,0 +1,29 @@
1
+ require 'morpheus/api/api_client'
2
+
3
+ class Morpheus::ProvisioningSettingsInterface < Morpheus::APIClient
4
+ def initialize(access_token, refresh_token,expires_at = nil, base_url=nil, api='provisioning-settings')
5
+ @access_token = access_token
6
+ @refresh_token = refresh_token
7
+ @base_url = base_url
8
+ @api_url = "#{base_url}/api/#{api}"
9
+ @expires_at = expires_at
10
+ end
11
+
12
+ def get()
13
+ url = @api_url
14
+ headers = { params: {}, authorization: "Bearer #{@access_token}" }
15
+ execute(method: :get, url: url, headers: headers)
16
+ end
17
+
18
+ def update(payload)
19
+ url = @api_url
20
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
21
+ execute(method: :put, url: url, headers: headers, payload: payload.to_json)
22
+ end
23
+
24
+ def template_types()
25
+ url = "#{@api_url}/template-types"
26
+ headers = { params: {}, authorization: "Bearer #{@access_token}" }
27
+ execute(method: :get, url: url, headers: headers)
28
+ end
29
+ end
@@ -107,7 +107,7 @@ class Morpheus::ServersInterface < Morpheus::APIClient
107
107
 
108
108
  def workflow(id,task_set_id,payload)
109
109
  url = "#{@base_url}/api/servers/#{id}/workflow"
110
- headers = { :params => {:taskSetId => task_set_id},:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
110
+ headers = { :params => {:workflowId => task_set_id},:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
111
111
  opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
112
112
  execute(opts)
113
113
  end
@@ -5,26 +5,49 @@ class Morpheus::ServicePlansInterface < Morpheus::APIClient
5
5
  @access_token = access_token
6
6
  @refresh_token = refresh_token
7
7
  @base_url = base_url
8
+ @api_url = "#{@base_url}/api/service-plans"
8
9
  @expires_at = expires_at
9
10
  end
10
11
 
11
12
  def list(params={})
12
- url = "#{@base_url}/api/service-plans"
13
+ url = @api_url
13
14
  headers = { params: params, authorization: "Bearer #{@access_token}" }
14
15
  execute(method: :get, url: url, headers: headers)
15
16
  end
16
17
 
17
- def get(options=nil)
18
- url = "#{@base_url}/api/service-plans"
19
- headers = { params: {}, authorization: "Bearer #{@access_token}" }
18
+ def get(id, params={})
19
+ url = "#{@api_url}/#{id}"
20
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
21
+ execute(method: :get, url: url, headers: headers)
22
+ end
23
+
24
+ def create(payload)
25
+ url = @api_url
26
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
27
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
28
+ end
29
+
30
+ def update(id, payload)
31
+ url = "#{@api_url}/#{id}"
32
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
33
+ execute(method: :put, url: url, headers: headers, payload: payload.to_json)
34
+ end
20
35
 
21
- if options.is_a?(Hash)
22
- headers[:params].merge!(options)
23
- elsif options.is_a?(Numeric)
24
- url = "#{@base_url}/api/service-plans/#{options}"
25
- elsif options.is_a?(String)
26
- headers[:params]['name'] = options
27
- end
36
+ def deactivate(id, params={})
37
+ url = "#{@api_url}/#{id}/deactivate"
38
+ headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
39
+ execute(method: :put, url: url, headers: headers)
40
+ end
41
+
42
+ def provision_types(params={})
43
+ url = "#{@api_url}/provision-types"
44
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
45
+ execute(method: :get, url: url, headers: headers)
46
+ end
47
+
48
+ def price_sets(params={})
49
+ url = "#{@api_url}/price-sets"
50
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
28
51
  execute(method: :get, url: url, headers: headers)
29
52
  end
30
53
  end
@@ -47,4 +47,12 @@ class Morpheus::TaskSetsInterface < Morpheus::APIClient
47
47
  headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
48
48
  execute(method: :delete, url: url, headers: headers)
49
49
  end
50
+
51
+ def run(id, options)
52
+ url = "#{@base_url}/api/task-sets/#{id}/execute"
53
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
54
+ payload = options
55
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
56
+ end
57
+
50
58
  end
@@ -63,4 +63,12 @@ class Morpheus::TasksInterface < Morpheus::APIClient
63
63
  headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
64
64
  execute(method: :delete, url: url, headers: headers)
65
65
  end
66
+
67
+ def run(id, options)
68
+ url = "#{@base_url}/api/tasks/#{id}/execute"
69
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
70
+ payload = options
71
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
72
+ end
73
+
66
74
  end
@@ -104,7 +104,6 @@ module Morpheus
104
104
  load 'morpheus/cli/security_group_rules.rb'
105
105
  load 'morpheus/cli/clusters.rb'
106
106
  load 'morpheus/cli/tenants_command.rb'
107
- load 'morpheus/cli/accounts.rb'
108
107
  load 'morpheus/cli/account_groups_command.rb'
109
108
  load 'morpheus/cli/users.rb'
110
109
  load 'morpheus/cli/change_password_command.rb'
@@ -139,7 +138,6 @@ module Morpheus
139
138
  load 'morpheus/cli/network_domains_command.rb'
140
139
  load 'morpheus/cli/network_proxies_command.rb'
141
140
  load 'morpheus/cli/cypher_command.rb'
142
- load 'morpheus/cli/old_cypher_command.rb'
143
141
  load 'morpheus/cli/image_builder_command.rb'
144
142
  load 'morpheus/cli/preseed_scripts_command.rb'
145
143
  load 'morpheus/cli/boot_scripts_command.rb'
@@ -156,7 +154,12 @@ module Morpheus
156
154
  load 'morpheus/cli/whitelabel_settings_command.rb'
157
155
  load 'morpheus/cli/wiki_command.rb'
158
156
  load 'morpheus/cli/approvals_command.rb'
159
-
157
+ load 'morpheus/cli/service_plans_command.rb'
158
+ load 'morpheus/cli/price_sets_command.rb'
159
+ load 'morpheus/cli/prices_command.rb'
160
+ load 'morpheus/cli/provisioning_settings_command.rb'
161
+ load 'morpheus/cli/budgets_command.rb'
162
+ load 'morpheus/cli/health_command.rb'
160
163
  # Your new commands go here...
161
164
 
162
165
  end
@@ -5,7 +5,7 @@ class Morpheus::Cli::ApplianceSettingsCommand
5
5
  include Morpheus::Cli::AccountsHelper
6
6
 
7
7
  set_command_name :'appliance-settings'
8
- set_command_hidden
8
+
9
9
  register_subcommands :get, :update
10
10
 
11
11
  set_default_subcommand :get
@@ -127,16 +127,24 @@ class Morpheus::Cli::ApplianceSettingsCommand
127
127
  params['corsAllowed'] = val == 'null' ? nil : val
128
128
  end
129
129
  opts.on("--registration-enabled [on|off]", ['on','off'], "Tenant registration enabled") do |val|
130
- params['registrationEnabled'] = ['true','on'].include?(val.to_s.strip)
130
+ params['registrationEnabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
131
131
  end
132
132
  opts.on("--default-tenant-role ROLE", String, "Default tenant role authority or ID") do |val|
133
- options[:defaultTenantRole] = val == 'null' ? nil : val
133
+ if val == 'null'
134
+ params['defaultRoleId'] = nil
135
+ else
136
+ options[:defaultTenantRole] = val
137
+ end
134
138
  end
135
139
  opts.on("--default-user-role ROLE", String, "Default user role authority or ID") do |val|
136
- options[:defaultUserRole] = val == 'null' ? nil : val
140
+ if val == 'null'
141
+ params['defaultUserRoleId'] = nil
142
+ else
143
+ options[:defaultUserRole] = val
144
+ end
137
145
  end
138
146
  opts.on("--docker-privileged-mode [on|off]", ['on','off'], "Docker privileged mode") do |val|
139
- params['dockerPrivilegedMode'] = ['true','on'].include?(val.to_s.strip)
147
+ params['dockerPrivilegedMode'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
140
148
  end
141
149
  opts.on("--expire-pwd-days NUMBER", Integer, "Expire password after specified days. Set to 0 to disable this feature") do |val|
142
150
  params['expirePwdDays'] = val.to_i
@@ -5,7 +5,7 @@ class Morpheus::Cli::ApprovalsCommand
5
5
  include Morpheus::Cli::AccountsHelper
6
6
 
7
7
  set_command_name :'approvals'
8
- set_command_hidden
8
+
9
9
  register_subcommands :list, :get, :approve, :deny, :cancel
10
10
  set_default_subcommand :list
11
11
 
@@ -7,12 +7,14 @@ require 'morpheus/cli/cli_command'
7
7
  require 'morpheus/cli/mixins/accounts_helper'
8
8
  require 'morpheus/cli/mixins/provisioning_helper'
9
9
  require 'morpheus/cli/mixins/processes_helper'
10
+ require 'morpheus/cli/mixins/logs_helper'
10
11
 
11
12
  class Morpheus::Cli::Apps
12
13
  include Morpheus::Cli::CliCommand
13
14
  include Morpheus::Cli::AccountsHelper
14
15
  include Morpheus::Cli::ProvisioningHelper
15
16
  include Morpheus::Cli::ProcessesHelper
17
+ include Morpheus::Cli::LogsHelper
16
18
  set_command_name :apps
17
19
  set_command_description "View and manage apps."
18
20
  register_subcommands :list, :count, :get, :view, :add, :update, :remove, :add_instance, :remove_instance, :logs, :security_groups, :apply_security_groups, :history
@@ -34,9 +36,11 @@ class Morpheus::Cli::Apps
34
36
  @apps_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).apps
35
37
  @blueprints_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).blueprints
36
38
  @instance_types_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instance_types
39
+ @library_layouts_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).library_layouts
37
40
  @instances_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instances
38
41
  @options_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).options
39
42
  @groups_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).groups
43
+ @clouds_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).clouds
40
44
  @logs_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).logs
41
45
  @processes_interface = @api_client.processes
42
46
  @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
@@ -985,6 +989,42 @@ class Morpheus::Cli::Apps
985
989
  options = {}
986
990
  optparse = Morpheus::Cli::OptionParser.new do |opts|
987
991
  opts.banner = subcommand_usage("[app]")
992
+ # opts.on('--hosts HOSTS', String, "Filter logs to specific Host ID(s)") do |val|
993
+ # params['servers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
994
+ # end
995
+ # opts.on('--servers HOSTS', String, "alias for --hosts") do |val|
996
+ # params['servers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
997
+ # end
998
+ # opts.on('--vms HOSTS', String, "alias for --hosts") do |val|
999
+ # params['servers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
1000
+ # end
1001
+ # opts.on('--container CONTAINER', String, "Filter logs to specific Container ID(s)") do |val|
1002
+ # params['containers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
1003
+ # end
1004
+ opts.on( '-n', '--node NODE_ID', "Scope logs to specific Container or VM" ) do |node_id|
1005
+ options[:node_id] = node_id.to_i
1006
+ end
1007
+ # opts.on('--nodes HOST', String, "alias for --containers") do |val|
1008
+ # params['containers'] = val.to_s.split(",").collect {|it| it.to_s.strip }.select {|it| it }.compact
1009
+ # end
1010
+ opts.on('--start TIMESTAMP','--start TIMESTAMP', "Start timestamp. Default is 30 days ago.") do |val|
1011
+ options[:start] = parse_time(val) #.utc.iso8601
1012
+ end
1013
+ opts.on('--end TIMESTAMP','--end TIMESTAMP', "End timestamp. Default is now.") do |val|
1014
+ options[:end] = parse_time(val) #.utc.iso8601
1015
+ end
1016
+ # opts.on('--interval TIME','--interval TIME', "Interval of time to include, in seconds. Default is 30 days ago.") do |val|
1017
+ # options[:interval] = parse_time(val).utc.iso8601
1018
+ # end
1019
+ opts.on('--level VALUE', String, "Log Level. DEBUG,INFO,WARN,ERROR") do |val|
1020
+ params['level'] = params['level'] ? [params['level'], val].flatten : val
1021
+ end
1022
+ opts.on('--table', '--table', "Format ouput as a table.") do
1023
+ options[:table] = true
1024
+ end
1025
+ opts.on('-a', '--all', "Display all details: entire message." ) do
1026
+ options[:details] = true
1027
+ end
988
1028
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
989
1029
  opts.footer = "List logs for an app.\n" +
990
1030
  "[app] is required. This is the name or id of an app."
@@ -998,53 +1038,73 @@ class Morpheus::Cli::Apps
998
1038
  connect(options)
999
1039
  begin
1000
1040
  app = find_app_by_name_or_id(args[0])
1001
- containers = []
1041
+ container_ids = []
1002
1042
  app['appTiers'].each do |app_tier|
1003
1043
  app_tier['appInstances'].each do |app_instance|
1004
- containers += app_instance['instance']['containers']
1044
+ container_ids += app_instance['instance']['containers']
1045
+ end if app_tier['appInstances']
1046
+ end if app['appTiers']
1047
+ if container_ids.empty?
1048
+ print cyan,"app is empty",reset,"\n"
1049
+ return 0
1050
+ # print_error yellow,"app is empty",reset,"\n"
1051
+ # return 1
1052
+ end
1053
+
1054
+ if options[:node_id]
1055
+ if container_ids.include?(options[:node_id])
1056
+ container_ids = [options[:node_id]]
1057
+ else
1058
+ print_red_alert "App does not include node #{options[:node_id]}"
1059
+ return 1
1005
1060
  end
1006
1061
  end
1007
1062
  params = {}
1008
1063
  params.merge!(parse_list_options(options))
1009
- params[:query] = params.delete(:phrase) unless params[:phrase].nil?
1064
+ params['query'] = params.delete('phrase') if params['phrase']
1010
1065
  params['order'] = params['direction'] unless params['direction'].nil? # old api version expects order instead of direction
1066
+ params['startMs'] = (options[:start].to_i * 1000) if options[:start]
1067
+ params['endMs'] = (options[:end].to_i * 1000) if options[:end]
1068
+ params['interval'] = options[:interval].to_s if options[:interval]
1011
1069
  @logs_interface.setopts(options)
1012
1070
  if options[:dry_run]
1013
- print_dry_run @logs_interface.dry.container_logs(containers, params)
1071
+ print_dry_run @logs_interface.dry.container_logs(container_ids, params)
1014
1072
  return
1015
1073
  end
1016
- json_response = @logs_interface.container_logs(containers, params)
1017
- render_result = render_with_format(json_response, options, 'data')
1074
+ json_response = @logs_interface.container_logs(container_ids, params)
1075
+ render_result = json_response['logs'] ? render_with_format(json_response, options, 'logs') : render_with_format(json_response, options, 'data')
1018
1076
  return 0 if render_result
1019
1077
 
1020
- logs = json_response
1021
1078
  title = "App Logs: #{app['name']}"
1022
1079
  subtitles = parse_list_subtitles(options)
1080
+ if options[:start]
1081
+ subtitles << "Start: #{options[:start]}".strip
1082
+ end
1083
+ if options[:end]
1084
+ subtitles << "End: #{options[:end]}".strip
1085
+ end
1086
+ if params[:query]
1087
+ subtitles << "Search: #{params[:query]}".strip
1088
+ end
1089
+ if params['servers']
1090
+ subtitles << "Servers: #{params['servers']}".strip
1091
+ end
1092
+ if params['containers']
1093
+ subtitles << "Containers: #{params['containers']}".strip
1094
+ end
1023
1095
  if params[:query]
1024
1096
  subtitles << "Search: #{params[:query]}".strip
1025
1097
  end
1026
- # todo: startMs, endMs, sorts insteaad of sort..etc
1098
+ if params['level']
1099
+ subtitles << "Level: #{params['level']}"
1100
+ end
1101
+ logs = json_response['data'] || json_response['logs']
1027
1102
  print_h1 title, subtitles, options
1028
- if logs['data'].empty?
1029
- puts "#{cyan}No logs found.#{reset}"
1103
+ if logs.empty?
1104
+ print "#{cyan}No logs found.#{reset}\n"
1030
1105
  else
1031
- logs['data'].each do |log_entry|
1032
- log_level = ''
1033
- case log_entry['level']
1034
- when 'INFO'
1035
- log_level = "#{blue}#{bold}INFO#{reset}"
1036
- when 'DEBUG'
1037
- log_level = "#{white}#{bold}DEBUG#{reset}"
1038
- when 'WARN'
1039
- log_level = "#{yellow}#{bold}WARN#{reset}"
1040
- when 'ERROR'
1041
- log_level = "#{red}#{bold}ERROR#{reset}"
1042
- when 'FATAL'
1043
- log_level = "#{red}#{bold}FATAL#{reset}"
1044
- end
1045
- puts "[#{log_entry['ts']}] #{log_level} - #{log_entry['message'].to_s.strip}"
1046
- end
1047
- print_results_pagination({'meta'=>{'total'=>json_response['total'],'size'=>json_response['data'].size,'max'=>(json_response['max'] || options[:max]),'offset'=>(json_response['offset'] || options[:offset] || 0)}})
1106
+ print format_log_records(logs, options)
1107
+ print_results_pagination({'meta'=>{'total'=>(json_response['total']['value'] rescue json_response['total']),'size'=>logs.size,'max'=>(json_response['max'] || options[:max]),'offset'=>(json_response['offset'] || options[:offset] || 0)}})
1048
1108
  end
1049
1109
  print reset,"\n"
1050
1110
  return 0
@@ -1384,7 +1444,7 @@ class Morpheus::Cli::Apps
1384
1444
  params = {}
1385
1445
  params['instanceIds'] = instance_ids
1386
1446
  params.merge!(parse_list_options(options))
1387
- # params[:query] = params.delete(:phrase) unless params[:phrase].nil?
1447
+ # params['query'] = params.delete('phrase') if params['phrase']
1388
1448
  @processes_interface.setopts(options)
1389
1449
  if options[:dry_run]
1390
1450
  print_dry_run @processes_interface.dry.list(params)
@@ -5,7 +5,7 @@ class Morpheus::Cli::BackupSettingsCommand
5
5
  include Morpheus::Cli::AccountsHelper
6
6
 
7
7
  set_command_name :'backup-settings'
8
- set_command_hidden
8
+
9
9
  register_subcommands :get, :update
10
10
  set_default_subcommand :get
11
11
 
@@ -33,6 +33,8 @@ class Morpheus::Cli::BlueprintsCommand
33
33
  @instance_types_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).instance_types
34
34
  @options_interface = Morpheus::APIClient.new(@access_token,nil,nil, @appliance_url).options
35
35
  @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
36
+ @clouds_interface = @api_client.clouds
37
+ @library_layouts_interface = @api_client.library_layouts
36
38
  end
37
39
 
38
40
  def handle(args)
@@ -0,0 +1,672 @@
1
+ require 'morpheus/cli/cli_command'
2
+ require 'money' # ew, let's write our own
3
+
4
+ class Morpheus::Cli::BudgetsCommand
5
+ include Morpheus::Cli::CliCommand
6
+ set_command_name :budgets
7
+ register_subcommands :list, :get, :add, :update, :remove
8
+
9
+ def connect(opts)
10
+ @api_client = establish_remote_appliance_connection(opts)
11
+ @budgets_interface = @api_client.budgets
12
+ @options_interface = @api_client.options
13
+ end
14
+
15
+ def handle(args)
16
+ handle_subcommand(args)
17
+ end
18
+
19
+ def list(args)
20
+ options = {}
21
+ params = {}
22
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
23
+ opts.banner = subcommand_usage()
24
+ build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
25
+ opts.footer = "List budgets."
26
+ end
27
+ optparse.parse!(args)
28
+ if args.count != 0
29
+ raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args}\n#{optparse}"
30
+ end
31
+ connect(options)
32
+ begin
33
+ params.merge!(parse_list_options(options))
34
+ @budgets_interface.setopts(options)
35
+ if options[:dry_run]
36
+ print_dry_run @budgets_interface.dry.list(params)
37
+ return 0
38
+ end
39
+ json_response = @budgets_interface.list(params)
40
+ render_result = render_with_format(json_response, options, 'budgets')
41
+ return 0 if render_result
42
+ budgets = json_response['budgets']
43
+ unless options[:quiet]
44
+ title = "Morpheus Budgets"
45
+ subtitles = []
46
+ subtitles += parse_list_subtitles(options)
47
+ print_h1 title, subtitles
48
+ if budgets.empty?
49
+ print yellow,"No budgets found.",reset,"\n"
50
+ else
51
+ columns = [
52
+ {"ID" => lambda {|budget| budget['id'] } },
53
+ {"NAME" => lambda {|budget| budget['name'] } },
54
+ {"DESCRIPTION" => lambda {|budget| truncate_string(budget['description'], 30) } },
55
+ # {"ENABLED" => lambda {|budget| format_boolean(budget['enabled']) } },
56
+ # {"SCOPE" => lambda {|it| format_budget_scope(it) } },
57
+ {"SCOPE" => lambda {|it| it['refName'] } },
58
+ {"PERIOD" => lambda {|it| it['year'] } },
59
+ {"INTERVAL" => lambda {|it| it['interval'].to_s.capitalize } },
60
+ # the UI doesn't consider timezone, so uhh do it this hacky way for now.
61
+ {"START DATE" => lambda {|it|
62
+ if it['timezone'] == 'UTC'
63
+ ((parse_time(it['startDate'], "%Y-%m-%d").strftime("%x")) rescue it['startDate']) # + ' UTC'
64
+ else
65
+ format_local_date(it['startDate'])
66
+ end
67
+ } },
68
+ {"END DATE" => lambda {|it|
69
+ if it['timezone'] == 'UTC'
70
+ ((parse_time(it['endDate'], "%Y-%m-%d").strftime("%x")) rescue it['endDate']) # + ' UTC'
71
+ else
72
+ format_local_date(it['endDate'])
73
+ end
74
+ } },
75
+ {"TOTAL" => lambda {|it| format_money(it['totalCost'], it['currency']) } },
76
+ {"AVERAGE" => lambda {|it| format_money(it['averageCost'], it['currency']) } },
77
+ # {"CREATED BY" => lambda {|budget| budget['createdByName'] ? budget['createdByName'] : budget['createdById'] } },
78
+ # {"CREATED" => lambda {|budget| format_local_dt(budget['dateCreated']) } },
79
+ # {"UPDATED" => lambda {|budget| format_local_dt(budget['lastUpdated']) } },
80
+ ]
81
+ if options[:include_fields]
82
+ columns = options[:include_fields]
83
+ end
84
+ print as_pretty_table(budgets, columns, options)
85
+ print_results_pagination(json_response)
86
+ end
87
+ print reset,"\n"
88
+ end
89
+ return 0
90
+ rescue RestClient::Exception => e
91
+ print_rest_exception(e, options)
92
+ exit 1
93
+ end
94
+ end
95
+
96
+ def get(args)
97
+ options = {}
98
+ params = {}
99
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
100
+ opts.banner = subcommand_usage("[budget]")
101
+ build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :dry_run, :remote])
102
+ opts.footer = "Get details about a budget.\n[budget] is required. Budget ID or name"
103
+ end
104
+ optparse.parse!(args)
105
+
106
+ if args.count != 1
107
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
108
+ end
109
+
110
+ connect(options)
111
+ begin
112
+ @budgets_interface.setopts(options)
113
+ if options[:dry_run]
114
+ if args[0].to_s =~ /\A\d{1,}\Z/
115
+ print_dry_run @budgets_interface.dry.get(args[0], params)
116
+ else
117
+ print_dry_run @budgets_interface.dry.list({name: args[0].to_s})
118
+ end
119
+ return 0
120
+ end
121
+ budget = find_budget_by_name_or_id(args[0])
122
+ return 1 if budget.nil?
123
+ # skip reload if already fetched via get(id)
124
+ json_response = {'budget' => budget}
125
+ if args[0].to_s != budget['id'].to_s
126
+ json_response = @budgets_interface.get(budget['id'], params)
127
+ budget = json_response['budget']
128
+ end
129
+ render_result = render_with_format(json_response, options, 'budget')
130
+ return 0 if render_result
131
+
132
+
133
+ print_h1 "Budget Details"
134
+ print cyan
135
+ budget_columns = {
136
+ "ID" => 'id',
137
+ "Name" => 'name',
138
+ "Description" => 'description',
139
+ "Enabled" => lambda {|budget| format_boolean(budget['enabled']) },
140
+ "Scope" => lambda {|it| format_budget_scope(it) },
141
+ "Period" => lambda {|it| it['year'] },
142
+ "Interval" => lambda {|it| it['interval'].to_s.capitalize },
143
+ # "Costs" => lambda {|it| it['costs'].inspect },
144
+ }
145
+ if budget['interval'] == 'year'
146
+ budget_columns.merge!({
147
+ "Annual" => lambda {|it|
148
+ (it['costs'] && it['costs']['year']) ? format_money(it['costs']['year'], it['currency']) : ''
149
+ },
150
+ })
151
+ elsif budget['interval'] == 'quarter'
152
+ budget_columns.merge!({
153
+ "Q1" => lambda {|it| (it['costs'] && it['costs']['q1']) ? format_money(it['costs']['q1'], it['currency']) : '' },
154
+ "Q2" => lambda {|it| (it['costs'] && it['costs']['q2']) ? format_money(it['costs']['q2'], it['currency']) : '' },
155
+ "Q3" => lambda {|it| (it['costs'] && it['costs']['q3']) ? format_money(it['costs']['q3'], it['currency']) : '' },
156
+ "Q4" => lambda {|it| (it['costs'] && it['costs']['q4']) ? format_money(it['costs']['q4'], it['currency']) : '' },
157
+ })
158
+ elsif budget['interval'] == 'month'
159
+ budget_columns.merge!({
160
+ "January" => lambda {|it| (it['costs'] && it['costs']['january']) ? format_money(it['costs']['january'], it['currency']) : '' },
161
+ "February" => lambda {|it| (it['costs'] && it['costs']['february']) ? format_money(it['costs']['february'], it['currency']) : '' },
162
+ "March" => lambda {|it| (it['costs'] && it['costs']['march']) ? format_money(it['costs']['march'], it['currency']) : '' },
163
+ "April" => lambda {|it| (it['costs'] && it['costs']['april']) ? format_money(it['costs']['april'], it['currency']) : '' },
164
+ "May" => lambda {|it| (it['costs'] && it['costs']['may']) ? format_money(it['costs']['may'], it['currency']) : '' },
165
+ "June" => lambda {|it| (it['costs'] && it['costs']['june']) ? format_money(it['costs']['june'], it['currency']) : '' },
166
+ "July" => lambda {|it| (it['costs'] && it['costs']['july']) ? format_money(it['costs']['july'], it['currency']) : '' },
167
+ "August" => lambda {|it| (it['costs'] && it['costs']['august']) ? format_money(it['costs']['august'], it['currency']) : '' },
168
+ "September" => lambda {|it| (it['costs'] && it['costs']['september']) ? format_money(it['costs']['september'], it['currency']) : '' },
169
+ "October" => lambda {|it| (it['costs'] && it['costs']['october']) ? format_money(it['costs']['october'], it['currency']) : '' },
170
+ "November" => lambda {|it| (it['costs'] && it['costs']['november']) ? format_money(it['costs']['nov'], it['currency']) : '' },
171
+ "December" => lambda {|it| (it['costs'] && it['costs']['december']) ? format_money(it['costs']['december'], it['currency']) : '' }
172
+ })
173
+ else
174
+ budget_columns.merge!({
175
+ "Costs" => lambda {|it|
176
+ if it['costs'].is_a?(Array)
177
+ it['costs'] ? it['costs'].join(', ') : ''
178
+ elsif it['costs'].is_a?(Hash)
179
+ it['costs'].to_s
180
+ else
181
+ it['costs'].to_s
182
+ end
183
+ },
184
+ })
185
+ end
186
+ budget_columns.merge!({
187
+ "Total" => lambda {|it| format_money(it['totalCost'], it['currency']) },
188
+ "Average" => lambda {|it| format_money(it['averageCost'], it['currency']) },
189
+ # the UI doesn't consider timezone, so uhh do it this hacky way for now.
190
+ "Start Date" => lambda {|it|
191
+ if it['timezone'] == 'UTC'
192
+ ((parse_time(it['startDate'], "%Y-%m-%d").strftime("%x")) rescue it['startDate']) # + ' UTC'
193
+ else
194
+ format_local_date(it['startDate'])
195
+ end
196
+ },
197
+ "End Date" => lambda {|it|
198
+ if it['timezone'] == 'UTC'
199
+ ((parse_time(it['endDate'], "%Y-%m-%d").strftime("%x")) rescue it['endDate']) # + ' UTC'
200
+ else
201
+ format_local_date(it['endDate'])
202
+ end
203
+ },
204
+ "Timezone" => lambda {|it| it['timezone'] },
205
+ })
206
+ budget_columns.merge!({
207
+ "Created By" => lambda {|it| it['createdByName'] ? it['createdByName'] : it['createdById'] },
208
+ "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
209
+ "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
210
+ })
211
+ print_description_list(budget_columns, budget, options)
212
+ # print reset,"\n"
213
+
214
+ # a chart of Budget cost vs Actual cost for each interval in the period.
215
+ print_h2 "Budget Summary", options
216
+ if budget['stats'] && budget['stats']['intervals']
217
+ begin
218
+ budget_summary_columns = {
219
+ # "Cost" => lambda {|it| it[:label] },
220
+ " " => lambda {|it| it[:label] },
221
+ }
222
+ budget_row = {label:"Budget"}
223
+ actual_row = {label:"Actual"}
224
+ # budget['stats']['intervals'].each do |stat_interval|
225
+ # interval_key = (stat_interval["shortName"] || stat_interval["shortYear"]).to_s.upcase
226
+ # if interval_key == "Y1" && budget['year']
227
+ # interval_key = "Year #{budget['year']}"
228
+ # end
229
+ # budget_summary_columns[interval_key] = lambda {|it|
230
+ # display_val = format_money(it[interval_key], budget['stats']['currency'])
231
+ # over_budget = actual_row[interval_key] && (actual_row[interval_key] > (budget_row[interval_key] || 0))
232
+ # if over_budget
233
+ # "#{red}#{display_val}#{cyan}"
234
+ # else
235
+ # "#{cyan}#{display_val}#{cyan}"
236
+ # end
237
+ # }
238
+ # budget_row[interval_key] = stat_interval["budget"].to_f
239
+ # actual_row[interval_key] = stat_interval["cost"].to_f
240
+ # end
241
+ budget['stats']['intervals'].each do |stat_interval|
242
+ currency = budget['currency'] || budget['stats']['currency']
243
+ interval_key = (stat_interval["shortName"] || stat_interval["shortYear"]).to_s.upcase
244
+ if interval_key == "Y1" && budget['year']
245
+ interval_key = "Year #{budget['year']}"
246
+ end
247
+ # add simple column definition, just use the key
248
+ budget_summary_columns[interval_key] = interval_key
249
+ budget_cost = stat_interval["budget"].to_f
250
+ actual_cost = stat_interval["cost"].to_f
251
+ over_budget = actual_cost > 0 && actual_cost > budget_cost
252
+ if over_budget
253
+ budget_row[interval_key] = "#{cyan}#{format_money(budget_cost, currency)}#{cyan}"
254
+ actual_row[interval_key] = "#{red}#{format_money(actual_cost, currency)}#{cyan}"
255
+ else
256
+ budget_row[interval_key] = "#{cyan}#{format_money(budget_cost, currency)}#{cyan}"
257
+ actual_row[interval_key] = "#{cyan}#{format_money(actual_cost, currency)}#{cyan}"
258
+ end
259
+ end
260
+ chart_data = [budget_row, actual_row]
261
+ print as_pretty_table(chart_data, budget_summary_columns, options)
262
+ print reset,"\n"
263
+ rescue => ex
264
+ print red,"Failed to render budget summary.",reset,"\n"
265
+ end
266
+ else
267
+ print yellow,"No budget stat data found.",reset,"\n"
268
+ end
269
+ return 0
270
+ rescue RestClient::Exception => e
271
+ print_rest_exception(e, options)
272
+ exit 1
273
+ end
274
+ end
275
+
276
+ def add(args)
277
+ options = {}
278
+ params = {}
279
+ costs = {}
280
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
281
+ opts.banner = subcommand_usage("[name] [options]")
282
+ build_option_type_options(opts, options, add_budget_option_types)
283
+ opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
284
+ costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
285
+ end
286
+ [:q1,:q2,:q3,:q4,
287
+ ].each do |quarter|
288
+ opts.on("--#{quarter.to_s} [amount]", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
289
+ costs[quarter.to_s] = parse_cost_amount(val)
290
+ end
291
+ end
292
+ [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december
293
+ ].each do |month|
294
+ opts.on("--#{month.to_s} [amount]", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
295
+ costs[month.to_s] = parse_cost_amount(val)
296
+ end
297
+ end
298
+ opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
299
+ params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
300
+ end
301
+ build_common_options(opts, options, [:payload, :options, :json, :dry_run, :remote])
302
+ opts.footer = "Create budget."
303
+ end
304
+ optparse.parse!(args)
305
+ if args.count > 1
306
+ raise_command_error "wrong number of arguments, expected 0-1 and got (#{args.count}) #{args}\n#{optparse}"
307
+ end
308
+ if args[0]
309
+ options[:options]['name'] ||= args[0]
310
+ end
311
+ connect(options)
312
+ begin
313
+ # construct payload
314
+ passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
315
+ payload = nil
316
+ if options[:payload]
317
+ payload = options[:payload]
318
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
319
+ else
320
+ payload = {
321
+ 'budget' => {
322
+ }
323
+ }
324
+ # allow arbitrary -O options
325
+ passed_options.delete('costs')
326
+ passed_options.delete('tenant')
327
+ passed_options.delete('group')
328
+ passed_options.delete('cloud')
329
+ passed_options.delete('user')
330
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
331
+ # prompt for options
332
+ if !costs.empty?
333
+ options[:options]['costs'] ||= {}
334
+ options[:options]['costs'].deep_merge!(costs)
335
+ end
336
+ options[:options]['interval'] = options[:options]['interval'].to_s.downcase if options[:options]['interval']
337
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(add_budget_option_types, options[:options], @api_client)
338
+ params.deep_merge!(v_prompt)
339
+ params['costs'] = prompt_costs(params, options)
340
+ # budgets api expects scope prefixed parameters like this
341
+ if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
342
+ params['scopeTenantId'] = params.delete('tenant')
343
+ end
344
+ if params['group'].is_a?(String) || params['group'].is_a?(Numeric)
345
+ params['scopeGroupId'] = params.delete('group')
346
+ end
347
+ if params['cloud'].is_a?(String) || params['cloud'].is_a?(Numeric)
348
+ params['scopeCloudId'] = params.delete('cloud')
349
+ end
350
+ if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
351
+ params['scopeUserId'] = params.delete('user')
352
+ end
353
+ payload.deep_merge!({'budget' => params}) unless params.empty?
354
+ end
355
+
356
+ @budgets_interface.setopts(options)
357
+ if options[:dry_run]
358
+ print_dry_run @budgets_interface.dry.create(payload)
359
+ return
360
+ end
361
+ json_response = @budgets_interface.create(payload)
362
+ if options[:json]
363
+ print JSON.pretty_generate(json_response)
364
+ print "\n"
365
+ else
366
+ display_name = json_response['budget'] ? json_response['budget']['name'] : ''
367
+ print_green_success "Budget #{display_name} added"
368
+ get([json_response['budget']['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
369
+ end
370
+ return 0
371
+ rescue RestClient::Exception => e
372
+ print_rest_exception(e, options)
373
+ exit 1
374
+ end
375
+ end
376
+
377
+ def update(args)
378
+ options = {}
379
+ params = {}
380
+ costs = {}
381
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
382
+ opts.banner = subcommand_usage("[budget] [options]")
383
+ build_option_type_options(opts, options, update_budget_option_types)
384
+ opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
385
+ costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
386
+ end
387
+ [:q1,:q2,:q3,:q4,
388
+ ].each do |quarter|
389
+ opts.on("--#{quarter.to_s} [amount]", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
390
+ costs[quarter.to_s] = parse_cost_amount(val)
391
+ end
392
+ end
393
+ [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december
394
+ ].each do |month|
395
+ opts.on("--#{month.to_s} [amount]", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
396
+ costs[month.to_s] = parse_cost_amount(val)
397
+ end
398
+ end
399
+ opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
400
+ params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
401
+ end
402
+ build_common_options(opts, options, [:payload, :options, :json, :dry_run, :remote])
403
+ opts.footer = "Update budget.\n[budget] is required. Budget ID or name"
404
+ end
405
+ optparse.parse!(args)
406
+
407
+ if args.count != 1
408
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
409
+ end
410
+
411
+ connect(options)
412
+ begin
413
+
414
+ budget = find_budget_by_name_or_id(args[0])
415
+ return 1 if budget.nil?
416
+
417
+ # construct payload
418
+ passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
419
+ payload = nil
420
+ if options[:payload]
421
+ payload = options[:payload]
422
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
423
+ else
424
+ payload = {
425
+ 'budget' => {
426
+ }
427
+ }
428
+ # allow arbitrary -O options
429
+ passed_options.delete('costs')
430
+ passed_options.delete('tenant')
431
+ passed_options.delete('group')
432
+ passed_options.delete('cloud')
433
+ passed_options.delete('user')
434
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
435
+ # prompt for options
436
+ #params = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options], @api_client, options[:params])
437
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options].merge(:no_prompt => true), @api_client)
438
+ params.deep_merge!(v_prompt)
439
+ # v_costs = prompt_costs({'interval' => budget['interval']}.merge(params), options.merge(:no_prompt => true))
440
+ # if v_costs && !v_costs.empty?
441
+ # params['costs'] = v_costs
442
+ # end
443
+ if !costs.empty?
444
+ params['costs'] = costs
445
+ end
446
+ # budgets api expects scope prefixed parameters like this
447
+ if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
448
+ params['scopeTenantId'] = params.delete('tenant')
449
+ end
450
+ if params['group'].is_a?(String) || params['group'].is_a?(Numeric)
451
+ params['scopeGroupId'] = params.delete('group')
452
+ end
453
+ if params['cloud'].is_a?(String) || params['cloud'].is_a?(Numeric)
454
+ params['scopeCloudId'] = params.delete('cloud')
455
+ end
456
+ if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
457
+ params['scopeUserId'] = params.delete('user')
458
+ end
459
+ payload.deep_merge!({'budget' => params}) unless params.empty?
460
+
461
+ if payload.empty? || payload['budget'].empty?
462
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
463
+ end
464
+ end
465
+ @budgets_interface.setopts(options)
466
+ if options[:dry_run]
467
+ print_dry_run @budgets_interface.dry.update(budget['id'], payload)
468
+ return
469
+ end
470
+ json_response = @budgets_interface.update(budget['id'], payload)
471
+ if options[:json]
472
+ print JSON.pretty_generate(json_response)
473
+ print "\n"
474
+ else
475
+ display_name = json_response['budget'] ? json_response['budget']['name'] : ''
476
+ print_green_success "Budget #{display_name} updated"
477
+ get([json_response['budget']['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
478
+ end
479
+ return 0
480
+ rescue RestClient::Exception => e
481
+ print_rest_exception(e, options)
482
+ exit 1
483
+ end
484
+ end
485
+
486
+ def remove(args)
487
+ options = {}
488
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
489
+ opts.banner = subcommand_usage("[name]")
490
+ build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
491
+ opts.footer = "Delete budget.\n[budget] is required. Budget ID or name"
492
+ end
493
+ optparse.parse!(args)
494
+
495
+ if args.count != 1
496
+ raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
497
+ end
498
+
499
+ connect(options)
500
+ begin
501
+ budget = find_budget_by_name_or_id(args[0])
502
+ return 1 if budget.nil?
503
+
504
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the budget #{budget['name']}?")
505
+ return 9, "aborted command"
506
+ end
507
+ @budgets_interface.setopts(options)
508
+ if options[:dry_run]
509
+ print_dry_run @budgets_interface.dry.destroy(budget['id'])
510
+ return
511
+ end
512
+ json_response = @budgets_interface.destroy(budget['id'])
513
+ if options[:json]
514
+ print JSON.pretty_generate(json_response)
515
+ print "\n"
516
+ else
517
+ print_green_success "Budget #{budget['name']} removed"
518
+ # list([] + (options[:remote] ? ["-r",options[:remote]] : []))
519
+ end
520
+ return 0
521
+ rescue RestClient::Exception => e
522
+ print_rest_exception(e, options)
523
+ exit 1
524
+ end
525
+ end
526
+
527
+ private
528
+
529
+ def find_budget_by_name_or_id(val)
530
+ if val.to_s =~ /\A\d{1,}\Z/
531
+ return find_budget_by_id(val)
532
+ else
533
+ return find_budget_by_name(val)
534
+ end
535
+ end
536
+
537
+ def find_budget_by_id(id)
538
+ raise "#{self.class} has not defined @budgets_interface" if @budgets_interface.nil?
539
+ begin
540
+ json_response = @budgets_interface.get(id)
541
+ return json_response['budget']
542
+ rescue RestClient::Exception => e
543
+ if e.response && e.response.code == 404
544
+ print_red_alert "Budget not found by id #{id}"
545
+ else
546
+ raise e
547
+ end
548
+ end
549
+ end
550
+
551
+ def find_budget_by_name(name)
552
+ raise "#{self.class} has not defined @budgets_interface" if @budgets_interface.nil?
553
+ budgets = @budgets_interface.list({name: name.to_s})['budgets']
554
+ if budgets.empty?
555
+ print_red_alert "Budget not found by name #{name}"
556
+ return nil
557
+ elsif budgets.size > 1
558
+ print_red_alert "#{budgets.size} Budgets found by name #{name}"
559
+ print as_pretty_table(budgets, [:id,:name], {color:red})
560
+ print reset,"\n"
561
+ return nil
562
+ else
563
+ return budgets[0]
564
+ end
565
+ end
566
+
567
+ def add_budget_option_types
568
+ [
569
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
570
+ {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'displayOrder' => 1},
571
+ # {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'defaultValue' => true},
572
+ {'fieldName' => 'scope', 'fieldLabel' => 'Scope', 'code' => 'budget.scope', 'type' => 'select', 'selectOptions' => [{'name'=>'Account','value'=>'account'},{'name'=>'Tenant','value'=>'tenant'},{'name'=>'Cloud','value'=>'cloud'},{'name'=>'Group','value'=>'group'},{'name'=>'User','value'=>'user'}], 'defaultValue' => 'account', 'required' => true, 'displayOrder' => 3},
573
+ {'fieldName' => 'tenant', 'fieldLabel' => 'Tenant', 'type' => 'select', 'optionSource' => lambda {
574
+ @options_interface.options_for_source("tenants", {})['data']
575
+ }, 'required' => true, 'dependsOnCode' => 'budget.scope:tenant', 'displayOrder' => 4},
576
+ {'fieldName' => 'user', 'fieldLabel' => 'User', 'type' => 'select', 'optionSource' => lambda {
577
+ @options_interface.options_for_source("users", {})['data']
578
+ }, 'required' => true, 'dependsOnCode' => 'budget.scope:user', 'displayOrder' => 5},
579
+ {'fieldName' => 'group', 'fieldLabel' => 'Group', 'type' => 'select', 'optionSource' => lambda {
580
+ @options_interface.options_for_source("groups", {})['data']
581
+ }, 'required' => true, 'dependsOnCode' => 'budget.scope:group', 'displayOrder' => 6},
582
+ {'fieldName' => 'cloud', 'fieldLabel' => 'Cloud', 'type' => 'select', 'optionSource' => lambda {
583
+ @options_interface.options_for_source("clouds", {})['data']
584
+ }, 'required' => true, 'dependsOnCode' => 'budget.scope:cloud', 'displayOrder' => 7},
585
+ {'fieldName' => 'year', 'fieldLabel' => 'Period', 'type' => 'text', 'required' => true, 'defaultValue' => Time.now.year, 'description' => "The period (year) the budget applies to. Default is the current year.", 'displayOrder' => 8},
586
+ {'fieldName' => 'interval', 'fieldLabel' => 'Interval', 'type' => 'select', 'selectOptions' => [{'name'=>'Year','value'=>'year'},{'name'=>'Quarter','value'=>'quarter'},{'name'=>'Month','value'=>'month'}], 'defaultValue' => 'year', 'required' => true, 'displayOrder' => 9}
587
+ ]
588
+ end
589
+
590
+ def update_budget_option_types
591
+ list = add_budget_option_types()
592
+ # list = list.reject {|it| ["interval"].include? it['fieldName'] }
593
+ list.each {|it| it.delete('required') }
594
+ list.each {|it| it.delete('defaultValue') }
595
+ list
596
+ end
597
+
598
+ def prompt_costs(params={}, options={})
599
+ interval = params['interval'] #.to_s.downcase
600
+ options[:options]||={}
601
+ costs = {}
602
+ costs_val = nil
603
+ #costs_val = params['costs'] ? params['costs'] : options[:options]['costs']
604
+ if costs_val.is_a?(Array)
605
+ costs = costs_val
606
+ elsif costs_val.is_a?(String)
607
+ costs = costs_val.to_s.split(',').collect {|it| it.to_s.strip.to_f }
608
+ else
609
+ if interval == 'year'
610
+ cost_option_types = [
611
+ {'fieldContext' => 'costs', 'fieldName' => 'year', 'fieldLabel' => 'Annual Cost', 'type' => 'text', 'defaultValue' => 0}
612
+ ]
613
+ values = Morpheus::Cli::OptionTypes.prompt(cost_option_types, options[:options], @api_client)
614
+ costs = values['costs'] ? values['costs'] : {}
615
+ # costs = {
616
+ # year: values['cost']
617
+ # }
618
+ elsif interval == 'quarter'
619
+ cost_option_types = [
620
+ {'fieldContext' => 'costs', 'fieldName' => 'q1', 'fieldLabel' => 'Q1', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 1},
621
+ {'fieldContext' => 'costs', 'fieldName' => 'q2', 'fieldLabel' => 'Q2', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 2},
622
+ {'fieldContext' => 'costs', 'fieldName' => 'q3', 'fieldLabel' => 'Q3', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 3},
623
+ {'fieldContext' => 'costs', 'fieldName' => 'q4', 'fieldLabel' => 'Q4', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 4}
624
+ ]
625
+ values = Morpheus::Cli::OptionTypes.prompt(cost_option_types, options[:options], @api_client)
626
+ costs = values['costs'] ? values['costs'] : {}
627
+ # costs = {
628
+ # q1: values['q1'], q2: values['q2'], q3: values['q3'], q4: values['q4']
629
+ # }
630
+ elsif interval == 'month'
631
+ cost_option_types = [
632
+ {'fieldContext' => 'costs', 'fieldName' => 'january', 'fieldLabel' => 'January', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 1},
633
+ {'fieldContext' => 'costs', 'fieldName' => 'february', 'fieldLabel' => 'February', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 2},
634
+ {'fieldContext' => 'costs', 'fieldName' => 'march', 'fieldLabel' => 'March', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 3},
635
+ {'fieldContext' => 'costs', 'fieldName' => 'april', 'fieldLabel' => 'April', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 4},
636
+ {'fieldContext' => 'costs', 'fieldName' => 'may', 'fieldLabel' => 'May', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 5},
637
+ {'fieldContext' => 'costs', 'fieldName' => 'june', 'fieldLabel' => 'June', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 6},
638
+ {'fieldContext' => 'costs', 'fieldName' => 'july', 'fieldLabel' => 'July', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 7},
639
+ {'fieldContext' => 'costs', 'fieldName' => 'august', 'fieldLabel' => 'August', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 8},
640
+ {'fieldContext' => 'costs', 'fieldName' => 'september', 'fieldLabel' => 'September', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 9},
641
+ {'fieldContext' => 'costs', 'fieldName' => 'october', 'fieldLabel' => 'October', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 10},
642
+ {'fieldContext' => 'costs', 'fieldName' => 'november', 'fieldLabel' => 'November', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 11},
643
+ {'fieldContext' => 'costs', 'fieldName' => 'december', 'fieldLabel' => 'December', 'type' => 'text', 'defaultValue' => 0, 'displayOrder' => 12},
644
+ ]
645
+ values = Morpheus::Cli::OptionTypes.prompt(cost_option_types, options[:options], @api_client)
646
+ costs = values['costs'] ? values['costs'] : {}
647
+ # costs = {
648
+ # january: values['january'], february: values['february'], march: values['march'],
649
+ # april: values['april'], may: values['may'], june: values['june'],
650
+ # july: values['july'], august: values['august'], september: values['september'],
651
+ # october: values['october'], november: values['november'], december: values['december']
652
+ # }
653
+ end
654
+ end
655
+ return costs
656
+ end
657
+
658
+ def format_budget_scope(budget)
659
+ if budget['refScope'] && budget['refName']
660
+ "(#{budget['refScope']}) #{budget['refName']}"
661
+ elsif budget['refType']
662
+ budget['refType'] ? "#{budget['refType']} (#{budget['refId']}) #{budget['refName']}".strip : budget['refName'].to_s
663
+ else
664
+ ""
665
+ end
666
+ end
667
+
668
+ def parse_cost_amount(val)
669
+ val.to_s.gsub(",","").to_f
670
+ end
671
+
672
+ end