morpheus-cli 8.1.2 → 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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +2 -2
  3. data/lib/morpheus/api/api_client.rb +8 -0
  4. data/lib/morpheus/api/clusters_interface.rb +24 -0
  5. data/lib/morpheus/api/groups_interface.rb +2 -1
  6. data/lib/morpheus/api/instances_interface.rb +1 -1
  7. data/lib/morpheus/api/options_interface.rb +1 -1
  8. data/lib/morpheus/api/servers_interface.rb +0 -1
  9. data/lib/morpheus/api/storage_volumes_interface.rb +8 -0
  10. data/lib/morpheus/api/support_bundles_interface.rb +46 -0
  11. data/lib/morpheus/api/systems_interface.rb +32 -0
  12. data/lib/morpheus/api/tokens_interface.rb +39 -0
  13. data/lib/morpheus/api/virtual_images_interface.rb +10 -2
  14. data/lib/morpheus/cli/cli_command.rb +6 -1
  15. data/lib/morpheus/cli/commands/backups_command.rb +9 -6
  16. data/lib/morpheus/cli/commands/clients_command.rb +60 -74
  17. data/lib/morpheus/cli/commands/clouds.rb +30 -2
  18. data/lib/morpheus/cli/commands/clusters.rb +77 -0
  19. data/lib/morpheus/cli/commands/execution_request_command.rb +6 -2
  20. data/lib/morpheus/cli/commands/groups.rb +46 -23
  21. data/lib/morpheus/cli/commands/hosts.rb +21 -14
  22. data/lib/morpheus/cli/commands/instances.rb +32 -6
  23. data/lib/morpheus/cli/commands/storage_volumes.rb +78 -3
  24. data/lib/morpheus/cli/commands/support_bundles_command.rb +606 -0
  25. data/lib/morpheus/cli/commands/systems.rb +606 -2
  26. data/lib/morpheus/cli/commands/tokens_command.rb +391 -0
  27. data/lib/morpheus/cli/commands/workflows.rb +16 -3
  28. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +30 -14
  29. data/lib/morpheus/cli/mixins/provisioning_helper.rb +21 -7
  30. data/lib/morpheus/cli/version.rb +1 -1
  31. data/test/api/systems_interface_test.rb +26 -0
  32. data/test/cli/storage_volumes_test.rb +79 -0
  33. data/test/cli/systems_test.rb +206 -0
  34. metadata +12 -2
@@ -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
 
@@ -3064,6 +3072,7 @@ class Morpheus::Cli::Clusters
3064
3072
  "Active" => lambda { |it| format_boolean(it['active']) },
3065
3073
  "Visibility" => lambda { |it| it['visibility'].nil? ? '' : it['visibility'].to_s.capitalize },
3066
3074
  "Tenants" => lambda { |it| it['tenants'].nil? ? '' : it['tenants'].collect {|it| it['name']}.join(', ') },
3075
+ "Supports VM Secure Metadata" => lambda { |it| format_boolean(it['supportsVmSecureMetadata']) },
3067
3076
  "Cluster" => lambda { |it| cluster['name'] }
3068
3077
  }
3069
3078
  print_description_list(description_cols, datastore)
@@ -3233,6 +3242,12 @@ class Morpheus::Cli::Clusters
3233
3242
  opts.on('--active [on|off]', String, "Enable datastore") do |val|
3234
3243
  options[:active] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
3235
3244
  end
3245
+ opts.on('--supports-vm-secure-metadata [on|off]', String, "Enable VM Secure Metadata support") do |val|
3246
+ options[:supportsVmSecureMetadata] = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
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
3236
3251
  add_perms_options(opts, options, ['plans', 'groupDefaults'])
3237
3252
  build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
3238
3253
  opts.footer = "Update a cluster datastore.\n" +
@@ -3250,6 +3265,7 @@ class Morpheus::Cli::Clusters
3250
3265
  cluster = find_cluster_by_name_or_id(args[0])
3251
3266
  return 1 if cluster.nil?
3252
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
3253
3269
  if datastore.nil?
3254
3270
  print_red_alert "Datastore not found by '#{args[1]}'"
3255
3271
  exit 1
