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.
- checksums.yaml +4 -4
- data/Dockerfile +1 -1
- data/lib/morpheus/api/api_client.rb +4 -0
- data/lib/morpheus/api/migrations_interface.rb +49 -0
- data/lib/morpheus/cli/commands/benchmark_command.rb +3 -4
- data/lib/morpheus/cli/commands/clouds.rb +29 -22
- data/lib/morpheus/cli/commands/cluster_types.rb +179 -0
- data/lib/morpheus/cli/commands/clusters.rb +37 -22
- data/lib/morpheus/cli/commands/hosts.rb +37 -3
- data/lib/morpheus/cli/commands/instances.rb +25 -0
- data/lib/morpheus/cli/commands/migrations.rb +453 -0
- data/lib/morpheus/cli/commands/networks_command.rb +1 -1
- data/lib/morpheus/cli/mixins/processes_helper.rb +136 -0
- data/lib/morpheus/cli/mixins/rest_command.rb +2 -2
- data/lib/morpheus/cli/option_types.rb +5 -3
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/routes.rb +2 -1
- data/test/cli/migrations_test.rb +39 -0
- metadata +7 -2
|
@@ -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
|
|
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
|
-
|
|
728
|
-
|
|
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 =
|
|
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)
|