morpheus-cli 9.0.2 → 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 (33) 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 +13 -0
  5. data/lib/morpheus/api/clouds_interface.rb +42 -0
  6. data/lib/morpheus/api/clusters_interface.rb +7 -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/cli/commands/affinity_groups_command.rb +554 -0
  10. data/lib/morpheus/cli/commands/backups_command.rb +1 -1
  11. data/lib/morpheus/cli/commands/clouds.rb +421 -29
  12. data/lib/morpheus/cli/commands/clusters.rb +37 -39
  13. data/lib/morpheus/cli/commands/distributed_workers_command.rb +282 -0
  14. data/lib/morpheus/cli/commands/host_vm_groups_command.rb +423 -0
  15. data/lib/morpheus/cli/mixins/affinity_helper.rb +118 -0
  16. data/lib/morpheus/cli/mixins/infrastructure_helper.rb +137 -0
  17. data/lib/morpheus/cli/option_types.rb +36 -1
  18. data/lib/morpheus/cli/version.rb +1 -1
  19. data/test/api/affinity_groups_interface_test.rb +43 -0
  20. data/test/api/clouds_interface_test.rb +67 -0
  21. data/test/api/host_vm_groups_interface_test.rb +35 -0
  22. data/test/cli/affinity_groups_cloud_cluster_scope_test.rb +270 -0
  23. data/test/cli/affinity_groups_cluster_scope_test.rb +171 -0
  24. data/test/cli/affinity_groups_test.rb +57 -0
  25. data/test/cli/affinity_groups_violations_scope_test.rb +141 -0
  26. data/test/cli/affinity_helper_test.rb +367 -0
  27. data/test/cli/backups_test.rb +212 -0
  28. data/test/cli/clusters_uuid_test.rb +131 -0
  29. data/test/cli/distributed_workers_test.rb +68 -0
  30. data/test/cli/host_vm_groups_cluster_scope_test.rb +187 -0
  31. data/test/cli/host_vm_groups_test.rb +63 -0
  32. data/test/cli/infrastructure_helper_cluster_resolver_test.rb +250 -0
  33. metadata +37 -2
@@ -7,6 +7,7 @@ class Morpheus::Cli::Clusters
7
7
  include Morpheus::Cli::WhoamiHelper
8
8
  include Morpheus::Cli::AccountsHelper
9
9
  include Morpheus::Cli::ExecutionRequestHelper
10
+ include Morpheus::Cli::AffinityHelper
10
11
 
11
12
  register_subcommands :list, :count, :get, :view, :add, :update, :remove, :logs, :history, {:'history-details' => :history_details}, {:'history-event' => :history_event_details}
12
13
  register_subcommands :list_types, :get_type
@@ -73,8 +74,11 @@ class Morpheus::Cli::Clusters
73
74
  opts.on('--all-labels LABEL', String, "Filter by labels, must match all of the values") do |val|
74
75
  add_query_parameter(params, 'allLabels', parse_labels(val))
75
76
  end
77
+ opts.on('--uuid UUID', String, "Filter by uuid, can be passed multiple times to match any of the values") do |val|
78
+ add_query_parameter(params, 'uuid', val)
79
+ end
76
80
  build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
77
- opts.footer = "List clusters."
81
+ opts.footer = "List clusters.\nFilter by one or more uuids using --uuid UUID."
78
82
  end
79
83
  optparse.parse!(args)
80
84
  if args.count != 0
@@ -148,7 +152,7 @@ class Morpheus::Cli::Clusters
148
152
  def get(args)
149
153
  options = {}
150
154
  optparse = Morpheus::Cli::OptionParser.new do |opts|
151
- opts.banner = subcommand_usage("[id]")
155
+ opts.banner = subcommand_usage("[cluster]")
152
156
  opts.on( nil, '--hosts', "Display masters and workers" ) do
153
157
  options[:show_masters] = true
154
158
  options[:show_workers] = true
@@ -172,7 +176,7 @@ class Morpheus::Cli::Clusters
172
176
  options[:refresh_until_status] = val.to_s.downcase
173
177
  end
174
178
  build_common_options(opts, options, [:json, :dry_run, :remote])
175
- opts.footer = "Get details about a cluster."
179
+ opts.footer = "Get details about a cluster.\n[cluster] is required. This is the id, name, or uuid of a cluster."
176
180
  end
177
181
  optparse.parse!(args)
178
182
  if args.count < 1
@@ -193,6 +197,8 @@ class Morpheus::Cli::Clusters
193
197
  if options[:dry_run]
194
198
  if arg.to_s =~ /\A\d{1,}\Z/