@@ -3264,6 +3280,15 @@ class Morpheus::Cli::Clusters
3264
3280
  else
3265
3281
  payload = {'datastore' => {}}
3266
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]
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
+
3267
3292
 
3268
3293
  perms = prompt_permissions(options.merge({:available_plans => namespace_service_plans}), datastore['owner']['id'] == current_user['accountId'] ? ['plans', 'groupDefaults'] : ['plans', 'groupDefaults', 'visibility', 'tenants'])
3269
3294
  perms_payload = {}
@@ -4421,6 +4446,58 @@ class Morpheus::Cli::Clusters
4421
4446
  Morpheus::Cli::ClusterTypes.new.get(args)
4422
4447
  end
4423
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
+
4424
4501
  private
4425
4502
 
4426
4503
  def print_clusters_table(clusters, opts={})
@@ -83,13 +83,13 @@ class Morpheus::Cli::ExecutionRequestCommand
83
83
  if options[:refresh_interval].nil? || options[:refresh_interval].to_f < 0
84
84
  options[:refresh_interval] = default_refresh_interval
85
85
  end
86
- if !(execution_request['status'] == 'pending' || execution_request['status'] == 'new')
86
+ if is_finished(execution_request)
87
87
  # it is finished
88
88
  else
89
89
  print cyan
90
90
  refresh_display_seconds = options[:refresh_interval] % 1.0 == 0 ? options[:refresh_interval].to_i : options[:refresh_interval]
91
91
  print "Execution request has not yet finished. Refreshing every #{refresh_display_seconds} seconds"
92
- while execution_request['status'] == 'pending' || execution_request['status'] == 'new' do
92
+ while !is_finished(execution_request) do
93
93
  sleep(options[:refresh_interval])
94
94
  print cyan,".",reset
95
95
  json_response = @execution_request_interface.get(execution_request_id, params)
@@ -326,4 +326,8 @@ class Morpheus::Cli::ExecutionRequestCommand
326
326
  out
327
327
  end
328
328
 
329
+ def is_finished(execution_request)
330
+ ['executing', 'pending', 'new'].include?(execution_request['status']) ? false : true
331
+ end
332
+
329
333
  end
@@ -2,6 +2,7 @@ require 'morpheus/cli/cli_command'
2
2
 
3
3
  class Morpheus::Cli::Groups
4
4
  include Morpheus::Cli::CliCommand
5
+ include Morpheus::Cli::AccountsHelper
5
6
  include Morpheus::Cli::InfrastructureHelper
6
7
  include Morpheus::Cli::OptionSourceHelper
7
8
 
@@ -19,6 +20,8 @@ class Morpheus::Cli::Groups
19
20
  @groups_interface = @api_client.groups
20
21
  @clouds_interface = @api_client.clouds
21
22
  @options_interface = @api_client.options
23
+ @accounts_interface = @api_client.accounts
24
+ @account_users_interface = @api_client.account_users
22
25
  @active_group_id = Morpheus::Cli::Groups.active_groups[@appliance_name]
23
26
  end
24
27
 
@@ -37,12 +40,31 @@ class Morpheus::Cli::Groups
37
40
  opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
38
41
  add_query_parameter(params, 'allLabels', parse_labels(val))
39
42
  end
43
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant groups") do
44
+ options[:include_tenants] = true
45
+ params['includeTenants'] = true
46
+ end
47
+ opts.on('--tenant TENANT', String, "Tenant Name or ID" ) do |val|
48
+ options[:tenant] = val
49
+ end
40
50
  build_standard_list_options(opts, options)
41
51
  opts.footer = "List groups."
42
52
  end
43
53
  optparse.parse!(args)
44
54
  verify_args!(args:args, optparse:optparse, count:0)
45
55
  connect(options)
56
+ if options[:tenant]
57
+ if options[:tenant].to_s !~ /\A\d{1,}\Z/
58
+ account = find_account_by_name_or_id(options[:tenant])
59
+ if account.nil?
60
+ return 1, "Tenant not found by name: #{options[:tenant]}"
61
+ else
62
+ params['tenantId'] = account['id']
63
+ end
64
+ else
65
+ params['tenantId'] = options[:tenant]
66
+ end
67
+ end
46
68
  params.merge!(parse_list_options(options))
