morpheus-cli 9.0.0 → 9.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1b224b836c51abacb14e3e75c0cc9d88a59b84932c1550f708ffbae41ea08ad3
4
- data.tar.gz: ee787f62e945490b8537dc9bbbf3a027dadbb7dde1272a57492c92b1111d5f39
3
+ metadata.gz: 1f2fdd08e0bf0dbe8567812778748a10d38dbb6efd64430fc50fdf1134528be3
4
+ data.tar.gz: 2219cdf94a929a790e75d727068567adb10d09a80c30971e96365726812d3334
5
5
  SHA512:
6
- metadata.gz: cbe5cf5150058a44b1441ff60de0473af2814390bede4750195bae54973918a828ee5b9a747e4470d1cc084eee45c0d62eff5cad647535a6ccef1d8765335005
7
- data.tar.gz: 2edbb553a70619a8e8ac47f14aa7f920949add47dd92beaaea0c8cdc1761e7b8aeafcde6e29a2ad0c1b16c31f37728f47d1f941d9af3f189e238233aa3bac59b
6
+ metadata.gz: 7b276775f4746e397b8d4b05c3c09c04d9b02409c7511e6a40a3ac80a160642d376fb10a2bf78477bfe84f3b3fc9abc1680ca332307efe4c250ec2b478a961f3
7
+ data.tar.gz: 7046dcbfff992519d64e93c1fd1383b0bb1c3166dfad466d56661a2ee65e0b45038a9d94d4c7812ab423c285267347609337e0312fddeeae82fe5f001b67cc6e
data/Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
1
  FROM ruby:2.7.5
2
2
 
3
- RUN gem install morpheus-cli -v 9.0.0
3
+ RUN gem install morpheus-cli -v 9.0.1
4
4
 
5
5
  ENTRYPOINT ["morpheus"]
@@ -389,5 +389,29 @@ class Morpheus::ClustersInterface < Morpheus::APIClient
389
389
  headers = { :params => params, :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
390
390
  execute(method: :delete, url: url, headers: headers)
391
391
  end
392
+
393
+ def available_updates(id, params={})
394
+ url = "#{base_path}/#{id}/available-updates"
395
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
396
+ execute(method: :get, url: url, headers: headers)
397
+ end
398
+
399
+ def list_updates(id, update_id, params={})
400
+ url = "#{base_path}/#{id}/available-updates"
401
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
402
+ execute(method: :get, url: url, headers: headers)
403
+ end
404
+
405
+ def get_update(id, update_id, params={})
406
+ url = "#{base_path}/#{id}/available-updates/#{update_id}"
407
+ headers = { params: params, authorization: "Bearer #{@access_token}" }
408
+ execute(method: :get, url: url, headers: headers)
409
+ end
410
+
411
+ def execute_update(id, payload)
412
+ url = "#{base_path}/#{id}/execute-update"
413
+ headers = { :authorization => "Bearer #{@access_token}", 'Content-Type' => 'application/json' }
414
+ execute(method: :post, url: url, headers: headers, payload: payload.to_json)
415
+ end
392
416
 
393
417
  end
@@ -296,7 +296,7 @@ class Morpheus::InstancesInterface < Morpheus::APIClient
296
296
  def service_plans(params={})
297
297
  url = "#{@base_url}/api/instances/service-plans"
298
298
  headers = { params: params, authorization: "Bearer #{@access_token}" }
299
- opts = {method: :get, url: url, headers: headers}
299
+ opts = {method: :get, url: url, headers: headers, timeout: 180}
300
300
  execute(opts)
301
301
  end
302
302
 
@@ -12,6 +12,6 @@ class Morpheus::OptionsInterface < Morpheus::APIClient
12
12
  url = "#{@base_url}/api/options/#{option_source_type}/#{source}"
13
13
  end
14
14
  headers = { params: params, authorization: "Bearer #{@access_token}" }
15
- execute(method: :get, url: url, headers: headers)
15
+ execute(method: :get, url: url, headers: headers, timeout: 180)
16
16
  end
17
17
  end
@@ -13,4 +13,12 @@ class Morpheus::StorageVolumesInterface < Morpheus::RestInterface
13
13
  execute(opts)
14
14
  end
15
15
 
16
+ # Transfer ownership of a storage volume to another tenant.
17
+ # The server expects { "storageVolume": { "tenant": { "id": <id> } } }
18
+ # and handles all authorization and business-rule validation.
19
+ def update_tenant(id, payload, params={}, headers={})
20
+ validate_id!(id)
21
+ execute(method: :put, url: "#{base_path}/#{CGI::escape(id.to_s)}", params: params, payload: payload, headers: headers)
22
+ end
23
+
16
24
  end
@@ -117,7 +117,11 @@ class Morpheus::VirtualImagesInterface < Morpheus::APIClient
117
117
  gz.close
118
118
  }
