morpheus-cli 5.0.0 → 5.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +16 -0
- data/lib/morpheus/api/billing_interface.rb +1 -0
- data/lib/morpheus/api/deploy_interface.rb +1 -1
- data/lib/morpheus/api/deployments_interface.rb +20 -1
- data/lib/morpheus/api/forgot_password_interface.rb +17 -0
- data/lib/morpheus/api/instances_interface.rb +16 -2
- data/lib/morpheus/api/invoices_interface.rb +12 -3
- data/lib/morpheus/api/search_interface.rb +13 -0
- data/lib/morpheus/api/servers_interface.rb +14 -0
- data/lib/morpheus/api/service_catalog_interface.rb +89 -0
- data/lib/morpheus/api/usage_interface.rb +18 -0
- data/lib/morpheus/cli.rb +6 -2
- data/lib/morpheus/cli/apps.rb +3 -23
- data/lib/morpheus/cli/budgets_command.rb +389 -319
- data/lib/morpheus/cli/{catalog_command.rb → catalog_item_types_command.rb} +182 -67
- data/lib/morpheus/cli/cli_command.rb +51 -10
- data/lib/morpheus/cli/commands/standard/curl_command.rb +26 -13
- data/lib/morpheus/cli/commands/standard/history_command.rb +9 -3
- data/lib/morpheus/cli/commands/standard/man_command.rb +74 -40
- data/lib/morpheus/cli/containers_command.rb +0 -24
- data/lib/morpheus/cli/cypher_command.rb +6 -2
- data/lib/morpheus/cli/dashboard_command.rb +260 -20
- data/lib/morpheus/cli/deploy.rb +199 -90
- data/lib/morpheus/cli/deployments.rb +341 -28
- data/lib/morpheus/cli/deploys.rb +206 -41
- data/lib/morpheus/cli/error_handler.rb +7 -0
- data/lib/morpheus/cli/forgot_password.rb +133 -0
- data/lib/morpheus/cli/groups.rb +1 -1
- data/lib/morpheus/cli/health_command.rb +59 -2
- data/lib/morpheus/cli/hosts.rb +271 -39
- data/lib/morpheus/cli/instances.rb +228 -129
- data/lib/morpheus/cli/invoices_command.rb +100 -20
- data/lib/morpheus/cli/jobs_command.rb +94 -92
- data/lib/morpheus/cli/library_option_lists_command.rb +1 -1
- data/lib/morpheus/cli/library_option_types_command.rb +10 -5
- data/lib/morpheus/cli/logs_command.rb +9 -6
- data/lib/morpheus/cli/mixins/accounts_helper.rb +5 -1
- data/lib/morpheus/cli/mixins/deployments_helper.rb +31 -2
- data/lib/morpheus/cli/mixins/print_helper.rb +13 -27
- data/lib/morpheus/cli/mixins/provisioning_helper.rb +108 -5
- data/lib/morpheus/cli/option_types.rb +271 -22
- data/lib/morpheus/cli/remote.rb +35 -10
- data/lib/morpheus/cli/reports_command.rb +99 -30
- data/lib/morpheus/cli/roles.rb +193 -155
- data/lib/morpheus/cli/search_command.rb +182 -0
- data/lib/morpheus/cli/service_catalog_command.rb +1474 -0
- data/lib/morpheus/cli/setup.rb +1 -1
- data/lib/morpheus/cli/shell.rb +33 -11
- data/lib/morpheus/cli/tasks.rb +29 -32
- data/lib/morpheus/cli/usage_command.rb +64 -11
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/cli/virtual_images.rb +429 -254
- data/lib/morpheus/cli/whoami.rb +6 -6
- data/lib/morpheus/cli/workflows.rb +33 -40
- data/lib/morpheus/formatters.rb +75 -18
- data/lib/morpheus/terminal.rb +6 -2
- metadata +10 -4
- data/lib/morpheus/cli/mixins/catalog_helper.rb +0 -66
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 91687d55f353976028bdebb05b8b588fc56ae1d590694da9948115098b05fb5f
|
4
|
+
data.tar.gz: 370d606fe6b918dcc18a94f191fc9706bba0aef4832a3ec1aa24da8fc8d3377a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dba600f78525d9176052b7a6319db0c2131c4e1cbf140eea951613ff5920b3b7be3b2cf597a9dfa77c512336a936122c1b2cfc2c0d4436ee857c016ecf6da200
|
7
|
+
data.tar.gz: 7cc0c47c12664abe7f012dd6520762fcbb8be6a9cc95a75d81688e7f3340bdcf8fbdd73c7347d3fc741bcc67853eaf42fe490ed097ba69558f76a1482d702fb9
|
data/.gitignore
CHANGED
data/Dockerfile
CHANGED
@@ -342,10 +342,18 @@ class Morpheus::APIClient
|
|
342
342
|
Morpheus::AuthInterface.new({url: @base_url, client_id: @client_id}).setopts(@options)
|
343
343
|
end
|
344
344
|
|
345
|
+
def forgot
|
346
|
+
Morpheus::ForgotPasswordInterface.new(common_interface_options).setopts(@options)
|
347
|
+
end
|
348
|
+
|
345
349
|
def whoami
|
346
350
|
Morpheus::WhoamiInterface.new(common_interface_options).setopts(@options)
|
347
351
|
end
|
348
352
|
|
353
|
+
def search
|
354
|
+
Morpheus::SearchInterface.new(common_interface_options).setopts(@options)
|
355
|
+
end
|
356
|
+
|
349
357
|
def user_settings
|
350
358
|
Morpheus::UserSettingsInterface.new(common_interface_options).setopts(@options)
|
351
359
|
end
|
@@ -768,6 +776,14 @@ class Morpheus::APIClient
|
|
768
776
|
Morpheus::CatalogItemTypesInterface.new(common_interface_options).setopts(@options)
|
769
777
|
end
|
770
778
|
|
779
|
+
def catalog
|
780
|
+
Morpheus::ServiceCatalogInterface.new(common_interface_options).setopts(@options)
|
781
|
+
end
|
782
|
+
|
783
|
+
def usage
|
784
|
+
Morpheus::UsageInterface.new(common_interface_options).setopts(@options)
|
785
|
+
end
|
786
|
+
|
771
787
|
def billing
|
772
788
|
Morpheus::BillingInterface.new(common_interface_options).setopts(@options)
|
773
789
|
end
|
@@ -18,7 +18,7 @@ class Morpheus::DeployInterface < Morpheus::APIClient
|
|
18
18
|
execute(method: :get, url: "#{base_path}", params: params)
|
19
19
|
end
|
20
20
|
|
21
|
-
def get(
|
21
|
+
def get(id, params={})
|
22
22
|
validate_id!(id)
|
23
23
|
execute(method: :get, url: "#{base_path}/#{id}", params: params)
|
24
24
|
end
|
@@ -40,7 +40,8 @@ class Morpheus::DeploymentsInterface < Morpheus::RestInterface
|
|
40
40
|
if destination.empty? || destination == "/" || destination == "." || destination.include?("../")
|
41
41
|
raise "#{self.class}.upload_file() passed a bad destination: '#{destination}'"
|
42
42
|
end
|
43
|
-
url = "#{@base_url}/#{base_path}/#{deployment_id}/versions/#{id}/files"
|
43
|
+
# url = "#{@base_url}/#{base_path}/#{deployment_id}/versions/#{id}/files"
|
44
|
+
url = "#{base_path}/#{deployment_id}/versions/#{id}/files"
|
44
45
|
if !destination.to_s.empty?
|
45
46
|
url += "/#{destination}"
|
46
47
|
end
|
@@ -57,4 +58,22 @@ class Morpheus::DeploymentsInterface < Morpheus::RestInterface
|
|
57
58
|
execute(method: :post, url: url, headers: headers, payload: payload, params: params, timeout: 172800)
|
58
59
|
end
|
59
60
|
|
61
|
+
# upload a file without multipart
|
62
|
+
# local_file is the full absolute local filename
|
63
|
+
# destination should be the full remote file path, including the file name.
|
64
|
+
def destroy_file(deployment_id, id, destination, params={})
|
65
|
+
if destination.empty? || destination == "/" || destination == "." || destination.include?("../")
|
66
|
+
raise "#{self.class}.upload_file() passed a bad destination: '#{destination}'"
|
67
|
+
end
|
68
|
+
# url = "#{@base_url}/#{base_path}/#{deployment_id}/versions/#{id}/files"
|
69
|
+
url = "#{base_path}/#{deployment_id}/versions/#{id}/files"
|
70
|
+
if !destination.to_s.empty?
|
71
|
+
url += "/#{destination}"
|
72
|
+
end
|
73
|
+
# use URI to escape path
|
74
|
+
uri = URI.parse(url)
|
75
|
+
url = uri.path
|
76
|
+
execute(method: :delete, url: url, params: params)
|
77
|
+
end
|
78
|
+
|
60
79
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'morpheus/api/api_client'
|
2
|
+
# There is no Authorization required for this API.
|
3
|
+
class Morpheus::ForgotPasswordInterface < Morpheus::APIClient
|
4
|
+
|
5
|
+
def authorization_required?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def send_email(payload, params={})
|
10
|
+
execute(method: :post, url: "/api/forgot/send-email", params: params, payload: payload.to_json)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset_password(payload, params={})
|
14
|
+
execute(method: :post, url: "/api/forgot/reset-password", params: params, payload: payload.to_json)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -169,10 +169,10 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
|
|
169
169
|
execute(opts)
|
170
170
|
end
|
171
171
|
|
172
|
-
def backup(id,
|
172
|
+
def backup(id, payload={})
|
173
173
|
url = "#{@base_url}/api/instances/#{id}/backup"
|
174
174
|
headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
|
175
|
-
opts = {method: :put, url: url, headers: headers}
|
175
|
+
opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
|
176
176
|
execute(opts)
|
177
177
|
end
|
178
178
|
|
@@ -218,6 +218,20 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
|
|
218
218
|
execute(opts)
|
219
219
|
end
|
220
220
|
|
221
|
+
def snapshot(id, payload={})
|
222
|
+
url = "#{@base_url}/api/instances/#{id}/snapshot"
|
223
|
+
headers = {:authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
|
224
|
+
opts = {method: :put, url: url, headers: headers, payload: payload.to_json}
|
225
|
+
execute(opts)
|
226
|
+
end
|
227
|
+
|
228
|
+
def snapshots(instance_id, params={})
|
229
|
+
url = "#{@base_url}/api/instances/#{instance_id}/snapshots"
|
230
|
+
headers = { params: params, authorization: "Bearer #{@access_token}" }
|
231
|
+
opts = {method: :get, url: url, headers: headers}
|
232
|
+
execute(opts)
|
233
|
+
end
|
234
|
+
|
221
235
|
def import_snapshot(id, params={}, payload={})
|
222
236
|
url = "#{@base_url}/api/instances/#{id}/import-snapshot"
|
223
237
|
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
|
@@ -169,4 +169,18 @@ class Morpheus::ServersInterface < Morpheus::APIClient
|
|
169
169
|
execute(opts)
|
170
170
|
end
|
171
171
|
|
172
|
+
def snapshots(id, params={})
|
173
|
+
url = "#{@base_url}/api/servers/#{id}/snapshots"
|
174
|
+
headers = { params: params, authorization: "Bearer #{@access_token}" }
|
175
|
+
opts = {method: :get, url: url, headers: headers}
|
176
|
+
execute(opts)
|
177
|
+
end
|
178
|
+
|
179
|
+
def software(id, params={})
|
180
|
+
url = "#{@base_url}/api/servers/#{id}/software"
|
181
|
+
headers = { params: params, authorization: "Bearer #{@access_token}" }
|
182
|
+
opts = {method: :get, url: url, headers: headers}
|
183
|
+
execute(opts)
|
184
|
+
end
|
185
|
+
|
172
186
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'morpheus/api/api_client'
|
2
|
+
# Service Catalog Persona interface
|
3
|
+
class Morpheus::ServiceCatalogInterface < Morpheus::APIClient
|
4
|
+
|
5
|
+
def base_path
|
6
|
+
# "/api/service-catalog"
|
7
|
+
"/api/catalog"
|
8
|
+
end
|
9
|
+
|
10
|
+
# dashboard
|
11
|
+
def dashboard(params={})
|
12
|
+
execute(method: :get, url: "#{base_path}/dashboard", params: params)
|
13
|
+
end
|
14
|
+
|
15
|
+
# list catalog types available for ordering
|
16
|
+
def list_types(params={})
|
17
|
+
execute(method: :get, url: "#{base_path}/types", params: params)
|
18
|
+
end
|
19
|
+
|
20
|
+
# get specific catalog type
|
21
|
+
def get_type(id, params={})
|
22
|
+
validate_id!(id)
|
23
|
+
execute(method: :get, url: "#{base_path}/types/#{id}", params: params)
|
24
|
+
end
|
25
|
+
|
26
|
+
# list catalog inventory (items)
|
27
|
+
def list_inventory(params={})
|
28
|
+
execute(method: :get, url: "#{base_path}/items", params: params)
|
29
|
+
end
|
30
|
+
|
31
|
+
# get catalog inventory item
|
32
|
+
def get_inventory(id, params={})
|
33
|
+
validate_id!(id)
|
34
|
+
execute(method: :get, url: "#{base_path}/items/#{id}", params: params)
|
35
|
+
end
|
36
|
+
|
37
|
+
# delete a catalog inventory item
|
38
|
+
def destroy_inventory(id, params = {})
|
39
|
+
validate_id!(id)
|
40
|
+
execute(method: :delete, url: "#{base_path}/items/#{id}", params: params)
|
41
|
+
end
|
42
|
+
|
43
|
+
# get cart (one per user)
|
44
|
+
def get_cart(params={})
|
45
|
+
execute(method: :get, url: "#{base_path}/cart", params: params)
|
46
|
+
end
|
47
|
+
|
48
|
+
# update cart (set cart name)
|
49
|
+
def update_cart(payload, params={})
|
50
|
+
execute(method: :put, url: "#{base_path}/cart", params: params, payload: payload.to_json)
|
51
|
+
end
|
52
|
+
|
53
|
+
# validate a new item, can be used before before adding it
|
54
|
+
def validate_cart_item(payload, params={})
|
55
|
+
execute(method: :post, url: "#{base_path}/cart/items/validate", params: params, payload: payload.to_json)
|
56
|
+
end
|
57
|
+
|
58
|
+
# add item to cart
|
59
|
+
def create_cart_item(payload, params={})
|
60
|
+
execute(method: :post, url: "#{base_path}/cart/items", params: params, payload: payload.to_json)
|
61
|
+
end
|
62
|
+
|
63
|
+
# update item in the cart
|
64
|
+
def update_cart_item(id, payload, params={})
|
65
|
+
validate_id!(id)
|
66
|
+
execute(method: :put, url: "#{base_path}/cart/items/#{id}", params: params, payload: payload.to_json)
|
67
|
+
end
|
68
|
+
|
69
|
+
# remove item from the cart
|
70
|
+
def destroy_cart_item(id, params={})
|
71
|
+
validate_id!(id)
|
72
|
+
execute(method: :delete, url: "#{base_path}/cart/items/#{id}", params: params)
|
73
|
+
end
|
74
|
+
|
75
|
+
# place order with cart
|
76
|
+
def checkout(payload, params={})
|
77
|
+
execute(method: :post, url: "#{base_path}/checkout", params: params, payload: payload.to_json)
|
78
|
+
end
|
79
|
+
|
80
|
+
# remove all items from cart and reset name
|
81
|
+
def clear_cart(params={})
|
82
|
+
execute(method: :delete, url: "#{base_path}/cart", params: params)
|
83
|
+
end
|
84
|
+
|
85
|
+
# create an order from scratch, without using a cart
|
86
|
+
def create_order(payload, params={})
|
87
|
+
execute(method: :post, url: "#{base_path}/orders", params: params, payload: payload.to_json)
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'morpheus/api/api_client'
|
2
|
+
|
3
|
+
class Morpheus::UsageInterface < Morpheus::APIClient
|
4
|
+
|
5
|
+
def base_path
|
6
|
+
"/api/usage" # not /usages ?
|
7
|
+
end
|
8
|
+
|
9
|
+
def list(params={})
|
10
|
+
execute(method: :get, url: "#{base_path}", params: params)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(id, params={})
|
14
|
+
validate_id!(id)
|
15
|
+
execute(method: :get, url: "#{base_path}/#{id}", params: params)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/lib/morpheus/cli.rb
CHANGED
@@ -74,9 +74,11 @@ module Morpheus
|
|
74
74
|
load 'morpheus/cli/setup.rb'
|
75
75
|
load 'morpheus/cli/login.rb'
|
76
76
|
load 'morpheus/cli/logout.rb'
|
77
|
+
load 'morpheus/cli/forgot_password.rb'
|
77
78
|
load 'morpheus/cli/whoami.rb'
|
78
79
|
load 'morpheus/cli/access_token_command.rb'
|
79
80
|
load 'morpheus/cli/user_settings_command.rb'
|
81
|
+
load 'morpheus/cli/search_command.rb'
|
80
82
|
load 'morpheus/cli/dashboard_command.rb'
|
81
83
|
load 'morpheus/cli/recent_activity_command.rb' # deprecated, removing soon
|
82
84
|
load 'morpheus/cli/activity_command.rb'
|
@@ -94,11 +96,12 @@ module Morpheus
|
|
94
96
|
load 'morpheus/cli/tasks.rb'
|
95
97
|
load 'morpheus/cli/workflows.rb'
|
96
98
|
load 'morpheus/cli/deployments.rb'
|
99
|
+
load 'morpheus/cli/deploy.rb'
|
100
|
+
load 'morpheus/cli/deploys.rb'
|
97
101
|
load 'morpheus/cli/instances.rb'
|
98
102
|
load 'morpheus/cli/containers_command.rb'
|
99
103
|
load 'morpheus/cli/apps.rb'
|
100
104
|
load 'morpheus/cli/blueprints_command.rb'
|
101
|
-
load 'morpheus/cli/deploys.rb'
|
102
105
|
load 'morpheus/cli/license.rb'
|
103
106
|
load 'morpheus/cli/instance_types.rb'
|
104
107
|
load 'morpheus/cli/jobs_command.rb'
|
@@ -172,7 +175,8 @@ module Morpheus
|
|
172
175
|
load 'morpheus/cli/projects_command.rb'
|
173
176
|
load 'morpheus/cli/backups_command.rb'
|
174
177
|
load 'morpheus/cli/backup_jobs_command.rb'
|
175
|
-
load 'morpheus/cli/
|
178
|
+
load 'morpheus/cli/catalog_item_types_command.rb' # catalog-types
|
179
|
+
load 'morpheus/cli/service_catalog_command.rb' # catalog
|
176
180
|
load 'morpheus/cli/usage_command.rb'
|
177
181
|
# add new commands here...
|
178
182
|
|
data/lib/morpheus/cli/apps.rb
CHANGED
@@ -72,9 +72,6 @@ class Morpheus::Cli::Apps
|
|
72
72
|
options[:owner] = val
|
73
73
|
end
|
74
74
|
opts.add_hidden_option('--created-by')
|
75
|
-
opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
|
76
|
-
options[:details] = true
|
77
|
-
end
|
78
75
|
opts.on('--pending-removal', "Include apps pending removal.") do
|
79
76
|
options[:showDeleted] = true
|
80
77
|
end
|
@@ -88,6 +85,9 @@ class Morpheus::Cli::Apps
|
|
88
85
|
opts.on('--status STATUS', "Filter by status.") do |val|
|
89
86
|
params['status'] = (params['status'] || []) + val.to_s.split(',').collect {|s| s.strip }.select {|s| s != "" }
|
90
87
|
end
|
88
|
+
opts.on('-a', '--details', "Display all details: memory and storage usage used / max values." ) do
|
89
|
+
options[:details] = true
|
90
|
+
end
|
91
91
|
build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
|
92
92
|
opts.footer = "List apps."
|
93
93
|
end
|
@@ -2185,26 +2185,6 @@ EOT
|
|
2185
2185
|
print reset
|
2186
2186
|
end
|
2187
2187
|
|
2188
|
-
def format_app_status(app, return_color=cyan)
|
2189
|
-
out = ""
|
2190
|
-
status_string = app['status'] || app['appStatus'] || ''
|
2191
|
-
if status_string == 'running'
|
2192
|
-
out = "#{green}#{status_string.upcase}#{return_color}"
|
2193
|
-
elsif status_string == 'provisioning'
|
2194
|
-
out = "#{cyan}#{status_string.upcase}#{cyan}"
|
2195
|
-
elsif status_string == 'stopped' or status_string == 'failed'
|
2196
|
-
out = "#{red}#{status_string.upcase}#{return_color}"
|
2197
|
-
elsif status_string == 'unknown'
|
2198
|
-
out = "#{yellow}#{status_string.upcase}#{return_color}"
|
2199
|
-
elsif status_string == 'warning' && app['instanceCount'].to_i == 0
|
2200
|
-
# show this instead of WARNING
|
2201
|
-
out = "#{cyan}EMPTY#{return_color}"
|
2202
|
-
else
|
2203
|
-
out = "#{yellow}#{status_string.upcase}#{return_color}"
|
2204
|
-
end
|
2205
|
-
out
|
2206
|
-
end
|
2207
|
-
|
2208
2188
|
def format_app_tiers(app)
|
2209
2189
|
out = ""
|
2210
2190
|
begin
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'morpheus/cli/cli_command'
|
2
2
|
require 'money' # ew, let's write our own
|
3
|
+
require 'time'
|
3
4
|
|
4
5
|
class Morpheus::Cli::BudgetsCommand
|
5
6
|
include Morpheus::Cli::CliCommand
|
@@ -21,76 +22,72 @@ class Morpheus::Cli::BudgetsCommand
|
|
21
22
|
params = {}
|
22
23
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
23
24
|
opts.banner = subcommand_usage()
|
24
|
-
|
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
|