47
69
  @groups_interface.setopts(options)
48
70
  if options[:dry_run]
@@ -71,6 +93,9 @@ class Morpheus::Cli::Groups
71
93
  options = {}
72
94
  optparse = Morpheus::Cli::OptionParser.new do|opts|
73
95
  opts.banner = subcommand_usage("[name]")
96
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant groups when finding group by name") do
97
+ options[:include_tenants] = true
98
+ end
74
99
  build_standard_get_options(opts, options)
75
100
  opts.footer = <<-EOT
76
101
  Get details about a group.
@@ -86,25 +111,20 @@ EOT
86
111
  end
87
112
  end
88
113
 
89
- def _get(arg, options={})
90
- begin
91
- if options[:dry_run]
92
- @groups_interface.setopts(options)
93
- if arg.to_s =~ /\A\d{1,}\Z/
94
- print_dry_run @groups_interface.dry.get(arg.to_i)
95
- else
96
- print_dry_run @groups_interface.dry.list({name:arg})
97
- end
98
- return 0
99
- end
100
- group = find_group_by_name_or_id(arg)
101
- @groups_interface.setopts(options)
102
- #json_response = @groups_interface.get(group['id'])
103
- json_response = {'group' => group}
104
-
105
- render_result = render_with_format(json_response, options)
106
- return 0 if render_result
107
-
114
+ def _get(id, options={})
115
+ params = {}
116
+ group = nil
117
+ if id.to_s !~ /\A\d{1,}\Z/
118
+ group = find_group_by_name_or_id(id, options[:include_tenants])
119
+ id = group['id']
120
+ end
121
+ @groups_interface.setopts(options)
122
+ if options[:dry_run]
123
+ print_dry_run @groups_interface.dry.get(id.to_i, params)
124
+ return
125
+ end
126
+ json_response = @groups_interface.get(id.to_i, params)
127
+ render_response(json_response, options, 'group') do
108
128
  group = json_response['group']
109
129
  group_stats = group['stats']
110
130
  # serverCounts moved to zone.stats.serverCounts
@@ -124,6 +144,7 @@ EOT
124
144
  "Location" => 'location',
125
145
  "Labels" => lambda {|it| format_list(it['labels'], '') rescue '' },
126
146
  "Clouds" => lambda {|it| it['zones'].collect {|z| z['name'] }.join(', ') },
147
+ "Tenant" => lambda {|it| it['account'] ? it['account']['name'] : '' },
127
148
  #"Instances" => lambda {|it| it['stats']['instanceCounts']['all'] rescue '' },
128
149
  # "Hosts" => lambda {|it| it['stats']['serverCounts']['host'] rescue it['serverCount'] },
129
150
  # "VMs" => lambda {|it| it['stats']['serverCounts']['vm'] rescue '' },
@@ -665,13 +686,14 @@ EOT
665
686
 
666
687
  protected
667
688
 
668
- def print_groups_table(groups, opts={})
669
- table_color = opts[:color] || cyan
689
+ def print_groups_table(groups, options={})
690
+ table_color = options[:color] || cyan
670
691
  active_group = @active_group_id ? groups.find {|group| group['id'] == @active_group_id } : nil
671
692
  rows = groups.collect do |group|
672
693
  {
673
694
  id: active_group ? (((@active_group_id && (@active_group_id == group['id'])) ? "=> " : " ") + group['id'].to_s) : group['id'],
674
695
  name: group['name'],
696
+ tenant: group['account'] ? group['account']['name'] : '',
675
697
  labels: group['labels'],
676
698
  location: group['location'],
677
699
  cloud_count: group['zones'] ? group['zones'].size : 0,
@@ -685,6 +707,7 @@ EOT
685
707
  #{:active => {:display_name => ""}},
686
708
  {:id => {:display_name => (active_group ? " ID" : "ID")}},
687
709
  {:name => {:width => 64}},
710
+ options[:include_tenants] || options[:tenant] ? {:tenant => {:width => 32}} : nil,
688
711
  {:location => {:width => 32}},
689
712
  {:labels => {:display_method => lambda {|it| format_list(it[:labels], '', 3) rescue '' }}},
690
713
  {:cloud_count => {:display_name => "CLOUDS"}},
@@ -692,8 +715,8 @@ EOT
692
715
  {:host_count => {:display_name => "HOSTS"}},
693
716
  {:vm_count => {:display_name => "VMS"}},
694
717
  {:baremetal_count => {:display_name => "BARE METAL"}},
695
- ]
696
- print as_pretty_table(rows, columns, opts)
718
+ ].compact
719
+ print as_pretty_table(rows, columns, options)
697
720
  end