119
119
  http_opts[:body] = Morpheus::BodyIO.new(rd)
120
- response = http.post(url, http_opts)
120
+ if Gem::Version.new(HTTP::VERSION) >= Gem::Version.new("6.0.0")
121
+ response = http.post(url, **http_opts)
122
+ else
123
+ response = http.post(url, http_opts)
124
+ end
121
125
  else
122
126
  if @dry_run
123
127
  return {method: :post, url: url, headers: headers, params: query_params, payload: payload}
@@ -126,7 +130,11 @@ class Morpheus::VirtualImagesInterface < Morpheus::APIClient
126
130
  http = HTTP.headers(headers)
127
131
  http_opts[:params] = query_params
128
132
  http_opts[:body] = payload
129
- response = http.post(url, http_opts)
133
+ if Gem::Version.new(HTTP::VERSION) >= Gem::Version.new("6.0.0")
134
+ response = http.post(url, **http_opts)
135
+ else
136
+ response = http.post(url, http_opts)
137
+ end
130
138
  end
131
139
  # puts "Took #{Time.now.to_i - start_time.to_i}"
132
140
  # return response
@@ -96,6 +96,8 @@ EOT
96
96
  print_h1 "Backup Details", [], options
97
97
  print cyan
98
98
  columns = backup_column_definitions
99
+ columns.delete("Backup Provider") if backup['backupProvider'].nil?
100
+ columns.delete("Storage Provider") if backup['storageProvider'].nil?
99
101
  columns.delete("Instance") if backup['instance'].nil?
100
102
  columns.delete("Container ID") if backup['containerId'].nil?
101
103
  columns.delete("Host") if backup['server'].nil?
