morpheus-cli 9.0.1 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +1 -1
  3. data/lib/morpheus/api/affinity_groups_interface.rb +46 -0
  4. data/lib/morpheus/api/api_client.rb +17 -0
  5. data/lib/morpheus/api/clouds_interface.rb +42 -0
  6. data/lib/morpheus/api/clusters_interface.rb +55 -1
  7. data/lib/morpheus/api/distributed_workers_interface.rb +41 -0
  8. data/lib/morpheus/api/host_vm_groups_interface.rb +37 -0
  9. data/lib/morpheus/api/servers_interface.rb +8 -0
  10. data/lib/morpheus/api/virtual_switches_interface.rb +9 -0
  11. data/lib/morpheus/cli/commands/affinity_groups_command.rb +554 -0
  12. data/lib/morpheus/cli/commands/backups_command.rb +11 -0
  13. data/lib/morpheus/cli/commands/clouds.rb +421 -29
  14. data/lib/morpheus/cli/commands/clusters.rb +507 -20
  15. data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
  16. data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
  17. data/lib/morpheus/cli/commands/hosts.rb +72 -1
  18. data/lib/morpheus/cli/commands/instances.rb +12 -1
  19. data/lib/morpheus/cli/commands/networks_command.rb +35 -3
  20. data/lib/morpheus/cli/commands/virtual_switches_command.rb +336 -0
  21. data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
  22. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +143 -4
  23. data/lib/morpheus/cli/option_types.rb +36 -1
  24. data/lib/morpheus/cli/version.rb +1 -1
  25. data/test/api/affinity_groups_interface_test.rb +43 -0
  26. data/test/api/clouds_interface_test.rb +67 -0
  27. data/test/api/host_vm_groups_interface_test.rb +35 -0
  28. data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
  29. data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
  30. data/test/cli/affinity_groups_test.rb +57 -0
  31. data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
  32. data/test/cli/affinity_helper_test.rb +367 -0
  33. data/test/cli/backups_test.rb +212 -0
  34. data/test/cli/clusters_uuid_test.rb +131 -0
  35. data/test/cli/distributed_workers_test.rb +68 -0
  36. data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
  37. data/test/cli/host_vm_groups_test.rb +63 -0
  38. data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
  39. metadata +39 -2
@@ -5,6 +5,7 @@ class Morpheus::Cli::NetworksCommand
5
5
  include Morpheus::Cli::WhoamiHelper
6
6
  include Morpheus::Cli::InfrastructureHelper
7
7
  include Morpheus::Cli::ProvisioningHelper
8
+ include Morpheus::Cli::AccountsHelper
8
9
 
9
10
  set_command_name :networks
10
11
 
@@ -28,6 +29,8 @@ class Morpheus::Cli::NetworksCommand
28
29
  @groups_interface = @api_client.groups
29
30
  @clouds_interface = @api_client.clouds
30
31
  @options_interface = @api_client.options
32
+ @accounts_interface = @api_client.accounts
33
+ @account_users_interface = @api_client.account_users
31
34
  end
32
35
 
33
36
  def handle(args)
@@ -57,6 +60,13 @@ class Morpheus::Cli::NetworksCommand
57
60
  opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
58
61
  add_query_parameter(params, 'allLabels', parse_labels(val))
59
62
  end
63
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant networks") do
64
+ options[:include_tenants] = true
65
+ params['includeTenants'] = true
66
+ end
67
+ opts.on('--tenant TENANT', String, "Tenant Name or ID" ) do |val|
68
+ options[:tenant] = val
69
+ end
60
70
  build_common_options(opts, options, [:list, :json, :yaml, :csv, :fields, :json, :dry_run, :remote])
61
71
  opts.footer = "List networks."
62
72
  end
@@ -73,6 +83,18 @@ class Morpheus::Cli::NetworksCommand
73
83
  return 1 if cloud.nil?
74
84
  params['zoneId'] = cloud['id']
75
85
  end
86
+ if options[:tenant]
87
+ if options[:tenant].to_s !~ /\A\d{1,}\Z/
88
+ account = find_account_by_name_or_id(options[:tenant])
89
+ if account.nil?
90
+ return 1, "Tenant not found by name: #{options[:tenant]}"
91
+ else
92
+ params['tenantId'] = account['id']
93
+ end
94
+ else
95
+ params['tenantId'] = options[:tenant]
96
+ end
97
+ end
76
98
  @networks_interface.setopts(options)
77
99
  if options[:dry_run]
78
100
  print_dry_run @networks_interface.dry.list(params)
@@ -109,6 +131,7 @@ class Morpheus::Cli::NetworksCommand
109
131
  type: network['type'] ? network['type']['name'] : '',
110
132
  group: network['group'] ? network['group']['name'] : 'Shared',
