morpheus-cli 8.0.11 → 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
@@ -99,13 +99,15 @@ class Morpheus::Cli::Shell
99
99
  end
100
100
  @auto_complete_commands = (@exploded_commands + @shell_commands + @alias_commands).collect {|it| it.to_s }
101
101
  @auto_complete = proc do |s|
102
- command_list = @auto_complete_commands
103
- result = command_list.grep(/^#{Regexp.escape(s)}/)
104
- if result.nil? || result.empty?
105
- Readline::FILENAME_COMPLETION_PROC.call(s) rescue []
102
+ results = @auto_complete_commands.grep(/^#{Regexp.escape(s)}/)
103
+ # do not append space unless there is only one match
104
+ # note: this does not work in newer rubies for some reason (>= 3.3.7)
105
+ if results.size == 1
106
+ Readline.completion_append_character = " "
106
107
  else
107
- result
108
+ Readline.completion_append_character = ""
108
109
  end
110
+ results
109
111
  end
110
112
  end
111
113
 
@@ -284,9 +286,10 @@ class Morpheus::Cli::Shell
284
286
  while !@exit_now_please do
285
287
  #Readline.input = my_terminal.stdin
286
288
  #Readline.input = $stdin
287
- Readline.completion_append_character = " "
289
+ Readline.completion_append_character = ""
288
290
  Readline.completion_proc = @auto_complete
289
- Readline.basic_word_break_characters = ""
291
+ Readline.basic_word_break_characters = "" rescue nil
292
+ Readline.completer_word_break_characters = "" rescue nil
290
293
  #Readline.basic_word_break_characters = "\t\n\"\‘`@$><=;|&{( "
291
294
  input = Readline.readline(@calculated_prompt, true).to_s
292
295
  input = input.strip
@@ -116,6 +116,7 @@ module Morpheus
116
116
  # username = $stdin.gets.chomp!
117
117
  Readline.completion_append_character = ""
118
118
  Readline.basic_word_break_characters = ''
119
+ Readline.completer_word_break_characters = '' rescue nil
119
120
  Readline.completion_proc = nil
120
121
  username = Readline.readline("Username: #{required_blue_prompt} ", false).to_s.chomp
121
122
  else
@@ -133,6 +134,7 @@ module Morpheus
133
134
 
134
135
  Readline.completion_append_character = ""
135
136
  Readline.basic_word_break_characters = ''
137
+ Readline.completer_word_break_characters = '' rescue nil
136
138
  Readline.completion_proc = nil
137
139
  # needs to work like $stdin.noecho
138
140
  Readline.pre_input_hook = lambda {
@@ -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