@@ -541,8 +543,9 @@ EOT
541
543
  {
542
544
  "ID" => 'id',
543
545
  "Name" => 'name',
544
- "Backup Type" => lambda {|it| format_backup_type_tag(it) },
545
- "Backup Location" => lambda {|it| format_backup_location_tag(it) },
546
+ "Backup Type" => lambda {|it| (it['backupType'] && it['backupType']['name']) ? it['backupType']['name'] : format_backup_type_tag(it) },
547
+ "Backup Provider" => lambda {|it| it['backupProvider']['name'] rescue '' },
548
+ "Storage Provider" => lambda {|it| it['storageProvider']['name'] rescue '' },
546
549
  "Location Type" => lambda {|it|
547
550
  if it['locationType'] == "instance"
548
551
  "Instance"
@@ -610,11 +613,11 @@ EOT
610
613
  def format_backup_location_tag(backup)
611
614
  # check storage provider or backup provider indicating remote storage
612
615
  if backup['storageProvider'] && backup['storageProvider']['id']
613
- provider_type = backup['storageProvider']['type'] || backup['storageProvider']['providerType'] || ''
614
- "#{green}REMOTE#{reset} (#{provider_type})"
616
+ provider_label = backup['storageProvider']['type'] || backup['storageProvider']['providerType'] || backup['storageProvider']['name'] || ''
617
+ "#{green}REMOTE#{reset} (#{provider_label})"
615
618
  elsif backup['backupProvider'] && backup['backupProvider']['id']
616
- provider_type = backup['backupProvider']['type'] || backup['backupProvider']['providerType'] || ''
617
- "#{green}REMOTE#{reset} (#{provider_type})"
619
+ provider_label = backup['backupProvider']['type'] || backup['backupProvider']['providerType'] || backup['backupProvider']['name'] || ''
620
+ "#{green}REMOTE#{reset} (#{provider_label})"
618
621
  else
619
622
  "#{blue}LOCAL#{reset}"
620
623
  end
@@ -13,6 +13,7 @@ class Morpheus::Cli::Clusters
13
13
  register_subcommands :list_workers, :add_worker, :remove_worker, :update_worker_count
14
14
  register_subcommands :list_masters
15
15
  register_subcommands :upgrade_cluster
16
+ register_subcommands :update_version
16
17
  register_subcommands :list_volumes, :remove_volume
17
18
  register_subcommands :list_namespaces, :get_namespace, :add_namespace, :update_namespace, :remove_namespace
18
19
  register_subcommands :list_containers, :remove_container, :restart_container, :get_container
@@ -697,6 +698,13 @@ class Morpheus::Cli::Clusters
697
698
  ssh_host_option = option_type_list.select{|it| it['fieldName'] == 'sshHosts'}.first
698
699
  ssh_host_option['minCount'] = server_count unless ssh_host_option.nil?
699
700
 
701
+ # Fix for bug in 9.0.0 - 9.0.2 with optionType data where defaultValue is set to "unmanaged" for witness.id, which is not a valid value. Remove it so the user can select a valid value.
702
+ option_type_list.each do |option_type|
703
+ if option_type['fieldName'] == 'witness.id' && option_type['defaultValue'] == 'unmanaged'
704
+ option_type.delete('defaultValue')
705
+ end
706
+ end
707
+
700
708
  # Server options
701
709
  server_payload.deep_merge!(Morpheus::Cli::OptionTypes.prompt(option_type_list, options[:options].deep_merge({:context_map => {'domain' => ''}}), @api_client, api_params, options[:no_prompt], true))
702
710
 
@@ -3237,6 +3245,9 @@ class Morpheus::Cli::Clusters
3237
3245
  opts.on('--supports-vm-secure-metadata [on|off]', String, "Enable VM Secure Metadata support") do |val|
3238
3246
  options[:supportsVmSecureMetadata] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
3239
3247
  end
3248
+ opts.on('--heartbeat-target [on|off]', String, "Set as the heartbeat target. Default is on") do |val|
3249
+ options[:heartbeatTarget] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == '1' || val.to_s == ''
3250
+ end
3240
3251
  add_perms_options(opts, options, ['plans', 'groupDefaults'])
3241
3252
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
3242
3253
  opts.footer = "Update a cluster datastore.\n" +
@@ -3254,6 +3265,7 @@ class Morpheus::Cli::Clusters
3254
3265
  cluster = find_cluster_by_name_or_id(args[0])
3255
3266
  return 1 if cluster.nil?
3256
3267
  datastore = find_datastore_by_name_or_id(cluster['id'], args[1])
3268
+ datastore_type = find_datastore_type_by_code(datastore['datastoreType']['code']) rescue nil
3257
3269
  if datastore.nil?
3258
3270
  print_red_alert "Datastore not found by '#{args[1]}'"
3259
3271
  exit 1
@@ -3269,6 +3281,14 @@ class Morpheus::Cli::Clusters
3269
3281
  payload = {'datastore' => {}}
3270
3282
  payload['datastore']['active'] = options[:active].nil? ? (Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'description' => 'Datastore Active', 'defaultValue' => true}], options[:options], @api_client))['active'] == 'on' : options[:active]
3271
3283
  payload['datastore']['supportsVmSecureMetadata'] = options[:supportsVmSecureMetadata] unless options[:supportsVmSecureMetadata].nil?
3284
+
3285
+ if !options[:heartbeatTarget].nil?
3286
+ payload['datastore']['heartbeatTarget'] = options[:heartbeatTarget]
3287
+ elsif datastore_type && datastore_type['heartbeatTargetCapable'] == true
3288
+ val = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'heartbeatTarget', 'fieldLabel' => 'Heartbeat Target', 'type' => 'checkbox', 'description' => 'Heartbeat Target', 'defaultValue' => datastore['heartbeatTarget']}], options[:options], @api_client)['heartbeatTarget']
3289
+ payload['datastore']['heartbeatTarget'] = val == 'on' || val == true unless val.nil? || val == ''
3290
+ end
3291
+
3272
3292
 
3273
3293
  perms = prompt_permissions(options.merge({:available_plans => namespace_service_plans}), datastore['owner']['id'] == current_user['accountId'] ? ['plans', 'groupDefaults'] : ['plans', 'groupDefaults', 'visibility', 'tenants'])
3274
3294
  perms_payload = {}
@@ -4426,6 +4446,58 @@ class Morpheus::Cli::Clusters
4426
4446
  Morpheus::Cli::ClusterTypes.new.get(args)
4427
4447
  end
4428
4448
 
