morpheus-cli 5.0.2 → 5.2.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Dockerfile +1 -1
  4. data/lib/morpheus/api/api_client.rb +4 -0
  5. data/lib/morpheus/api/instances_interface.rb +30 -2
  6. data/lib/morpheus/api/invoices_interface.rb +12 -3
  7. data/lib/morpheus/api/servers_interface.rb +7 -0
  8. data/lib/morpheus/api/service_catalog_interface.rb +89 -0
  9. data/lib/morpheus/cli.rb +2 -1
  10. data/lib/morpheus/cli/apps.rb +3 -23
  11. data/lib/morpheus/cli/budgets_command.rb +389 -319
  12. data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
  13. data/lib/morpheus/cli/cli_command.rb +25 -1
  14. data/lib/morpheus/cli/commands/standard/curl_command.rb +25 -10
  15. data/lib/morpheus/cli/commands/standard/history_command.rb +6 -2
  16. data/lib/morpheus/cli/containers_command.rb +0 -24
  17. data/lib/morpheus/cli/cypher_command.rb +6 -2
  18. data/lib/morpheus/cli/dashboard_command.rb +260 -20
  19. data/lib/morpheus/cli/health_command.rb +57 -0
  20. data/lib/morpheus/cli/hosts.rb +128 -11
  21. data/lib/morpheus/cli/instances.rb +270 -108
  22. data/lib/morpheus/cli/invoices_command.rb +67 -4
  23. data/lib/morpheus/cli/jobs_command.rb +94 -92
  24. data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
  25. data/lib/morpheus/cli/library_option_types_command.rb +10 -5
  26. data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
  27. data/lib/morpheus/cli/mixins/print_helper.rb +13 -6
  28. data/lib/morpheus/cli/mixins/provisioning_helper.rb +88 -5
  29. data/lib/morpheus/cli/option_types.rb +10 -10
  30. data/lib/morpheus/cli/projects_command.rb +1 -1
  31. data/lib/morpheus/cli/roles.rb +193 -155
  32. data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
  33. data/lib/morpheus/cli/tasks.rb +9 -11
  34. data/lib/morpheus/cli/version.rb +1 -1
  35. data/lib/morpheus/cli/virtual_images.rb +162 -68
  36. data/lib/morpheus/formatters.rb +55 -20
  37. metadata +5 -4
  38. data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25ca8fc7cabb51b4042eb212172b60191d6863cf9310ffce4e386d016692f7f1
4
- data.tar.gz: d1183424bef986bb22123d60b45e97ed077c8ea30c5bfd249f8bf503c0a22d13
3
+ metadata.gz: 3798eb711a94541135bbefd28bbf1b33db18460ab70d7caf4d6f1cde90dafed3
4
+ data.tar.gz: b24f15db530f004a00899a86d8c609ee5dc44e71ee7b69cc821e33f91070b4f1
5
5
  SHA512:
6
- metadata.gz: d006acc9447822675c3593de443679ac2eee514bf1ffe041ddfa5ddc44223e8edfbbeee9393056c59fb98c14d4beb240b0ce9725a3bcc56eb5e1095066d15d1c
7
- data.tar.gz: ca41986fc3561f562b799971514b92dfe08027e06ad5443b0e116713fee9132d62b84905c929209be03a7dc0f3855e97adf7fe053e64832067addca692ef4268
6
+ metadata.gz: 165402498f461633ca18f4fd273e27cc5dcf7fddfd219722912563ea1c318a4412b9f6c6f5a014382e14bac1bfa060ffb4a794b72182739e57c1299ce685e7d2
7
+ data.tar.gz: 2305c2c67c28e206c9507abda3a95b2e62dadb0a6e73e44fd4b79a4feaa40f40fe713e47619f0383a30dfca22828ff439446063e5e7417746b658b08ced63804
data/.gitignore CHANGED
@@ -21,3 +21,4 @@ tmp
21
21
  *.a
22
22
  mkmf.log
23
23
  .DS_Store
24
+ .idea
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.5.1
2
2
 
3
- RUN gem install morpheus-cli -v 5.0.2
3
+ RUN gem install morpheus-cli -v 5.2.4
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -776,6 +776,10 @@ class Morpheus::APIClient
776
776
  Morpheus::CatalogItemTypesInterface.new(common_interface_options).setopts(@options)
777
777
  end
778
778
 
779
+ def catalog
780
+ Morpheus::ServiceCatalogInterface.new(common_interface_options).setopts(@options)
781
+ end
782
+
779
783
  def usage
780
784
  Morpheus::UsageInterface.new(common_interface_options).setopts(@options)
781
785
  end
@@ -169,10 +169,10 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
169
169
  execute(opts)
170
170
  end
171
171
 
172
- def backup(id,server=true)
172
+ def backup(id, payload={})
173
173
  url = "#{@base_url}/api/instances/#{id}/backup"
174
174
  headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
175
- opts = {method: :put, url: url, headers: headers}
175
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
176
176
  execute(opts)
177
177
  end
178
178
 
@@ -190,6 +190,27 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
190
190
  execute(opts)
191
191
  end
192
192
 
