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 +4 -4
- data/Dockerfile +1 -1
- data/README.md +3 -3
- data/lib/morpheus/api/instances_interface.rb +21 -0
- data/lib/morpheus/api/invoices_interface.rb +12 -3
- data/lib/morpheus/cli/activity_command.rb +7 -4
- data/lib/morpheus/cli/backup_jobs_command.rb +1 -1
- data/lib/morpheus/cli/backups_command.rb +2 -3
- data/lib/morpheus/cli/budgets_command.rb +389 -319
- data/lib/morpheus/cli/cli_command.rb +19 -9
- data/lib/morpheus/cli/commands/standard/curl_command.rb +25 -10
- data/lib/morpheus/cli/commands/standard/history_command.rb +6 -2
- data/lib/morpheus/cli/dashboard_command.rb +260 -20
- data/lib/morpheus/cli/execution_request_command.rb +15 -5
- data/lib/morpheus/cli/hosts.rb +12 -0
- data/lib/morpheus/cli/instances.rb +116 -2
- data/lib/morpheus/cli/invoices_command.rb +89 -95
- data/lib/morpheus/cli/jobs_command.rb +94 -92
- data/lib/morpheus/cli/library_option_types_command.rb +5 -3
- data/lib/morpheus/cli/mixins/print_helper.rb +16 -6
- data/lib/morpheus/cli/projects_command.rb +1 -1
- data/lib/morpheus/cli/remote.rb +2 -1
- data/lib/morpheus/cli/user_sources_command.rb +118 -134
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/formatters.rb +21 -11
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c04cf89b798df6d8dba342efe64eec23b5f97bbeef368072d56829c8b517707
|
4
|
+
data.tar.gz: 50a9448b2a167439efbd3f3bcd2f0ea91870681986108d975b058a40b5e14d69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58613849cca0d5994cc866d6de770f0657b65ccbd3961e352ac934f21600f0888be755c70f54cc964a317fa12201a78800eb59447a664ec28df7eb00799cdc1f
|
7
|
+
data.tar.gz: 01307e6f8b6c91813a948d79b5dedc0914d5731b026d6152ddf1ea59d8ab9a830bca5305a28087623d088787464dc76ee3667673759467a2910723b06645a3b7
|
data/Dockerfile
CHANGED
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://
|
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: "
|
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: "
|
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: "/
|
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
|
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
|
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
|
-
|
25
|
+
build_standard_list_options(opts, options)
|
25
26
|
opts.footer = "List budgets."
|
26
27
|
end
|
27
28
|
optparse.parse!(args)
|
28
|
-
|
29
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
85
|
+
print as_pretty_table(budgets, columns, options)
|
86
|
+
print_results_pagination(json_response)
|
88
87
|
end
|
89
|
-
|
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
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
130
|
-
|
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
|
-
"
|
205
|
-
|
206
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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 = (
|
244
|
-
|
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 =
|
250
|
-
actual_cost =
|
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
|
-
|
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
|
-
|
288
|
-
|
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
|
-
|
294
|
-
|
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
|
-
|
302
|
-
opts.footer =
|
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
|
-
|
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
|
-
|
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
|
-
|
388
|
-
|
389
|
-
|
390
|
-
costs[
|
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
|
-
[:
|
394
|
-
|
395
|
-
|
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
|
-
|
403
|
-
opts.footer =
|
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
|
-
|
400
|
+
|
401
|
+
budget = find_budget_by_name_or_id(args[0])
|
402
|
+
return 1 if budget.nil?
|
413
403
|
|
414
|
-
|
415
|
-
|
404
|
+
original_year = budget['year']
|
405
|
+
original_interval = budget['interval']
|
406
|
+
original_costs = budget['costs'].is_a?(Array) ? budget['costs'] : nil
|
416
407
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
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
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
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
|
457
|
-
|
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
|
-
|
460
|
-
|
461
|
-
|
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
|
-
|
466
|
-
if
|
467
|
-
|
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
|
-
|
471
|
-
|
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
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
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
|
586
|
-
{'fieldName' => '
|
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|
|
594
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
603
|
-
#
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
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
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
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
|