morpheus-cli 9.0.0 → 9.0.2

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,336 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ # CLI command Virtual Switch management
4
+ # UI is Tools: Virtual Switches
5
+ # API is /api/virtual-switches and returns virtualSwitches
6
+ class Morpheus::Cli::VirtualSwitchesCommand
7
+ include Morpheus::Cli::CliCommand
8
+ include Morpheus::Cli::OptionSourceHelper
9
+
10
+ set_command_name :'virtual-switches'
11
+ set_command_description "View and manage Virtual Switches"
12
+ set_command_hidden
13
+ register_subcommands :list, :get, :add, :update, :remove
14
+
15
+ def connect(opts)
16
+ @api_client = establish_remote_appliance_connection(opts)
17
+ @virtual_switches_interface = @api_client.virtual_switches
18
+ @clusters_interface = @api_client.clusters
19
+ @networks_interface = @api_client.networks
20
+ @options_interface = @api_client.options
21
+
22
+ end
23
+
24
+ def handle(args)
25
+ handle_subcommand(args)
26
+ end
27
+
28
+ def list(args)
29
+ options = {}
30
+ params = {}
31
+ ref_ids = []
32
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
33
+ opts.banner = subcommand_usage("[search]")
34
+ opts.on( '--enabled [on|off]', String, "Filter by enabled" ) do |val|
35
+ params['enabled'] = (val.to_s != 'false' && val.to_s != 'off')
36
+ end
37
+ build_standard_list_options(opts, options)
38
+ opts.footer = "List Virtual Switches."
39
+ end
40
+ optparse.parse!(args)
41
+ connect(options)
42
+ if args.count > 0
43
+ options[:phrase] = args.join(" ")
44
+ end
45
+ params.merge!(parse_list_options(options))
46
+ @virtual_switches_interface.setopts(options)
47
+ if options[:dry_run]
48
+ print_dry_run @virtual_switches_interface.dry.list(params)
49
+ return
50
+ end
51
+ json_response = @virtual_switches_interface.list(params)
52
+ render_response(json_response, options, virtual_switch_list_key) do
53
+ virtual_switches = json_response[virtual_switch_list_key]
54
+ print_h1 "Morpheus Virtual Switches", parse_list_subtitles(options), options
55
+ if virtual_switches.empty?
56
+ print cyan,"No Virtual Switches found.",reset,"\n"
57
+ else
58
+ columns = {
59
+ "ID" => 'id',
60
+ "Name" => 'name',
61
+ "Type" => lambda {|it| it['type']['name'] rescue it['baseType'] },
62
+ "NIC Count" => lambda {|it| it['nicCount'] },
63
+ "Network Count" => lambda {|it| it['networkCount'] },
64
+ }
65
+ print as_pretty_table(virtual_switches, columns.upcase_keys!, options)
66
+ print_results_pagination(json_response)
67
+ end
68
+ print reset,"\n"
69
+ end
70
+ return 0, nil
71
+ end
72
+
73
+ def get(args)
74
+ params = {}
75
+ options = {}
76
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
77
+ opts.banner = subcommand_usage("[switch]")
78
+ build_standard_get_options(opts, options)
79
+ opts.footer = <<-EOT
80
+ Get details about a specific Virtual Switch.
81
+ [switch] is required. This is the name or id of a Virtual Switch.
82
+ EOT
83
+ end
84
+ optparse.parse!(args)
85
+ verify_args!(args:args, optparse:optparse, min:1)
86
+ connect(options)
87
+ params.merge!(parse_query_options(options))
88
+ id_list = parse_id_list(args)
89
+ return run_command_for_each_arg(id_list) do |arg|
90
+ _get(arg, params, options)
91
+ end
92
+ end
93
+
94
+ def _get(id, params, options)
95
+ virtual_switch = nil
96
+ if id.to_s !~ /\A\d{1,}\Z/
97
+ virtual_switch = find_virtual_switch_by_name(id)
98
+ return 1, "Virtual Switch not found for #{id}" if virtual_switch.nil?
99
+ id = virtual_switch['id']
100
+ end
101
+ @virtual_switches_interface.setopts(options)
102
+ if options[:dry_run]
103
+ print_dry_run @virtual_switches_interface.dry.get(id, params)
104
+ return
105
+ end
106
+ json_response = @virtual_switches_interface.get(id, params)
107
+ virtual_switch = json_response[virtual_switch_object_key]
108
+ render_response(json_response, options, virtual_switch_object_key) do
109
+ print_h1 "Virtual Switch Details", [], options
110
+ print cyan
111
+ columns = {
112
+ "ID" => 'id',
113
+ "Name" => 'name',
114
+ "Type" => lambda {|it| it['type']['name'] rescue it['baseType'] },
115
+ "NIC Count" => lambda {|it| it['nicCount'] },
116
+ "Network Count" => lambda {|it| it['networkCount'] },
117
+ }
118
+ print_description_list(columns, virtual_switch, options)
119
+
120
+
121
+ print reset,"\n"
122
+ end
123
+ return 0, nil
124
+ end
125
+
126
+ def add(args)
127
+ options = {}
128
+ params = {}
129
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
130
+ opts.banner = subcommand_usage("[name] [options]")
131
+ build_option_type_options(opts, options, add_virtual_switch_option_types)
132
+ build_standard_add_options(opts, options)
133
+ opts.footer = <<-EOT
134
+ Create a new Virtual Switch.
135
+ EOT
136
+ end
137
+ optparse.parse!(args)
138
+ verify_args!(args:args, optparse:optparse, min:0, max:1)
139
+ options[:options]['name'] = args[0] if args[0]
140
+ if options[:options]['logo']
141
+ options[:options]['iconPath'] = 'custom'
142
+ end
143
+ connect(options)
144
+ payload = {}
145
+ if options[:payload]
146
+ payload = options[:payload]
147
+ payload.deep_merge!({virtual_switch_object_key => parse_passed_options(options)})
148
+ else
149
+ params.deep_merge!(parse_passed_options(options))
150
+ # prompt for option types
151
+ option_types = add_virtual_switch_option_types
152
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(option_types, options[:options], @api_client, options[:params])
153
+ params.deep_merge!(v_prompt)
154
+ # convert checkbox "on" and "off" to true and false
155
+ params.booleanize!
156
+
157
+ # massage association params a bit
158
+ # params['apps'] = ...
159
+ payload.deep_merge!({virtual_switch_object_key => params})
160
+ end
161
+ @virtual_switches_interface.setopts(options)
162
+ if options[:dry_run]
163
+ print_dry_run @virtual_switches_interface.dry.create(payload)
164
+ return 0, nil
165
+ end
166
+ json_response = @virtual_switches_interface.create(payload)
167
+ virtual_switch = json_response[virtual_switch_object_key]
168
+ render_response(json_response, options, virtual_switch_object_key) do
169
+ print_green_success "Added virtual switch #{virtual_switch['name']}"
170
+ return _get(virtual_switch["id"], {}, options)
171
+ end
172
+ return 0, nil
173
+ end
174
+
175
+ def update(args)
176
+ options = {}
177
+ params = {}
178
+ payload = {}
179
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
180
+ opts.banner = subcommand_usage("[switch] [options]")
181
+ build_option_type_options(opts, options, update_virtual_switch_option_types)
182
+ build_standard_update_options(opts, options)
183
+ opts.footer = <<-EOT
184
+ Update a virtual switch.
185
+ [switch] is required. This is the name or id of a virtual switch.
186
+ EOT
187
+ end
188
+ optparse.parse!(args)
189
+ verify_args!(args:args, optparse:optparse, count:1)
190
+ if options[:options]['logo']
191
+ options[:options]['iconPath'] = 'custom'
192
+ end
193
+ connect(options)
194
+ virtual_switch = find_virtual_switch_by_name_or_id(args[0])
195
+ return 1 if virtual_switch.nil?
196
+ payload = {}
197
+ if options[:payload]
198
+ payload = options[:payload]
199
+ payload.deep_merge!({virtual_switch_object_key => parse_passed_options(options)})
200
+ else
201
+ params.deep_merge!(parse_passed_options(options))
202
+ # do not prompt on update
203
+ v_prompt = Morpheus::Cli::OptionTypes.no_prompt(update_virtual_switch_option_types, options[:options], @api_client, options[:params])
204
+ v_prompt.deep_compact!
205
+ params.deep_merge!(v_prompt)
206
+ # convert checkbox "on" and "off" to true and false
207
+ params.booleanize!
208
+ # massage association params a bit
209
+
210
+ # params['apps'] = ...
211
+ payload.deep_merge!({virtual_switch_object_key => params})
212
+ if payload[virtual_switch_object_key].empty? # || options[:no_prompt]
213
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
214
+ end
215
+ end
216
+ @virtual_switches_interface.setopts(options)
217
+ if options[:dry_run]
218
+ print_dry_run @virtual_switches_interface.dry.update(virtual_switch['id'], payload)
219
+ return
220
+ end
221
+ json_response = @virtual_switches_interface.update(virtual_switch['id'], payload)
222
+ virtual_switch = json_response[virtual_switch_object_key]
223
+ render_response(json_response, options, virtual_switch_object_key) do
224
+ print_green_success "Updated virtual switch #{virtual_switch['name']}"
225
+ return _get(virtual_switch["id"], {}, options)
226
+ end
227
+ return 0, nil
228
+ end
229
+
230
+ def remove(args)
231
+ options = {}
232
+ params = {}
233
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
234
+ opts.banner = subcommand_usage("[switch] [options]")
235
+ build_standard_remove_options(opts, options)
236
+ opts.footer = <<-EOT
237
+ Delete a virtual switch.
238
+ [switch] is required. This is the name or id of a virtual switch.
239
+ EOT
240
+ end
241
+ optparse.parse!(args)
242
+ verify_args!(args:args, optparse:optparse, count:1)
243
+ connect(options)
244
+ virtual_switch = find_virtual_switch_by_name_or_id(args[0])
245
+ return 1 if virtual_switch.nil?
246
+ @virtual_switches_interface.setopts(options)
247
+ if options[:dry_run]
248
+ print_dry_run @virtual_switches_interface.dry.destroy(virtual_switch['id'], params)
249
+ return
250
+ end
251
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the virtual switch #{virtual_switch['name']}?")
252
+ return 9, "aborted command"
253
+ end
254
+ json_response = @virtual_switches_interface.destroy(virtual_switch['id'], params)
255
+ render_response(json_response, options) do
256
+ print_green_success "Removed virtual switch #{virtual_switch['name']}"
257
+ end
258
+ return 0, nil
259
+ end
260
+
261
+ private
262
+
263
+ def add_virtual_switch_option_types
264
+ [
265
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'description' => 'Choose a unique name for the Virtual Switch'},
266
+ #{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'description' => 'Description'},
267
+ {'switch' => '-t', 'fieldName' => 'type', 'fieldLabel' => 'Type', 'type' => 'select', 'description' => 'Type of virtual switch', 'selectOptions' => [{'name' => 'VM Network', 'value' => 'general'}, {'name' => 'ISCSI', 'value' => 'iscsi'}, {'name' => 'NFS', 'value' => 'nfs'}, {'name' => 'Live Migration', 'value' => 'live_migration'}, {'name' => 'SDN', 'value' => 'sdn'}], 'required' => true},
268
+ ]
269
+ end
270
+
271
+ def update_virtual_switch_option_types
272
+ list = add_virtual_switch_option_types.collect {|it|
273
+ it.delete('required')
274
+ it.delete('defaultValue')
275
+ it
276
+ }
277
+ list = list.reject {|it| ["type"].include? it['fieldName'] }
278
+ list
279
+ end
280
+
281
+ def find_virtual_switch_by_name_or_id(val)
282
+ if val.to_s =~ /\A\d{1,}\Z/
283
+ return find_virtual_switch_by_id(val)
284
+ else
285
+ return find_virtual_switch_by_name(val)
286
+ end
287
+ end
288
+
289
+ def find_virtual_switch_by_id(id)
290
+ begin
291
+ json_response = virtual_switches_interface.get(id.to_i)
292
+ return json_response[virtual_switch_object_key]
293
+ rescue RestClient::Exception => e
294
+ if e.response && e.response.code == 404
295
+ print_red_alert "Virtual Switch not found by id '#{id}'"
296
+ else
297
+ raise e
298
+ end
299
+ end
300
+ end
301
+
302
+ def find_virtual_switch_by_name(name)
303
+ json_response = virtual_switches_interface.list({name: name.to_s})
304
+ virtual_switches = json_response[virtual_switch_list_key]
305
+ if virtual_switches.empty?
306
+ print_red_alert "Virtual Switch not found by name '#{name}'"
307
+ return nil
308
+ elsif virtual_switches.size > 1
309
+ print_red_alert "#{virtual_switches.size} Virtual Switches found by name '#{name}'"
310
+ print_error "\n"
311
+ puts_error as_pretty_table(virtual_switches, [:id, :name], {color:red})
312
+ print_red_alert "Try using ID instead"
313
+ print_error reset,"\n"
314
+ return nil
315
+ else
316
+ return virtual_switches[0]
317
+ end
318
+ end
319
+
320
+ def format_virtual_switch_status(virtual_switch, return_color=cyan)
321
+ out = ""
322
+ status_string = virtual_switch['status'].to_s.downcase
323
+ if status_string
324
+ if ['available','ok'].include?(status_string)
325
+ out << "#{green}#{status_string.upcase}"
326
+ elsif ['unavailable','error'].include?(status_string)
327
+ out << "#{red}#{status_string.upcase}"
328
+ else
329
+ out << "#{return_color}#{status_string.upcase}"
330
+ end
331
+ end
332
+ out + return_color
333
+ end
334
+
335
+
336
+ end
@@ -166,11 +166,11 @@ module Morpheus::Cli::InfrastructureHelper
166
166
 