193
+ def clone_image(id, payload)
194
+ url = "#{@base_url}/api/instances/#{id}/clone-image"
195
+ headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
196
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
197
+ execute(opts)
198
+ end
199
+
200
+ def lock(id, payload)
201
+ url = "#{@base_url}/api/instances/#{id}/lock"
202
+ headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
203
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
204
+ execute(opts)
205
+ end
206
+
207
+ def unlock(id, payload)
208
+ url = "#{@base_url}/api/instances/#{id}/unlock"
209
+ headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
210
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
211
+ execute(opts)
212
+ end
213
+
193
214
  def firewall_disable(id)
194
215
  url = "#{@base_url}/api/instances/#{id}/security-groups/disable"
195
216
  headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
@@ -218,6 +239,13 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
218
239
  execute(opts)
219
240
  end
220
241
 
242
+ def snapshot(id, payload={})
243
+ url = "#{@base_url}/api/instances/#{id}/snapshot"
244
+ headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
245
+ opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
246
+ execute(opts)
247
+ end
248
+
221
249
  def snapshots(instance_id, params={})
222
250
  url = "#{@base_url}/api/instances/#{instance_id}/snapshots"
223
251
  headers = { params: params, authorization: "Bearer #{@access_token}" }
@@ -2,18 +2,27 @@ require 'morpheus/api/api_client'
2
2
 
3
3
  class Morpheus::InvoicesInterface < Morpheus::APIClient
4
4
 
5
+ def base_path
6
+ "/api/invoices"
7
+ end
8
+
5
9
  def list(params={})
6
- execute(method: :get, url: "/api/invoices", headers: {params: params})
10
+ execute(method: :get, url: "#{base_path}", headers: {params: params})
7
11
  end
8
12
 
9
13
  def get(id, params={})
10
14
  raise "#{self.class}.get() passed a blank id!" if id.to_s == ''
11
- execute(method: :get, url: "/api/invoices/#{id}", headers: {params: params})
15
+ execute(method: :get, url: "#{base_path}/#{id}", headers: {params: params})
16
+ end
17
+
18
+ def update(id, payload)
19
+ validate_id!(id)
20
+ execute(url: "#{base_path}/#{id}", payload: payload.to_json, method: :put)
12
21
  end
13
22
 
14
23
  def refresh(params={}, payload={})
15
24
  headers = {:params => params, 'Content-Type' => 'application/json'}
16
- execute(method: :post, url: "/api/invoices/refresh", headers: headers, payload: payload.to_json)
25
+ execute(method: :post, url: "#{base_path}/refresh", headers: headers, payload: payload.to_json)
17
26
  end
18
27
 
19
28
  end
@@ -176,4 +176,11 @@ class Morpheus::ServersInterface < Morpheus::APIClient
176
176
  execute(opts)
177
177
  end
178
178
 
179
+ def software(id, params={})
180
+ url = "#{@base_url}/api/servers/#{id}/software"
181
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
182
+ opts = {method: :get, url: url, headers: headers}
183
+ execute(opts)
184
+ end
185
+
179
186
  end
@@ -0,0 +1,89 @@
1
+ require 'morpheus/api/api_client'
2
+ # Service Catalog Persona interface
3
+ class Morpheus::ServiceCatalogInterface < Morpheus::APIClient
4
+
5
+ def base_path
6
+ # "/api/service-catalog"
7
+ "/api/catalog"
8
+ end
9
+
10
+ # dashboard
11
+ def dashboard(params={})
12
+ execute(method: :get, url: "#{base_path}/dashboard", params: params)
13
+ end
14
+
15
+ # list catalog types available for ordering
16
+ def list_types(params={})
17
+ execute(method: :get, url: "#{base_path}/types", params: params)
18
+ end
19
+
20
+ # get specific catalog type
21
+ def get_type(id, params={})
22
+ validate_id!(id)
23
+ execute(method: :get, url: "#{base_path}/types/#{id}", params: params)
24
+ end
25
+
26
+ # list catalog inventory (items)
27
+ def list_inventory(params={})
28
+ execute(method: :get, url: "#{base_path}/items", params: params)
29
+ end
30
+
31
+ # get catalog inventory item
32
+ def get_inventory(id, params={})
33
+ validate_id!(id)
34
+ execute(method: :get, url: "#{base_path}/items/#{id}", params: params)
35
+ end
36
+
37
+ # delete a catalog inventory item
38
+ def destroy_inventory(id, params = {})
39
+ validate_id!(id)
40
+ execute(method: :delete, url: "#{base_path}/items/#{id}", params: params)
41
+ end
42
+
43
+ # get cart (one per user)
44
+ def get_cart(params={})
45
+ execute(method: :get, url: "#{base_path}/cart", params: params)
46
+ end
47
+
48
+ # update cart (set cart name)
49
+ def update_cart(payload, params={})
50
+ execute(method: :put, url: "#{base_path}/cart", params: params, payload: payload.to_json)
51
+ end
52
+
53
+ # validate a new item, can be used before before adding it
54
+ def validate_cart_item(payload, params={})
55
+ execute(method: :post, url: "#{base_path}/cart/items/validate", params: params, payload: payload.to_json)
56
+ end
57
+
58
+ # add item to cart
59
+ def create_cart_item(payload, params={})
60
+ execute(method: :post, url: "#{base_path}/cart/items", params: params, payload: payload.to_json)
61
+ end
62
+
63
+ # update item in the cart
64
+ def update_cart_item(id, payload, params={})
65
+ validate_id!(id)
66
+ execute(method: :put, url: "#{base_path}/cart/items/#{id}", params: params, payload: payload.to_json)
67
+ end
68
+
69
+ # remove item from the cart
70
+ def destroy_cart_item(id, params={})
71
+ validate_id!(id)
72
+ execute(method: :delete, url: "#{base_path}/cart/items/#{id}", params: params)
73
+ end
74
+
75
+ # place order with cart
76
+ def checkout(payload, params={})
77
+ execute(method: :post, url: "#{base_path}/checkout", params: params, payload: payload.to_json)
78
+ end
79
+
80
+ # remove all items from cart and reset name
81
+ def clear_cart(params={})
82
+ execute(method: :delete, url: "#{base_path}/cart", params: params)
83
+ end
84
+
85
+ # create an order from scratch, without using a cart
86
+ def create_order(payload, params={})
87
+ execute(method: :post, url: "#{base_path}/orders", params: params, payload: payload.to_json)
88
+ end
89
+ end
@@ -175,7 +175,8 @@ module Morpheus
175
175
  load 'morpheus/cli/projects_command.rb'