4449
+ def update_version(args)
4450
+ params = {}
4451
+ options = {}
4452
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
4453
+ opts.banner = subcommand_usage("[cluster]")
4454
+ build_standard_update_options(opts, options, [:auto_confirm])
4455
+ opts.footer = "Updates HVM cluster layout to a new version.\n" +
4456
+ "[cluster] is required. This is the name or id of an existing cluster.\n"
4457
+ end
4458
+ optparse.parse!(args)
4459
+ verify_args!(args:args, optparse:optparse, count:1)
4460
+ connect(options)
4461
+
4462
+ cluster = find_cluster_by_name_or_id(args[0])
4463
+ return 1 if cluster.nil?
4464
+
4465
+ payload = {}
4466
+ if options[:payload]
4467
+ payload = options[:payload]
4468
+ payload.deep_merge!(parse_passed_options(options))
4469
+ else
4470
+ payload.deep_merge!(parse_passed_options(options))
4471
+
4472
+ available_updates = @clusters_interface.available_updates(cluster['id'])['updateDefinitions']
4473
+ if available_updates.empty?
4474
+ print yellow,"No updates available for cluster #{cluster['name']}",reset,"\n"
4475
+ return 1, "No updates available for cluster #{cluster['name']}"
4476
+ end
4477
+ version_options = available_updates.collect { |it| {'name' => it['name'], 'value' => it['id']} }
4478
+ update_definition_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'updateDefinitionId', 'type' => 'select', 'fieldLabel' => 'Update', 'selectOptions' => version_options, 'required' => true, 'description' => 'Select version update to execute' }],options[:options],api_client,{})['updateDefinitionId']
4479
+ payload.deep_merge!({'updateDefinitionId' => update_definition_id})
4480
+ end
4481
+ target_version = available_updates.find { |it| it['id'] == payload['updateDefinitionId'] }['updateVersion'] rescue nil
4482
+ if target_version.nil?
4483
+ print_red_alert "Unable to determine target version for update definition #{payload['updateDefinitionId']}"
4484
+ return 1, "Unable to determine target version for update definition #{payload['updateDefinitionId']}"
4485
+ end
4486
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to update cluster #{cluster['name']} to version #{target_version}?")
4487
+ return 9, "aborted command"
4488
+ end
4489
+ @clusters_interface.setopts(options)
4490
+ if options[:dry_run]
4491
+ print_dry_run @clusters_interface.dry.execute_update(cluster['id'], payload)
4492
+ return
4493
+ end
4494
+ json_response = @clusters_interface.execute_update(cluster['id'], payload)
4495
+ render_response(json_response, options) do
4496
+ print_green_success "Cluster #{cluster['name']} is being updated to version #{target_version}..."
4497
+ end
4498
+ return 0, nil
4499
+ end
4500
+
4429
4501
  private
4430
4502
 
4431
4503
  def print_clusters_table(clusters, opts={})
@@ -315,7 +315,7 @@ class Morpheus::Cli::Instances
315
315
  }
316
316
  row
317
317
  }
