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 +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/clusters_interface.rb +24 -0
- data/lib/morpheus/api/instances_interface.rb +1 -1
- data/lib/morpheus/api/options_interface.rb +1 -1
- data/lib/morpheus/api/storage_volumes_interface.rb +8 -0
- data/lib/morpheus/api/virtual_images_interface.rb +10 -2
- data/lib/morpheus/cli/commands/backups_command.rb +9 -6
- data/lib/morpheus/cli/commands/clusters.rb +72 -0
- data/lib/morpheus/cli/commands/instances.rb +1 -1
- data/lib/morpheus/cli/commands/storage_volumes.rb +77 -2
- data/lib/morpheus/cli/version.rb +1 -1
- data/test/cli/storage_volumes_test.rb +79 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1f2fdd08e0bf0dbe8567812778748a10d38dbb6efd64430fc50fdf1134528be3
|
|
4
|
+
data.tar.gz: 2219cdf94a929a790e75d727068567adb10d09a80c30971e96365726812d3334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b276775f4746e397b8d4b05c3c09c04d9b02409c7511e6a40a3ac80a160642d376fb10a2bf78477bfe84f3b3fc9abc1680ca332307efe4c250ec2b478a961f3
|
|
7
|
+
data.tar.gz: 7046dcbfff992519d64e93c1fd1383b0bb1c3166dfad466d56661a2ee65e0b45038a9d94d4c7812ab423c285267347609337e0312fddeeae82fe5f001b67cc6e
|
data/Dockerfile
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
614
|
-
"#{green}REMOTE#{reset} (#{
|
|
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
|
-
|
|
617
|
-
"#{green}REMOTE#{reset} (#{
|
|
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)
|
data/lib/morpheus/cli/version.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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
|