111
133
  cloud: network['zone'] ? network['zone']['name'] : '',
134
+ tenant: network['owner'] ? network['owner']['name'] : '',
112
135
  cidr: network['cidr'],
113
136
  pool: network['pool'] ? network['pool']['name'] : '',
114
137
  dhcp: network['dhcpServer'] ? 'Yes' : 'No',
@@ -128,6 +151,7 @@ class Morpheus::Cli::NetworksCommand
128
151
  type: "Subnet",
129
152
  group: network['group'] ? network['group']['name'] : 'Shared',
130
153
  cloud: network['zone'] ? network['zone']['name'] : '',
154
+ tenant: network['owner'] ? network['owner']['name'] : '',
131
155
  cidr: subnet['cidr'],
132
156
  pool: subnet['pool'] ? subnet['pool']['name'] : '',
133
157
  dhcp: subnet['dhcpServer'] ? 'Yes' : 'No',
@@ -139,9 +163,13 @@ class Morpheus::Cli::NetworksCommand
139
163
  end
140
164
  end
141
165
  end
142
- columns = [:id, :name, :labels, :type, :group, :cloud, :cidr, :pool, :dhcp, :subnets, :active, :visibility, :tenants]
166
+ columns = [:id, :name, :labels, :type, :group, :cloud, :tenant, :cidr, :pool, :dhcp, :subnets, :active, :visibility, :tenants]
143
167
  if options[:include_fields]
144
168
  columns = options[:include_fields]
169
+ else
170
+ if !options[:include_tenants] && !options[:tenant]
171
+ columns.delete(:tenant)
172
+ end
145
173
  end
146
174
  print cyan
147
175
  print as_pretty_table(rows, columns, options)
@@ -160,6 +188,9 @@ class Morpheus::Cli::NetworksCommand
160
188
  options = {}
161
189
  optparse = Morpheus::Cli::OptionParser.new do |opts|
162
190
  opts.banner = subcommand_usage("[network]")
191
+ opts.on('--include-tenants','--include-tenants', "Include sub tenant networks when finding network by name") do
192
+ options[:include_tenants] = true
193
+ end
163
194
  build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
164
195
  opts.footer = "Get details about a network." + "\n" +
165
196
  "[network] is required. This is the name or id of a network."
@@ -177,11 +208,11 @@ class Morpheus::Cli::NetworksCommand
177
208
  if args[0].to_s =~ /\A\d{1,}\Z/
178
209
  print_dry_run @networks_interface.dry.get(args[0].to_i)
179
210
  else
180
- print_dry_run @networks_interface.dry.list({name:args[0]})
211
+ print_dry_run @networks_interface.dry.list({name:args[0]}, options[:include_tenants])
181
212
  end
182
213
  return
183
214
  end
184
- network = find_network_by_name_or_id(args[0])
215
+ network = find_network_by_name_or_id(args[0], options[:include_tenants])
185
216
  return 1 if network.nil?
186
217
  json_response = {'network' => network} # skip redundant request
187
218
  # json_response = @networks_interface.get(network['id'])
@@ -206,6 +237,7 @@ class Morpheus::Cli::NetworksCommand
206
237
  "Type" => lambda {|it| it['type'] ? it['type']['name'] : '' },
207
238
  "Group" => lambda {|it| it['group'] ? it['group']['name'] : 'Shared' },
208
239
  "Cloud" => lambda {|it| it['zone'] ? it['zone']['name'] : '' },
240
+ "Tenant" => lambda {|it| it['owner'] ? it['owner']['name'] : '' },
209
241
  "IPv4 Enabled" => 'ipv4Enabled',
210
242
  "IPv6 Enabled" => 'ipv6Enabled',
211
243
  "CIDR" => 'cidr',