176
176
  load 'morpheus/cli/backups_command.rb'
177
177
  load 'morpheus/cli/backup_jobs_command.rb'
178
- load 'morpheus/cli/catalog_command.rb'
178
+ load 'morpheus/cli/catalog_item_types_command.rb' # catalog-types
179
+ load 'morpheus/cli/service_catalog_command.rb' # catalog
179
180
  load 'morpheus/cli/usage_command.rb'
180
181
  # add new commands here...
181
182
 
@@ -72,9 +72,6 @@ class Morpheus::Cli::Apps
72
72
  options[:owner] = val
73
73
  end
74
74
  opts.add_hidden_option('--created-by')
75
- opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
76
- options[:details] = true
77
- end
78
75
  opts.on('--pending-removal', "Include apps pending removal.") do
79
76
  options[:showDeleted] = true
80
77
  end
@@ -88,6 +85,9 @@ class Morpheus::Cli::Apps
88
85
  opts.on('--status STATUS', "Filter by status.") do |val|
89
86
  params['status'] = (params['status'] || []) + val.to_s.split(',').collect {|s| s.strip }.select {|s| s != "" }
90
87
  end
88
+ opts.on('-a', '--details', "Display all details: memory and storage usage used / max values." ) do
89
+ options[:details] = true
90
+ end
91
91
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
92
92
  opts.footer = "List apps."
93
93
  end
@@ -2185,26 +2185,6 @@ EOT
2185
2185
  print reset
2186
2186
  end
2187
2187
 
2188
- def format_app_status(app, return_color=cyan)
2189
- out = ""
2190
- status_string = app['status'] || app['appStatus'] || ''
2191
- if status_string == 'running'
2192
- out = "#{green}#{status_string.upcase}#{return_color}"
2193
- elsif status_string == 'provisioning'
2194
- out = "#{cyan}#{status_string.upcase}#{cyan}"
2195
- elsif status_string == 'stopped' or status_string == 'failed'
2196
- out = "#{red}#{status_string.upcase}#{return_color}"
2197
- elsif status_string == 'unknown'
2198
- out = "#{yellow}#{status_string.upcase}#{return_color}"
2199
- elsif status_string == 'warning' && app['instanceCount'].to_i == 0
2200
- # show this instead of WARNING
2201
- out = "#{cyan}EMPTY#{return_color}"
2202
- else
2203
- out = "#{yellow}#{status_string.upcase}#{return_color}"
2204
- end
2205
- out
2206
- end
2207
-
2208
2188
  def format_app_tiers(app)
2209
2189
  out = ""
2210
2190
  begin
@@ -1,5 +1,6 @@
1
1
  require 'morpheus/cli/cli_command'
2
2
  require 'money' # ew, let's write our own
3
+ require 'time'
3
4
 
4
5
  class Morpheus::Cli::BudgetsCommand
5
6
  include Morpheus::Cli::CliCommand
@@ -21,76 +22,72 @@ class Morpheus::Cli::BudgetsCommand
21
22
  params = {}
22
23
  optparse = Morpheus::Cli::OptionParser.new do |opts|
23
24
  opts.banner = subcommand_usage()
24
- build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
25
+ build_standard_list_options(opts, options)
25
26
  opts.footer = "List budgets."
26
27
  end
27
28
  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}"
29
+ # verify_args!(args:args, optparse:optparse, count:0)
30
+ if args.count > 0
31
+ options[:phrase] = args.join(" ")
30
32
  end
33
+ params.merge!(parse_list_options(options))
31
34
  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 cyan,"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)
