morpheus-cli 8.0.13 → 8.1.1
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/bin/morpheus +7 -4
- data/lib/morpheus/api/api_client.rb +8 -0
- data/lib/morpheus/api/image_builder_image_builds_interface.rb +19 -8
- data/lib/morpheus/api/roles_interface.rb +14 -0
- data/lib/morpheus/api/system_types_interface.rb +13 -0
- data/lib/morpheus/api/systems_interface.rb +23 -0
- data/lib/morpheus/cli/commands/backups_command.rb +31 -0
- data/lib/morpheus/cli/commands/image_builder_command.rb +18 -16
- data/lib/morpheus/cli/commands/library_option_lists_command.rb +10 -2
- data/lib/morpheus/cli/commands/roles.rb +333 -38
- data/lib/morpheus/cli/commands/service_plans_command.rb +7 -0
- data/lib/morpheus/cli/commands/storage_providers_command.rb +6 -2
- data/lib/morpheus/cli/commands/systems.rb +599 -0
- data/lib/morpheus/cli/commands/tenants_command.rb +12 -3
- data/lib/morpheus/cli/mixins/accounts_helper.rb +1 -0
- data/lib/morpheus/cli/mixins/backups_helper.rb +29 -1
- data/lib/morpheus/cli/version.rb +1 -1
- metadata +5 -2
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
require 'morpheus/cli/cli_command'
|
|
2
|
+
|
|
3
|
+
class Morpheus::Cli::Systems
|
|
4
|
+
include Morpheus::Cli::CliCommand
|
|
5
|
+
include Morpheus::Cli::RestCommand
|
|
6
|
+
|
|
7
|
+
set_command_name :systems
|
|
8
|
+
set_command_description "View and manage systems."
|
|
9
|
+
register_subcommands :list, :get, :add, :update, :remove, :'add-uninitialized', {:'initialize' => 'exec_initialize'}, {:'validate' => 'exec_validate'}
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
# Systems API uses lowercase keys in payloads.
|
|
14
|
+
def system_object_key
|
|
15
|
+
'system'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def system_list_key
|
|
19
|
+
'systems'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def system_list_column_definitions(options)
|
|
23
|
+
{
|
|
24
|
+
"ID" => 'id',
|
|
25
|
+
"Name" => 'name',
|
|
26
|
+
"Type" => lambda {|it| it['type'] ? it['type']['name'] : '' },
|
|
27
|
+
"Layout" => lambda {|it| it['layout'] ? it['layout']['name'] : '' },
|
|
28
|
+
"Status" => 'status',
|
|
29
|
+
"Enabled" => lambda {|it| format_boolean(it['enabled']) },
|
|
30
|
+
"Date Created" => lambda {|it| format_local_dt(it['dateCreated']) }
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def system_column_definitions(options)
|
|
35
|
+
{
|
|
36
|
+
"ID" => 'id',
|
|
37
|
+
"Name" => 'name',
|
|
38
|
+
"Description" => 'description',
|
|
39
|
+
"Status" => 'status',
|
|
40
|
+
"Status Message" => 'statusMessage',
|
|
41
|
+
"Enabled" => lambda {|it| format_boolean(it['enabled']) },
|
|
42
|
+
"External ID" => 'externalId',
|
|
43
|
+
"Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
|
|
44
|
+
"Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def render_response_for_get(json_response, options)
|
|
49
|
+
render_response(json_response, options, rest_object_key) do
|
|
50
|
+
record = json_response[rest_object_key] || json_response
|
|
51
|
+
print_h1 rest_label, [], options
|
|
52
|
+
print cyan
|
|
53
|
+
print_description_list(rest_column_definitions(options), record, options)
|
|
54
|
+
print reset,"\n"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add(args)
|
|
59
|
+
options = {}
|
|
60
|
+
params = {}
|
|
61
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
62
|
+
opts.banner = subcommand_usage("[name]")
|
|
63
|
+
opts.on('--name NAME', String, "System Name") do |val|
|
|
64
|
+
params['name'] = val.to_s
|
|
65
|
+
end
|
|
66
|
+
opts.on('--description [TEXT]', String, "Description") do |val|
|
|
67
|
+
params['description'] = val.to_s
|
|
68
|
+
end
|
|
69
|
+
opts.on('--type TYPE', String, "System Type ID or name") do |val|
|
|
70
|
+
params['type'] = val
|
|
71
|
+
end
|
|
72
|
+
opts.on('--layout LAYOUT', String, "System Layout ID or name") do |val|
|
|
73
|
+
params['layout'] = val
|
|
74
|
+
end
|
|
75
|
+
build_standard_add_options(opts, options)
|
|
76
|
+
opts.footer = "Create a new system.\n[name] is optional and can be passed as the first argument."
|
|
77
|
+
end
|
|
78
|
+
optparse.parse!(args)
|
|
79
|
+
connect(options)
|
|
80
|
+
|
|
81
|
+
payload = nil
|
|
82
|
+
if options[:payload]
|
|
83
|
+
payload = options[:payload]
|
|
84
|
+
payload[rest_object_key] ||= {}
|
|
85
|
+
payload[rest_object_key].deep_merge!(params) unless params.empty?
|
|
86
|
+
payload[rest_object_key]['name'] ||= args[0] if args[0]
|
|
87
|
+
else
|
|
88
|
+
system_payload = {}
|
|
89
|
+
|
|
90
|
+
# Name
|
|
91
|
+
system_payload['name'] = params['name'] || args[0]
|
|
92
|
+
if !system_payload['name'] && !options[:no_prompt]
|
|
93
|
+
system_payload['name'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Name', 'required' => true}], options[:options], @api_client, {})['name']
|
|
94
|
+
end
|
|
95
|
+
raise_command_error "Name is required.\n#{optparse}" if system_payload['name'].to_s.empty?
|
|
96
|
+
|
|
97
|
+
# Description
|
|
98
|
+
if !params['description'] && !options[:no_prompt]
|
|
99
|
+
system_payload['description'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'required' => false}], options[:options], @api_client, {})['description']
|
|
100
|
+
else
|
|
101
|
+
system_payload['description'] = params['description']
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Type
|
|
105
|
+
available_types = system_types_for_dropdown
|
|
106
|
+
type_val = params['type']
|
|
107
|
+
if type_val
|
|
108
|
+
type_id = type_val =~ /\A\d+\Z/ ? type_val.to_i : available_types.find { |t| t['name'] == type_val || t['code'] == type_val }&.dig('id')
|
|
109
|
+
raise_command_error "System type not found: #{type_val}" unless type_id
|
|
110
|
+
system_payload['type'] = {'id' => type_id}
|
|
111
|
+
elsif !options[:no_prompt]
|
|
112
|
+
if available_types.empty?
|
|
113
|
+
raise_command_error "No system types found."
|
|
114
|
+
else
|
|
115
|
+
print cyan, "Available System Types\n", reset
|
|
116
|
+
available_types.each do |t|
|
|
117
|
+
print " #{t['id']}) #{t['name']}#{t['code'] ? " (#{t['code']})" : ''}\n"
|
|
118
|
+
end
|
|
119
|
+
selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'type' => 'text', 'fieldLabel' => 'System Type ID', 'required' => true}], options[:options], @api_client, {})['type']
|
|
120
|
+
type_id = available_types.find { |t| t['id'].to_s == selected.to_s }&.dig('id')
|
|
121
|
+
raise_command_error "Invalid system type id: #{selected}" unless type_id
|
|
122
|
+
system_payload['type'] = {'id' => type_id}
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Layout
|
|
127
|
+
available_layouts = system_layouts_for_dropdown(system_payload.dig('type', 'id'))
|
|
128
|
+
layout_val = params['layout']
|
|
129
|
+
if layout_val
|
|
130
|
+
layout_id = layout_val =~ /\A\d+\Z/ ? layout_val.to_i : available_layouts.find { |l| l['name'] == layout_val || l['code'] == layout_val }&.dig('id')
|
|
131
|
+
raise_command_error "System layout not found: #{layout_val}" unless layout_id
|
|
132
|
+
system_payload['layout'] = {'id' => layout_id}
|
|
133
|
+
elsif !options[:no_prompt]
|
|
134
|
+
if available_layouts.empty?
|
|
135
|
+
raise_command_error "No system layouts found for selected type."
|
|
136
|
+
else
|
|
137
|
+
print cyan, "Available System Layouts\n", reset
|
|
138
|
+
available_layouts.each do |l|
|
|
139
|
+
print " #{l['id']}) #{l['name']}#{l['code'] ? " (#{l['code']})" : ''}\n"
|
|
140
|
+
end
|
|
141
|
+
selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'layout', 'type' => 'text', 'fieldLabel' => 'System Layout ID', 'required' => true}], options[:options], @api_client, {})['layout']
|
|
142
|
+
layout_id = available_layouts.find { |l| l['id'].to_s == selected.to_s }&.dig('id')
|
|
143
|
+
raise_command_error "Invalid system layout id: #{selected}" unless layout_id
|
|
144
|
+
system_payload['layout'] = {'id' => layout_id}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
payload = {rest_object_key => system_payload}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if options[:dry_run]
|
|
152
|
+
print_dry_run rest_interface.dry.create(payload)
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
rest_interface.setopts(options)
|
|
157
|
+
json_response = rest_interface.create(payload)
|
|
158
|
+
render_response(json_response, options, rest_object_key) do
|
|
159
|
+
system_id = json_response['id'] || json_response.dig(rest_object_key, 'id')
|
|
160
|
+
print_green_success "System created"
|
|
161
|
+
get([system_id.to_s] + (options[:remote] ? ['-r', options[:remote]] : [])) if system_id
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def add_uninitialized(args)
|
|
166
|
+
options = {}
|
|
167
|
+
params = {}
|
|
168
|
+
components = []
|
|
169
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
170
|
+
opts.banner = subcommand_usage("[name]")
|
|
171
|
+
opts.on('--name NAME', String, "System Name") do |val|
|
|
172
|
+
params['name'] = val.to_s
|
|
173
|
+
end
|
|
174
|
+
opts.on('--description [TEXT]', String, "Description") do |val|
|
|
175
|
+
params['description'] = val.to_s
|
|
176
|
+
end
|
|
177
|
+
opts.on('--type TYPE', String, "System Type ID or name") do |val|
|
|
178
|
+
params['type'] = val
|
|
179
|
+
end
|
|
180
|
+
opts.on('--layout LAYOUT', String, "System Layout ID or name") do |val|
|
|
181
|
+
params['layout'] = val
|
|
182
|
+
end
|
|
183
|
+
opts.on('--config JSON', String, "System config JSON") do |val|
|
|
184
|
+
params['config'] = JSON.parse(val)
|
|
185
|
+
end
|
|
186
|
+
opts.on('--externalId ID', String, "External ID") do |val|
|
|
187
|
+
params['externalId'] = val.to_s
|
|
188
|
+
end
|
|
189
|
+
opts.on('--component JSON', String, "Component JSON (can be repeated). e.g. '{\"typeCode\":\"compute-node\",\"name\":\"CN-1\",\"config\":{\"ip\":\"10.0.0.1\"}}'") do |val|
|
|
190
|
+
components << JSON.parse(val)
|
|
191
|
+
end
|
|
192
|
+
opts.on('--components JSON', String, "Components JSON array") do |val|
|
|
193
|
+
components.concat(JSON.parse(val))
|
|
194
|
+
end
|
|
195
|
+
build_standard_add_options(opts, options)
|
|
196
|
+
opts.footer = <<-EOT
|
|
197
|
+
Create a new system in an uninitialized state.
|
|
198
|
+
This creates a skeleton system with components but does not invoke provider initialization.
|
|
199
|
+
[name] is optional and can be passed as the first argument.
|
|
200
|
+
EOT
|
|
201
|
+
end
|
|
202
|
+
optparse.parse!(args)
|
|
203
|
+
connect(options)
|
|
204
|
+
|
|
205
|
+
payload = nil
|
|
206
|
+
if options[:payload]
|
|
207
|
+
payload = options[:payload]
|
|
208
|
+
payload[rest_object_key] ||= {}
|
|
209
|
+
payload[rest_object_key].deep_merge!(params) unless params.empty?
|
|
210
|
+
payload[rest_object_key]['name'] ||= args[0] if args[0]
|
|
211
|
+
payload[rest_object_key]['components'] = components unless components.empty?
|
|
212
|
+
else
|
|
213
|
+
system_payload = {}
|
|
214
|
+
|
|
215
|
+
# Name
|
|
216
|
+
system_payload['name'] = params['name'] || args[0]
|
|
217
|
+
if !system_payload['name'] && !options[:no_prompt]
|
|
218
|
+
system_payload['name'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Name', 'required' => true}], options[:options], @api_client, {})['name']
|
|
219
|
+
end
|
|
220
|
+
raise_command_error "Name is required.\n#{optparse}" if system_payload['name'].to_s.empty?
|
|
221
|
+
|
|
222
|
+
# Description
|
|
223
|
+
if !params['description'] && !options[:no_prompt]
|
|
224
|
+
system_payload['description'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'required' => false}], options[:options], @api_client, {})['description']
|
|
225
|
+
else
|
|
226
|
+
system_payload['description'] = params['description']
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Type
|
|
230
|
+
available_types = system_types_for_dropdown
|
|
231
|
+
type_val = params['type']
|
|
232
|
+
if type_val
|
|
233
|
+
type_id = type_val =~ /\A\d+\Z/ ? type_val.to_i : available_types.find { |t| t['name'] == type_val || t['code'] == type_val }&.dig('id')
|
|
234
|
+
raise_command_error "System type not found: #{type_val}" unless type_id
|
|
235
|
+
system_payload['type'] = {'id' => type_id}
|
|
236
|
+
elsif !options[:no_prompt]
|
|
237
|
+
if available_types.empty?
|
|
238
|
+
raise_command_error "No system types found."
|
|
239
|
+
else
|
|
240
|
+
print cyan, "Available System Types\n", reset
|
|
241
|
+
available_types.each do |t|
|
|
242
|
+
print " #{t['id']}) #{t['name']}#{t['code'] ? " (#{t['code']})" : ''}\n"
|
|
243
|
+
end
|
|
244
|
+
selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'type' => 'text', 'fieldLabel' => 'System Type ID', 'required' => true}], options[:options], @api_client, {})['type']
|
|
245
|
+
type_id = available_types.find { |t| t['id'].to_s == selected.to_s }&.dig('id')
|
|
246
|
+
raise_command_error "Invalid system type id: #{selected}" unless type_id
|
|
247
|
+
system_payload['type'] = {'id' => type_id}
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Layout
|
|
252
|
+
available_layouts = system_layouts_for_dropdown(system_payload.dig('type', 'id'))
|
|
253
|
+
selected_layout = nil
|
|
254
|
+
layout_val = params['layout']
|
|
255
|
+
if layout_val
|
|
256
|
+
layout_id = layout_val =~ /\A\d+\Z/ ? layout_val.to_i : available_layouts.find { |l| l['name'] == layout_val || l['code'] == layout_val }&.dig('id')
|
|
257
|
+
raise_command_error "System layout not found: #{layout_val}" unless layout_id
|
|
258
|
+
system_payload['layout'] = {'id' => layout_id}
|
|
259
|
+
selected_layout = available_layouts.find { |l| l['id'].to_i == layout_id.to_i }
|
|
260
|
+
elsif !options[:no_prompt]
|
|
261
|
+
if available_layouts.empty?
|
|
262
|
+
raise_command_error "No system layouts found for selected type."
|
|
263
|
+
else
|
|
264
|
+
print cyan, "Available System Layouts\n", reset
|
|
265
|
+
available_layouts.each do |l|
|
|
266
|
+
print " #{l['id']}) #{l['name']}#{l['code'] ? " (#{l['code']})" : ''}\n"
|
|
267
|
+
end
|
|
268
|
+
selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'layout', 'type' => 'text', 'fieldLabel' => 'System Layout ID', 'required' => true}], options[:options], @api_client, {})['layout']
|
|
269
|
+
selected_layout = available_layouts.find { |l| l['id'].to_s == selected.to_s }
|
|
270
|
+
raise_command_error "Invalid system layout id: #{selected}" unless selected_layout
|
|
271
|
+
system_payload['layout'] = {'id' => selected_layout['id']}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Config
|
|
276
|
+
system_payload['config'] = params['config'] if params['config']
|
|
277
|
+
|
|
278
|
+
# External ID
|
|
279
|
+
system_payload['externalId'] = params['externalId'] if params['externalId']
|
|
280
|
+
|
|
281
|
+
# Components — prompt interactively if none provided via flags
|
|
282
|
+
if components.empty? && !options[:no_prompt] && selected_layout
|
|
283
|
+
available_component_types = selected_layout['componentTypes'] || []
|
|
284
|
+
if available_component_types.any?
|
|
285
|
+
print cyan, "\nAvailable Component Types for layout '#{selected_layout['name']}':\n", reset
|
|
286
|
+
available_component_types.each do |ct|
|
|
287
|
+
print " #{ct['code']}#{ct['category'] ? " [#{ct['category']}]" : ''} - #{ct['name']}\n"
|
|
288
|
+
end
|
|
289
|
+
print "\n"
|
|
290
|
+
add_more = true
|
|
291
|
+
while add_more
|
|
292
|
+
add_component = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'addComponent', 'type' => 'text', 'fieldLabel' => 'Add a component? (yes/no)', 'required' => true, 'defaultValue' => 'yes'}], options[:options], @api_client, {})['addComponent']
|
|
293
|
+
if add_component.to_s.downcase =~ /^(y|yes)$/
|
|
294
|
+
# Component type selection
|
|
295
|
+
print cyan, "Component Types:\n", reset
|
|
296
|
+
available_component_types.each_with_index do |ct, idx|
|
|
297
|
+
print " #{idx + 1}) #{ct['name']} (#{ct['code']})\n"
|
|
298
|
+
end
|
|
299
|
+
selected_ct = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'componentType', 'type' => 'text', 'fieldLabel' => 'Component Type (number or code)', 'required' => true}], options[:options], @api_client, {})['componentType']
|
|
300
|
+
component_type = nil
|
|
301
|
+
if selected_ct =~ /\A\d+\Z/
|
|
302
|
+
idx = selected_ct.to_i - 1
|
|
303
|
+
component_type = available_component_types[idx] if idx >= 0 && idx < available_component_types.size
|
|
304
|
+
else
|
|
305
|
+
component_type = available_component_types.find { |ct| ct['code'] == selected_ct }
|
|
306
|
+
end
|
|
307
|
+
if component_type.nil?
|
|
308
|
+
print_red_alert "Invalid component type: #{selected_ct}"
|
|
309
|
+
next
|
|
310
|
+
end
|
|
311
|
+
# Component name
|
|
312
|
+
default_name = component_type['name']
|
|
313
|
+
comp_name = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'componentName', 'type' => 'text', 'fieldLabel' => 'Component Name', 'required' => false, 'defaultValue' => default_name}], options[:options], @api_client, {})['componentName']
|
|
314
|
+
comp = {'typeCode' => component_type['code'], 'name' => comp_name || default_name}
|
|
315
|
+
# Component config
|
|
316
|
+
comp_config = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'componentConfig', 'type' => 'text', 'fieldLabel' => 'Component Config JSON (optional)', 'required' => false}], options[:options], @api_client, {})['componentConfig']
|
|
317
|
+
if comp_config && !comp_config.to_s.empty?
|
|
318
|
+
begin
|
|
319
|
+
comp['config'] = JSON.parse(comp_config)
|
|
320
|
+
rescue JSON::ParserError => e
|
|
321
|
+
print_red_alert "Invalid JSON: #{e.message}"
|
|
322
|
+
next
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
components << comp
|
|
326
|
+
print_green_success "Added component: #{comp['name']} (#{comp['typeCode']})"
|
|
327
|
+
else
|
|
328
|
+
add_more = false
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
system_payload['components'] = components unless components.empty?
|
|
334
|
+
|
|
335
|
+
payload = {rest_object_key => system_payload}
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
if options[:dry_run]
|
|
339
|
+
print_dry_run rest_interface.dry.save_uninitialized(payload)
|
|
340
|
+
return
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
rest_interface.setopts(options)
|
|
344
|
+
json_response = rest_interface.save_uninitialized(payload)
|
|
345
|
+
render_response(json_response, options, rest_object_key) do
|
|
346
|
+
system_id = json_response['id'] || json_response.dig(rest_object_key, 'id')
|
|
347
|
+
print_green_success "Uninitialized system created"
|
|
348
|
+
get([system_id.to_s] + (options[:remote] ? ['-r', options[:remote]] : [])) if system_id
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def exec_initialize(args)
|
|
353
|
+
options = {}
|
|
354
|
+
params = {}
|
|
355
|
+
components = []
|
|
356
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
357
|
+
opts.banner = subcommand_usage("[system]")
|
|
358
|
+
opts.on('--name NAME', String, "Update the system name before initializing") do |val|
|
|
359
|
+
params['name'] = val.to_s
|
|
360
|
+
end
|
|
361
|
+
opts.on('--description [TEXT]', String, "Update the description before initializing") do |val|
|
|
362
|
+
params['description'] = val.to_s
|
|
363
|
+
end
|
|
364
|
+
opts.on('--externalId ID', String, "Set the external ID before initializing") do |val|
|
|
365
|
+
params['externalId'] = val.to_s
|
|
366
|
+
end
|
|
367
|
+
opts.on('--config JSON', String, "Set config JSON before initializing") do |val|
|
|
368
|
+
params['config'] = JSON.parse(val)
|
|
369
|
+
end
|
|
370
|
+
opts.on('--component JSON', String, "Component update JSON (can be repeated). e.g. '{\"typeCode\":\"compute-node\",\"externalId\":\"ext-001\",\"config\":{\"ip\":\"10.0.0.1\"}}'") do |val|
|
|
371
|
+
components << JSON.parse(val)
|
|
372
|
+
end
|
|
373
|
+
opts.on('--components JSON', String, "Component updates JSON array") do |val|
|
|
374
|
+
components.concat(JSON.parse(val))
|
|
375
|
+
end
|
|
376
|
+
build_standard_update_options(opts, options)
|
|
377
|
+
opts.footer = <<-EOT
|
|
378
|
+
Initialize an existing system that is in an uninitialized state.
|
|
379
|
+
This invokes the provider's prepare and initialize lifecycle methods.
|
|
380
|
+
[system] is required. This is the name or id of a system.
|
|
381
|
+
EOT
|
|
382
|
+
end
|
|
383
|
+
optparse.parse!(args)
|
|
384
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
385
|
+
connect(options)
|
|
386
|
+
|
|
387
|
+
system_record = nil
|
|
388
|
+
if args[0].to_s =~ /\A\d{1,}\Z/
|
|
389
|
+
json_response = rest_interface.get(args[0].to_i)
|
|
390
|
+
system_record = json_response[rest_object_key] || json_response
|
|
391
|
+
else
|
|
392
|
+
system_record = find_by_name(rest_key, args[0])
|
|
393
|
+
end
|
|
394
|
+
return 1, "System not found for '#{args[0]}'" if system_record.nil?
|
|
395
|
+
|
|
396
|
+
# Prompt for component updates if none provided via flags and not in no-prompt mode
|
|
397
|
+
if components.empty? && !options[:no_prompt] && !options[:payload]
|
|
398
|
+
existing_components = system_record['components'] || []
|
|
399
|
+
if existing_components.any?
|
|
400
|
+
print cyan, "\nExisting Components:\n", reset
|
|
401
|
+
existing_components.each_with_index do |c, idx|
|
|
402
|
+
type_code = c.dig('type', 'code') || 'unknown'
|
|
403
|
+
print " #{idx + 1}) #{c['name']} (#{type_code})#{c['externalId'] ? " externalId=#{c['externalId']}" : ''}\n"
|
|
404
|
+
end
|
|
405
|
+
print "\n"
|
|
406
|
+
update_components = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'updateComponents', 'type' => 'text', 'fieldLabel' => 'Update any components? (yes/no)', 'required' => true, 'defaultValue' => 'no'}], options[:options], @api_client, {})['updateComponents']
|
|
407
|
+
if update_components.to_s.downcase =~ /^(y|yes)$/
|
|
408
|
+
existing_components.each do |c|
|
|
409
|
+
type_code = c.dig('type', 'code') || 'unknown'
|
|
410
|
+
print cyan, "\nComponent: #{c['name']} (#{type_code})\n", reset
|
|
411
|
+
comp_update = {'typeCode' => type_code}
|
|
412
|
+
|
|
413
|
+
ext_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'externalId', 'type' => 'text', 'fieldLabel' => " External ID (current: #{c['externalId'] || 'none'})", 'required' => false}], options[:options], @api_client, {})['externalId']
|
|
414
|
+
comp_update['externalId'] = ext_id unless ext_id.to_s.empty?
|
|
415
|
+
|
|
416
|
+
comp_config = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'config', 'type' => 'text', 'fieldLabel' => ' Config JSON (optional)', 'required' => false}], options[:options], @api_client, {})['config']
|
|
417
|
+
if comp_config && !comp_config.to_s.empty?
|
|
418
|
+
begin
|
|
419
|
+
comp_update['config'] = JSON.parse(comp_config)
|
|
420
|
+
rescue JSON::ParserError => e
|
|
421
|
+
print_red_alert "Invalid JSON for component config: #{e.message}"
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
components << comp_update if comp_update.keys.length > 1
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
payload = {}
|
|
432
|
+
if options[:payload]
|
|
433
|
+
payload = options[:payload]
|
|
434
|
+
payload[rest_object_key] ||= {}
|
|
435
|
+
payload[rest_object_key].deep_merge!(params) unless params.empty?
|
|
436
|
+
payload[rest_object_key]['components'] = components unless components.empty?
|
|
437
|
+
else
|
|
438
|
+
params['components'] = components unless components.empty?
|
|
439
|
+
payload = {rest_object_key => params}
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
if options[:dry_run]
|
|
443
|
+
print_dry_run rest_interface.dry.initialize_system(system_record['id'], payload)
|
|
444
|
+
return
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
rest_interface.setopts(options)
|
|
448
|
+
json_response = rest_interface.initialize_system(system_record['id'], payload)
|
|
449
|
+
render_response(json_response, options, rest_object_key) do
|
|
450
|
+
print_green_success "System #{system_record['name']} initialized"
|
|
451
|
+
get([system_record['id'].to_s] + (options[:remote] ? ['-r', options[:remote]] : []))
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def exec_validate(args)
|
|
456
|
+
options = {}
|
|
457
|
+
params = {}
|
|
458
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
459
|
+
opts.banner = subcommand_usage("[system]")
|
|
460
|
+
build_standard_get_options(opts, options)
|
|
461
|
+
opts.footer = <<-EOT
|
|
462
|
+
Validate an existing system by running the provider's pre-check phase.
|
|
463
|
+
No state changes are made. Returns success if the provider's prepare check passes.
|
|
464
|
+
[system] is required. This is the name or id of a system.
|
|
465
|
+
EOT
|
|
466
|
+
end
|
|
467
|
+
optparse.parse!(args)
|
|
468
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
469
|
+
connect(options)
|
|
470
|
+
|
|
471
|
+
system = nil
|
|
472
|
+
if args[0].to_s =~ /\A\d{1,}\Z/
|
|
473
|
+
json_response = rest_interface.get(args[0].to_i)
|
|
474
|
+
system = json_response[rest_object_key] || json_response
|
|
475
|
+
else
|
|
476
|
+
system = find_by_name(rest_key, args[0])
|
|
477
|
+
end
|
|
478
|
+
return 1, "System not found for '#{args[0]}'" if system.nil?
|
|
479
|
+
|
|
480
|
+
if options[:dry_run]
|
|
481
|
+
print_dry_run rest_interface.dry.validate_system(system['id'])
|
|
482
|
+
return
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
rest_interface.setopts(options)
|
|
486
|
+
json_response = rest_interface.validate_system(system['id'])
|
|
487
|
+
render_response(json_response, options) do
|
|
488
|
+
if json_response['success']
|
|
489
|
+
print_green_success "System #{system['name']} validated successfully"
|
|
490
|
+
else
|
|
491
|
+
print_red_alert "System #{system['name']} validation failed: #{json_response['msg']}"
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def remove(args)
|
|
497
|
+
options = {}
|
|
498
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
499
|
+
opts.banner = subcommand_usage("[system]")
|
|
500
|
+
build_standard_remove_options(opts, options)
|
|
501
|
+
opts.footer = <<-EOT
|
|
502
|
+
Delete an existing system.
|
|
503
|
+
[system] is required. This is the name or id of a system.
|
|
504
|
+
EOT
|
|
505
|
+
end
|
|
506
|
+
optparse.parse!(args)
|
|
507
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
508
|
+
connect(options)
|
|
509
|
+
|
|
510
|
+
system = nil
|
|
511
|
+
if args[0].to_s =~ /\A\d{1,}\Z/
|
|
512
|
+
json_response = rest_interface.get(args[0].to_i)
|
|
513
|
+
system = json_response[rest_object_key] || json_response
|
|
514
|
+
else
|
|
515
|
+
system = find_by_name(rest_key, args[0])
|
|
516
|
+
end
|
|
517
|
+
return 1, "System not found for '#{args[0]}'" if system.nil?
|
|
518
|
+
|
|
519
|
+
unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the system #{system['name']}?")
|
|
520
|
+
return 9, "aborted"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
if options[:dry_run]
|
|
524
|
+
print_dry_run rest_interface.dry.destroy(system['id'])
|
|
525
|
+
return
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
rest_interface.setopts(options)
|
|
529
|
+
json_response = rest_interface.destroy(system['id'])
|
|
530
|
+
render_response(json_response, options) do
|
|
531
|
+
print_green_success "System #{system['name']} removed"
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def update(args)
|
|
536
|
+
options = {}
|
|
537
|
+
params = {}
|
|
538
|
+
optparse = Morpheus::Cli::OptionParser.new do |opts|
|
|
539
|
+
opts.banner = subcommand_usage("[system] --name --description")
|
|
540
|
+
opts.on("--name NAME", String, "Updates System Name") do |val|
|
|
541
|
+
params['name'] = val.to_s
|
|
542
|
+
end
|
|
543
|
+
opts.on("--description [TEXT]", String, "Updates System Description") do |val|
|
|
544
|
+
params['description'] = val.to_s
|
|
545
|
+
end
|
|
546
|
+
build_standard_update_options(opts, options, [:find_by_name])
|
|
547
|
+
opts.footer = <<-EOT
|
|
548
|
+
Update an existing system.
|
|
549
|
+
[system] is required. This is the name or id of a system.
|
|
550
|
+
EOT
|
|
551
|
+
end
|
|
552
|
+
optparse.parse!(args)
|
|
553
|
+
verify_args!(args: args, optparse: optparse, count: 1)
|
|
554
|
+
connect(options)
|
|
555
|
+
|
|
556
|
+
system = nil
|
|
557
|
+
if args[0].to_s =~ /\A\d{1,}\Z/
|
|
558
|
+
json_response = rest_interface.get(args[0].to_i)
|
|
559
|
+
system = json_response[rest_object_key] || json_response
|
|
560
|
+
else
|
|
561
|
+
system = find_by_name(rest_key, args[0])
|
|
562
|
+
end
|
|
563
|
+
return 1, "System not found for '#{args[0]}'" if system.nil?
|
|
564
|
+
|
|
565
|
+
passed_options = parse_passed_options(options)
|
|
566
|
+
params.deep_merge!(passed_options) unless passed_options.empty?
|
|
567
|
+
params.booleanize!
|
|
568
|
+
|
|
569
|
+
payload = parse_payload(options) || {rest_object_key => params}
|
|
570
|
+
if payload[rest_object_key].nil? || payload[rest_object_key].empty?
|
|
571
|
+
raise_command_error "Specify at least one option to update.\n#{optparse}"
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
if options[:dry_run]
|
|
575
|
+
print_dry_run rest_interface.dry.update(system['id'], payload)
|
|
576
|
+
return
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
rest_interface.setopts(options)
|
|
580
|
+
json_response = rest_interface.update(system['id'], payload)
|
|
581
|
+
render_response(json_response, options, rest_object_key) do
|
|
582
|
+
print_green_success "Updated system #{system['id']}"
|
|
583
|
+
get([system['id']] + (options[:remote] ? ['-r', options[:remote]] : []))
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
def system_types_for_dropdown
|
|
588
|
+
result = @api_client.system_types.list({'max' => 100})
|
|
589
|
+
items = result ? (result['systemTypes'] || result[:systemTypes] || result['types'] || result[:types] || []) : []
|
|
590
|
+
items.map { |t| {'id' => t['id'] || t[:id], 'name' => t['name'] || t[:name], 'value' => (t['id'] || t[:id]).to_s, 'code' => t['code'] || t[:code]} }
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def system_layouts_for_dropdown(type_id = nil)
|
|
594
|
+
return [] if type_id.nil?
|
|
595
|
+
result = @api_client.system_types.list_layouts(type_id, {'max' => 100})
|
|
596
|
+
items = result ? (result['systemTypeLayouts'] || result[:systemTypeLayouts] || result['layouts'] || result[:layouts] || []) : []
|
|
597
|
+
items.map { |l| {'id' => l['id'] || l[:id], 'name' => l['name'] || l[:name], 'value' => (l['id'] || l[:id]).to_s, 'code' => l['code'] || l[:code], 'componentTypes' => l['componentTypes'] || l[:componentTypes] || []} }
|
|
598
|
+
end
|
|
599
|
+
end
|
|
@@ -3,6 +3,7 @@ require 'morpheus/cli/cli_command'
|
|
|
3
3
|
class Morpheus::Cli::TenantsCommand
|
|
4
4
|
include Morpheus::Cli::CliCommand
|
|
5
5
|
include Morpheus::Cli::AccountsHelper
|
|
6
|
+
include Morpheus::Cli::WhoamiHelper
|
|
6
7
|
set_command_name :tenants
|
|
7
8
|
set_command_description "View and manage tenants (accounts)."
|
|
8
9
|
register_subcommands :list, :count, :get, :add, :update, :remove
|
|
@@ -21,6 +22,7 @@ class Morpheus::Cli::TenantsCommand
|
|
|
21
22
|
@account_users_interface = @api_client.account_users
|
|
22
23
|
@accounts_interface = @api_client.accounts
|
|
23
24
|
@roles_interface = @api_client.roles
|
|
25
|
+
@whoami_interface = @api_client.whoami
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def handle(args)
|
|
@@ -286,18 +288,25 @@ EOT
|
|
|
286
288
|
[
|
|
287
289
|
{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1},
|
|
288
290
|
{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'displayOrder' => 2},
|
|
289
|
-
{'fieldContext' => '
|
|
290
|
-
@
|
|
291
|
+
{'fieldContext' => 'parentAccount', 'fieldName' => 'id', 'fieldLabel' => 'Parent Tenant', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
|
|
292
|
+
@accounts_interface.list({max:10000})['accounts'].collect {|it|
|
|
291
293
|
{"name" => (it["authority"] || it["name"]), "value" => it["id"]}
|
|
292
294
|
}
|
|
293
295
|
}, 'displayOrder' => 3},
|
|
296
|
+
{'fieldContext' => 'role', 'fieldName' => 'id', 'fieldLabel' => 'Base Role', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
|
|
297
|
+
tenant_id = (api_params['parentAccount']['id'] rescue nil)
|
|
298
|
+
params = {max: 10000, roleType:'account', tenantId: tenant_id}
|
|
299
|
+
@roles_interface.list(nil, params)['roles'].collect {|it|
|
|
300
|
+
{"name" => (it["authority"] || it["name"]), "value" => it["id"]}
|
|
301
|
+
}
|
|
302
|
+
}, 'displayOrder' => 4},
|
|
294
303
|
{'fieldName' => 'currency', 'fieldLabel' => 'Currency', 'type' => 'text', 'defaultValue' => 'USD', 'displayOrder' => 4}
|
|
295
304
|
]
|
|
296
305
|
end
|
|
297
306
|
|
|
298
307
|
def update_account_option_types
|
|
299
308
|
list = add_account_option_types()
|
|
300
|
-
|
|
309
|
+
list = list.reject {|it| it['fieldContext'] == 'parentAccount' }
|
|
301
310
|
list.each {|it| it.delete('required') }
|
|
302
311
|
list.each {|it| it.delete('defaultValue') }
|
|
303
312
|
list
|
|
@@ -45,6 +45,7 @@ module Morpheus::Cli::AccountsHelper
|
|
|
45
45
|
"# Instances" => 'stats.instanceCount',
|
|
46
46
|
"# Users" => 'stats.userCount',
|
|
47
47
|
"Role" => lambda {|it| it['role']['authority'] rescue nil },
|
|
48
|
+
"Parent Tenant" => lambda {|it| it['parent']['name'] rescue nil },
|
|
48
49
|
"Master" => lambda {|it| format_boolean(it['master']) },
|
|
49
50
|
"Currency" => 'currency',
|
|
50
51
|
"Status" => lambda {|it|
|