@@ -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
@@ -0,0 +1,118 @@
1
+ require 'morpheus/cli/mixins/print_helper'
2
+
3
+ # Mixin for Morpheus::Cli command classes
4
+ # Single source of truth for affinity group / host-vm group enums and formatting.
5
+ #
6
+ # Keep this in sync with morpheus-ui and the OpenAPI shared affinity-type schema.
7
+ # Defining these values here (rather than inline in each command) prevents the
8
+ # definitions from drifting apart between cluster-scoped, cloud-scoped and
9
+ # top-level flat-by-ID commands.
10
+ module Morpheus::Cli::AffinityHelper
11
+
12
+ def self.included(klass)
13
+ klass.send :include, Morpheus::Cli::PrintHelper
14
+ end
15
+
16
+ # All affinity types supported by the API.
17
+ # The _MUST variants are mandatory placement constraints and are what the
18
+ # violations endpoint reports on.
19
+ AFFINITY_TYPES = [
20
+ {'name' => 'Keep Together (Should)', 'value' => 'KEEP_TOGETHER'},
21
+ {'name' => 'Keep Separate (Should)', 'value' => 'KEEP_SEPARATE'},
22
+ {'name' => 'Keep Together (Must)', 'value' => 'KEEP_TOGETHER_MUST'},
23
+ {'name' => 'Keep Separate (Must)', 'value' => 'KEEP_SEPARATE_MUST'},
24
+ ].freeze
25
+
26
+ # Group types, per HostVmGroup.HostVmGroupType (morpheus-ui rc-9.1.0,
27
+ # morpheus-domains/grails-app/domain/com/morpheus/HostVmGroup.groovy).
28
+ # SITE_GROUP exists in the enum but is not valid on either side of a combined
29
+ # VM-to-Host rule -- AffinityGroupService.applyRuleShapeFields rejects it.
30
+ HOST_VM_GROUP_TYPES = [
31
+ {'name' => 'Site Group', 'value' => 'SITE_GROUP'},
32
+ {'name' => 'Host Group', 'value' => 'HOST_GROUP'},
33
+ {'name' => 'VM Group', 'value' => 'VM_GROUP'},
34
+ ].freeze
35
+
36
+ def affinity_type_select_options
37
+ AFFINITY_TYPES.map {|it| it.dup }
38
+ end
39
+
40
+ def host_vm_group_type_select_options
41
+ HOST_VM_GROUP_TYPES.map {|it| it.dup }
42
+ end
43
+
44
+ # affinityType is editable on update as well as create.
45
+ # AffinityGroupService.updateAffinityGroup applies params.affinityGroup.affinityType,
46
+ # so callers may switch between MUST and non-MUST behaviour during an edit.
47
+ def affinity_type_option_type(required: false, default: nil)
48
+ opt = {
49
+ 'fieldName' => 'affinityType',
50
+ 'fieldLabel' => 'Type',
51
+ 'type' => 'select',
52
+ 'selectOptions' => affinity_type_select_options,
53
+ 'description' => 'Affinity type. The _MUST variants are mandatory placement constraints.',
54
+ }
55
+ opt['required'] = true if required
56
+ opt['defaultValue'] = default if default
57
+ opt
58
+ end
59
+
60
+ def host_vm_group_type_option_type(required: false, default: nil)
61
+ opt = {
62
+ 'fieldName' => 'type',
63
+ 'fieldLabel' => 'Group Type',
64
+ 'type' => 'select',
65
+ 'selectOptions' => host_vm_group_type_select_options,
66
+ 'description' => 'Choose group type.',
67
+ }
68
+ opt['required'] = true if required
69
+ opt['defaultValue'] = default if default
70
+ opt
71
+ end
72
+
73
+ def format_affinity_type(affinity_type)
74
+ found = AFFINITY_TYPES.find {|it| it['value'] == affinity_type.to_s }
75
+ found ? found['name'].sub(' (Should)', '') : affinity_type.to_s
76
+ end
77
+
78
+ def format_host_vm_group_type(type)
79
+ found = HOST_VM_GROUP_TYPES.find {|it| it['value'] == type.to_s }
80
+ found ? found['name'] : type.to_s
81
+ end
82
+
83
+ # Groups carry poolId; keep only those in the chosen cluster.
84
+ def filter_by_pool(rows, pool_id)
85
+ return rows if pool_id.nil?
86
+ rows.select {|r| r['poolId'].nil? || r['poolId'].to_s == pool_id.to_s }
87
+ end
88
+
89
+ # Surface the server's own error text for affinity failures.
90
+ #
91
+ # The API returns {success:false, msg:"<human text>", errors:{<field>:"<i18n key>"}}
92
+ # -- there is no top-level errorCode. AffinityGroupService and
93
+ # VmwareComputeService.combinedRuleGroupSyncGuard already compose actionable
94
+ # messages (cross-cluster group refs, groups not yet synced to vSphere, rule
95
+ # shape violations), so print them verbatim rather than restating them here.
96
+ def handle_affinity_rest_exception(e, options)
97
+ if e.respond_to?(:response) && e.response
98
+ begin
99
+ body = JSON.parse(e.response.to_s)
100
+ if body.is_a?(Hash) && (body['msg'] || body['errors'])
101
+ print_red_alert body['msg'] if body['msg']
102
+ errors = body['errors']
103
+ if errors.is_a?(Hash) && !errors.empty?
104
+ # Same stream as print_red_alert, so msg and field detail stay together.
105
+ errors.each do |field, message|
106
+ $stderr.print "#{red} #{field}: #{message}#{reset}\n"
107
+ end
108
+ end
109
+ return
110
+ end
111
+ rescue TypeError, JSON::ParserError
112
+ # fall through to the generic handler
113
+ end
114
+ end
115
+ print_rest_exception(e, options)
116
+ end
117
+
118
+ end