35
+
36
+ params.merge!(parse_list_options(options))
37
+ @budgets_interface.setopts(options)
38
+ if options[:dry_run]
39
+ print_dry_run @budgets_interface.dry.list(params)
40
+ return 0
41
+ end
42
+ json_response = @budgets_interface.list(params)
43
+ budgets = json_response['budgets']
44
+ render_response(json_response, options, 'budgets') do
45
+ title = "Morpheus Budgets"
46
+ subtitles = []
47
+ subtitles += parse_list_subtitles(options)
48
+ print_h1 title, subtitles
49
+ if budgets.empty?
50
+ print cyan,"No budgets found.",reset,"\n"
51
+ else
52
+ columns = [
53
+ {"ID" => lambda {|budget| budget['id'] } },
54
+ {"NAME" => lambda {|budget| budget['name'] } },
55
+ {"DESCRIPTION" => lambda {|budget| truncate_string(budget['description'], 30) } },
56
+ # {"ENABLED" => lambda {|budget| format_boolean(budget['enabled']) } },
57
+ # {"SCOPE" => lambda {|it| format_budget_scope(it) } },
58
+ {"SCOPE" => lambda {|it| it['refName'] } },
59
+ {"PERIOD" => lambda {|it| it['year'] } },
60
+ {"INTERVAL" => lambda {|it| it['interval'].to_s.capitalize } },
61
+ # the UI doesn't consider timezone, so uhh do it this hacky way for now.
62
+ {"START DATE" => lambda {|it|
63
+ if it['timezone'] == 'UTC'
64
+ ((parse_time(it['startDate'], "%Y-%m-%d").strftime("%x")) rescue it['startDate']) # + ' UTC'
65
+ else
66
+ format_local_date(it['startDate'])
67
+ end
68
+ } },
69
+ {"END DATE" => lambda {|it|
70
+ if it['timezone'] == 'UTC'
71
+ ((parse_time(it['endDate'], "%Y-%m-%d").strftime("%x")) rescue it['endDate']) # + ' UTC'
72
+ else
73
+ format_local_date(it['endDate'])
74
+ end
75
+ } },
76
+ {"TOTAL" => lambda {|it| format_money(it['totalCost'], it['currency']) } },
77
+ {"AVERAGE" => lambda {|it| format_money(it['averageCost'], it['currency']) } },
78
+ # {"CREATED BY" => lambda {|budget| budget['createdByName'] ? budget['createdByName'] : budget['createdById'] } },
79
+ # {"CREATED" => lambda {|budget| format_local_dt(budget['dateCreated']) } },
80
+ # {"UPDATED" => lambda {|budget| format_local_dt(budget['lastUpdated']) } },
81
+ ]
82
+ if options[:include_fields]
83
+ columns = options[:include_fields]
86
84
  end
87
- print reset,"\n"
85
+ print as_pretty_table(budgets, columns, options)
86
+ print_results_pagination(json_response)
88
87
  end
89
- return 0
90
- rescue RestClient::Exception => e
91
- print_rest_exception(e, options)
92
- exit 1
88
+ print reset,"\n"
93
89
  end
90
+ return budgets.empty? ? [3, "no budgets found"] : [0, nil]
94
91
  end
95
92
 
96
93
  def get(args)
@@ -98,37 +95,32 @@ class Morpheus::Cli::BudgetsCommand
98
95
  params = {}
99
96
  optparse = Morpheus::Cli::OptionParser.new do |opts|
100
97
  opts.banner = subcommand_usage("[budget]")
101
- build_common_options(opts, options, [:query, :json, :yaml, :csv, :fields, :dry_run, :remote])
98
+ build_standard_get_options(opts, options)
102
99
  opts.footer = "Get details about a budget.\n[budget] is required. Budget ID or name"
103
100
  end
104
101
  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
-
102
+ verify_args!(args:args, optparse:optparse, count:1)
110
103
  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']
104
+ params.merge!(parse_query_options(options))
105
+ @budgets_interface.setopts(options)
106
+ if options[:dry_run]
107
+ if args[0].to_s =~ /\A\d{1,}\Z/
108
+ print_dry_run @budgets_interface.dry.get(args[0], params)
109
+ else
110
+ print_dry_run @budgets_interface.dry.list({name: args[0].to_s})
128
111
  end
129
- render_result = render_with_format(json_response, options, 'budget')
130
- return 0 if render_result
131
-
112
+ return 0
113
+ end
114
+ budget = find_budget_by_name_or_id(args[0])
115
+ return 1 if budget.nil?
116
+ # skip reload if already fetched via get(id)
117
+ json_response = {'budget' => budget}
118
+ if args[0].to_s != budget['id'].to_s
119
+ json_response = @budgets_interface.get(budget['id'], params)
120
+ budget = json_response['budget']
121
+ end
122
+
123
+ render_response(json_response, options, 'budget') do
132
124
 
133
125
  print_h1 "Budget Details"
134
126
  print cyan
@@ -140,52 +132,6 @@ class Morpheus::Cli::BudgetsCommand
140
132
  "Scope" => lambda {|it| format_budget_scope(it) },
141
133
  "Period" => lambda {|it| it['year'] },
142
134
  "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
135
  # the UI doesn't consider timezone, so uhh do it this hacky way for now.
190
136
  "Start Date" => lambda {|it|
191
137
  if it['timezone'] == 'UTC'
@@ -201,13 +147,19 @@ class Morpheus::Cli::BudgetsCommand
201
147
  format_local_date(it['endDate'])
202
148
  end
203
149
  },
204
- "Timezone" => lambda {|it| it['timezone'] },
205
- })
206
- budget_columns.merge!({
150
+ # "Costs" => lambda {|it|
151
+ # if it['costs'].is_a?(Array)
152
+ # it['costs'] ? it['costs'].join(', ') : ''
153
+ # elsif it['costs'].is_a?(Hash)
154
+ # it['costs'].to_s
155
+ # else
156
+ # it['costs'].to_s
157
+ # end
158
+ # },
207
159
  "Created By" => lambda {|it| it['createdByName'] ? it['createdByName'] : it['createdById'] },
208
160
  "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
209
161
  "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
210
- })
162
+ }
211
163
  print_description_list(budget_columns, budget, options)
