morpheus-cli 6.3.4 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +4 -0
- data/lib/morpheus/api/catalog_item_types_interface.rb +1 -2
- data/lib/morpheus/api/hub_interface.rb +25 -0
- data/lib/morpheus/cli/commands/catalog_item_types_command.rb +3 -5
- data/lib/morpheus/cli/commands/hub.rb +215 -0
- data/lib/morpheus/cli/commands/jobs_command.rb +10 -6
- data/lib/morpheus/cli/commands/license.rb +151 -86
- data/lib/morpheus/cli/mixins/jobs_helper.rb +3 -0
- data/lib/morpheus/cli/mixins/print_helper.rb +80 -2
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/routes.rb +2 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 763ccc16e1c853b5d76e8466a21fd94abbd098615a884ff252f72015d5031338
|
4
|
+
data.tar.gz: c8ea0dcb3534ca6c3c557ce58763dfcef22ed59e7bdd33bde6eff00ee8513c1f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a68ef51fb607ce70ba7c93bdeb5ab637c741ce03ad24e987a63a2ba0db2d2ffac674bae2ff4aab02c543c76df93da178fe5a1d8e709df99444f2b4ba8e506945
|
7
|
+
data.tar.gz: 90b6702b8ebc4b75a0aa29da1802155dab2aca529c2d9538adb363f6c663b4241f632c7b22cc17e9e5b949598994e152107072ff19d7429bf944c69fcc4ad35e
|
data/Dockerfile
CHANGED
@@ -846,6 +846,10 @@ class Morpheus::APIClient
|
|
846
846
|
Morpheus::HealthInterface.new(common_interface_options).setopts(@options)
|
847
847
|
end
|
848
848
|
|
849
|
+
def hub
|
850
|
+
Morpheus::HubInterface.new(common_interface_options).setopts(@options)
|
851
|
+
end
|
852
|
+
|
849
853
|
def audit
|
850
854
|
Morpheus::AuditInterface.new(common_interface_options).setopts(@options)
|
851
855
|
end
|
@@ -8,8 +8,7 @@ class Morpheus::CatalogItemTypesInterface < Morpheus::RestInterface
|
|
8
8
|
|
9
9
|
# NOT json, multipart file upload, uses PUT update endpoint
|
10
10
|
def update_logo(id, logo_file, dark_logo_file=nil)
|
11
|
-
|
12
|
-
url = "#{base_path}/#{id}"
|
11
|
+
url = "#{base_path}/#{id}/update-logo"
|
13
12
|
headers = { :params => {}, :authorization => "Bearer #{@access_token}"}
|
14
13
|
payload = {}
|
15
14
|
payload["catalogItemType"] = {}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'morpheus/api/api_client'
|
2
|
+
|
3
|
+
class Morpheus::HubInterface < Morpheus::APIClient
|
4
|
+
|
5
|
+
def base_path
|
6
|
+
"/api/hub"
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(params={}, headers={})
|
10
|
+
execute(method: :get, url: "#{base_path}", params: params, headers: headers)
|
11
|
+
end
|
12
|
+
|
13
|
+
def usage(params={}, headers={})
|
14
|
+
execute(method: :get, url: "#{base_path}/usage", params: params, headers: headers)
|
15
|
+
end
|
16
|
+
|
17
|
+
def checkin(payload={}, params={}, headers={})
|
18
|
+
execute(method: :post, url: "#{base_path}/checkin", payload: payload, params: params, headers: headers)
|
19
|
+
end
|
20
|
+
|
21
|
+
def register(payload={}, params={}, headers={})
|
22
|
+
execute(method: :post, url: "#{base_path}/register", payload: payload, params: params, headers: headers)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -71,9 +71,6 @@ class Morpheus::Cli::CatalogItemTypesCommand
|
|
71
71
|
print cyan,"No catalog item types found.",reset,"\n"
|
72
72
|
else
|
73
73
|
list_columns = catalog_item_type_list_column_definitions.upcase_keys!
|
74
|
-
list_columns.delete("Blueprint")
|
75
|
-
list_columns.delete("Workflow")
|
76
|
-
list_columns.delete("Context")
|
77
74
|
#list_columns["Config"] = lambda {|it| truncate_string(it['config'], 100) }
|
78
75
|
print as_pretty_table(catalog_item_types, list_columns.upcase_keys!, options)
|
79
76
|
print_results_pagination(json_response)
|
@@ -701,12 +698,13 @@ EOT
|
|
701
698
|
"Description" => 'description',
|
702
699
|
"Type" => lambda {|it| format_catalog_type(it) },
|
703
700
|
"Visibility" => 'visibility',
|
704
|
-
"Layout Code" => 'layoutCode',
|
701
|
+
#"Layout Code" => 'layoutCode',
|
705
702
|
"Blueprint" => lambda {|it| it['blueprint'] ? it['blueprint']['name'] : nil },
|
706
703
|
"Workflow" => lambda {|it| it['workflow'] ? it['workflow']['name'] : nil },
|
707
|
-
"Context" => lambda {|it| it['context'] },
|
704
|
+
# "Context" => lambda {|it| it['context'] },
|
708
705
|
# "Content" => lambda {|it| it['content'] },
|
709
706
|
"Form Type" => lambda {|it| it['formType'] == 'form' ? "Form" : "Inputs" },
|
707
|
+
"Form" => lambda {|it| it['form'] ? it['form']['name'] : nil },
|
710
708
|
"Enabled" => lambda {|it| format_boolean(it['enabled']) },
|
711
709
|
"Featured" => lambda {|it| format_boolean(it['featured']) },
|
712
710
|
#"Config" => lambda {|it| it['config'] },
|
@@ -0,0 +1,215 @@
|
|
1
|
+
require 'morpheus/cli/cli_command'
|
2
|
+
|
3
|
+
class Morpheus::Cli::Hub
|
4
|
+
include Morpheus::Cli::CliCommand
|
5
|
+
|
6
|
+
set_command_description "View hub config and usage metrics."
|
7
|
+
set_command_name :'hub'
|
8
|
+
register_subcommands :get, {:usage => :usage_data}, :checkin, :register
|
9
|
+
|
10
|
+
# this is a hidden utility command
|
11
|
+
set_command_hidden
|
12
|
+
|
13
|
+
def connect(opts)
|
14
|
+
@api_client = establish_remote_appliance_connection(opts)
|
15
|
+
@hub_interface = @api_client.hub
|
16
|
+
end
|
17
|
+
|
18
|
+
def handle(args)
|
19
|
+
handle_subcommand(args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(args)
|
23
|
+
options = {}
|
24
|
+
params = {}
|
25
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
26
|
+
opts.banner = subcommand_usage()
|
27
|
+
build_standard_get_options(opts, options)
|
28
|
+
opts.footer = "View hub configuration."
|
29
|
+
end
|
30
|
+
optparse.parse!(args)
|
31
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
32
|
+
connect(options)
|
33
|
+
params.merge!(parse_query_options(options))
|
34
|
+
# execute api request
|
35
|
+
@hub_interface.setopts(options)
|
36
|
+
if options[:dry_run]
|
37
|
+
print_dry_run @hub_interface.dry.get(params)
|
38
|
+
return
|
39
|
+
end
|
40
|
+
json_response = @hub_interface.get(params)
|
41
|
+
# render response
|
42
|
+
render_response(json_response, options) do
|
43
|
+
print_h1 "Morpheus Hub Config", [], options
|
44
|
+
print_description_list({
|
45
|
+
"Hub URL" => lambda {|it| it['hub']['url'] },
|
46
|
+
"Appliance ID" => lambda {|it| it['hub']['applianceUniqueId'] },
|
47
|
+
"Registered" => lambda {|it| format_boolean(it['hub']['registered']) },
|
48
|
+
"Stats Reporting" => lambda {|it| format_boolean(it['hub']['reportStatus']) },
|
49
|
+
"Send Data" => lambda {|it| format_boolean(it['hub']['sendData']) },
|
50
|
+
}, json_response, options)
|
51
|
+
print reset,"\n"
|
52
|
+
end
|
53
|
+
return 0, nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def usage_data(args)
|
57
|
+
options = {}
|
58
|
+
params = {}
|
59
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
60
|
+
opts.banner = subcommand_usage()
|
61
|
+
build_standard_get_options(opts, options)
|
62
|
+
opts.footer = "View appliance usage data that is sent to the hub."
|
63
|
+
end
|
64
|
+
optparse.parse!(args)
|
65
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
66
|
+
connect(options)
|
67
|
+
params.merge!(parse_query_options(options))
|
68
|
+
# execute api request
|
69
|
+
@hub_interface.setopts(options)
|
70
|
+
if options[:dry_run]
|
71
|
+
print_dry_run @hub_interface.dry.usage(params)
|
72
|
+
return
|
73
|
+
end
|
74
|
+
json_response = @hub_interface.usage(params)
|
75
|
+
# render response
|
76
|
+
render_response(json_response, options) do
|
77
|
+
usage_data = json_response['data']
|
78
|
+
print_h1 "Morpheus Hub Usage", [], options
|
79
|
+
print_hub_usage_data_details(json_response, options)
|
80
|
+
print reset,"\n"
|
81
|
+
end
|
82
|
+
return 0, nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def checkin(args)
|
86
|
+
options = {}
|
87
|
+
params = {}
|
88
|
+
payload = {}
|
89
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
90
|
+
opts.banner = subcommand_usage()
|
91
|
+
opts.on( nil, '--asynchronous', "Execute the checkin asynchronously" ) do
|
92
|
+
params[:async] = true
|
93
|
+
end
|
94
|
+
build_standard_post_options(opts, options)
|
95
|
+
opts.footer = <<-EOT
|
96
|
+
Checkin with the hub.
|
97
|
+
This sends the current appliance usage data to the hub and
|
98
|
+
it is only done if the appliance is registered with the hub
|
99
|
+
and the appliance license has Stats Reporting enabled.
|
100
|
+
EOT
|
101
|
+
end
|
102
|
+
optparse.parse!(args)
|
103
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
104
|
+
connect(options)
|
105
|
+
payload = parse_payload(options)
|
106
|
+
# execute api request
|
107
|
+
@hub_interface.setopts(options)
|
108
|
+
if options[:dry_run]
|
109
|
+
print_dry_run @hub_interface.dry.checkin(payload, params)
|
110
|
+
return
|
111
|
+
end
|
112
|
+
json_response = @hub_interface.checkin(payload, params)
|
113
|
+
render_response(json_response, options) do
|
114
|
+
msg = json_response["msg"] || "Hub checkin complete"
|
115
|
+
print_green_success msg
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def register(args)
|
120
|
+
options = {}
|
121
|
+
params = {}
|
122
|
+
payload = {}
|
123
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
124
|
+
opts.banner = subcommand_usage()
|
125
|
+
opts.on( '-f', '--force', "Force registration" ) do
|
126
|
+
params[:force] = true
|
127
|
+
end
|
128
|
+
build_standard_post_options(opts, options)
|
129
|
+
opts.footer = <<-EOT
|
130
|
+
Register appliance with the hub to get a unique id for the appliance to checkin with.
|
131
|
+
The registration is skipped if the appliance is already registered.
|
132
|
+
The --force option can be used to execute this even if it is already registered.
|
133
|
+
EOT
|
134
|
+
end
|
135
|
+
optparse.parse!(args)
|
136
|
+
verify_args!(args:args, optparse:optparse, count:0)
|
137
|
+
connect(options)
|
138
|
+
payload = parse_payload(options)
|
139
|
+
# execute api request
|
140
|
+
@hub_interface.setopts(options)
|
141
|
+
if options[:dry_run]
|
142
|
+
print_dry_run @hub_interface.dry.register(payload, params)
|
143
|
+
return
|
144
|
+
end
|
145
|
+
json_response = @hub_interface.register(payload, params)
|
146
|
+
render_response(json_response, options) do
|
147
|
+
msg = json_response["msg"] || "Hub registration complete"
|
148
|
+
print_green_success msg
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
def print_hub_usage_data_details(json_response, options)
|
155
|
+
usage_data = json_response['data']
|
156
|
+
|
157
|
+
#print_h2 "Appliance Info", options
|
158
|
+
|
159
|
+
print_description_list({
|
160
|
+
"Appliance URL" => lambda {|it| it['applianceUrl'] },
|
161
|
+
"Appliance Version" => lambda {|it| it['applianceVersion'] },
|
162
|
+
# "Appliance Unique ID" => lambda {|it| ['hubUniqueId'] },
|
163
|
+
"Stats Version" => lambda {|it| it['statsVersion'] },
|
164
|
+
"Last Login" => lambda {|it| it['lastLoggedIn'] ? format_local_dt(it['lastLoggedIn'] / 1000).to_s : '' },
|
165
|
+
"Timestamp (ms)" => lambda {|it| it['ts'] ? format_local_dt(it['ts'] / 1000).to_s : '' },
|
166
|
+
}, usage_data, options)
|
167
|
+
|
168
|
+
|
169
|
+
# print_h2 "Appliance Usage", options
|
170
|
+
# if usage_data['appliance']
|
171
|
+
# # print_h2 "Appliance", options
|
172
|
+
# print_description_list({
|
173
|
+
# "Total Groups" => lambda {|it| it['appliance']['totalGroups'] },
|
174
|
+
# "Total Clouds" => lambda {|it| it['appliance']['totalClouds'] },
|
175
|
+
# }, usage_data, options)
|
176
|
+
# end
|
177
|
+
|
178
|
+
print_h2 "Appliance", options
|
179
|
+
# print_details_raw(usage_data['appliance'], options)
|
180
|
+
print_details(usage_data['appliance'], {
|
181
|
+
pretty: true,
|
182
|
+
column_format: {
|
183
|
+
totalMemory: lambda {|it| format_bytes it['totalMemory'] },
|
184
|
+
totalMemoryUsed: lambda {|it| format_bytes it['totalMemoryUsed'] },
|
185
|
+
totalStorage: lambda {|it| format_bytes it['totalStorage'] },
|
186
|
+
totalStorageUsed: lambda {|it| format_bytes it['totalStorageUsed'] },
|
187
|
+
managedMemoryTotal: lambda {|it| format_bytes it['managedMemoryTotal'] },
|
188
|
+
managedMemoryUsed: lambda {|it| format_bytes it['managedMemoryUsed'] },
|
189
|
+
managedStorageTotal: lambda {|it| format_bytes it['managedStorageTotal'] },
|
190
|
+
managedStorageUsed: lambda {|it| format_bytes it['managedStorageUsed'] },
|
191
|
+
unmanagedMemoryTotal: lambda {|it| format_bytes it['unmanagedMemoryTotal'] },
|
192
|
+
unmanagedMemoryUsed: lambda {|it| format_bytes it['unmanagedMemoryUsed'] },
|
193
|
+
unmanagedStorageTotal: lambda {|it| format_bytes it['unmanagedStorageTotal'] },
|
194
|
+
unmanagedStorageUsed: lambda {|it| format_bytes it['unmanagedStorageUsed'] },
|
195
|
+
cloudTypes: lambda {|it| it['cloudTypes'] ? it['cloudTypes'].collect {|row| "#{row['code']} (#{row['count']})"}.join(", ") : '' },
|
196
|
+
instanceTypes: lambda {|it| it['instanceTypes'] ? it['instanceTypes'].collect {|row| "#{row['code']} (#{row['count']})"}.join(", ") : '' },
|
197
|
+
provisionTypes: lambda {|it| it['provisionTypes'] ? it['provisionTypes'].collect {|row| "#{row['code']} (#{row['count']})"}.join(", ") : '' },
|
198
|
+
serverTypes: lambda {|it| it['serverTypes'] ? it['serverTypes'].collect {|row| "#{row['code']} (#{row['count']})"}.join(", ") : '' },
|
199
|
+
clusterTypes: lambda {|it| it['clusterTypes'] ? it['clusterTypes'].collect {|row| "#{row['code']} (#{row['count']})"}.join(", ") : '' },
|
200
|
+
lastLoggedIn: lambda {|it| it['lastLoggedIn'] ? format_local_dt(it['lastLoggedIn'] / 1000).to_s : '' },
|
201
|
+
ts: lambda {|it| it['lastLoggedIn'] ? format_local_dt(it['ts'] / 1000).to_s : '' },
|
202
|
+
}
|
203
|
+
})
|
204
|
+
|
205
|
+
# print_h2 "Clouds", options
|
206
|
+
# print_h2 "Hosts", options
|
207
|
+
# print_h2 "Instances", options
|
208
|
+
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
end
|
@@ -840,6 +840,9 @@ class Morpheus::Cli::JobsCommand
|
|
840
840
|
opts.on("--internal [true|false]", String, "Filters executions based on internal flag. Internal executions are excluded by default.") do |val|
|
841
841
|
params["internalOnly"] = (val.to_s != "false")
|
842
842
|
end
|
843
|
+
opts.on("--automation [true|false]", String, "Filters executions based on automation flag. Non-automation executions include ansible and kubernetes job types.") do |val|
|
844
|
+
params["automation"] = (val.to_s != "false")
|
845
|
+
end
|
843
846
|
build_standard_list_options(opts, options, [:details])
|
844
847
|
opts.footer = "List job executions.\n" +
|
845
848
|
"[job] is optional. Job ID or name to filter executions."
|
@@ -876,16 +879,15 @@ class Morpheus::Cli::JobsCommand
|
|
876
879
|
if params["internalOnly"]
|
877
880
|
subtitles << "internalOnly: #{params['internalOnly']}"
|
878
881
|
end
|
882
|
+
if !params["automation"].nil?
|
883
|
+
subtitles << "automation: #{params['automation']}"
|
884
|
+
end
|
879
885
|
print_h1 title, subtitles, options
|
880
886
|
print_job_executions(job_executions, options)
|
881
|
-
print_results_pagination(json_response)
|
887
|
+
print_results_pagination(json_response) unless job_executions.size == 0
|
882
888
|
print reset,"\n"
|
883
889
|
end
|
884
|
-
|
885
|
-
return 3, "no executions found"
|
886
|
-
else
|
887
|
-
return 0, nil
|
888
|
-
end
|
890
|
+
return 0, nil
|
889
891
|
end
|
890
892
|
|
891
893
|
def get_execution(args)
|
@@ -968,7 +970,9 @@ class Morpheus::Cli::JobsCommand
|
|
968
970
|
end
|
969
971
|
if event_data[:error] && event_data[:error].strip.length > 0
|
970
972
|
print_h2 "Error"
|
973
|
+
print red
|
971
974
|
print event['message'] || event['error']
|
975
|
+
print reset
|
972
976
|
end
|
973
977
|
print reset,"\n"
|
974
978
|
return 0
|
@@ -17,7 +17,6 @@ class Morpheus::Cli::License
|
|
17
17
|
|
18
18
|
def connect(opts)
|
19
19
|
@api_client = establish_remote_appliance_connection(opts)
|
20
|
-
@api_client = @api_client
|
21
20
|
@license_interface = @api_client.license
|
22
21
|
end
|
23
22
|
|
@@ -40,78 +39,26 @@ class Morpheus::Cli::License
|
|
40
39
|
raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
|
41
40
|
end
|
42
41
|
connect(options)
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
42
|
+
|
43
|
+
# make api request
|
44
|
+
@license_interface.setopts(options)
|
45
|
+
if options[:dry_run]
|
46
|
+
print_dry_run @license_interface.dry.get()
|
47
|
+
return 0, nil
|
48
|
+
end
|
49
|
+
json_response = @license_interface.get()
|
50
|
+
render_response(json_response, options) do
|
52
51
|
license = json_response['license']
|
53
52
|
current_usage = json_response['currentUsage'] || {}
|
54
|
-
|
55
|
-
used_storage = current_usage['storage']
|
56
|
-
used_workloads = current_usage['workloads']
|
57
|
-
|
58
|
-
# Determine exit status and any error conditions for the command
|
59
|
-
exit_code = 0
|
60
|
-
err = nil
|
53
|
+
print_h1 "License"
|
61
54
|
if license.nil?
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
return exit_code, err if render_result
|
69
|
-
|
70
|
-
if options[:quiet]
|
71
|
-
return exit_code, err
|
72
|
-
end
|
73
|
-
|
74
|
-
if exit_code != 0
|
75
|
-
print_error red, err.to_s, reset, "\n"
|
76
|
-
return exit_code, err
|
55
|
+
print "#{yellow}No license currently installed#{reset}\n\n"
|
56
|
+
else
|
57
|
+
print_license_details(json_response)
|
58
|
+
print_h2 "Current Usage", [], options
|
59
|
+
print_license_usage(license, current_usage)
|
60
|
+
print reset,"\n"
|
77
61
|
end
|
78
|
-
|
79
|
-
# render output
|
80
|
-
|
81
|
-
print_h1 "License"
|
82
|
-
max_memory = "Unlimited"
|
83
|
-
max_storage = "Unlimited"
|
84
|
-
max_memory = Filesize.from("#{license['maxMemory']} B").pretty if license['maxMemory'].to_i != 0
|
85
|
-
max_storage = Filesize.from("#{license['maxStorage']} B").pretty if license['maxStorage'].to_i != 0
|
86
|
-
used_memory = Filesize.from("#{used_memory} B").pretty if used_memory.to_i != 0
|
87
|
-
used_storage = Filesize.from("#{used_storage} B").pretty if used_storage.to_i != 0
|
88
|
-
#used_workloads = "n/a" if used_workloads.nil?
|
89
|
-
max_workloads = license["maxInstances"].to_i == 0 ? 'Unlimited' : license["maxInstances"]
|
90
|
-
print cyan
|
91
|
-
description_cols = {
|
92
|
-
"Account" => 'accountName',
|
93
|
-
"Product Tier" => lambda {|it| format_product_tier(it) },
|
94
|
-
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
95
|
-
"End Date" => lambda {|it|
|
96
|
-
if it['endDate']
|
97
|
-
format_local_dt(it['endDate']).to_s + ' (' + format_duration(Time.now, it['endDate']).to_s + ')'
|
98
|
-
else
|
99
|
-
'Never'
|
100
|
-
end
|
101
|
-
},
|
102
|
-
"Memory" => lambda {|it| "#{used_memory} / #{max_memory}" },
|
103
|
-
"Storage" => lambda {|it| "#{used_storage} / #{max_storage}" },
|
104
|
-
"Workloads" => lambda {|it| "#{used_workloads} / #{max_workloads}" },
|
105
|
-
"Hard Limit" => lambda {|it| it[""] == false ? 'Yes' : 'No' },
|
106
|
-
}
|
107
|
-
print_description_list(description_cols, license)
|
108
|
-
print reset,"\n"
|
109
|
-
|
110
|
-
return exit_code, err
|
111
|
-
|
112
|
-
rescue RestClient::Exception => e
|
113
|
-
print_rest_exception(e, options)
|
114
|
-
return false
|
115
62
|
end
|
116
63
|
end
|
117
64
|
|
@@ -126,6 +73,8 @@ class Morpheus::Cli::License
|
|
126
73
|
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
127
74
|
opts.banner = subcommand_usage("[key]")
|
128
75
|
build_common_options(opts, options, [:json, :dry_run, :remote])
|
76
|
+
opts.footer = "Install a new license key.\n" +
|
77
|
+
"This will potentially change the enabled features and capabilities of your appliance."
|
129
78
|
end
|
130
79
|
optparse.parse!(args)
|
131
80
|
if args.count > 1
|
@@ -192,7 +141,7 @@ class Morpheus::Cli::License
|
|
192
141
|
end
|
193
142
|
json_response = @license_interface.test(key)
|
194
143
|
license = json_response['license']
|
195
|
-
|
144
|
+
current_usage = json_response['currentUsage'] || {}
|
196
145
|
exit_code, err = 0, nil
|
197
146
|
if license.nil?
|
198
147
|
err = "Unable to decode license."
|
@@ -213,22 +162,9 @@ class Morpheus::Cli::License
|
|
213
162
|
|
214
163
|
# all good
|
215
164
|
print_h1 "License"
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
max_storage = Filesize.from("#{license['maxStorage']} B").pretty if license['maxStorage'].to_i != 0
|
220
|
-
print cyan
|
221
|
-
description_cols = {
|
222
|
-
"Account" => 'accountName',
|
223
|
-
"Product Tier" => lambda {|it| format_product_tier(it) },
|
224
|
-
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
225
|
-
"End Date" => lambda {|it| format_local_dt(it['endDate']) },
|
226
|
-
"Max Memory" => lambda {|it| max_memory },
|
227
|
-
"Max Storage" => lambda {|it| max_storage },
|
228
|
-
"Max Instances" => lambda {|it| it["maxInstances"].to_i == 0 ? 'Unlimited' : it["maxInstances"] },
|
229
|
-
"Hard Limit" => lambda {|it| it[""] == false ? 'Yes' : 'No' },
|
230
|
-
}
|
231
|
-
print_description_list(description_cols, license)
|
165
|
+
print_license_details(json_response)
|
166
|
+
print_h2 "License Usage", [], options
|
167
|
+
print_license_usage(license, current_usage)
|
232
168
|
print reset,"\n"
|
233
169
|
return exit_code, err
|
234
170
|
rescue RestClient::Exception => e
|
@@ -299,4 +235,133 @@ class Morpheus::Cli::License
|
|
299
235
|
product_tier.to_s.capitalize
|
300
236
|
end
|
301
237
|
end
|
238
|
+
|
239
|
+
def format_limit_type(license)
|
240
|
+
limit_type = license['limitType'] || 'workload'
|
241
|
+
if limit_type == 'standard' || limit_type == 'metrics'
|
242
|
+
'Standard'
|
243
|
+
else
|
244
|
+
limit_type.to_s.capitalize
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def format_limit(max, used)
|
249
|
+
formatted_max = max.to_i > 0 ? format_number(max) : "Unlimited"
|
250
|
+
if used
|
251
|
+
formatted_used = format_number(used)
|
252
|
+
"#{formatted_used} / #{formatted_max}"
|
253
|
+
else
|
254
|
+
"#{formatted_max}"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def format_limit_bytes(max, used)
|
259
|
+
formatted_max = max.to_i > 0 ? format_bytes(max) : "Unlimited"
|
260
|
+
if used
|
261
|
+
formatted_used = format_bytes(used)
|
262
|
+
"#{formatted_used} / #{formatted_max}"
|
263
|
+
else
|
264
|
+
"#{formatted_max}"
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def print_license_details(json_response)
|
269
|
+
license = json_response['license']
|
270
|
+
current_usage = json_response['currentUsage'] || {}
|
271
|
+
description_cols = {
|
272
|
+
"Account" => 'accountName',
|
273
|
+
"Product Tier" => lambda {|it| format_product_tier(it) },
|
274
|
+
"Start Date" => lambda {|it| format_local_dt(it['startDate']) },
|
275
|
+
"End Date" => lambda {|it|
|
276
|
+
if it['endDate']
|
277
|
+
format_local_dt(it['endDate']).to_s + ' (' + format_duration(Time.now, it['endDate']).to_s + ')'
|
278
|
+
else
|
279
|
+
'Never'
|
280
|
+
end
|
281
|
+
},
|
282
|
+
"Multi-Tenant" => lambda {|it| format_boolean it["multiTenant"] },
|
283
|
+
"White Label" => lambda {|it| format_boolean it["whitelabel"] },
|
284
|
+
"Stats Reporting" => lambda {|it| format_boolean it["reportStatus"] },
|
285
|
+
"Hard Limit" => lambda {|it| format_boolean it["hardLimit"] },
|
286
|
+
"Limit Type" => lambda {|it| format_limit_type(it) },
|
287
|
+
}
|
288
|
+
|
289
|
+
description_cols.delete("Multi-Tenant") if !license['multiTenant']
|
290
|
+
description_cols.delete("White Label") if !license['whitelabel']
|
291
|
+
|
292
|
+
if license['zoneTypes'] && license['zoneTypes'].size > 0
|
293
|
+
description_cols["Included Clouds"] = lambda {|it| it['zoneTypes'].join(', ') }
|
294
|
+
elsif license['zoneTypesExcluded'] && license['zoneTypesExcluded'].size > 0
|
295
|
+
description_cols["Excluded Clouds"] = lambda {|it| it['zoneTypesExcluded'].join(', ') }
|
296
|
+
end
|
297
|
+
print_description_list(description_cols, license)
|
298
|
+
end
|
299
|
+
|
300
|
+
def print_license_usage(license, current_usage)
|
301
|
+
unlimited_label = "Unlimited"
|
302
|
+
# unlimited_label = "∞"
|
303
|
+
if license['limitType'] == 'standard'
|
304
|
+
# new standard metrics limit
|
305
|
+
used_managed_servers = current_usage['managedServers']
|
306
|
+
used_discovered_servers = current_usage['discoveredServers']
|
307
|
+
used_hosts = current_usage['hosts']
|
308
|
+
used_mvm = current_usage['mvm']
|
309
|
+
used_iac = current_usage['iac']
|
310
|
+
used_xaas = current_usage['xaas']
|
311
|
+
used_executions = current_usage['executions']
|
312
|
+
used_distributed_workers = current_usage['distributedWorkers']
|
313
|
+
used_discovered_objects = current_usage['discoveredObjects']
|
314
|
+
|
315
|
+
|
316
|
+
max_managed_servers = license["maxManagedServers"].to_i
|
317
|
+
max_discovered_servers = license['maxDiscoveredServers'].to_i
|
318
|
+
max_hosts = license['maxHosts'].to_i
|
319
|
+
max_mvm = license['maxMvm'].to_i
|
320
|
+
max_iac = license['maxIac'].to_i
|
321
|
+
max_xaas = license['maxXaas'].to_i
|
322
|
+
max_executions = license['maxExecutions'].to_i
|
323
|
+
max_distributed_workers = license['maxDistributedWorkers'].to_i
|
324
|
+
max_discovered_objects = license['maxDiscoveredObjects'].to_i
|
325
|
+
label_width = 20
|
326
|
+
chart_opts = {max_bars: 20, unlimited_label: '0%', percent_sigdig: 0}
|
327
|
+
out = ""
|
328
|
+
out << cyan + "Managed Servers".rjust(label_width, ' ') + ": " + generate_usage_bar(used_managed_servers, max_managed_servers, chart_opts) + cyan + used_managed_servers.to_s.rjust(8, ' ') + " / " + (max_managed_servers.to_i > 0 ? max_managed_servers.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
329
|
+
out << cyan + "Discovered Servers".rjust(label_width, ' ') + ": " + generate_usage_bar(used_discovered_servers, max_discovered_servers, chart_opts) + cyan + used_discovered_servers.to_s.rjust(8, ' ') + " / " + (max_discovered_servers.to_i > 0 ? max_discovered_servers.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
330
|
+
out << cyan + "Hosts".rjust(label_width, ' ') + ": " + generate_usage_bar(used_hosts, max_hosts, chart_opts) + cyan + used_hosts.to_s.rjust(8, ' ') + " / " + (max_hosts.to_i > 0 ? max_hosts.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
331
|
+
out << cyan + "MVM Hosts".rjust(label_width, ' ') + ": " + generate_usage_bar(used_mvm, max_mvm, chart_opts) + cyan + used_mvm.to_s.rjust(8, ' ') + " / " + (max_mvm.to_i > 0 ? max_mvm.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
332
|
+
out << cyan + "Iac Deployments".rjust(label_width, ' ') + ": " + generate_usage_bar(used_iac, max_iac, chart_opts) + cyan + used_iac.to_s.rjust(8, ' ') + " / " + (max_iac.to_i > 0 ? max_iac.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
333
|
+
out << cyan + "Xaas Instances".rjust(label_width, ' ') + ": " + generate_usage_bar(used_xaas, max_xaas, chart_opts) + cyan + used_xaas.to_s.rjust(8, ' ') + " / " + (max_xaas.to_i > 0 ? max_xaas.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
334
|
+
out << cyan + "Executions".rjust(label_width, ' ') + ": " + generate_usage_bar(used_executions, max_executions, chart_opts) + cyan + used_executions.to_s.rjust(8, ' ') + " / " + (max_executions.to_i > 0 ? max_executions.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
335
|
+
out << cyan + "Distributed Workers".rjust(label_width, ' ') + ": " + generate_usage_bar(used_distributed_workers, max_distributed_workers, chart_opts) + cyan + used_distributed_workers.to_s.rjust(8, ' ') + " / " + (max_distributed_workers.to_i > 0 ? max_distributed_workers.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
336
|
+
#out << cyan + "Discovered Objects".rjust(label_width, ' ') + ": " + generate_usage_bar(used_discovered_objects, max_discovered_objects, chart_opts) + cyan + used_discovered_objects.to_s.rjust(8, ' ') + " / " + (max_discovered_objects.to_i > 0 ? max_discovered_objects.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
337
|
+
print out
|
338
|
+
else
|
339
|
+
# old workloads limit
|
340
|
+
used_memory = current_usage['memory']
|
341
|
+
used_storage = current_usage['storage']
|
342
|
+
used_workloads = current_usage['workloads']
|
343
|
+
max_memory = license['maxMemory'].to_i
|
344
|
+
max_storage = license['maxStorage'].to_i
|
345
|
+
max_workloads = license['maxInstances'].to_i
|
346
|
+
label_width = 15
|
347
|
+
chart_opts = {max_bars: 20, unlimited_label: '0%', percent_sigdig: 0}
|
348
|
+
out = ""
|
349
|
+
out << cyan + "Workloads".rjust(label_width, ' ') + ": " + generate_usage_bar(used_workloads, max_workloads, chart_opts) + cyan + used_workloads.to_s.rjust(15, ' ') + " / " + (max_workloads.to_i > 0 ? max_workloads.to_s : unlimited_label).to_s.ljust(15, ' ') + "\n"
|
350
|
+
out << cyan + "Memory".rjust(label_width, ' ') + ": " + generate_usage_bar(used_memory, max_memory, chart_opts) + cyan + Filesize.from("#{used_memory} B").pretty.strip.rjust(15, ' ') + " / " + (max_memory.to_i > 0 ? Filesize.from("#{max_memory} B").pretty : unlimited_label).strip.ljust(15, ' ') + "\n"
|
351
|
+
out << cyan + "Storage".rjust(label_width, ' ') + ": " + generate_usage_bar(used_storage, max_storage, chart_opts) + cyan + Filesize.from("#{used_storage} B").pretty.strip.rjust(15, ' ') + " / " + (max_storage.to_i > 0 ? Filesize.from("#{max_storage} B").pretty : unlimited_label).strip.ljust(15, ' ') + "\n"
|
352
|
+
print out
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# todo: use this to dry print_license_usage
|
357
|
+
def format_limit(label, used, max, opts={})
|
358
|
+
unlimited_label = "Unlimited"
|
359
|
+
# unlimited_label = "∞"
|
360
|
+
label_width = opts[:label_width] || 15
|
361
|
+
chart_opts = {max_bars: 20, unlimited_label: '0%', percent_sigdig: 0}
|
362
|
+
chart_opts.merge!(opts[:chart_opts]) if opts[:chart_opts]
|
363
|
+
cyan + label.rjust(label_width, ' ') + ": " + generate_usage_bar(used, max, chart_opts) + cyan + used.to_s.rjust(label_width, ' ') + " / " + (max.to_i > 0 ? max.to_s : unlimited_label).to_s.ljust(label_width, ' ') + "\n"
|
364
|
+
end
|
365
|
+
|
366
|
+
|
302
367
|
end
|
@@ -84,6 +84,7 @@ module Morpheus::Cli::JobsHelper
|
|
84
84
|
end
|
85
85
|
if process_data[:error] && process_data[:error].strip.length > 0
|
86
86
|
print_h2 "Error"
|
87
|
+
print red
|
87
88
|
print (process['message'] || process['error']).to_s.strip
|
88
89
|
print reset,"\n"
|
89
90
|
end
|
@@ -129,6 +130,8 @@ module Morpheus::Cli::JobsHelper
|
|
129
130
|
out << "#{green}#{status_string.upcase}"
|
130
131
|
elsif ['error', 'offline', 'failed', 'failure'].include?(status_string)
|
131
132
|
out << "#{red}#{status_string.upcase}"
|
133
|
+
elsif ['running'].include?(status_string)
|
134
|
+
out << "#{cyan}#{status_string.upcase}"
|
132
135
|
else
|
133
136
|
out << "#{yellow}#{status_string.upcase}"
|
134
137
|
end
|
@@ -514,7 +514,8 @@ module Morpheus::Cli::PrintHelper
|
|
514
514
|
else
|
515
515
|
percent = ((used_value.to_f / max_value.to_f) * 100)
|
516
516
|
end
|
517
|
-
|
517
|
+
unlimited_label = opts[:unlimited_label] || "n/a"
|
518
|
+
percent_label = ((used_value.nil? || max_value.to_f == 0.0) ? unlimited_label : "#{percent.round(percent_sigdig)}%").rjust(6, ' ')
|
518
519
|
bar_display = ""
|
519
520
|
if percent > 100
|
520
521
|
max_bars.times { bars << "|" }
|
@@ -1000,6 +1001,83 @@ module Morpheus::Cli::PrintHelper
|
|
1000
1001
|
print as_description_list(obj, columns, opts)
|
1001
1002
|
end
|
1002
1003
|
|
1004
|
+
def print_pretty_details(obj, opts={})
|
1005
|
+
print as_pretty_details(obj, opts)
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def as_pretty_details(obj, opts={})
|
1009
|
+
print as_pretty_details(obj, opts.merge({pretty: true}))
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def print_details(obj, opts={})
|
1013
|
+
print as_details(obj, opts)
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def as_details(obj, opts={})
|
1017
|
+
#return "" if obj.nil?
|
1018
|
+
columns = {}
|
1019
|
+
keys = obj.keys
|
1020
|
+
if opts[:sort]
|
1021
|
+
keys.sort!
|
1022
|
+
end
|
1023
|
+
keys.each do |key|
|
1024
|
+
display_proc = nil
|
1025
|
+
if opts[:column_format] && opts[:column_format].key?(key.to_sym)
|
1026
|
+
display_proc = opts[:column_format][key.to_sym]
|
1027
|
+
else
|
1028
|
+
display_proc = lambda {|it| format_detail_value(it[key]) }
|
1029
|
+
end
|
1030
|
+
label = opts[:pretty] ? key.to_s.titleize : key.to_s
|
1031
|
+
columns[label] = display_proc
|
1032
|
+
end
|
1033
|
+
as_description_list(obj, columns, opts)
|
1034
|
+
end
|
1035
|
+
|
1036
|
+
def format_detail_value(value)
|
1037
|
+
rtn = value
|
1038
|
+
if value.is_a?(Array)
|
1039
|
+
# show the first three objects
|
1040
|
+
rtn = value.first(3).collect { |row| format_abbreviated_value(row) }.join(", ")
|
1041
|
+
elsif value.is_a?(Hash)
|
1042
|
+
# show the first three properties
|
1043
|
+
keys = values.keys.select { |key| value[key].is_a?(Numeric) || value[key].is_a?(String) }.first(3)
|
1044
|
+
rtn = keys.collect { |key|
|
1045
|
+
"#{key.to_s.titleize}: #{format_abbreviated_value(value[key])}"
|
1046
|
+
}.join(", ")
|
1047
|
+
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
1048
|
+
rtn = format_boolean(value)
|
1049
|
+
else
|
1050
|
+
rtn = value.to_s
|
1051
|
+
end
|
1052
|
+
return rtn
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
def format_abbreviated_value(value)
|
1056
|
+
if value.is_a?(Hash)
|
1057
|
+
if value.keys.size == 0
|
1058
|
+
"{}"
|
1059
|
+
else
|
1060
|
+
# show the first three properties
|
1061
|
+
keys = value.keys.select { |key| value[key].is_a?(Numeric) || value[key].is_a?(String) }.first(3)
|
1062
|
+
keys.collect { |key|
|
1063
|
+
"#{key.to_s.titleize}: #{value[key]}"
|
1064
|
+
}.join(", ")
|
1065
|
+
end
|
1066
|
+
elsif value.is_a?(Array)
|
1067
|
+
if value.size == 0
|
1068
|
+
return "[]"
|
1069
|
+
elsif obj.size == 1
|
1070
|
+
"[(#{obj.size})]"
|
1071
|
+
else
|
1072
|
+
"[(#{obj.size})]"
|
1073
|
+
end
|
1074
|
+
elsif value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
1075
|
+
format_boolean(value)
|
1076
|
+
else
|
1077
|
+
value.to_s
|
1078
|
+
end
|
1079
|
+
end
|
1080
|
+
|
1003
1081
|
# build_column_definitions constructs an Array of column definitions (OpenStruct)
|
1004
1082
|
# Each column is defined by a label (String), and a display_method (Proc)
|
1005
1083
|
#
|
@@ -1059,7 +1137,7 @@ module Morpheus::Cli::PrintHelper
|
|
1059
1137
|
column_def.display_method = lambda {|data| get_object_value(data, v) }
|
1060
1138
|
elsif v.is_a?(Proc)
|
1061
1139
|
column_def.display_method = v
|
1062
|
-
elsif v.is_a?(Hash) || v.is_a?(
|
1140
|
+
elsif v.is_a?(Hash) || v.is_a?(OpenStruct)
|
1063
1141
|
if v[:display_name] || v[:label]
|
1064
1142
|
column_def.label = v[:display_name] || v[:label]
|
1065
1143
|
end
|
data/lib/morpheus/cli/version.rb
CHANGED
data/lib/morpheus/routes.rb
CHANGED
@@ -223,9 +223,10 @@ module Morpheus::Routes
|
|
223
223
|
path = "/admin/settings/environments"
|
224
224
|
when "software-licenses"
|
225
225
|
path = "/admin/settings/software-licenses"
|
226
|
-
when "license"
|
226
|
+
when "license","licenses"
|
227
227
|
path = "/admin/settings/#!license"
|
228
228
|
end
|
229
|
+
# todo: this is weird, fix it so "view license matches license before software-licenses without needing the above alias..
|
229
230
|
# dasherize path and attempt to match the plural first
|
230
231
|
plural_path = path.pluralize
|
231
232
|
paths = [path.dasherize]
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: morpheus-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 7.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Estes
|
@@ -11,7 +11,7 @@ authors:
|
|
11
11
|
autorequire:
|
12
12
|
bindir: bin
|
13
13
|
cert_chain: []
|
14
|
-
date: 2024-
|
14
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
17
17
|
name: bundler
|
@@ -239,6 +239,7 @@ files:
|
|
239
239
|
- lib/morpheus/api/guidance_interface.rb
|
240
240
|
- lib/morpheus/api/guidance_settings_interface.rb
|
241
241
|
- lib/morpheus/api/health_interface.rb
|
242
|
+
- lib/morpheus/api/hub_interface.rb
|
242
243
|
- lib/morpheus/api/image_builder_boot_scripts_interface.rb
|
243
244
|
- lib/morpheus/api/image_builder_image_builds_interface.rb
|
244
245
|
- lib/morpheus/api/image_builder_interface.rb
|
@@ -426,6 +427,7 @@ files:
|
|
426
427
|
- lib/morpheus/cli/commands/health_command.rb
|
427
428
|
- lib/morpheus/cli/commands/history_command.rb
|
428
429
|
- lib/morpheus/cli/commands/hosts.rb
|
430
|
+
- lib/morpheus/cli/commands/hub.rb
|
429
431
|
- lib/morpheus/cli/commands/image_builder_command.rb
|
430
432
|
- lib/morpheus/cli/commands/instance_types.rb
|
431
433
|
- lib/morpheus/cli/commands/instances.rb
|