318
- columns = [:id, {:name => {:max_width => 50}}, :tenant, :group, :cloud,
318
+ columns = [:id, {:name => {:max_width => 50}}, :labels, :tenant, :group, :cloud,
319
319
  :type, :version, :environment, :plan,
320
320
  {:created => {:display_name => "CREATED"}},
321
321
  {:user => {:display_name => "OWNER", :max_width => 20}},
@@ -4,13 +4,14 @@ class Morpheus::Cli::StorageVolumes
4
4
  include Morpheus::Cli::CliCommand
5
5
  include Morpheus::Cli::RestCommand
6
6
  include Morpheus::Cli::StorageVolumesHelper
7
+ include Morpheus::Cli::AccountsHelper
7
8
 
8
9
  set_command_name :'storage-volumes'
9
10
  set_command_description "View and manage storage volumes."
10
- register_subcommands :list, :get, :add, :remove, :resize
11
+ register_subcommands :list, :get, :add, :remove, :resize, :update_tenant
11
12
 
12
13
  # RestCommand settings
13
- register_interfaces :storage_volumes, :storage_volume_types
14
+ register_interfaces :storage_volumes, :storage_volume_types, :accounts
14
15
  set_rest_has_type true
15
16
 
16
17
  protected
@@ -28,12 +29,28 @@ class Morpheus::Cli::StorageVolumes
28
29
  opts.on('--category VALUE', String, "Filter by category") do |val|
29
30
  params['category'] = val
30
31
  end
32
+ opts.on('--include-tenants', '--include-tenants', "Include sub-tenant storage volumes (master tenant only)") do
33
+ options[:include_tenants] = true
34
+ params['includeTenants'] = true
35
+ end
36
+ opts.on('--tenant TENANT', String, "Filter by Tenant Name or ID (master tenant only)") do |val|
37
+ options[:tenant] = val
38
+ end
31
39
  # build_standard_list_options(opts, options)
32
40
  super
33
41
  end
34
42
 
35
43
  def parse_list_options!(args, options, params)
36
44
  parse_parameter_as_resource_id!(:storage_server, options, params)
45
+ if options[:tenant]
46
+ if options[:tenant].to_s =~ /\A\d+\Z/
47
+ params['tenantId'] = options[:tenant]
48
+ else
49
+ account = find_account_by_name_or_id(options[:tenant])
50
+ return 1 if account.nil?
51
+ params['tenantId'] = account['id']
52
+ end
53
+ end
37
54
  super
38
55
  end
39
56
 
@@ -122,6 +139,64 @@ class Morpheus::Cli::StorageVolumes
122
139
  end
123
140
  end
124
141
 
142
+ def update_tenant(args)
143
+ options = {}
144
+ params = {}
145
+ tenant_id = nil
146
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
147
+ opts.banner = subcommand_usage("[volume] --tenant TENANT")
148
+ opts.on('--tenant TENANT', String, "Target Tenant (Account) Name or ID to transfer ownership to.") do |val|
149
+ tenant_id = val
150
+ end
151
+ build_standard_update_options(opts, options)
152
+ opts.footer = <<-EOT
153
+ Transfer ownership of a storage volume to another tenant.
154
+ [volume] is required. This is the name or id of a storage volume.
155
+ --tenant is required unless provided via --payload. This is the name or id of the target tenant.
156
+ Only an unattached volume may be transferred. All authorization and business-rule
157
+ validation is performed by the server; its message is shown verbatim on failure.
158
+ EOT
159
+ end
160
+ optparse.parse!(args)
161
+ verify_args!(args:args, optparse:optparse, count:1)
162
+ # CLI-side validation: require a target tenant unless a full payload is supplied.
163
+ # All other authorization and business rules are validated server-side.
164
+ if !options[:payload] && tenant_id.nil?
165
+ raise_command_error "--tenant is required to transfer ownership.\n#{optparse}"
166
+ end
167
+ connect(options)
168
+ volume = find_volume_by_name_or_id(args[0])
169
+ return 1 if volume.nil?
170
+ id = volume['id']
171
+ passed_options = parse_passed_options(options)
172
+ payload = {}
173
+ if options[:payload]
174
+ payload = options[:payload]
175
+ payload.deep_merge!({storage_volume_object_key => passed_options}) unless passed_options.empty?
176
+ else
177
+ account = find_account_by_name_or_id(tenant_id)
178
+ return 1 if account.nil?
179
+ payload[storage_volume_object_key] = {'tenant' => {'id' => account['id']}}
180
+ payload.deep_merge!({storage_volume_object_key => passed_options}) unless passed_options.empty?
181
+ end
182
+ @storage_volumes_interface.setopts(options)
183
+ if options[:dry_run]
184
+ print_dry_run @storage_volumes_interface.dry.update_tenant(id, payload)
185
+ return 0, nil
186
+ end
187
+ json_response = @storage_volumes_interface.update_tenant(id, payload)
188
+ render_response(json_response, options, storage_volume_object_key) do
189
+ record = json_response[storage_volume_object_key]
190
+ owner_name = (record && record['owner'] ? record['owner']['name'] : (record && record['account'] ? record['account']['name'] : tenant_id))
191
+ print_green_success "Transferred storage volume #{record ? record['name'] : id} to tenant #{owner_name}"
192
+ print_h1 rest_label, [], options
193
+ print cyan
194
+ print_description_list(storage_volume_column_definitions(options), record, options)
195
+ print reset,"\n"
196
+ end
197
+ return 0, nil
198
+ end
199
+
125
200
  def find_volume_by_id(id)
126
201
  begin
127
202
  json_response = @storage_volumes_interface.get(id.to_i)
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "9.0.0"
4
+ VERSION = "9.0.1"
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.1
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-07-24 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: tins
@@ -662,6 +662,7 @@ files:
662
662
  - test/cli/remote_test.rb
663
663
  - test/cli/roles_test.rb
664
664
  - test/cli/shell_test.rb
665
+ - test/cli/storage_volumes_test.rb
665
666
  - test/cli/systems_test.rb
666
667
  - test/cli/version_test.rb
667
668
  - test/cli/view_test.rb
@@ -713,6 +714,7 @@ test_files:
713
714
  - test/cli/remote_test.rb
714
715
  - test/cli/roles_test.rb
715
716
  - test/cli/shell_test.rb
717
+ - test/cli/storage_volumes_test.rb
716
718
  - test/cli/systems_test.rb
717
719
  - test/cli/version_test.rb
718
720
  - test/cli/view_test.rb