698
721
 
699
722
  def add_group_option_types()
@@ -113,8 +113,12 @@ class Morpheus::Cli::Hosts
113
113
  opts.on( '--created-by USER', "Created By User Username or ID" ) do |val|
114
114
  options[:created_by] = val
115
115
  end
116
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant servers. The default is true for this endpoint.") do
117
+ options[:include_tenants] = true
118
+ params['includeTenants'] = true
119
+ end
116
120
  opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
117
- options[:account] = val
121
+ options[:tenant] = val
118
122
  end
119
123
  opts.on('-l', '--labels LABEL', String, "Filter by labels, can match any of the values") do |val|
120
124
  add_query_parameter(params, 'labels', parse_labels(val))
@@ -154,8 +158,8 @@ class Morpheus::Cli::Hosts
154
158
 
155
159
  params.merge!(parse_list_options(options))
156
160
  account = nil
157
- if options[:account]
158
- account = find_account_by_name_or_id(options[:account])
161
+ if options[:tenant]
162
+ account = find_account_by_name_or_id(options[:tenant])
159
163
  if account.nil?
160
164
  return 1
161
165
  else
@@ -315,9 +319,9 @@ class Morpheus::Cli::Hosts
315
319
  "External Name" => :external_name,
316
320
  "Hostname" => :hostname,
317
321
  "Type" => :type,
322
+ "Cloud" => :cloud,
318
323
  "Owner" => :owner,
319
324
  "Tenant" => :tenant,
320
- "Cloud" => :cloud,
321
325
  "Plan" => :plan,
322
326
  "IP" => :ip,
323
327
  "Private IP" => :internal_ip,
@@ -335,8 +339,6 @@ class Morpheus::Cli::Hosts
335
339
  columns.delete("Hostname")
336
340
  columns.delete("Plan")
337
341
  columns.delete("Private IP")
338
- columns.delete("Owner")
339
- columns.delete("Tenant")
340
342
  columns.delete("Power")
341
343
  columns.delete("Created")
342
344
  columns.delete("Updated")
@@ -369,7 +371,7 @@ class Morpheus::Cli::Hosts
369
371
  optparse = Morpheus::Cli::OptionParser.new do |opts|
370
372
  opts.banner = subcommand_usage("[options]")
371
373
  opts.on( '--tenant TENANT', "Tenant Name or ID" ) do |val|
372
- options[:account] = val
374
+ options[:tenant] = val
373
375
  end
374
376
  opts.on( '-g', '--group GROUP', "Group Name or ID" ) do |val|
375
377
  options[:group] = val
@@ -434,8 +436,8 @@ class Morpheus::Cli::Hosts
434
436
  begin
435
437
  params.merge!(parse_list_options(options))
436
438
  account = nil
437
- if options[:account]
438
- account = find_account_by_name_or_id(options[:account])
439
+ if options[:tenant]
440
+ account = find_account_by_name_or_id(options[:tenant])
439
441
  if account.nil?
440
442
  return 1
441
443
  else
@@ -512,6 +514,9 @@ class Morpheus::Cli::Hosts
512
514
  opts.on('--refresh-until STATUS', String, "Refresh until a specified status is reached.") do |val|
513
515
  options[:refresh_until_status] = val.to_s.downcase
514
516
  end
517
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant servers when finding server by name") do
518
+ options[:include_tenants] = true
519
+ end
515
520
  build_standard_get_options(opts, options)
516
521
  end
517
522
  optparse.parse!(args)