212
164
  # print reset,"\n"
213
165
 
@@ -221,33 +173,17 @@ class Morpheus::Cli::BudgetsCommand
221
173
  }
222
174
  budget_row = {label:"Budget"}
223
175
  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|
176
+ multi_year = false
177
+ if budget['startDate'] && budget['endDate'] && parse_time(budget['startDate']).year != parse_time(budget['endDate']).year
178
+ multi_year = true
179
+ end
180
+ budget['stats']['intervals'].each do |it|
242
181
  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
182
+ interval_key = format_budget_interval_label(budget, it).to_s.upcase
183
+ # interval_date = parse_time(it["startDate"]) rescue nil
248
184
  budget_summary_columns[interval_key] = interval_key
249
- budget_cost = stat_interval["budget"].to_f
250
- actual_cost = stat_interval["cost"].to_f
185
+ budget_cost = it["budget"].to_f
186
+ actual_cost = it["cost"].to_f
251
187
  over_budget = actual_cost > 0 && actual_cost > budget_cost
252
188
  if over_budget
253
189
  budget_row[interval_key] = "#{cyan}#{format_money(budget_cost, currency)}#{cyan}"
@@ -262,44 +198,65 @@ class Morpheus::Cli::BudgetsCommand
262
198
  print reset,"\n"
263
199
  rescue => ex
264
200
  print red,"Failed to render budget summary.",reset,"\n"
201
+ raise ex
265
202
  end
266
203
  else
267
204
  print cyan,"No budget stat data found.",reset,"\n"
268
205
  end
269
- return 0
270
- rescue RestClient::Exception => e
271
- print_rest_exception(e, options)
272
- exit 1
273
206
  end
207
+ return 0, nil
274
208
  end
275
209
 
276
210
  def add(args)
277
211
  options = {}
278
212
  params = {}
279
- costs = {}
213
+ costs = []
280
214
  optparse = Morpheus::Cli::OptionParser.new do |opts|
281
215
  opts.banner = subcommand_usage("[name] [options]")
282
216
  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
217
+ # opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
218
+ # costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
219
+ # end
220
+ opts.on('--costs LIST', String, "Budget cost amounts, one for each interval in the budget. eg \"350\" for one year, \"25,25,25,100\" for quarters, and \"10,10,10,10,10,10,10,10,10,10,10,50\" for each month") do |val|
221
+ val = val.to_s.gsub('[', '').gsub(']', '')
222
+ costs = val.to_s.split(',').collect {|it| parse_cost_amount(it) }
223
+ end
224
+ (1..12).each.with_index do |cost_index, i|
225
+ opts.on("--cost#{cost_index} VALUE", String, "Cost #{cost_index.to_s.capitalize} amount") do |val|
226
+ #params["cost#{cost_index.to_s}"] = parse_cost_amount(val)
227
+ costs[i] = parse_cost_amount(val)
228
+ end
229
+ opts.add_hidden_option("--cost#{cost_index}")
285
230
  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)
231
+ [:q1,:q2,:q3,:q4,].each.with_index do |quarter, i|
232
+ opts.on("--#{quarter.to_s} VALUE", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
233
+ costs[i] = parse_cost_amount(val)
290
234
  end
235
+ opts.add_hidden_option("--#{quarter.to_s}")
291
236
  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)
237
+ [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december].each_with_index do |month, i|
238
+ opts.on("--#{month.to_s} VALUE", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
239
+ costs[i] = parse_cost_amount(val)
296
240
  end
241
+ opts.add_hidden_option("--#{month.to_s}")
297
242
  end
298
243
  opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
299
244
  params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
300
245
  end
301
- build_common_options(opts, options, [:payload, :options, :json, :dry_run, :remote])
302
- opts.footer = "Create budget."
246
+ build_standard_add_options(opts, options)
247
+ opts.footer = <<-EOT
248
+ Create a budget.
249
+ The default period is the current year, eg. "#{Time.now.year}"
250
+ and the default interval is "year".
251
+ Costs can be passed as an array of values, one for each interval. eg. --costs "[999]"
252
+
253
+ Examples:
254
+ budgets add example-budget --interval "year" --costs "[2500]"
255
+ budgets add example-qtr-budget --interval "quarter" --costs "[500,500,500,1000]"
256
+ budgets add example-monthly-budget --interval "month" --costs "[400,100,100,100,100,100,100,100,100,100,400,800]"
257
+ budgets add example-future-budget --year "2022" --interval "year" --costs "[5000]"
258
+ budgets add example-custom-budget --year "custom" --interval "year" --start "2021-01-01" --end "2023-12-31" --costs "[2500,5000,10000]"
259
+ EOT
303
260
  end
304
261
  optparse.parse!(args)
305
262
  if args.count > 1
@@ -322,21 +279,37 @@ class Morpheus::Cli::BudgetsCommand
322
279
  }
323
280
  }
324
281
  # allow arbitrary -O options
325
- passed_options.delete('costs')
282
+ #passed_options.delete('costs')
326
283
  passed_options.delete('tenant')
