morpheus-cli 8.0.12.2 → 8.1.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.
@@ -0,0 +1,270 @@
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
10
+
11
+ set_command_hidden
12
+
13
+ protected
14
+
15
+ # Systems API uses lowercase keys in payloads.
16
+ def system_object_key
17
+ 'system'
18
+ end
19
+
20
+ def system_list_key
21
+ 'systems'
22
+ end
23
+
24
+ def system_list_column_definitions(options)
25
+ {
26
+ "ID" => 'id',
27
+ "Name" => 'name',
28
+ "Type" => lambda {|it| it['type'] ? it['type']['name'] : '' },
29
+ "Layout" => lambda {|it| it['layout'] ? it['layout']['name'] : '' },
30
+ "Status" => 'status',
31
+ "Enabled" => lambda {|it| format_boolean(it['enabled']) },
32
+ "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) }
33
+ }
34
+ end
35
+
36
+ def system_column_definitions(options)
37
+ {
38
+ "ID" => 'id',
39
+ "Name" => 'name',
40
+ "Description" => 'description',
41
+ "Status" => 'status',
42
+ "Status Message" => 'statusMessage',
43
+ "Enabled" => lambda {|it| format_boolean(it['enabled']) },
44
+ "External ID" => 'externalId',
45
+ "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
46
+ "Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
47
+ }
48
+ end
49
+
50
+ def render_response_for_get(json_response, options)
51
+ render_response(json_response, options, rest_object_key) do
52
+ record = json_response[rest_object_key] || json_response
53
+ print_h1 rest_label, [], options
54
+ print cyan
55
+ print_description_list(rest_column_definitions(options), record, options)
56
+ print reset,"\n"
57
+ end
58
+ end
59
+
60
+ def add(args)
61
+ options = {}
62
+ params = {}
63
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
64
+ opts.banner = subcommand_usage("[name]")
65
+ opts.on('--name NAME', String, "System Name") do |val|
66
+ params['name'] = val.to_s
67
+ end
68
+ opts.on('--description [TEXT]', String, "Description") do |val|
69
+ params['description'] = val.to_s
70
+ end
71
+ opts.on('--type TYPE', String, "System Type ID or name") do |val|
72
+ params['type'] = val
73
+ end
74
+ opts.on('--layout LAYOUT', String, "System Layout ID or name") do |val|
75
+ params['layout'] = val
76
+ end
77
+ build_standard_add_options(opts, options)
78
+ opts.footer = "Create a new system.\n[name] is optional and can be passed as the first argument."
79
+ end
80
+ optparse.parse!(args)
81
+ connect(options)
82
+
83
+ payload = nil
84
+ if options[:payload]
85
+ payload = options[:payload]
86
+ payload[rest_object_key] ||= {}
87
+ payload[rest_object_key].deep_merge!(params) unless params.empty?
88
+ payload[rest_object_key]['name'] ||= args[0] if args[0]
89
+ else
90
+ system_payload = {}
91
+
92
+ # Name
93
+ system_payload['name'] = params['name'] || args[0]
94
+ if !system_payload['name'] && !options[:no_prompt]
95
+ system_payload['name'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'type' => 'text', 'fieldLabel' => 'Name', 'required' => true}], options[:options], @api_client, {})['name']
96
+ end
97
+ raise_command_error "Name is required.\n#{optparse}" if system_payload['name'].to_s.empty?
98
+
99
+ # Description
100
+ if !params['description'] && !options[:no_prompt]
101
+ system_payload['description'] = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'type' => 'text', 'fieldLabel' => 'Description', 'required' => false}], options[:options], @api_client, {})['description']
102
+ else
103
+ system_payload['description'] = params['description']
104
+ end
105
+
106
+ # Type
107
+ available_types = system_types_for_dropdown
108
+ type_val = params['type']
109
+ if type_val
110
+ 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')
111
+ raise_command_error "System type not found: #{type_val}" unless type_id
112
+ system_payload['type'] = {'id' => type_id}
113
+ elsif !options[:no_prompt]
114
+ if available_types.empty?
115
+ raise_command_error "No system types found."
116
+ else
117
+ print cyan, "Available System Types\n", reset
118
+ available_types.each do |t|
119
+ print " #{t['id']}) #{t['name']}#{t['code'] ? " (#{t['code']})" : ''}\n"
120
+ end
121
+ selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'type', 'type' => 'text', 'fieldLabel' => 'System Type ID', 'required' => true}], options[:options], @api_client, {})['type']
122
+ type_id = available_types.find { |t| t['id'].to_s == selected.to_s }&.dig('id')
123
+ raise_command_error "Invalid system type id: #{selected}" unless type_id
124
+ system_payload['type'] = {'id' => type_id}
125
+ end
126
+ end
127
+
128
+ # Layout
129
+ available_layouts = system_layouts_for_dropdown(system_payload.dig('type', 'id'))
130
+ layout_val = params['layout']
131
+ if layout_val
132
+ 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')
133
+ raise_command_error "System layout not found: #{layout_val}" unless layout_id
134
+ system_payload['layout'] = {'id' => layout_id}
135
+ elsif !options[:no_prompt]
136
+ if available_layouts.empty?
137
+ raise_command_error "No system layouts found for selected type."
138
+ else
139
+ print cyan, "Available System Layouts\n", reset
140
+ available_layouts.each do |l|
141
+ print " #{l['id']}) #{l['name']}#{l['code'] ? " (#{l['code']})" : ''}\n"
142
+ end
143
+ selected = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'layout', 'type' => 'text', 'fieldLabel' => 'System Layout ID', 'required' => true}], options[:options], @api_client, {})['layout']
144
+ layout_id = available_layouts.find { |l| l['id'].to_s == selected.to_s }&.dig('id')
145
+ raise_command_error "Invalid system layout id: #{selected}" unless layout_id
146
+ system_payload['layout'] = {'id' => layout_id}
147
+ end
148
+ end
149
+
150
+ payload = {rest_object_key => system_payload}
151
+ end
152
+
153
+ if options[:dry_run]
154
+ print_dry_run rest_interface.dry.create(payload)
155
+ return
156
+ end
157
+
158
+ rest_interface.setopts(options)
159
+ json_response = rest_interface.create(payload)
160
+ render_response(json_response, options, rest_object_key) do
161
+ system_id = json_response['id'] || json_response.dig(rest_object_key, 'id')
162
+ print_green_success "System created"
163
+ get([system_id.to_s] + (options[:remote] ? ['-r', options[:remote]] : [])) if system_id
164
+ end
165
+ end
166
+
167
+ def remove(args)
168
+ options = {}
169
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
170
+ opts.banner = subcommand_usage("[system]")
171
+ build_standard_remove_options(opts, options)
172
+ opts.footer = <<-EOT
173
+ Delete an existing system.
174
+ [system] is required. This is the name or id of a system.
175
+ EOT
176
+ end
177
+ optparse.parse!(args)
178
+ verify_args!(args: args, optparse: optparse, count: 1)
179
+ connect(options)
180
+
181
+ system = nil
182
+ if args[0].to_s =~ /\A\d{1,}\Z/
183
+ json_response = rest_interface.get(args[0].to_i)
184
+ system = json_response[rest_object_key] || json_response
185
+ else
186
+ system = find_by_name(rest_key, args[0])
187
+ end
188
+ return 1, "System not found for '#{args[0]}'" if system.nil?
189
+
190
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the system #{system['name']}?")
191
+ return 9, "aborted"
192
+ end
193
+
194
+ if options[:dry_run]
195
+ print_dry_run rest_interface.dry.destroy(system['id'])
196
+ return
197
+ end
198
+
199
+ rest_interface.setopts(options)
200
+ json_response = rest_interface.destroy(system['id'])
201
+ render_response(json_response, options) do
202
+ print_green_success "System #{system['name']} removed"
203
+ end
204
+ end
205
+
206
+ def update(args)
207
+ options = {}
208
+ params = {}
209
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
210
+ opts.banner = subcommand_usage("[system] --name --description")
211
+ opts.on("--name NAME", String, "Updates System Name") do |val|
212
+ params['name'] = val.to_s
213
+ end
214
+ opts.on("--description [TEXT]", String, "Updates System Description") do |val|
215
+ params['description'] = val.to_s
216
+ end
217
+ build_standard_update_options(opts, options, [:find_by_name])
218
+ opts.footer = <<-EOT
219
+ Update an existing system.
220
+ [system] is required. This is the name or id of a system.
221
+ EOT
222
+ end
223
+ optparse.parse!(args)
224
+ verify_args!(args: args, optparse: optparse, count: 1)
225
+ connect(options)
226
+
227
+ system = nil
228
+ if args[0].to_s =~ /\A\d{1,}\Z/
229
+ json_response = rest_interface.get(args[0].to_i)
230
+ system = json_response[rest_object_key] || json_response
231
+ else
232
+ system = find_by_name(rest_key, args[0])
233
+ end
234
+ return 1, "System not found for '#{args[0]}'" if system.nil?
235
+
236
+ passed_options = parse_passed_options(options)
237
+ params.deep_merge!(passed_options) unless passed_options.empty?
238
+ params.booleanize!
239
+
240
+ payload = parse_payload(options) || {rest_object_key => params}
241
+ if payload[rest_object_key].nil? || payload[rest_object_key].empty?
242
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
243
+ end
244
+
245
+ if options[:dry_run]
246
+ print_dry_run rest_interface.dry.update(system['id'], payload)
247
+ return
248
+ end
249
+
250
+ rest_interface.setopts(options)
251
+ json_response = rest_interface.update(system['id'], payload)
252
+ render_response(json_response, options, rest_object_key) do
253
+ print_green_success "Updated system #{system['id']}"
254
+ get([system['id']] + (options[:remote] ? ['-r', options[:remote]] : []))
255
+ end
256
+ end
257
+
258
+ def system_types_for_dropdown
259
+ result = @api_client.system_types.list({'max' => 100})
260
+ items = result ? (result['systemTypes'] || result[:systemTypes] || result['types'] || result[:types] || []) : []
261
+ 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]} }
262
+ end
263
+
264
+ def system_layouts_for_dropdown(type_id = nil)
265
+ return [] if type_id.nil?
266
+ result = @api_client.system_types.list_layouts(type_id, {'max' => 100})
267
+ items = result ? (result['systemTypeLayouts'] || result[:systemTypeLayouts] || result['layouts'] || result[:layouts] || []) : []
268
+ 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]} }
269
+ end
270
+ 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' => 'role', 'fieldName' => 'id', 'fieldLabel' => 'Base Role', 'type' => 'select', 'optionSource' => lambda { |api_client, api_params|
290
- @roles_interface.list(nil, {roleType:'account'})['roles'].collect {|it|
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
- # list = list.reject {|it| ["interval"].include? it['fieldName'] }
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|
@@ -117,6 +117,8 @@ module Morpheus::Cli::BackupsHelper
117
117
  {
118
118
  "ID" => 'id',
119
119
  "Backup" => lambda {|it| it['backup']['name'] rescue '' },
120
+ "Type" => lambda {|it| format_backup_result_type_tag(it) },
121
+ "Location" => lambda {|it| format_backup_result_location_tag(it) },
120
122
  "Status" => lambda {|it| format_backup_result_status(it) },
121
123
  #"Duration" => lambda {|it| format_duration(it['startDate'], it['endDate']) },
122
124
  "Duration" => lambda {|it| format_duration_milliseconds(it['durationMillis']) if it['durationMillis'].to_i > 0 },
@@ -168,5 +170,31 @@ module Morpheus::Cli::BackupsHelper
168
170
  def format_backup_restore_status(backup_restore, return_color=cyan)
169
171
  format_backup_result_status(backup_restore, return_color)
170
172
  end
173
+
174
+ # format backup result type tag based on associated backup data
175
+ def format_backup_result_type_tag(backup_result)
176
+ backup = backup_result['backup'] || {}
177
+ if backup['cronExpression'] || (backup['schedule'] && backup['schedule']['id']) || (backup['job'] && backup['job']['id'])
178
+ "#{cyan}POLICY#{reset}"
179
+ else
180
+ "#{yellow}MANUAL#{reset}"
181
+ end
182
+ end
183
+
184
+ # format backup result location tag based on storage provider
185
+ def format_backup_result_location_tag(backup_result)
186
+ backup = backup_result['backup'] || {}
187
+ if backup_result['storageProvider'] && backup_result['storageProvider']['id']
188
+ "#{green}REMOTE#{reset}"
189
+ elsif backup['storageProvider'] && backup['storageProvider']['id']
190
+ "#{green}REMOTE#{reset}"
191
+ elsif backup_result['backupProvider'] && backup_result['backupProvider']['id']
192
+ "#{green}REMOTE#{reset}"
193
+ elsif backup['backupProvider'] && backup['backupProvider']['id']
194
+ "#{green}REMOTE#{reset}"
195
+ else
196
+ "#{blue}LOCAL#{reset}"
197
+ end
198
+ end
171
199
 
172
- end
200
+ end
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "8.0.12.2"
4
+ VERSION = "8.1.0"
5
5
  end
6
6
  end
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: 8.0.12.2
4
+ version: 8.1.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: 2026-01-05 00:00:00.000000000 Z
14
+ date: 2026-03-11 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: tins
@@ -381,6 +381,8 @@ files:
381
381
  - lib/morpheus/api/storage_volumes_interface.rb
382
382
  - lib/morpheus/api/subnet_types_interface.rb
383
383
  - lib/morpheus/api/subnets_interface.rb
384
+ - lib/morpheus/api/system_types_interface.rb
385
+ - lib/morpheus/api/systems_interface.rb
384
386
  - lib/morpheus/api/task_sets_interface.rb
385
387
  - lib/morpheus/api/tasks_interface.rb
386
388
  - lib/morpheus/api/usage_interface.rb
@@ -570,6 +572,7 @@ files:
570
572
  - lib/morpheus/cli/commands/storage_volume_types.rb
571
573
  - lib/morpheus/cli/commands/storage_volumes.rb
572
574
  - lib/morpheus/cli/commands/subnets_command.rb
575
+ - lib/morpheus/cli/commands/systems.rb
573
576
  - lib/morpheus/cli/commands/tasks.rb
574
577
  - lib/morpheus/cli/commands/tee_command.rb
575
578
  - lib/morpheus/cli/commands/tenants_command.rb