@@ -540,7 +545,7 @@ class Morpheus::Cli::Hosts
540
545
  if arg.to_s =~ /\A\d{1,}\Z/
541
546
  json_response = @servers_interface.get(arg.to_i)
542
547
  else
543
- server = find_host_by_name_or_id(arg)
548
+ server = find_host_by_name_or_id(arg, options[:include_tenants])
544
549
  json_response = @servers_interface.get(server['id'])
545
550
  # json_response = {"server" => server} need stats
546
551
  end
@@ -2745,8 +2750,10 @@ EOT
2745
2750
  end
2746
2751
  end
2747
2752
 
2748
- def find_host_by_name(name)
2749
- results = @servers_interface.list({name: name})
2753
+ def find_host_by_name(name, include_tenants=false)
2754
+ params = {name: name.to_s}
2755
+ params['includeTenants'] = true if include_tenants
2756
+ results = @servers_interface.list(params)
2750
2757
  if results['servers'].empty?
2751
2758
  print_red_alert "Server not found by name #{name}"
2752
2759
  exit 1
@@ -2759,11 +2766,11 @@ EOT
2759
2766
  return results['servers'][0]
2760
2767
  end
2761
2768
 
2762
- def find_host_by_name_or_id(val)
2769
+ def find_host_by_name_or_id(val, include_tenants=false)
2763
2770
  if val.to_s =~ /\A\d{1,}\Z/
2764
2771
  return find_host_by_id(val)
2765
2772
  else
2766
- return find_host_by_name(val)
2773
+ return find_host_by_name(val, include_tenants)
2767
2774
  end
2768
2775
  end
2769
2776
 
@@ -150,6 +150,13 @@ class Morpheus::Cli::Instances
150
150
  options[:details] = true
151
151
  params['details'] = true # get more data from server this way
152
152
  end
153
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant instances") do
154
+ options[:include_tenants] = true
155
+ params['includeTenants'] = true
156
+ end
157
+ opts.on('--tenant TENANT', String, "Tenant Name or ID" ) do |val|
158
+ options[:tenant] = val
159
+ end
153
160
  build_standard_list_options(opts, options)
154
161
  opts.footer = "List instances."
155
162
  end
@@ -204,6 +211,19 @@ class Morpheus::Cli::Instances
204
211
  end
205
212
  end
206
213
 
214
+ if options[:tenant]
215
+ if options[:tenant].to_s !~ /\A\d{1,}\Z/
216
+ account = find_account_by_name_or_id(options[:tenant])
217
+ if account.nil?
218
+ return 1, "Tenant not found by name: #{options[:tenant]}"
219
+ else
220
+ params['tenantId'] = account['id']
221
+ end
222
+ else
223
+ params['tenantId'] = options[:tenant]
224
+ end
225
+ end
226
+
207
227
  @instances_interface.setopts(options)
208
228
  if options[:dry_run]
209
229
  print_dry_run @instances_interface.dry.list(params)
@@ -241,6 +261,7 @@ class Morpheus::Cli::Instances
241
261
  if options[:owner]
242
262
  subtitles << "Created By: #{options[:owner]}"
243
263
  end
264
+
244
265
  subtitles += parse_list_subtitles(options)
245
266
  print_h1 title, subtitles, options
246
267
  if instances.empty?
@@ -279,7 +300,7 @@ class Morpheus::Cli::Instances
279
300
  connection: format_instance_connection_string(instance),
280
301
  environment: instance['instanceContext'],
281
302
  user: (instance['owner'] ? (instance['owner']['username'] || instance['owner']['id']) : (instance['createdBy'].is_a?(Hash) ? instance['createdBy']['username'] : instance['createdBy'])),
282
- tenant: (instance['owner'] ? (instance['owner']['username'] || instance['owner']['id']) : (instance['createdBy'].is_a?(Hash) ? instance['createdBy']['username'] : instance['createdBy'])),
303
+ tenant: (instance['tenant'] ? instance['tenant']['name'] : ''),
283
304
  nodes: instance['containers'].count,
284
305
  status: format_instance_status(instance, cyan),
285
306
  type: instance['instanceType']['name'],
@@ -294,18 +315,20 @@ class Morpheus::Cli::Instances
294
315
  }