327
284
  passed_options.delete('group')
328
285
  passed_options.delete('cloud')
329
286
  passed_options.delete('user')
330
287
  payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
331
288
  # 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
289
  v_prompt = Morpheus::Cli::OptionTypes.prompt(add_budget_option_types, options[:options], @api_client)
338
290
  params.deep_merge!(v_prompt)
339
- params['costs'] = prompt_costs(params, options)
291
+ # downcase year 'custom' always
292
+ if params['year']
293
+ params['interval'] = params['interval'].to_s.downcase
294
+ end
295
+ # downcase interval always
296
+ if params['interval']
297
+ params['interval'] = params['interval'].to_s.downcase
298
+ end
299
+ # parse MM/DD/YY but need to convert to to ISO format YYYY-MM-DD for api
300
+ standard_start_date = (params['startDate'] ? Time.strptime(params['startDate'], "%x") : nil) rescue nil
301
+ if standard_start_date
302
+ params['startDate'] = format_date(standard_start_date, {format:"%Y-%m-%d"})
303
+ end
304
+ standard_end_date = (params['endDate'] ? Time.strptime(params['endDate'], "%x") : nil) rescue nil
305
+ if standard_end_date
306
+ params['endDate'] = format_date(standard_end_date, {format:"%Y-%m-%d"})
307
+ end
308
+ if !costs.empty?
309
+ params['costs'] = costs
310
+ else
311
+ params['costs'] = prompt_costs(params, options)
312
+ end
340
313
  # budgets api expects scope prefixed parameters like this
341
314
  if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
342
315
  params['scopeTenantId'] = params.delete('tenant')
@@ -377,30 +350,45 @@ class Morpheus::Cli::BudgetsCommand
377
350
  def update(args)
378
351
  options = {}
379
352
  params = {}
380
- costs = {}
353
+ costs = []
381
354
  optparse = Morpheus::Cli::OptionParser.new do |opts|
382
355
  opts.banner = subcommand_usage("[budget] [options]")
383
356
  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
357
+ # opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
358
+ # costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
359
+ # end
360
+ opts.on('--costs COSTS', String, "Budget cost amounts, one for each interval in the budget. eg. [999]") do |val|
361
+ val = val.to_s.gsub('[', '').gsub(']', '')
362
+ costs = val.to_s.split(',').collect {|it| parse_cost_amount(it) }
386
363
  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)
364
+ (1..12).each.with_index do |cost_index, i|
365
+ opts.on("--cost#{cost_index} VALUE", String, "Cost #{cost_index.to_s.capitalize} amount") do |val|
366
+ #params["cost#{cost_index.to_s}"] = parse_cost_amount(val)
367
+ costs[i] = parse_cost_amount(val)
391
368
  end
369
+ opts.add_hidden_option("--cost#{cost_index}")
392
370
  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)
371
+ [:q1,:q2,:q3,:q4,].each.with_index do |quarter, i|
372
+ opts.on("--#{quarter.to_s} VALUE", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
373
+ costs[i] = parse_cost_amount(val)
397
374
  end
375
+ opts.add_hidden_option("--#{quarter.to_s}")
376
+ end
377
+ [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december].each_with_index do |month, i|
378
+ opts.on("--#{month.to_s} VALUE", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
379
+ costs[i] = parse_cost_amount(val)
380
+ end
381
+ opts.add_hidden_option("--#{month.to_s}")
398
382
  end
399
383
  opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
400
384
  params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
401
385
  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"
386
+ build_standard_update_options(opts, options)
387
+ opts.footer = <<-EOT
388
+ Update a budget.
389
+ [budget] is required. Budget ID or name
390
+ EOT
391
+ opts.footer = "Update a budget.\n[budget] is required. Budget ID or name"
404
392
  end
405
393
  optparse.parse!(args)
406
394
 
@@ -409,78 +397,99 @@ class Morpheus::Cli::BudgetsCommand
409
397
  end
410
398
 
411
399
  connect(options)
412
- begin
400
+
401
+ budget = find_budget_by_name_or_id(args[0])
402
+ return 1 if budget.nil?
413
403
 
414
- budget = find_budget_by_name_or_id(args[0])
415
- return 1 if budget.nil?
404
+ original_year = budget['year']
405
+ original_interval = budget['interval']
406
+ original_costs = budget['costs'].is_a?(Array) ? budget['costs'] : nil
416
407
 
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
- }
408
+ # construct payload
409
+ passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
410
+ payload = nil
411
+ if options[:payload]
412
+ payload = options[:payload]
413
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
414
+ else
415
+ payload = {
416
+ 'budget' => {
427
417
  }
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')
418
+ }
419
+ # allow arbitrary -O options
420
+ #passed_options.delete('costs')
421
+ passed_options.delete('tenant')
422
+ passed_options.delete('group')
423
+ passed_options.delete('cloud')
424
+ passed_options.delete('user')
425
+ payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
426
+ # prompt for options
427
+ #params = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options], @api_client, options[:params])
428
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options].merge(:no_prompt => true), @api_client)
429
+ params.deep_merge!(v_prompt)
430
+ # downcase year 'custom' always
431
+ if params['year']
432
+ params['interval'] = params['interval'].to_s.downcase
433
+ end
434
+ # downcase interval always
435
+ if params['interval']
436
+ params['interval'] = params['interval'].to_s.downcase
437
+ end
438
+ # parse MM/DD/YY but need to convert to to ISO format YYYY-MM-DD for api
439
+ if params['startDate']
440
+ params['startDate'] = format_date(parse_time(params['startDate']), {format:"%Y-%m-%d"})
441
+ end
442
+ if params['endDate']
443
+ params['endDate'] = format_date(parse_time(params['endDate']), {format:"%Y-%m-%d"})
444
+ end
445
+ if !costs.empty?
446
+ params['costs'] = costs
447
+ # merge original costs in on update unless interval is changing too, should check original_year too probably if going to custom...
448
+ if params['interval'] && params['interval'] != original_interval
449
+ original_costs = nil
455
450
  end
