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.
- 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/commands/shell.rb +10 -7
- data/lib/morpheus/cli/credentials.rb +2 -0
- 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 +11 -3
- data/lib/morpheus/cli/version.rb +1 -1
- data/lib/morpheus/routes.rb +2 -1
- data/lib/morpheus/terminal.rb +0 -24
- 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
|
|
@@ -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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|