195
199
  print_dry_run @clusters_interface.dry.get(arg.to_i)
200
+ elsif cluster_uuid?(arg)
201
+ print_dry_run @clusters_interface.dry.get(arg.to_s)
196
202
  else
197
203
  print_dry_run @clusters_interface.dry.list({name:arg})
198
204
  end
@@ -5029,11 +5035,17 @@ class Morpheus::Cli::Clusters
5029
5035
  def find_cluster_by_name_or_id(val)
5030
5036
  if val.to_s =~ /\A\d{1,}\Z/
5031
5037
  find_cluster_by_id(val)
5038
+ elsif cluster_uuid?(val)
5039
+ find_cluster_by_uuid(val)
5032
5040
  else
5033
5041
  find_cluster_by_name(val)
5034
5042
  end
5035
5043
  end
5036
5044
 
5045
+ def cluster_uuid?(val)
5046
+ !!(val.to_s =~ /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
5047
+ end
5048
+
5037
5049
  def find_cluster_by_id(id)
5038
5050
  json_results = @clusters_interface.get(id.to_i)
5039
5051
  if json_results['cluster'].empty?
@@ -5043,6 +5055,22 @@ class Morpheus::Cli::Clusters
5043
5055
  json_results['cluster']
5044
5056
  end
5045
5057
 
5058
+ def find_cluster_by_uuid(uuid)
5059
+ json_results = @clusters_interface.get(uuid.to_s)
5060
+ if json_results['cluster'].nil? || json_results['cluster'].empty?
5061
+ print_red_alert "Cluster not found by uuid #{uuid}"
5062
+ exit 1
5063
+ end
5064
+ json_results['cluster']
5065
+ rescue RestClient::Exception => e
5066
+ if e.response && e.response.code == 404
5067
+ print_red_alert "Cluster not found by uuid #{uuid}"
5068
+ exit 1
5069
+ else
5070
+ raise e
5071
+ end
5072
+ end
5073
+
5046
5074
  def find_cluster_by_name(name)
5047
5075
  json_results = @clusters_interface.list({name: name})
5048
5076
  if json_results['clusters'].empty? || json_results['clusters'].count > 1
@@ -5783,15 +5811,10 @@ class Morpheus::Cli::Clusters
5783
5811
  end
5784
5812
  end
5785
5813
 
5786
- def add_affinity_group_option_types
5814
+ def add_affinity_group_option_types
5787
5815
  [
5788
5816
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5789
- {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [
5790
- {'name' => 'Keep Together (Should)', 'value' => 'KEEP_TOGETHER'},
5791
- {'name' => 'Keep Separate (Should)', 'value' => 'KEEP_SEPARATE'},
5792
- {'name' => 'Keep Together (Must)', 'value' => 'KEEP_TOGETHER_MUST'},
5793
- {'name' => 'Keep Separate (Must)', 'value' => 'KEEP_SEPARATE_MUST'}
5794
- ], 'description' => 'Choose affinity type.', 'required' => true, 'defaultValue' => 'KEEP_TOGETHER'},
5817
+ affinity_type_option_type(required: true, default: 'KEEP_TOGETHER'),
5795
5818
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox', 'defaultValue' => true},
5796
5819
  ]
5797
5820
  end
@@ -5799,34 +5822,17 @@ class Morpheus::Cli::Clusters
5799
5822
  def update_affinity_group_option_types
5800
5823
  [
5801
5824
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text'},
5802
- {'fieldName' => 'affinityType', 'fieldLabel' => 'Type', 'type' => 'select', 'selectOptions' => [
5803
- {'name' => 'Keep Together (Should)', 'value' => 'KEEP_TOGETHER'},
5804
- {'name' => 'Keep Separate (Should)', 'value' => 'KEEP_SEPARATE'},
5805
- {'name' => 'Keep Together (Must)', 'value' => 'KEEP_TOGETHER_MUST'},
5806
- {'name' => 'Keep Separate (Must)', 'value' => 'KEEP_SEPARATE_MUST'}
5807
- ], 'description' => 'Change affinity type.'},
5825
+ affinity_type_option_type,
5808
5826
  {'fieldName' => 'active', 'fieldLabel' => 'Active', 'type' => 'checkbox'},
5809
- {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers', 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5827
+ {'fieldName' => 'servers', 'fieldLabel' => 'Server', 'type' => 'multiTypeahead', 'optionSource' => 'searchServers',
5828
+ 'searchParameter' => 'phrase', 'description' => 'Select servers to be in the affinity group.'},
5810
5829
  ]
5811
5830
  end
5812
5831
 
5813
- def format_affinity_type(affinity_type)
5814
- case affinity_type.to_s
5815
- when 'KEEP_SEPARATE' then 'Keep Separate'
5816
- when 'KEEP_TOGETHER' then 'Keep Together'
5817
- when 'KEEP_SEPARATE_MUST' then 'Keep Separate (Must)'
5818
- when 'KEEP_TOGETHER_MUST' then 'Keep Together (Must)'
5819
- else affinity_type.to_s
5820
- end
5821
- end
5822
-
5823
5832
  def add_host_vm_group_option_types
5824
5833
  [
5825
5834
  {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
5826
- {'fieldName' => 'type', 'fieldLabel' => 'Group Type', 'type' => 'select', 'selectOptions' => [
5827
- {'name' => 'Host Group', 'value' => 'HOST_GROUP'},
5828
- {'name' => 'VM Group', 'value' => 'VM_GROUP'}
5829
- ], 'required' => true, 'defaultValue' => 'HOST_GROUP', 'description' => 'Choose group type.'},
5835
+ host_vm_group_type_option_type(required: true, default: 'HOST_GROUP'),
5830
5836
  ]
5831
5837
  end
5832
5838
 
@@ -5836,14 +5842,6 @@ class Morpheus::Cli::Clusters
5836
5842
  ]
5837
5843
  end
5838
5844
 
5839
- def format_host_vm_group_type(type)
5840
- case type.to_s
5841
- when 'HOST_GROUP' then 'Host Group'
5842
- when 'VM_GROUP' then 'VM Group'
5843
- else type.to_s
5844
- end
5845
- end
5846
-
5847
5845
  def find_cluster_host_vm_group_by_name_or_id(cluster_id, val)
5848
5846
  if val.to_s =~ /\A\d{1,}\Z/
5849
5847
  @clusters_interface.get_host_vm_group(cluster_id, val)['hostVmGroup'] rescue nil
@@ -0,0 +1,282 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ class Morpheus::Cli::DistributedWorkers
4
+ include Morpheus::Cli::CliCommand
5
+
6
+ register_subcommands :list, :get, :add, :update, :remove
7
+
8
+ def connect(opts)
9
+ @api_client = establish_remote_appliance_connection(opts)
10
+ @distributed_workers_interface = @api_client.distributed_workers
11
+ end
12
+
13
+ def handle(args)
14
+ handle_subcommand(args)
15
+ end
16
+
17
+ def list(args)
18
+ options = {}
19
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
20
+ opts.banner = subcommand_usage()
21
+ build_standard_list_options(opts, options)
22
+ opts.footer = "List distributed workers."
23
+ end
24
+ optparse.parse!(args)
25
+ connect(options)
26
+ verify_args!(args:args, optparse:optparse, count:0)
27
+ params = parse_list_options(options)
28
+ @distributed_workers_interface.setopts(options)
29
+ if options[:dry_run]
30
+ print_dry_run @distributed_workers_interface.dry.list(params)
31
+ return
32
+ end
33
+ json_response = @distributed_workers_interface.list(params)
34
+ render_response(json_response, options, 'distributedWorkers') do
35
+ distributed_workers = json_response['distributedWorkers']
36
+ print_h1 "Morpheus Distributed Workers", parse_list_subtitles(options), options
37
+ if distributed_workers.empty?
38
+ print cyan,"No distributed workers found.",reset,"\n"
39
+ else
40
+ rows = distributed_workers.collect do |distributed_worker|
41
+ {
42
+ id: distributed_worker['id'],
43
+ name: distributed_worker['name'],
44
+ enabled: format_boolean(distributed_worker['enabled']),
45
+ proxyPort: distributed_worker['proxyPort'],
46
+ proxyHostList: distributed_worker['proxyHostList'],
47
+ applianceUrl: distributed_worker['applianceUrl']
48
+ }
49
+ end
50
+ columns = [:id, :name, :enabled, {:proxyPort => {:display_name => "Proxy Port"} }, {:proxyHostList => {:display_name => "Proxy Host List"} }, {:applianceUrl => {:display_name => "Appliance URL"} }]
51
+ print as_pretty_table(rows, columns, options)
52
+ print_results_pagination(json_response)
53
+ end
54
+ print reset,"\n"
55
+ end
56
+ return 0, nil
57
+ end
58
+
59
+ def get(args)
60
+ options = {}
61
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
62
+ opts.banner = subcommand_usage("[distributed-worker]")
63
+ build_standard_get_options(opts, options)
64
+ opts.footer = "Get details about a distributed worker.\n[distributed-worker] is required. This is the name or id of a distributed worker."
65
+ end
66
+ optparse.parse!(args)
67
+ verify_args!(args:args, optparse:optparse, count:1)
68
+ connect(options)
69
+ _get(args[0], options)
70
+ end
71
+
72
+ def _get(id, options)
73
+ @distributed_workers_interface.setopts(options)
74
+ if options[:dry_run]
75
+ if id.to_s =~ /\A\d{1,}\Z/
76
+ print_dry_run @distributed_workers_interface.dry.get(id.to_i)
77
+ else
78
+ print_dry_run @distributed_workers_interface.dry.list({name: id})
79
+ end
80
+ return
81
+ end
82
+ distributed_worker = find_distributed_worker_by_name_or_id(id)
83
+ return 1 if distributed_worker.nil?
84
+ json_response = {'distributedWorker' => distributed_worker}
85
+ render_response(json_response, options, 'distributedWorker') do
86
+ render_distributed_worker_details(distributed_worker)
87
+ end
88
+ return 0, nil
89
+ end
90
+
91
+ def add(args)
92
+ options = {}
93
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
94
+ opts.banner = subcommand_usage("[name] [options]")
95
+ build_option_type_options(opts, options, add_distributed_worker_option_types)
96
+ build_standard_add_options(opts, options)
97
+ opts.footer = <<-EOT
98
+ Add a distributed worker.
99
+ [name] is required. This is the name of the new distributed worker.
100
+ The generated apiKey is only shown once, on creation. Be sure to save it.
101
+ EOT
102
+ end
103
+ optparse.parse!(args)
104
+ verify_args!(args:args, optparse:optparse, max:1)
105
+ connect(options)
106
+ options[:options] ||= {}
107
+ if args[0]
108
+ options[:options]['name'] ||= args[0]
109
+ end
110
+ payload = parse_payload(options, 'distributedWorker')
111
+ if payload.nil?
112
+ params = Morpheus::Cli::OptionTypes.prompt(add_distributed_worker_option_types, options[:options], @api_client, options[:params])
113
+ payload = {'distributedWorker' => params}
114
+ end
115
+ @distributed_workers_interface.setopts(options)
116
+ if options[:dry_run]
117
+ print_dry_run @distributed_workers_interface.dry.create(payload)
118
+ return
119
+ end
120
+ json_response = @distributed_workers_interface.create(payload)
121
+ render_response(json_response, options, 'distributedWorker') do
122
+ distributed_worker = json_response['distributedWorker']
123
+ print_green_success "Added distributed worker #{distributed_worker['name']}"
124
+ api_key = distributed_worker['apiKey']
125
+ if api_key
126
+ print "\n"
127
+ print_h2 "API Key"
128
+ print yellow, "Save this API Key now. It will not be shown again.", reset, "\n"
129
+ print cyan, api_key, reset, "\n"
130
+ end
131
+ render_distributed_worker_details(distributed_worker)
132
+ end
133
+ return 0, nil
134
+ end
135
+
136
+ def update(args)
137
+ options = {}
138
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
139
+ opts.banner = subcommand_usage("[distributed-worker] [options]")
140
+ build_option_type_options(opts, options, update_distributed_worker_option_types)
141
+ build_standard_update_options(opts, options)
142
+ opts.footer = <<-EOT
143
+ Update a distributed worker.
144
+ [distributed-worker] is required. This is the name or id of a distributed worker.
145
+ EOT
146
+ end
147
+ optparse.parse!(args)
148
+ verify_args!(args:args, optparse:optparse, count:1)
149
+ connect(options)
150
+ distributed_worker = find_distributed_worker_by_name_or_id(args[0])
151
+ return 1 if distributed_worker.nil?
152
+ payload = parse_payload(options, 'distributedWorker')
153
+ if payload.nil?
154
+ params = options[:options] || {}
155
+ params = params.select {|k,v| params[k].to_s != "" }
156
+ if params.empty?
157
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
158
+ end
159
+ payload = {'distributedWorker' => params}
160
+ end
161
+ payload['distributedWorker']['id'] = distributed_worker['id']
162
+ @distributed_workers_interface.setopts(options)
163
+ if options[:dry_run]
164
+ print_dry_run @distributed_workers_interface.dry.update(distributed_worker['id'], payload)
165
+ return
166
+ end
167
+ json_response = @distributed_workers_interface.update(distributed_worker['id'], payload)
168
+ render_response(json_response, options, 'distributedWorker') do
169
+ print_green_success "Updated distributed worker #{distributed_worker['name']}"
170
+ _get(distributed_worker['id'], options)
171
+ end
172
+ return 0, nil
173
+ end
174
+
175
+ def remove(args)
176
+ options = {}
177
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
178
+ opts.banner = subcommand_usage("[distributed-worker]")
179
+ build_standard_remove_options(opts, options)
180
+ opts.footer = <<-EOT
181
+ Delete a distributed worker.
182
+ [distributed-worker] is required. This is the name or id of a distributed worker.
183
+ EOT
184
+ end
185
+ optparse.parse!(args)
186
+ verify_args!(args:args, optparse:optparse, count:1)
187
+ connect(options)
188
+ distributed_worker = find_distributed_worker_by_name_or_id(args[0])
189
+ return 1 if distributed_worker.nil?
190
+ unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the distributed worker #{distributed_worker['name']}?", options)
191
+ return 9, "aborted command"
192
+ end
193
+ @distributed_workers_interface.setopts(options)
194
+ if options[:dry_run]
195
+ print_dry_run @distributed_workers_interface.dry.destroy(distributed_worker['id'])
196
+ return
197
+ end
198
+ json_response = @distributed_workers_interface.destroy(distributed_worker['id'])
199
+ render_response(json_response, options, 'distributedWorker') do
200
+ print_green_success "Removed distributed worker #{distributed_worker['name']}"
201
+ end
202
+ return 0, nil
203
+ end
204
+
205
+ private
206
+
207
+ def render_distributed_worker_details(distributed_worker)
208
+ print_h1 "Distributed Worker Details"
209
+ print cyan
210
+ description_cols = {
211
+ "ID" => 'id',
212
+ "Name" => 'name',
213
+ "Description" => 'description',
214
+ "Enabled" => lambda {|it| format_boolean(it['enabled']) },
215
+ "Active" => lambda {|it| format_boolean(it['active']) },
216
+ "Proxy Port" => 'proxyPort',
217
+ "Proxy Host List" => 'proxyHostList',
218
+ "Appliance URL" => 'applianceUrl',
219
+ "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
220
+ "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) }
221
+ }
222
+ print_description_list(description_cols, distributed_worker)
223
+ print reset,"\n"
224
+ end
225
+
226
+ def find_distributed_worker_by_name_or_id(val)
227
+ if val.to_s =~ /\A\d{1,}\Z/
228
+ return find_distributed_worker_by_id(val)
229
+ else
230
+ return find_distributed_worker_by_name(val)
231
+ end
232
+ end
233
+
234
+ def find_distributed_worker_by_id(id)
235
+ begin
236
+ json_response = @distributed_workers_interface.get(id.to_i)
237
+ return json_response['distributedWorker']
238
+ rescue RestClient::Exception => e
239
+ if e.response && e.response.code == 404
240
+ print_red_alert "Distributed Worker not found by id #{id}"
241
+ return nil
242
+ else
243
+ raise e
244
+ end
245
+ end
246
+ end
247
+
248
+ def find_distributed_worker_by_name(name)
249
+ distributed_workers = @distributed_workers_interface.list({name: name.to_s})['distributedWorkers']
250
+ if distributed_workers.empty?
251
+ print_red_alert "Distributed Worker not found by name #{name}"
252
+ return nil
253
+ elsif distributed_workers.size > 1
254
+ print_red_alert "#{distributed_workers.size} distributed workers found by name #{name}"
255
+ rows = distributed_workers.collect do |it|
256
+ {id: it['id'], name: it['name']}
257
+ end
258
+ print "\n"
259
+ puts as_pretty_table(rows, [:id, :name], {color: red})
260
+ return nil
261
+ else
262
+ return distributed_workers[0]
263
+ end
264
+ end
265
+
266
+ def add_distributed_worker_option_types
267
+ [
268
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true, 'displayOrder' => 1, 'description' => 'A unique name for the distributed worker.'},
269
+ {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false, 'displayOrder' => 2},
270
+ {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'required' => false, 'defaultValue' => true, 'displayOrder' => 3},
271
+ {'fieldName' => 'proxyHostList', 'fieldLabel' => 'Proxy Host List', 'type' => 'text', 'required' => false, 'displayOrder' => 4, 'description' => 'A comma or space separated list of proxy hosts.'},
272
+ {'fieldName' => 'applianceUrl', 'fieldLabel' => 'Appliance URL', 'type' => 'text', 'required' => false, 'displayOrder' => 5},
273
+ ]
274
+ end
275
+
276
+ def update_distributed_worker_option_types
277
+ list = add_distributed_worker_option_types
278
+ list.each {|it| it['required'] = false; it.delete('defaultValue') }
279
+ list
280
+ end
281
+
282
+ end