morpheus-cli 5.2.1 → 5.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4409a856333a1b2957649aac67f62677b943706754ebc810c614fa5f879b120d
4
- data.tar.gz: bb968663da852f4ea631fc9b0c89c115f6fb1067049585953c33b7c24e4b284a
3
+ metadata.gz: 4c04cf89b798df6d8dba342efe64eec23b5f97bbeef368072d56829c8b517707
4
+ data.tar.gz: 50a9448b2a167439efbd3f3bcd2f0ea91870681986108d975b058a40b5e14d69
5
5
  SHA512:
6
- metadata.gz: 2be204c65dd3c2a35ce5ac7170e70a2297aa34ee1348db2fc736a5915a2678c0c45361269a801db228f202e27dcdcfaaf69afc7f49d9eded429c16d5381dd28f
7
- data.tar.gz: a5018a327ae43078e4661c1c3744e75b6cc1adfc8854ca09d99a0a47f8e0678d2f8914b46d724bd8189011b13a42f2ac5779b3980611af1655cdebc4393a3e53
6
+ metadata.gz: 58613849cca0d5994cc866d6de770f0657b65ccbd3961e352ac934f21600f0888be755c70f54cc964a317fa12201a78800eb59447a664ec28df7eb00799cdc1f
7
+ data.tar.gz: 01307e6f8b6c91813a948d79b5dedc0914d5731b026d6152ddf1ea59d8ab9a830bca5305a28087623d088787464dc76ee3667673759467a2910723b06645a3b7
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.5.1
2
2
 
3
- RUN gem install morpheus-cli -v 5.2.0
3
+ RUN gem install morpheus-cli -v 5.3.0
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
data/README.md CHANGED
@@ -1,12 +1,12 @@
1
+ <img src="https://morpheusdata.com/wp-content/uploads/2020/04/morpheus-logo-v2.svg" width="200px">
2
+
1
3
  # Morpheus CLI
2
4
 
3
5
  - Website: https://www.morpheusdata.com/
4
6
  - Guide: [Morpheus CLI Wiki](https://github.com/gomorpheus/morpheus-cli/wiki)
5
- - Docs: [Morpheus Documentation](https://docs.morpheusdata.com)
7
+ - Docs: [Morpheus CLI Documentation](https://clidocs.morpheusdata.com)
6
8
  - Support: [Morpheus Support](https://support.morpheusdata.com)
7
9
 
8
- <img src="https://www.morpheusdata.com/wp-content/uploads/2018/06/cropped-morpheus_highres.png" width="600px">
9
-
10
10
  This library is a Ruby gem that provides a command line interface for interacting with the Morpheus Data appliance. The features provided include provisioning clusters, hosts, and containers, deploying and monitoring applications, automating tasks, and much more.
11
11
 
12
12
  ## Installation
@@ -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' }
@@ -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
@@ -22,6 +22,9 @@ class Morpheus::Cli::ActivityCommand
22
22
  params, options = {}, {}
23
23
  optparse = Morpheus::Cli::OptionParser.new do |opts|
24
24
  opts.banner = subcommand_usage()
25
+ opts.on('-a', '--details', "Display more details object id, full date and time, etc." ) do
26
+ options[:details] = true
27
+ end
25
28
  opts.on('-t','--type TYPE', "Activity Type eg. Provisioning, Admin") do |val|
26
29
  options[:type] ||= []
27
30
  options[:type] << val
@@ -115,20 +118,20 @@ EOT
115
118
  # {"SEVERITY" => lambda {|record| format_activity_severity(record['severity']) } },
116
119
  {"TYPE" => lambda {|record| record['activityType'] } },
117
120
  {"NAME" => lambda {|record| record['name'] } },
118
- {"RESOURCE" => lambda {|record| "#{record['objectType']} #{record['objectId']}" } },
121
+ options[:details] ? {"RESOURCE" => lambda {|record| "#{record['objectType']} #{record['objectId']}" } } : nil,
119
122
  {"MESSAGE" => lambda {|record| record['message'] || '' } },
120
123
  {"USER" => lambda {|record| record['user'] ? record['user']['username'] : record['userName'] } },
121
124
  #{"DATE" => lambda {|record| "#{format_duration_ago(record['ts'] || record['timestamp'])}" } },
122
125
  {"DATE" => lambda {|record|
123
- # show full time if searching for custom timerange, otherwise the default is to show relative time
124
- if params['start'] || params['end'] || params['timeframe']
126
+ # show full time if searching for custom timerange or --details, otherwise the default is to show relative time
127
+ if params['start'] || params['end'] || params['timeframe'] || options[:details]
125
128
  "#{format_local_dt(record['ts'] || record['timestamp'])}"
126
129
  else
127
130
  "#{format_duration_ago(record['ts'] || record['timestamp'])}"
128
131
  end
129
132
 
130
133
  } },
131
- ]
134
+ ].compact
132
135
  print as_pretty_table(activity, columns, options)
133
136
  print_results_pagination(json_response)
134
137
  end
@@ -10,7 +10,7 @@ class Morpheus::Cli::BackupJobsCommand
10
10
 
11
11
  set_command_name :'backup-jobs'
12
12
 
13
- register_subcommands :list, :get, :add, :update, :remove, :run
13
+ register_subcommands :list, :get #, :add, :update, :remove, :run
14
14
 
15
15
  def connect(opts)
16
16
  @api_client = establish_remote_appliance_connection(opts)
@@ -10,7 +10,7 @@ class Morpheus::Cli::BackupsCommand
10
10
 
11
11
  set_command_name :'backups'
12
12
 
13
- register_subcommands :list, :get, :add, :update, :remove, :run, :restore
13
+ register_subcommands :list, :get #, :add, :update, :remove, :run, :restore
14
14
 
15
15
  def connect(opts)
16
16
  @api_client = establish_remote_appliance_connection(opts)
@@ -82,8 +82,7 @@ EOT
82
82
  end
83
83
  end
84
84
 
85
- def _get(id, options)
86
- params = {}
85
+ def _get(id, params, options)
87
86
  @backups_interface.setopts(options)
88
87
  if options[:dry_run]
89
88
  print_dry_run @backups_interface.dry.get(id, params)
@@ -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