456
- if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
457
- params['scopeUserId'] = params.delete('user')
451
+ if original_costs
452
+ original_costs.each_with_index do |original_cost, i|
453
+ if params['costs'][i].nil?
454
+ params['costs'][i] = original_cost
455
+ end
456
+ end
458
457
  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}"
458
+ else
459
+ if params['interval'] && params['interval'] != original_interval
460
+ raise_command_error "Changing interval requires setting the costs as well.\n#{optparse}"
463
461
  end
464
462
  end
465
- @budgets_interface.setopts(options)
466
- if options[:dry_run]
467
- print_dry_run @budgets_interface.dry.update(budget['id'], payload)
468
- return
463
+ # budgets api expects scope prefixed parameters like this
464
+ if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
465
+ params['scopeTenantId'] = params.delete('tenant')
469
466
  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]] : []))
467
+ if params['group'].is_a?(String) || params['group'].is_a?(Numeric)
468
+ params['scopeGroupId'] = params.delete('group')
478
469
  end
479
- return 0
480
- rescue RestClient::Exception => e
481
- print_rest_exception(e, options)
482
- exit 1
470
+ if params['cloud'].is_a?(String) || params['cloud'].is_a?(Numeric)
471
+ params['scopeCloudId'] = params.delete('cloud')
472
+ end
473
+ if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
474
+ params['scopeUserId'] = params.delete('user')
475
+ end
476
+ payload.deep_merge!({'budget' => params}) unless params.empty?
477
+ if payload.empty? || payload['budget'].empty?
478
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
479
+ end
480
+ end
481
+ @budgets_interface.setopts(options)
482
+ if options[:dry_run]
483
+ print_dry_run @budgets_interface.dry.update(budget['id'], payload)
484
+ return
485
+ end
486
+ json_response = @budgets_interface.update(budget['id'], payload)
487
+ render_response(json_response, options, 'budget') do
488
+ display_name = json_response['budget'] ? json_response['budget']['name'] : ''
489
+ print_green_success "Budget #{display_name} updated"
490
+ get([json_response['budget']['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
483
491
  end
492
+ return 0, nil
484
493
  end
485
494
 
486
495
  def remove(args)
@@ -582,75 +591,125 @@ class Morpheus::Cli::BudgetsCommand
582
591
  {'fieldName' => 'cloud', 'fieldLabel' => 'Cloud', 'type' => 'select', 'optionSource' => lambda {|api_client, api_params|
583
592
  @options_interface.options_for_source("clouds", {})['data']
584
593
  }, '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}
594
+ {'fieldName' => 'year', 'fieldLabel' => 'Period', 'code' => 'budget.year', 'type' => 'text', 'required' => true, 'defaultValue' => Time.now.year, 'description' => "The period (year) the budget applies, YYYY or 'custom' to enter Start Date and End Date manually", 'displayOrder' => 8},
595
+ {'fieldName' => 'startDate', 'fieldLabel' => 'Start Date', 'type' => 'text', 'required' => true, 'description' => 'The Start Date for custom period budget eg. 2021-01-01', 'dependsOnCode' => 'budget.year:custom', 'displayOrder' => 9},
596
+ {'fieldName' => 'endDate', 'fieldLabel' => 'End Date', 'type' => 'text', 'required' => true, 'description' => 'The End Date for custom period budget eg. 2023-12-31 (must be 1, 2 or 3 years from Start Date)', 'dependsOnCode' => 'budget.year:custom', 'displayOrder' => 10},
597
+ {'fieldName' => 'interval', 'fieldLabel' => 'Interval', 'type' => 'select', 'selectOptions' => [{'name'=>'Year','value'=>'year'},{'name'=>'Quarter','value'=>'quarter'},{'name'=>'Month','value'=>'month'}], 'defaultValue' => 'year', 'required' => true, 'description' => 'The budget interval, determines cost amounts: "year", "quarter" or "month"', 'displayOrder' => 11}
587
598
  ]
588
599
  end
589
600
 
590
601
  def update_budget_option_types
591
602
  list = add_budget_option_types()
592
603
  # list = list.reject {|it| ["interval"].include? it['fieldName'] }
593
- list.each {|it| it.delete('required') }
594
- list.each {|it| it.delete('defaultValue') }
604
+ list.each {|it|
605
+ it.delete('required')
606
+ it.delete('defaultValue')
607
+ it.delete('dependsOnCode')
608
+ }
595
609
  list
596
610
  end
597
611
 
598
612
  def prompt_costs(params={}, options={})
613
+ # user did -O costs="[3.50,3.50,3.50,5.00]" so just pass through
614
+ default_costs = []
615
+ if options[:options]['costs'] && options[:options]['costs'].is_a?(Array)
616
+ default_costs = options[:options]['costs']
617
+ default_costs.each_with_index do |default_cost, i|
618
+ interval_index = i + 1
619
+ if !default_cost.nil?
620
+ options[:options]["cost#{interval_index}"] = default_cost
621
+ end
622
+ end
623
+ end
624
+ # prompt for each Period Cost based on interval [year|quarter|month]
625
+ budget_period_year = (params['year'] || params['periodValue'])
626
+ is_custom = budget_period_year == 'custom'
599
627
  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 }
628
+ total_years = 1
629
+ total_months = 12
630
+ costs = []
631
+ # custom timeframe so prompt from start to end by interval
632
+ start_date = nil
633
+ end_date = nil
634
+
635
+ if is_custom
636
+ start_date = parse_time(params['startDate'])
637
+ if start_date.nil?
638
+ raise_command_error "startDate is required for custom period budgets"
639
+ end
640
+ end_date = parse_time(params['endDate'])
641
+ if end_date.nil?
642
+ raise_command_error "endDate is required for custom period budgets"
643
+ end
608
644
  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
- # }
645
+ budget_year = budget_period_year ? budget_period_year.to_i : Time.now.year.to_i
646
+ start_date = Time.new(budget_year, 1, 1)
647
+ end_date = Time.new(budget_year, 12, 31)
648
+ end
649
+ epoch_start_month = (start_date.year * 12) + start_date.month
650
+ epoch_end_month = (end_date.year * 12) + end_date.month
651
+ # total_months gets + 1 because endDate is same year, on last day of the month, Dec 31 by default
652
+ total_months = (epoch_end_month - epoch_start_month) + 1
653
+ total_years = (total_months / 12)
654
+ cost_option_types = []
655
+ interval_count = total_months
656
+ if interval == 'year'
657
+ interval_count = total_months / 12
658
+ elsif interval == 'quarter'
659
+ interval_count = total_months / 3
660
+ end
661
+
662
+ is_fiscal = start_date.month != 1 || start_date.day != 1
663
+
664
+ # debug budget shenanigans
665
+ # puts "START: #{start_date}"
666
+ # puts "END: #{end_date}"
667
+ # puts "EPOCH MONTHS: #{epoch_start_month} - #{epoch_end_month}"
668
+ # puts "TOTAL MONTHS: #{total_months}"
669
+ # puts "INTERVAL COUNT IS: #{interval_count}"
670
+
671
+ if total_months < 0
672
+ raise_command_error "budget cannot end (#{end_date}) before it starts (#{start_date})"
673
+ end
674
+ if (total_months % 12) != 0 || (total_months > 36)
675
+ raise_command_error "budget custom period must be 12, 24, or 36 months."
676
+ end
677
+ if interval == 'year'
678
+ (1..interval_count).each_with_index do |interval_index, i|
679
+ interval_start_month = epoch_start_month + (i * 12)
680
+ interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
681
+ display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
682
+ field_name = "cost#{interval_index}"
683
+ field_label = "#{display_year} Cost"
684
+ cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
685
+ end
686
+ elsif interval == 'quarter'
687
+ (1..interval_count).each_with_index do |interval_index, i|
688
+ interval_start_month = epoch_start_month + (i * 3)
689
+ interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
690
+ interval_end_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
691
+ display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
692
+ field_name = "cost#{interval_index}"
693
+ # field_label = "Q#{interval_index} Cost"
694
+ field_label = "Q#{(i % 4) + 1} #{display_year} Cost"
695
+ cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
653
696
  end
697
+ elsif interval == 'month'
698
+ (1..interval_count).each_with_index do |interval_index, i|
699
+ interval_start_month = epoch_start_month + i
700
+ interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
701
+ display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
702
+ field_name = "cost#{interval_index}"
703
+ # field_label = "#{interval_date.strftime('%B %Y')} Cost"
704
+ field_label = "#{interval_date.strftime('%B')} #{display_year} Cost"
705
+ cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
706
+ end
707
+ end
708
+ # values is a Hash like {"cost1": 99.0, "cost2": 55.0}
709
+ values = Morpheus::Cli::OptionTypes.prompt(cost_option_types, options[:options], @api_client)
710
+ values.each do |k,v|
711
+ interval_index = k[4..-1].to_i
712
+ costs[interval_index-1] = parse_cost_amount(v).to_f
654
713
  end
655
714
  return costs
656
715
  end
@@ -665,8 +724,19 @@ class Morpheus::Cli::BudgetsCommand
665
724
  end
666
725
  end
667
726
 
727
+ # convert String like "$5,499.99" to Float 5499.99
668
728
  def parse_cost_amount(val)
669
- val.to_s.gsub(",","").to_f
729
+ val.to_s.gsub(",","").gsub('$','').strip.to_f
730
+ end
731
+
732
+ def format_budget_interval_label(budget, budget_interval)
733
+ label = ""
734
+ if budget_interval['chartName']
735
+ label = budget_interval['chartName']
736
+ else
737
+ label = (budget_interval['shortName'] || budget_interval['shortYear']).to_s
738
+ end
739
+ return label
670
740
  end
671
741
 
672
742
  end