167
167
  # Networks
168
168
 
169
- def find_network_by_name_or_id(val)
169
+ def find_network_by_name_or_id(val, include_tenants=false)
170
170
  if val.to_s =~ /\A\d{1,}\Z/
171
171
  return find_network_by_id(val)
172
172
  else
173
- return find_network_by_name(val)
173
+ return find_network_by_name(val, include_tenants)
174
174
  end
175
175
  end
176
176
 
@@ -188,8 +188,10 @@ module Morpheus::Cli::InfrastructureHelper
188
188
  end
189
189
  end
190
190
 
191
- def find_network_by_name(name)
192
- json_response = networks_interface.list({name: name.to_s})
191
+ def find_network_by_name(name, include_tenants=false)
192
+ params = {name: name.to_s}
193
+ params['includeTenants'] = true if include_tenants
194
+ json_response = networks_interface.list(params)
193
195
  networks = json_response['networks']
194
196
  if networks.empty?
195
197
  print_red_alert "Network not found by name #{name}"
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "9.0.0"
4
+ VERSION = "9.0.2"
5
5
  end
6
6
  end
@@ -0,0 +1,79 @@
1
+ require 'morpheus_test'
2
+
3
+ # Tests for Morpheus::Cli::StorageVolumes
4
+ class MorpheusTest::StorageVolumesTest < MorpheusTest::TestCase
5
+
6
+ def test_storage_volumes_list
7
+ assert_execute %(storage-volumes list)
8
+ end
9
+
10
+ def test_storage_volumes_get
11
+ storage_volume = client.storage_volumes.list({})['storageVolumes'][0]
12
+ if storage_volume
13
+ assert_execute %(storage-volumes get "#{storage_volume['id']}")
14
+ else
15
+ puts "No storage volumes found, unable to execute test `#{__method__}`"
16
+ end
17
+ end
18
+
19
+ # CLI-side validation: --tenant is required (no network call needed).
20
+ def test_storage_volumes_update_tenant_requires_tenant
21
+ assert_error %(storage-volumes update-tenant 1), "Expected missing --tenant to fail with a usage error"
22
+ end
23
+
24
+ # Happy path is verified with --dry-run so the test never mutates real ownership.
25
+ # Confirms the volume is resolved and the PUT payload is built as expected.
26
+ def test_storage_volumes_update_tenant_dry_run
27
+ storage_volume = client.storage_volumes.list({})['storageVolumes'].find {|it| it['status'] == 'unattached' }
28
+ storage_volume ||= client.storage_volumes.list({})['storageVolumes'][0]
29
+ if storage_volume
30
+ tenant = client.accounts.list({})['accounts'][0]
31
+ if tenant
32
+ assert_execute %(storage-volumes update-tenant "#{storage_volume['id']}" --tenant "#{tenant['id']}" --dry-run)
33
+ else
34
+ puts "No tenants found, unable to execute test `#{__method__}`"
35
+ end
36
+ else
37
+ puts "No storage volumes found, unable to execute test `#{__method__}`"
38
+ end
39
+ end
40
+
41
+ # --include-tenants adds includeTenants=true to the list request (master tenant only).
42
+ def test_storage_volumes_list_include_tenants_dry_run
43
+ assert_execute %(storage-volumes list --include-tenants --dry-run)
44
+ output = capture_terminal_stdout { terminal.execute %(storage-volumes list --include-tenants --dry-run) }
45
+ assert_match(/includeTenants=true/, output, "Expected includeTenants=true in the dry-run request")
46
+ end
47
+
48
+ # --tenant with a numeric id resolves directly to tenantId without an account lookup.
49
+ def test_storage_volumes_list_tenant_id_dry_run
50
+ assert_execute %(storage-volumes list --tenant 1 --dry-run)
51
+ output = capture_terminal_stdout { terminal.execute %(storage-volumes list --tenant 1 --dry-run) }
52
+ assert_match(/tenantId=1/, output, "Expected tenantId=1 in the dry-run request")
53
+ end
54
+
55
+ # --tenant with a name resolves to an id via the accounts API, then sets tenantId.
56
+ def test_storage_volumes_list_tenant_name_dry_run
57
+ tenant = client.accounts.list({})['accounts'][0]
58
+ if tenant
59
+ output = capture_terminal_stdout { terminal.execute %(storage-volumes list --tenant "#{tenant['name']}" --dry-run) }
60
+ assert_match(/tenantId=#{tenant['id']}/, output, "Expected tenantId=#{tenant['id']} resolved from tenant name")
61
+ else
62
+ puts "No tenants found, unable to execute test `#{__method__}`"
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # Capture everything written to the terminal's stdout while the block runs.
69
+ def capture_terminal_stdout
70
+ original_stdout = terminal.stdout
71
+ buffer = StringIO.new
72
+ terminal.set_stdout(buffer)
73
+ yield
74
+ buffer.string
75
+ ensure
76
+ terminal.set_stdout(original_stdout)
77
+ end
78
+
79
+ 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: 9.0.0
4
+ version: 9.0.2
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-06-15 00:00:00.000000000 Z
14
+ date: 2026-08-26 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: tins
@@ -399,6 +399,7 @@ files:
399
399
  - lib/morpheus/api/vdi_pools_interface.rb
400
400
  - lib/morpheus/api/virtual_images_interface.rb
401
401
  - lib/morpheus/api/virtual_servers_interface.rb
402
+ - lib/morpheus/api/virtual_switches_interface.rb
402
403
  - lib/morpheus/api/whitelabel_settings_interface.rb
403
404
  - lib/morpheus/api/whoami_interface.rb
404
405
  - lib/morpheus/api/wiki_interface.rb
@@ -594,6 +595,7 @@ files:
594
595
  - lib/morpheus/cli/commands/version_command.rb
595
596
  - lib/morpheus/cli/commands/view.rb
596
597
  - lib/morpheus/cli/commands/virtual_images.rb
598
+ - lib/morpheus/cli/commands/virtual_switches_command.rb
597
599
  - lib/morpheus/cli/commands/whitelabel_settings_command.rb
598
600
  - lib/morpheus/cli/commands/whoami.rb
599
601
  - lib/morpheus/cli/commands/wiki_command.rb
@@ -662,6 +664,7 @@ files:
662
664
  - test/cli/remote_test.rb
663
665
  - test/cli/roles_test.rb
664
666
  - test/cli/shell_test.rb
667
+ - test/cli/storage_volumes_test.rb
665
668
  - test/cli/systems_test.rb
666
669
  - test/cli/version_test.rb
667
670
  - test/cli/view_test.rb
@@ -713,6 +716,7 @@ test_files:
713
716
  - test/cli/remote_test.rb
714
717
  - test/cli/roles_test.rb
715
718
  - test/cli/shell_test.rb
719
+ - test/cli/storage_volumes_test.rb
716
720
  - test/cli/systems_test.rb
717
721
  - test/cli/version_test.rb
718
722
  - test/cli/view_test.rb