morpheus-cli 8.0.11.1 → 8.0.12

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.
@@ -0,0 +1,453 @@
1
+ require 'morpheus/cli/cli_command'
2
+
3
+ class Morpheus::Cli::Migrations
4
+ include Morpheus::Cli::CliCommand
5
+ include Morpheus::Cli::RestCommand
6
+ include Morpheus::Cli::ProcessesHelper
7
+ #include Morpheus::Cli::MigrationsHelper
8
+
9
+ set_command_name :'migrations'
10
+ set_command_description "View and manage migrations."
11
+ register_subcommands :list, :get, :add, :update, :run, :remove, :history
12
+
13
+ # RestCommand settings
14
+ register_interfaces :migrations, :processes
15
+ # set_rest_has_type false
16
+ # set_rest_type :migration_types
17
+
18
+ def render_response_for_get(json_response, options)
19
+ render_response(json_response, options, rest_object_key) do
20
+ record = json_response[rest_object_key]
21
+ print_h1 rest_label, [], options
22
+ print cyan
23
+ print_description_list(rest_column_definitions(options), record, options)
24
+ # show Migration Configuration
25
+ # config = record['config']
26
+ # if config && !config.empty?
27
+ # print_h2 "Virtual Machines"
28
+ # print_description_list(config.keys, config)
29
+ # end
30
+ # Datastores
31
+ datastores = record['datastores']
32
+ print_h2 "Datastores", options
33
+ if datastores && datastores.size > 0
34
+ columns = [
35
+ {"Source" => lambda {|it| it['sourceDatastore'] ? "#{it['sourceDatastore']['name']} [#{it['sourceDatastore']['id']}]" : "" } },
36
+ {"Destination" => lambda {|it| it['destinationDatastore'] ? "#{it['destinationDatastore']['name']} [#{it['destinationDatastore']['id']}]" : "" } },
37
+ ]
38
+ print as_pretty_table(datastores, columns, options)
39
+ else
40
+ print cyan,"No datatores in migration",reset,"\n"
41
+ end
42
+ # Networks
43
+ print_h2 "Networks", options
44
+ networks = record['networks']
45
+ if networks && networks.size > 0
46
+ columns = [
47
+ {"Source" => lambda {|it| it['sourceNetwork'] ? "#{it['sourceNetwork']['name']} [#{it['sourceNetwork']['id']}]" : "" } },
48
+ {"Destination" => lambda {|it| it['destinationNetwork'] ? "#{it['destinationNetwork']['name']} [#{it['destinationNetwork']['id']}]" : "" } },
49
+ ]
50
+ print as_pretty_table(networks, columns, options)
51
+ else
52
+ print cyan,"No networks found in migration",reset,"\n"
53
+ end
54
+ # Virtual Machines
55
+ print_h2 "Virtual Machines", options
56
+ servers = record['servers']
57
+ if servers && servers.size > 0
58
+ columns = [
59
+ # {"ID" => lambda {|it| it['sourceServer'] ? it['sourceServer']['id'] : "" } },
60
+ # {"Name" => lambda {|it| it['sourceServer'] ? it['sourceServer']['name'] : "" } },
61
+ {"Source" => lambda {|it| it['sourceServer'] ? "#{it['sourceServer']['name']} [#{it['sourceServer']['id']}]" : "" } },
62
+ {"Destination" => lambda {|it| it['destinationServer'] ? "#{it['destinationServer']['name']} [#{it['destinationServer']['id']}]" : "" } },
63
+ {"Status" => lambda {|it| format_migration_server_status(it) } }
64
+ ]
65
+ print as_pretty_table(servers, columns, options)
66
+ else
67
+ print cyan,"No virtual machines found in migration",reset,"\n"
68
+ end
69
+ print reset,"\n"
70
+ end
71
+ end
72
+
73
+ def history(args)
74
+ handle_history_command(args, "migration", "Migration", "migrationPlan") do |id|
75
+ record = rest_find_by_name_or_id(id)
76
+ if record.nil?
77
+ # raise_command_error "#{rest_name} not found for '#{id}'"
78
+ return 1, "#{rest_name} not found for '#{id}'"
79
+ end
80
+ record
81
+ end
82
+ end
83
+
84
+ def add(args)
85
+ options = {}
86
+ params = {}
87
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
88
+ opts.banner = subcommand_usage("[name] [options]")
89
+ build_option_type_options(opts, options, add_migration_option_types)
90
+ build_standard_add_options(opts, options)
91
+ opts.footer = <<-EOT
92
+ Create a new migration plan.
93
+ EOT
94
+ end
95
+ optparse.parse!(args)
96
+ verify_args!(args:args, optparse:optparse, min:0, max:1)
97
+ options[:options]['name'] = args[0] if args[0]
98
+ connect(options)
99
+ payload = {}
100
+ if options[:payload]
101
+ payload = options[:payload]
102
+ payload.deep_merge!({rest_object_key => parse_passed_options(options)})
103
+ else
104
+ params.deep_merge!(parse_passed_options(options))
105
+ # prompt for option types
106
+ # skip config if using interactive prompt
107
+ add_option_types = add_migration_option_types
108
+ # handle some option types in a special way
109
+ servers_option_type = add_option_types.find {|it| it['fieldName'] == 'servers' } # || {'switch' => 'servers', 'fieldName' => 'servers', 'fieldLabel' => 'Virtual Machines', 'type' => 'multiSelect', 'optionSource' => 'searchServers', 'required' => true, 'description' => 'Virtual Machines to be migrated, comma separated list of server names or IDs.'}
110
+ add_option_types.reject! {|it| it['fieldName'] == 'servers' }
111
+ # prompt
112
+ v_prompt = Morpheus::Cli::OptionTypes.prompt(add_option_types, options[:options], @api_client, options[:params])
113
+ params.deep_merge!(v_prompt)
114
+ # convert checkbox "on" and "off" to true and false
115
+ params.booleanize!
116
+
117
+ # prompt for servers
118
+ server_ids = nil
119
+ if params['sourceServerIds']
120
+ server_ids = parse_id_list(params.delete('sourceServerIds'))
121
+ elsif params['servers']
122
+ server_ids = parse_id_list(params.delete('servers'))
123
+ end
124
+
125
+ if server_ids
126
+ # lookup each value as an id or name and collect id
127
+ # server_ids = server_ids.collect {|it| find_server_by_name_or_id(it)}.compact.collect {|it| it['id']}
128
+ # available_servers = @api_client.options.options_for_source("searchServers", {'cloudId' => params['sourceCloudId'], 'max' => 1000})['data']
129
+ available_servers = @api_client.migrations.source_servers({'sourceCloudId' => params['sourceCloudId'], 'max' => 5000})['sourceServers']
130
+ bad_ids = []
131
+ server_ids = server_ids.collect {|server_id|
132
+ found_option = available_servers.find {|it| it['id'].to_s == server_id.to_s || it['name'] == server_id.to_s }
133
+ if found_option
134
+ found_option['value'] || found_option['id']
135
+ else
136
+ bad_ids << server_id
137
+ end
138
+ }
139
+ if bad_ids.size > 0
140
+ raise_command_error "No such server found for: #{bad_ids.join(', ')}"
141
+ end
142
+ else
143
+ # prompt for servers
144
+ # servers_option_type = {'fieldName' => 'servers', 'fieldLabel' => 'Virtual Machines', 'type' => 'multiSelect', 'optionSource' => 'searchServers', 'description' => 'Select virtual machine servers to be migrated.', 'required' => true}
145
+ api_params = {'cloudId' => params['sourceCloudId']}
146
+ # server_ids = Morpheus::Cli::OptionTypes.prompt([servers_option_type], options[:options], @api_client, {'cloudId' => params['sourceCloudId'], 'max' => 1000})['servers']
147
+ server_ids = Morpheus::Cli::OptionTypes.prompt([servers_option_type], options[:options], @api_client, {'sourceCloudId' => params['sourceCloudId'], 'max' => 5000})['servers']
148
+ # todo: Add prompt for Add more servers?
149
+ # while self.confirm("Add more #{servers_option_type['fieldLabel']}?", {:default => false}) do
150
+ # more_ids = Morpheus::Cli::OptionTypes.prompt([servers_option_type.merge({'required' => false})], {}, @api_client, api_params)['servers']
151
+ # server_ids += more_ids
152
+ # end
153
+ end
154
+ server_ids.uniq!
155
+ params['sourceServerIds'] = server_ids
156
+
157
+ # prompt for datastores
158
+ datastore_mappings = []
159
+ source_datastores = @api_client.migrations.source_storage({'sourceCloudId' => params['sourceCloudId'], 'sourceServerIds' => params['sourceServerIds'].join(",")})['sourceStorage']
160
+ source_datastores.each do |datastore|
161
+ target_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => "datastore.#{datastore['id']}", 'fieldLabel' => "Datastore #{datastore['name']}", 'type' => 'select', 'required' => true, 'defaultFirstOption' => true, 'description' => "Datastore destination for datastore #{datastore['name']} [#{datastore['id']}]", 'optionSource' => lambda {|api_client, api_params|
162
+ api_client.migrations.target_storage(api_params)['targetStorage'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
163
+ } }], options[:options], @api_client, {'targetCloudId' => params['targetCloudId'], 'targetPoolId' => params['targetPoolId']})["datastore"]["#{datastore['id']}"]
164
+ datastore_mappings << {'sourceDatastore' => {'id' => datastore['id']}, 'destinationDatastore' => {'id' => target_id}}
165
+ end
166
+ params['datastores'] = datastore_mappings
167
+ params.delete('datastore') # remove options passed in as -O datastore.id=
168
+
169
+ # prompt for networks
170
+ network_mappings = []
171
+ source_networks = @api_client.migrations.source_networks({'sourceCloudId' => params['sourceCloudId'], 'sourceServerIds' => params['sourceServerIds'].join(",")})['sourceNetworks']
172
+ source_networks.each do |network|
173
+ target_id = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => "network.#{network['id']}", 'fieldLabel' => "Network #{network['name']}", 'type' => 'select', 'required' => true, 'defaultFirstOption' => true, 'description' => "Network destination for network #{network['name']} [#{network['id']}]", 'optionSource' => lambda {|api_client, api_params|
174
+ api_client.migrations.target_networks(api_params)['targetNetworks'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
175
+ } }], options[:options], @api_client, {'targetCloudId' => params['targetCloudId'], 'targetPoolId' => params['targetPoolId']})["network"]["#{network['id']}"]
176
+ network_mappings << {'sourceNetwork' => {'id' => network['id']}, 'destinationNetwork' => {'id' => target_id}}
177
+ end
178
+ params['networks'] = network_mappings
179
+ params.delete('network') # remove options passed in as -O network.id=
180
+
181
+ payload.deep_merge!({rest_object_key => params})
182
+ end
183
+ @migrations_interface.setopts(options)
184
+ if options[:dry_run]
185
+ print_dry_run @migrations_interface.dry.create(payload)
186
+ return 0, nil
187
+ end
188
+ json_response = @migrations_interface.create(payload)
189
+ migration = json_response[rest_object_key]
190
+ render_response(json_response, options, rest_object_key) do
191
+ print_green_success "Added migration #{migration['name']}"
192
+ return _get(migration["id"], {}, options)
193
+ end
194
+ return 0, nil
195
+ end
196
+
197
+ def update(args)
198
+ options = {}
199
+ params = {}
200
+ payload = {}
201
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
202
+ opts.banner = subcommand_usage("[migration] [options]")
203
+ build_option_type_options(opts, options, update_migration_option_types)
204
+ build_standard_update_options(opts, options)
205
+ opts.footer = <<-EOT
206
+ Update a migration plan.
207
+ [migration] is required. This is the name or id of a migration plan.
208
+ EOT
209
+ end
210
+ optparse.parse!(args)
211
+ verify_args!(args:args, optparse:optparse, count:1)
212
+ connect(options)
213
+ migration = find_migration_by_name_or_id(args[0])
214
+ return 1 if migration.nil?
215
+ payload = {}
216
+ if options[:payload]
217
+ payload = options[:payload]
218
+ payload.deep_merge!({rest_object_key => parse_passed_options(options)})
219
+ else
220
+ params.deep_merge!(parse_passed_options(options))
221
+ # prompt for option types
222
+ # skip config if using interactive prompt
223
+ update_option_types = update_migration_option_types
224
+ # handle some option types in a special way
225
+ servers_option_type = update_option_types.find {|it| it['fieldName'] == 'servers' } # || {'switch' => 'servers', 'fieldName' => 'servers', 'fieldLabel' => 'Virtual Machines', 'type' => 'multiSelect', 'optionSource' => 'searchServers', 'required' => true, 'description' => 'Virtual Machines to be migrated, comma separated list of server names or IDs.'}
226
+ update_option_types.reject! {|it| it['fieldName'] == 'servers' }
227
+ # prompt (edit uses no_prompt)
228
+ # need these parameters for prompting..
229
+ default_api_params = {}
230
+ default_api_params['sourceCloudId'] = migration['sourceCloud']['id'] if migration['sourceCloud']
231
+ default_api_params['targetCloudId'] = migration['targetCloud']['id'] if migration['targetCloud']
232
+ default_api_params['targetGroupId'] = migration['targetGroup']['id'] if migration['targetGroup']
233
+ default_api_params['targetPoolId'] = "pool-" + migration['targetPool']['id'].to_s if migration['targetPool']
234
+ options[:params] = default_api_params.merge(options[:options])
235
+ v_prompt = Morpheus::Cli::OptionTypes.no_prompt(update_option_types, options[:options], @api_client, options[:params])
236
+ params.deep_merge!(v_prompt)
237
+ # convert checkbox "on" and "off" to true and false
238
+ params.booleanize!
239
+
240
+ # prompt for servers
241
+ server_ids = nil
242
+ if params['sourceServerIds']
243
+ server_ids = parse_id_list(params.delete('sourceServerIds'))
244
+ elsif params['servers']
245
+ server_ids = parse_id_list(params.delete('servers'))
246
+ end
247
+
248
+ if server_ids
249
+ # lookup each value as an id or name and collect id
250
+ # server_ids = server_ids.collect {|it| find_server_by_name_or_id(it)}.compact.collect {|it| it['id']}
251
+ # available_servers = @api_client.options.options_for_source("searchServers", {'cloudId' => params['sourceCloudId'], 'max' => 1000})['data']
252
+ available_servers = @api_client.migrations.source_servers({'sourceCloudId' => params['sourceCloudId'], 'max' => 5000})['sourceServers']
253
+ bad_ids = []
254
+ server_ids = server_ids.collect {|server_id|
255
+ found_option = available_servers.find {|it| it['id'].to_s == server_id.to_s || it['name'] == server_id.to_s }
256
+ if found_option
257
+ found_option['value'] || found_option['id']
258
+ else
259
+ bad_ids << server_id
260
+ end
261
+ }
262
+ if bad_ids.size > 0
263
+ raise_command_error "No such server found for: #{bad_ids.join(', ')}"
264
+ end
265
+ server_ids.uniq!
266
+ params['sourceServerIds'] = server_ids
267
+ else
268
+ # no prompt for update
269
+ end
270
+
271
+ source_server_ids = params['sourceServerIds'] || migration['servers'].collect {|it| it['sourceServer'] ? it['sourceServer']['id'] : nil }.compact
272
+ source_cloud_id = params['sourceCloudId'] || (migration['sourceCloud'] ? migration['sourceCloud']['id'] : nil)
273
+ target_cloud_id = params['targetCloudId'] || (migration['targetCloud'] ? migration['targetCloud']['id'] : nil)
274
+ target_pool_id = params['targetPoolId'] || (migration['targetPool'] ? migration['targetPool']['id'] : nil)
275
+
276
+ # prompt for datastores
277
+ if options[:options]['datastore'].is_a?(Hash)
278
+ datastore_mappings = []
279
+ source_datastores = @api_client.migrations.source_storage({'sourceCloudId' => source_cloud_id, 'sourceServerIds' => source_server_ids.join(",")})['sourceStorage']
280
+ source_datastores.each do |datastore|
281
+ found_mapping = migration['datastores'].find {|it| it['sourceDatastore'] && it['sourceDatastore']['id'] == datastore['id'] }
282
+ default_value = found_mapping && found_mapping['destinationDatastore'] ? found_mapping['destinationDatastore']['name'] : nil
283
+ target_id = Morpheus::Cli::OptionTypes.no_prompt([{'fieldName' => "datastore.#{datastore['id']}", 'fieldLabel' => "Datastore #{datastore['name']}", 'type' => 'select', 'description' => "Datastore destination for datastore #{datastore['name']} [#{datastore['id']}]", 'defaultValue' => default_value, 'optionSource' => lambda {|api_client, api_params|
284
+ api_client.migrations.target_storage(api_params)['targetStorage'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
285
+ } }], options[:options], @api_client, {'targetCloudId' => target_cloud_id, 'targetPoolId' => target_pool_id})["datastore"]["#{datastore['id']}"]
286
+ datastore_mappings << {'sourceDatastore' => {'id' => datastore['id']}, 'destinationDatastore' => {'id' => target_id}}
287
+ end
288
+ params['datastores'] = datastore_mappings
289
+ params.delete('datastore') # remove options passed in as -O datastore.id=
290
+ end
291
+
292
+ # prompt for networks
293
+ if options[:options]['network'].is_a?(Hash)
294
+ network_mappings = []
295
+ source_networks = @api_client.migrations.source_networks({'sourceCloudId' => source_cloud_id, 'sourceServerIds' => source_server_ids.join(",")})['sourceNetworks']
296
+ source_networks.each do |network|
297
+ found_mapping = migration['networks'].find {|it| it['sourceNetwork'] && it['sourceNetwork']['id'] == network['id'] }
298
+ default_value = found_mapping && found_mapping['destinationNetwork'] ? found_mapping['destinationNetwork']['name'] : nil
299
+ target_id = Morpheus::Cli::OptionTypes.no_prompt([{'fieldName' => "network.#{network['id']}", 'fieldLabel' => "Network #{network['name']}", 'type' => 'select', 'description' => "Network destination for network #{network['name']} [#{network['id']}]", 'defaultValue' => default_value, 'optionSource' => lambda {|api_client, api_params|
300
+ api_client.migrations.target_networks(api_params)['targetNetworks'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
301
+ } }], options[:options], @api_client, {'targetCloudId' => target_cloud_id, 'targetPoolId' => target_pool_id})["network"]["#{network['id']}"]
302
+ network_mappings << {'sourceNetwork' => {'id' => network['id']}, 'destinationNetwork' => {'id' => target_id}}
303
+ end
304
+ params['networks'] = network_mappings
305
+ params.delete('network') # remove options passed in as -O network.id=
306
+ end
307
+
308
+ if params.empty? # || options[:no_prompt]
309
+ raise_command_error "Specify at least one option to update.\n#{optparse}"
310
+ end
311
+ payload.deep_merge!({rest_object_key => params})
312
+ end
313
+ @migrations_interface.setopts(options)
314
+ if options[:dry_run]
315
+ print_dry_run @migrations_interface.dry.update(migration['id'], payload)
316
+ return
317
+ end
318
+ json_response = @migrations_interface.update(migration['id'], payload)
319
+ migration = json_response[rest_object_key]
320
+ render_response(json_response, options, rest_object_key) do
321
+ print_green_success "Updated migration #{migration['name']}"
322
+ return _get(migration["id"], {}, options)
323
+ end
324
+ return 0, nil
325
+ end
326
+
327
+ def run(args)
328
+ options = {}
329
+ params = {}
330
+ payload = {}
331
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
332
+ opts.banner = subcommand_usage("[migration]")
333
+ build_standard_post_options(opts, options, [:auto_confirm])
334
+ opts.footer = <<-EOT
335
+ Runs a migration plan to transition it from pending to scheduled for execution.
336
+ [migration] is required. This is the name or id of a migration.
337
+ EOT
338
+ end
339
+ optparse.parse!(args)
340
+ verify_args!(args:args, optparse:optparse, count:1)
341
+ connect(options)
342
+ migration = find_migration_by_name_or_id(args[0])
343
+ return 1 if migration.nil?
344
+ parse_payload(options) do |payload|
345
+ end
346
+ servers = migration['servers']
347
+ print cyan, "The following #{servers.size == 1 ? 'server' : servers.size.to_s + ' servers'} will be migrated:", "\n"
348
+ puts ""
349
+ print as_pretty_table(servers, {"Virtual Machine" => lambda {|it| it['sourceServer'] ? "#{it['sourceServer']['name']} [#{it['sourceServer']['id']}]" : "" } }, options)
350
+ puts ""
351
+ confirm!("Are you sure you want to execute the migration plan?", options)
352
+ execute_api(@migrations_interface, :run, [migration['id']], options, 'migration') do |json_response|
353
+ print_green_success "Running migration #{migration['name']}"
354
+ end
355
+ end
356
+
357
+ protected
358
+
359
+ def migration_list_column_definitions(options)
360
+ {
361
+ "ID" => 'id',
362
+ "Name" => 'name',
363
+ "VMs" => lambda {|it| it['servers'] ? it['servers'].size : 0 },
364
+ "Status" => lambda {|it| format_migration_status(it) },
365
+ }
366
+ end
367
+
368
+ def migration_column_definitions(options)
369
+ {
370
+ "ID" => 'id',
371
+ "Name" => 'name',
372
+ "Source Cloud" => lambda {|it| it['sourceCloud'] ? it['sourceCloud']['name'] : '' },
373
+ "Destination Cloud" => lambda {|it| it['targetCloud'] ? it['targetCloud']['name'] : '' },
374
+ "Resource Pool" => lambda {|it| it['targetPool'] ? it['targetPool']['name'] : '' },
375
+ "Group" => lambda {|it| it['targetGroup'] ? it['targetGroup']['name'] : '' },
376
+ "Skip Prechecks" => lambda {|it| format_boolean(it['skipPrechecks']) },
377
+ "Install Guest Tools" => lambda {|it| format_boolean(it['installGuestTools']) },
378
+ # "ReInitialize Server" => lambda {|it| format_boolean(it['reInitializeServerOnMigration']) },
379
+ "VMs" => lambda {|it| it['servers'] ? it['servers'].size : 0 },
380
+ "Status" => lambda {|it| format_migration_status(it) },
381
+ }
382
+ end
383
+
384
+ def add_migration_option_types()
385
+ [
386
+ {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
387
+ # {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text'},
388
+ {'switch' => 'source-cloud', 'fieldName' => 'sourceCloudId', 'fieldLabel' => 'Source Cloud', 'type' => 'select', 'required' => true, 'description' => 'Source Cloud', 'optionSource' => lambda {|api_client, api_params|
389
+ api_client.migrations.source_clouds(api_params)['sourceClouds'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
390
+ } },
391
+ {'switch' => 'cloud', 'fieldName' => 'targetCloudId', 'fieldLabel' => 'Destination Cloud', 'type' => 'select', 'required' => true, 'description' => 'Destination Cloud', 'optionSource' => lambda {|api_client, api_params|
392
+ api_client.migrations.target_clouds(api_params)['targetClouds'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
393
+ } },
394
+ {'switch' => 'group', 'fieldName' => 'targetGroupId', 'fieldLabel' => 'Group', 'type' => 'select', 'optionSource' => 'targetGroups', 'required' => true, 'defaultFirstOption' => true, 'description' => 'Destination Group'},
395
+ {'switch' => 'pool', 'fieldName' => 'targetPoolId', 'fieldLabel' => 'Resource Pool', 'type' => 'select', 'required' => true, 'defaultFirstOption' => true, 'optionSource' => lambda {|api_client, api_params|
396
+ api_params = api_params.merge({'provisionTypeCode' => 'kvm', 'zoneId' => api_params['targetCloudId'], 'groupId' => api_params['targetGroupId']})
397
+ api_client.options.options_for_source("zonePools", api_params)['data']
398
+ } },
399
+ {'switch' => 'servers', 'fieldName' => 'servers', 'fieldLabel' => 'Virtual Machines', 'type' => 'multiSelect', 'required' => true, 'description' => 'Virtual Machines to be migrated, comma separated list of server names or IDs.', 'optionSource' => lambda {|api_client, api_params|
400
+ api_client.migrations.source_servers(api_params)['sourceServers'].collect {|it| {'name' => it['name'], 'value' => it['id']} }
401
+ } },
402
+ {'fieldName' => 'skipPrechecks', 'fieldLabel' => 'Skip Prechecks', 'type' => 'checkbox', 'required' => false, 'defaultValue' => false},
403
+ {'fieldName' => 'installGuestTools', 'fieldLabel' => 'Install Guest Tools', 'type' => 'checkbox', 'required' => false, 'defaultValue' => true},
404
+ ]
405
+ end
406
+
407
+ def add_migration_advanced_option_types()
408
+ [
409
+ # {'fieldName' => 'visibility', 'fieldLabel' => 'Visibility', 'fieldGroup' => 'Advanced', 'type' => 'select', 'selectOptions' => [{'name' => 'Private', 'value' => 'private'},{'name' => 'Public', 'value' => 'public'}], 'required' => false, 'description' => 'Visibility', 'category' => 'permissions'},
410
+ # {'fieldName' => 'tenants', 'fieldLabel' => 'Tenants', 'fieldGroup' => 'Advanced', 'type' => 'multiSelect', 'optionSource' => lambda { |api_client, api_params|
411
+ # api_client.options.options_for_source("allTenants", {})['data']
412
+ # }},
413
+ ]
414
+ end
415
+
416
+ def update_migration_option_types()
417
+ add_migration_option_types.collect {|it|
418
+ it.delete('required')
419
+ it.delete('defaultValue')
420
+ it.delete('defaultFirstOption')
421
+ it
422
+ }
423
+ end
424
+
425
+ def update_migration_advanced_option_types()
426
+ add_migration_advanced_option_types()
427
+ end
428
+
429
+ def format_migration_status(migration, return_color=cyan)
430
+ # migration statuses: pending, scheduled, precheck, running, failed, completed
431
+ out = ""
432
+ status_string = migration['status']
433
+ if status_string.nil? || status_string.empty? || status_string == "unknown"
434
+ out << "#{white}UNKNOWN#{return_color}"
435
+ elsif status_string == 'completed'
436
+ out << "#{green}#{status_string.upcase}#{return_color}"
437
+ elsif status_string == 'pending' || status_string == 'scheduled' || status_string == 'precheck' || status_string == 'running'
438
+ out << "#{cyan}#{status_string.upcase}#{return_color}"
439
+ else
440
+ out << "#{red}#{status_string ? status_string.upcase : 'N/A'}#{migration['statusMessage'] ? "#{return_color} - #{migration['statusMessage']}" : ''}#{return_color}"
441
+ end
442
+ out
443
+ end
444
+
445
+ def format_migration_server_status(migration_server, return_color=cyan)
446
+ return format_migration_status(migration_server, return_color)
447
+ end
448
+
449
+ def find_migration_by_name_or_id(arg)
450
+ find_by_name_or_id(rest_key, arg)
451
+ end
452
+
453
+ end
@@ -791,7 +791,7 @@ class Morpheus::Cli::NetworksCommand
791
791
  end
792
792
 
793
793
  # Allow IP Override
794
- if network_type['staticOverrideEditable'] && payload['network']['allowStaticOverride'].nil?
794
+ if payload['network']['allowStaticOverride'].nil?
795
795
  if options['allowStaticOverride'] != nil
796
796
  payload['network']['allowStaticOverride'] = options['allowStaticOverride']
797
797
  else
@@ -147,4 +147,140 @@ module Morpheus::Cli::ProcessesHelper
147
147
  return process
148
148
  end
149
149
 
150
+ def handle_history_command(args, arg_name, label, ref_type, &block)
151
+ raw_args = args.dup
152
+ options = {}
153
+ #options[:show_output] = true
154
+ optparse = Morpheus::Cli::OptionParser.new do |opts|
155
+ opts.banner = "Usage: #{prog_name} #{command_name} history [#{arg_name}]"
156
+ opts.on( nil, '--events', "Display sub processes (events)." ) do
157
+ options[:show_events] = true
158
+ end
159
+ opts.on( nil, '--output', "Display process output." ) do
160
+ options[:show_output] = true
161
+ end
162
+ opts.on('--details', "Display more details: memory and storage usage used / max values." ) do
163
+ options[:show_events] = true
164
+ options[:show_output] = true
165
+ options[:details] = true
166
+ end
167
+ # opts.on('--process-id ID', String, "Display details about a specfic process only." ) do |val|
168
+ # options[:process_id] = val
169
+ # end
170
+ # opts.on('--event-id ID', String, "Display details about a specfic process event only." ) do |val|
171
+ # options[:event_id] = val
172
+ # end
173
+ build_standard_list_options(opts, options)
174
+ opts.footer = "List historical processes for a specific #{label}.\n" +
175
+ "[#{arg_name}] is required. This is the name or id of an #{label}."
176
+ end
177
+ optparse.parse!(args)
178
+
179
+ # shortcut to other actions
180
+ # if options[:process_id]
181
+ # return history_details(raw_args)
182
+ # elsif options[:event_id]
183
+ # return history_event_details(raw_args)
184
+ # end
185
+
186
+ verify_args!(args:args, optparse:optparse, count:1)
187
+ connect(options)
188
+
189
+ record = block.call(args[0])
190
+ # block should raise_command_error if not found
191
+ if record.nil?
192
+ raise_command_error "#{label} not found for name or id '#{args[0]}'"
193
+ end
194
+ params = {}
195
+ params.merge!(parse_list_options(options))
196
+ # params['query'] = params.delete('phrase') if params['phrase']
197
+ params['refType'] = ref_type
198
+ params['refId'] = record['id']
199
+ @processes_interface.setopts(options)
200
+ if options[:dry_run]
201
+ print_dry_run @processes_interface.dry.list(params)
202
+ return
203
+ end
204
+ json_response = @processes_interface.list(params)
205
+ render_response(json_response, options, "processes") do
206
+ title = "#{label} History: #{record['name'] || record['id']}"
207
+ subtitles = parse_list_subtitles(options)
208
+ print_h1 title, subtitles, options
209
+ processes = json_response['processes']
210
+ if processes.empty?
211
+ print "#{cyan}No process history found.#{reset}\n\n"
212
+ else
213
+ history_records = []
214
+ processes.each do |process|
215
+ row = {
216
+ id: process['id'],
217
+ eventId: nil,
218
+ uniqueId: process['uniqueId'],
219
+ name: process['displayName'],
220
+ description: process['description'],
221
+ processType: process['processType'] ? (process['processType']['name'] || process['processType']['code']) : process['processTypeName'],
222
+ createdBy: process['createdBy'] ? (process['createdBy']['displayName'] || process['createdBy']['username']) : '',
223
+ startDate: format_local_dt(process['startDate']),
224
+ duration: format_process_duration(process),
225
+ status: format_process_status(process),
226
+ error: format_process_error(process, options[:details] ? nil : 20),
227
+ output: format_process_output(process, options[:details] ? nil : 20)
228
+ }
229
+ history_records << row
230
+ process_events = process['events'] || process['processEvents']
231
+ if options[:show_events]
232
+ if process_events
233
+ process_events.each do |process_event|
234
+ event_row = {
235
+ id: process['id'],
236
+ eventId: process_event['id'],
237
+ uniqueId: process_event['uniqueId'],
238
+ name: process_event['displayName'], # blank like the UI
239
+ description: process_event['description'],
240
+ processType: process_event['processType'] ? (process_event['processType']['name'] || process_event['processType']['code']) : process['processTypeName'],
241
+ createdBy: process_event['createdBy'] ? (process_event['createdBy']['displayName'] || process_event['createdBy']['username']) : '',
242
+ startDate: format_local_dt(process_event['startDate']),
243
+ duration: format_process_duration(process_event),
244
+ status: format_process_status(process_event),
245
+ error: format_process_error(process_event, options[:details] ? nil : 20),
246
+ output: format_process_output(process_event, options[:details] ? nil : 20)
247
+ }
248
+ history_records << event_row
249
+ end
250
+ else
251
+
252
+ end
253
+ end
254
+ end
255
+ columns = [
256
+ {:id => {:display_name => "PROCESS ID"} },
257
+ :name,
258
+ :description,
259
+ {:processType => {:display_name => "PROCESS TYPE"} },
260
+ {:createdBy => {:display_name => "CREATED BY"} },
261
+ {:startDate => {:display_name => "START DATE"} },
262
+ {:duration => {:display_name => "ETA/DURATION"} },
263
+ :status,
264
+ :error
265
+ ]
266
+ if options[:show_events]
267
+ columns.insert(1, {:eventId => {:display_name => "EVENT ID"} })
268
+ end
269
+ if options[:show_output]
270
+ columns << :output
271
+ end
272
+ # custom pretty table columns ...
273
+ if options[:include_fields]
274
+ columns = options[:include_fields]
275
+ end
276
+ print cyan
277
+ print as_pretty_table(history_records, columns, options)
278
+ #print_results_pagination(json_response)
279
+ print_results_pagination(json_response, {:label => "process", :n_label => "processes"})
280
+ print reset, "\n"
281
+ end
282
+ end
283
+ return 0, nil
284
+ end
285
+
150
286
  end
@@ -555,7 +555,7 @@ EOT
555
555
  end
556
556
 
557
557
  def _get(id, params, options)
558
- if id !~ /\A\d{1,}\Z/ && rest_has_name
558
+ if id.to_s !~ /\A\d{1,}\Z/ && rest_has_name
559
559
  record = rest_find_by_name_or_id(id)
560
560
  if record.nil?
561
561
  return 1, "#{rest_label} not found for '#{id}'"
@@ -952,7 +952,7 @@ EOT
952
952
  end
953
953
 
954
954
  def _get_type(id, params, options)
955
- if id !~ /\A\d{1,}\Z/ # && rest_type_has_name
955
+ if id.to_s !~ /\A\d{1,}\Z/ # && rest_type_has_name
956
956
  record = rest_type_find_by_name_or_id(id)
957
957
  if record.nil?
958
958
  return 1, "#{rest_type_label} not found for '#{id}'"
@@ -724,10 +724,11 @@ module Morpheus
724
724
  end
725
725
  end
726
726
  # default to the first option
727
- if !value_found && default_value.nil? && option_type['defaultFirstOption'] && select_options && select_options[0]
728
- # default_value = select_options[0][value_field]
727
+ first_option = select_options ? select_options.find {|opt| opt['isGroup'] != true } : nil
728
+ if !value_found && default_value.nil? && option_type['defaultFirstOption'] && first_option
729
+ # default_value = first_option[value_field]
729
730
  # nicer to display name instead, it will match and replace with value
730
- default_value = select_options[0]['name'] ? select_options[0]['name'] : select_options[0][value_field]
731
+ default_value = first_option['name'] ? first_option['name'] : first_option[value_field]
731
732
  end
732
733
 
733
734
  if no_prompt
@@ -978,6 +979,7 @@ module Morpheus
978
979
  query_value = (input || '')
979
980
  api_params ||= {}
980
981
  api_params['query'] = query_value
982
+ api_params['phrase'] = query_value
981
983
  # skip refresh if you just hit enter
982
984
  if !query_value.empty? || (select_options.nil? || select_options.empty?)
983
985
  select_options = load_options(option_type, api_client, api_params, query_value)
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Morpheus
3
3
  module Cli
4
- VERSION = "8.0.11.1"
4
+ VERSION = "8.0.12"
5
5
  end
6
6
  end