295
316
  row
296
317
  }
297
- columns = [:id, {:name => {:max_width => 50}}, :labels, :group, :cloud,
318
+ columns = [:id, {:name => {:max_width => 50}}, :labels, :tenant, :group, :cloud,
298
319
  :type, :version, :environment, :plan,
299
320
  {:created => {:display_name => "CREATED"}},
300
- # {:tenant => {:display_name => "TENANT"}},
301
321
  {:user => {:display_name => "OWNER", :max_width => 20}},
302
- :nodes, {:connection => {:max_width => 30}}, :status, :cpu, :memory, :storage]
322
+ :nodes, {:connection => {:max_width => 30}}, :status, :cpu, :memory, :storage].compact
303
323
  # custom pretty table columns ... this is handled in as_pretty_table now(),
304
324
  # todo: remove all these.. and try to always pass rows as the json data itself..
305
325
  if options[:details] != true
306
326
  columns.delete(:labels)
307
327
  columns.delete(:plan)
308
328
  end
329
+ if !options[:include_tenants] && !options[:tenant]
330
+ columns.delete(:tenant)
331
+ end
309
332
  print cyan
310
333
  print as_pretty_table(rows, columns, options)
311
334
  print reset
@@ -1337,6 +1360,9 @@ class Morpheus::Cli::Instances
1337
1360
  opts.on('--refresh-until STATUS', String, "Refresh until a specified status is reached.") do |val|
1338
1361
  options[:refresh_until_status] = val.to_s.downcase
1339
1362
  end
1363
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant instances when finding instance by name") do
1364
+ options[:include_tenants] = true
1365
+ end
1340
1366
  # opts.on( nil, '--threshold', "Alias for --scaling" ) do
1341
1367
  # options[:include_scaling] = true
1342
1368
  # end
@@ -1369,7 +1395,7 @@ class Morpheus::Cli::Instances
1369
1395
  end
1370
1396
  instance = nil
1371
1397
  if id.to_s !~ /\A\d{1,}\Z/
1372
- instance = find_instance_by_name_or_id(id)
1398
+ instance = find_instance_by_name_or_id(id, options[:include_tenants])
1373
1399
  return 1, "Instance not found by name #{id}" if instance.nil?
1374
1400
  id = instance['id']
1375
1401
  end
@@ -1463,7 +1489,7 @@ class Morpheus::Cli::Instances
1463
1489
  it['createdBy'] ? (it['createdBy']['username'] || it['createdBy']['id']) : ''
1464
1490
  end
1465
1491
  },
1466
- #"Tenant" => lambda {|it| it['tenant'] ? it['tenant']['name'] : '' },
1492
+ "Tenant" => lambda {|it| it['tenant'] ? it['tenant']['name'] : '' },
1467
1493
  "Apps" => lambda {|it| anded_list(it['apps'] ? it['apps'].collect {|app| app['name'] } : [])},
1468
1494
  "Date Created" => lambda {|it| format_local_dt(it['dateCreated']) },
1469
1495
  # "Last Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
@@ -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
 
@@ -68,7 +85,7 @@ class Morpheus::Cli::StorageVolumes
68
85
  def add_storage_volume_option_types()
69
86
  [
70
87
  {'fieldContext' => 'storageServer', 'fieldName' => 'id', 'fieldLabel' => 'Storage Server', 'type' => 'select', 'optionSource' => 'storageServers', 'optionParams' => {'createType' => 'block'}, 'required' => true},
71
- {'fieldContext' => 'storageGroup', 'fieldName' => 'id', 'fieldLabel' => 'Storage Group', 'type' => 'select', 'optionSource' => 'storageGroups', 'required' => true},
88
+ {'fieldContext' => 'storageGroup', 'fieldName' => 'id', 'fieldLabel' => 'Storage Group', 'type' => 'select', 'optionSource' => 'storageGroups', 'required' => false},
72
89
  {'shorthand' => '-t', 'fieldName' => 'type', 'fieldLabel' => 'Storage Volume Type', 'type' => 'select', 'optionSource' => 'storageVolumeTypes', 'required' => true},
73
90
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
74
91
  ]
@@ -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)