Skip to content

Commit

Permalink
Merge pull request #19698 from fdupont-redhat/v2v_use_podman_for_wrapper
Browse files Browse the repository at this point in the history
[V2V] Use podman with UCI image instead of virt-v2v-wrapper directly

(cherry picked from commit 370cc23)

Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1788988
  • Loading branch information
agrare authored and simaishi committed Feb 24, 2020
1 parent 078d2a2 commit 2de9665
Show file tree
Hide file tree
Showing 7 changed files with 326 additions and 153 deletions.
165 changes: 132 additions & 33 deletions app/models/conversion_host.rb
Expand Up @@ -124,9 +124,10 @@ def check_concurrent_tasks
# underlying provider.
#
def check_ssh_connection
connect_ssh { |ssu| ssu.shell_exec('uname -a') }
command = AwesomeSpawn.build_command_line("uname", [:a])
connect_ssh { |ssu| ssu.shell_exec(command, nil, nil, nil) }
true
rescue StandardError
rescue
false
end

Expand Down Expand Up @@ -158,14 +159,100 @@ def ipaddress(family = 'ipv4')
# @raise [MiqException::MiqSshUtilHostKeyMismatch] if conversion host key has changed
# @raise [JSON::GeneratorError] if limits hash can't be converted to JSON
# @raise [StandardError] if any other problem happens
def apply_task_limits(path, limits = {})
connect_ssh { |ssu| ssu.put_file(path, limits.to_json) }
def apply_task_limits(task_id, limits = {})
connect_ssh do |ssu|
ssu.put_file("/tmp/#{task_id}-limits.json", limits.to_json)
command = AwesomeSpawn.build_command_line("mv", ["/tmp/#{task_id}-limits.json", "/var/lib/uci/#{task_id}/limits.json"])
ssu.shell_exec(command, nil, nil, nil)
end
rescue MiqException::MiqInvalidCredentialsError, MiqException::MiqSshUtilHostKeyMismatch => err
raise "Failed to connect and apply limits in file '#{path}' with [#{err.class}: #{err}]"
raise "Failed to connect and apply limits for task '#{task_id}' with [#{err.class}: #{err}]"
rescue JSON::GeneratorError => err
raise "Could not generate JSON from limits '#{limits}' with [#{err.class}: #{err}]"
rescue StandardError => err
raise "Could not apply the limits in '#{path}' on '#{resource.name}' with [#{err.class}: #{err}]"
rescue => err
raise "Could not apply the limits for task '#{task_id}' on '#{resource.name}' with [#{err.class}: #{err}]"
end

# Prepare the conversion assets for a specific task.
#
# @param [Integer] id of the task that needs the preparation
# @param [Hash] conversion options to write on the conversion host
#
# @return [Integer] length of data written to conversion options file
#
# @raise [MiqException::MiqInvalidCredentialsError] if conversion host credentials are invalid
# @raise [MiqException::MiqSshUtilHostKeyMismatch] if conversion host key has changed
# @raise [JSON::GeneratorError] if limits hash can't be converted to JSON
# @raise [StandardError] if any other problem happens
def prepare_conversion(task_id, conversion_options)
filtered_options = filter_options(conversion_options)

connect_ssh do |ssu|
# Prepare the conversion folders
command = AwesomeSpawn.build_command_line("mkdir", [:p, "/var/lib/uci/#{task_id}", "/var/log/uci/#{task_id}"])
ssu.shell_exec(command, nil, nil, nil)

# Write the conversion options file
ssu.put_file("/tmp/#{task_id}-input.json", conversion_options.to_json)
command = AwesomeSpawn.build_command_line("mv", ["/tmp/#{task_id}-input.json", "/var/lib/uci/#{task_id}/input.json"])
ssu.shell_exec(command, nil, nil, nil)
end
rescue MiqException::MiqInvalidCredentialsError, MiqException::MiqSshUtilHostKeyMismatch => err
raise "Failed to connect and prepare conversion for task '#{task_id}' with [#{err.class}: #{err}]"
rescue JSON::GeneratorError => err
raise "Could not generate JSON for task '#{task_id}' from options '#{filtered_options}' with [#{err.class}: #{err}]"
rescue => err
raise "Preparation of conversion for task '#{task_id}' failed on '#{resource.name}' with [#{err.class}: #{err}]"
end

# Checks that LUKS keys vault exists and is valid JSON
# We don't care about the file content, as virt-v2v-wrapper will check it later
#
# @return [Boolean] true if the file can be retrieved and parsed, false otherwise
#
# @raise [MiqException::MiqInvalidCredentialsError] if conversion host credentials are invalid
# @raise [MiqException::MiqSshUtilHostKeyMismatch] if conversion host key has changed
# @raise [JSON::ParserError] if file cannot be parsed as JSON
def luks_keys_vault_valid?
luks_keys_vault_json = connect_ssh { |ssu| ssu.get_file("/root/.v2v_luks_keys_vault.json", nil) }
JSON.parse(luks_keys_vault_json)
true
rescue MiqException::MiqInvalidCredentialsError, MiqException::MiqSshUtilHostKeyMismatch => err
raise "Failed to connect and retrieve LUKS keys vault from file '/root/.v2v_luks_keys_vault.json' with [#{err.class}: #{err}]"
rescue JSON::ParserError
raise "Could not parse conversion state data from file '/root/.v2v_luks_keys_vault.json': #{json_state}"
rescue
false
end

# Build the podman command to execute conversion
#
# @param [Integer] id of the task that conversion applies to
#
# @return [String] podman command to be executed on conversion host
def build_podman_command(task_id, conversion_options)
uci_settings = Settings.transformation.uci.container
uci_image = uci_settings.image
uci_image = "#{uci_settings.registry}/#{image}" if uci_settings.registry.present?

params = [
"run",
:detach,
:privileged,
[:name, "conversion-#{task_id}"],
[:network, "host"],
[:volume, "/dev:/dev"],
[:volume, "/etc/pki/ca-trust:/etc/pki/ca-trust"],
[:volume, "/var/tmp:/var/tmp"],
[:volume, "/var/lib/uci/#{task_id}:/var/lib/uci"],
[:volume, "/var/log/uci/#{task_id}:/var/log/uci"],
[:volume, "/opt/vmware-vix-disklib-distrib:/opt/vmware-vix-disklib-distrib"]
]
params << [:volume, "/root/.ssh/id_rsa:/var/lib/uci/ssh_private_key"] if conversion_options[:transport_method] == 'ssh'
params << [:volume, "/root/.v2v_luks_keys_vault.json:/var/lib/uci/luks_keys_vault.json"] if luks_keys_vault_valid?
params << uci_image

AwesomeSpawn.build_command_line("/usr/bin/podman", params)
end

# Run the virt-v2v-wrapper script on the remote host and return a hash
Expand All @@ -174,31 +261,31 @@ def apply_task_limits(path, limits = {})
# Certain sensitive fields are filtered in the error messages to prevent
# that information from showing up in the UI or logs.
#
def run_conversion(conversion_options)
ignore = %w[password fingerprint key]
filtered_options = conversion_options.clone.tap { |h| h.each { |k, _v| h[k] = "__FILTERED__" if ignore.any? { |i| k.to_s.end_with?(i) } } }
result = connect_ssh { |ssu| ssu.shell_exec('/usr/bin/virt-v2v-wrapper', nil, nil, conversion_options.to_json) }
JSON.parse(result)
# @param [Integer] id of the task that conversion applies to
def run_conversion(task_id, conversion_options)
filtered_options = filter_options(conversion_options)
prepare_conversion(task_id, conversion_options)
connect_ssh { |ssu| ssu.shell_exec(build_podman_command(task_id, conversion_options), nil, nil, nil) }
rescue MiqException::MiqInvalidCredentialsError, MiqException::MiqSshUtilHostKeyMismatch => err
raise "Failed to connect and run conversion using options #{filtered_options} with [#{err.class}: #{err}]"
rescue JSON::ParserError
raise "Could not parse result data after running virt-v2v-wrapper using options: #{filtered_options}. Result was: #{result}."
rescue StandardError => err
raise "Starting conversion failed on '#{resource.name}' with [#{err.class}: #{err}]"
rescue => err
raise "Starting conversion for task '#{task_id}' failed on '#{resource.name}' with [#{err.class}: #{err}]"
end

def create_cutover_file(path)
connect_ssh { |ssu| ssu.shell_exec("touch #{path}") }
def create_cutover_file(task_id)
command = AwesomeSpawn.build_command_line("touch", ["/var/lib/uci/#{task_id}/cutover"])
connect_ssh { |ssu| ssu.shell_exec(command, nil, nil, nil) }
true
rescue StandardError
rescue
false
end

# Kill a specific remote process over ssh, sending the specified +signal+, or 'TERM'
# if no signal is specified.
#
def kill_process(pid, signal = 'TERM')
connect_ssh { |ssu| ssu.shell_exec("/bin/kill -s #{signal} #{pid}") }
def kill_virtv2v(task_id, signal)
command = AwesomeSpawn.build_command_line("/usr/bin/podman", ["exec", "conversion-#{task_id}", "/usr/bin/killall", :s, signal, "virt-v2v"])
connect_ssh { |ssu| ssu.shell_exec(command, nil, nil, nil) }
true
rescue
false
Expand All @@ -207,23 +294,23 @@ def kill_process(pid, signal = 'TERM')
# Retrieve the conversion state information from a remote file as a stream.
# Then parse and return the stream data as a hash using JSON.parse.
#
def get_conversion_state(path)
json_state = connect_ssh { |ssu| ssu.get_file(path, nil) }
def get_conversion_state(task_id)
json_state = connect_ssh { |ssu| ssu.get_file("/var/lib/uci/#{task_id}/state.json", nil) }
JSON.parse(json_state)
rescue MiqException::MiqInvalidCredentialsError, MiqException::MiqSshUtilHostKeyMismatch => err
raise "Failed to connect and retrieve conversion state data from file '#{path}' with [#{err.class}: #{err}"
raise "Failed to connect and retrieve conversion state data from file '/var/lib/uci/#{task_id}/state.json' with [#{err.class}: #{err}]"
rescue JSON::ParserError
raise "Could not parse conversion state data from file '#{path}': #{json_state}"
rescue StandardError => err
raise "Error retrieving and parsing conversion state file '#{path}' from '#{resource.name}' with [#{err.class}: #{err}"
raise "Could not parse conversion state data from file '/var/lib/uci/#{task_id}/state.json': #{json_state}"
rescue => err
raise "Error retrieving and parsing conversion state file '/var/lib/uci/#{task_id}/state.json' from '#{resource.name}' with [#{err.class}: #{err}"
end

# Get and return the contents of the remote conversion log at +path+.
#
def get_conversion_log(path)
connect_ssh { |ssu| ssu.get_file(path, nil) }
rescue => e
raise "Could not get conversion log '#{path}' from '#{resource.name}' with [#{e.class}: #{e}"
rescue => err
raise "Could not get conversion log '#{path}' from '#{resource.name}' with [#{err.class}: #{err}"
end

def check_conversion_host_role(miq_task_id = nil)
Expand Down Expand Up @@ -299,6 +386,12 @@ def find_credentials(auth_type = 'v2v')
authentication
end

# Utility method to filter certain entries of a hash based on key name
def filter_options(options)
ignore = %w[password fingerprint key]
options.clone.tap { |h| h.each { |k, _v| h[k] = "__FILTERED__" if ignore.any? { |i| k.to_s.end_with?(i) } } }
end

# Connect to the conversion host using the MiqSshUtil wrapper using the authentication
# parameters appropriate for that type of resource.
#
Expand Down Expand Up @@ -336,10 +429,15 @@ def ansible_playbook(playbook, extra_vars = {}, miq_task_id = nil, auth_type = '

host = hostname || ipaddress

command = "ansible-playbook #{playbook} --inventory #{host}, --become --extra-vars=\"ansible_ssh_common_args='-o StrictHostKeyChecking=no'\""
params = [
playbook,
:become,
[:inventory, "#{host},"],
{:extra_vars= => "ansible_ssh_common_args='-o StrictHostKeyChecking=no'"}
]

auth = find_credentials(auth_type)
command << " --user #{auth.userid}"
params << [:user, auth.userid]

case auth
when AuthUseridPassword
Expand All @@ -351,13 +449,14 @@ def ansible_playbook(playbook, extra_vars = {}, miq_task_id = nil, auth_type = '
ensure
ssh_private_key_file.close
end
command << " --private-key #{ssh_private_key_file.path}"
params << {:private_key => ssh_private_key_file.path}
else
raise MiqException::MiqInvalidCredentialsError, _("Unknown auth type: %{auth_type}") % {:auth_type => auth.authtype}
end

command << " --extra-vars '#{extra_vars.to_json}'"
params << {:extra_vars => "'#{extra_vars.to_json}'"}

command = AwesomeSpawn.build_command_line("ansible-playbook", params)
result = AwesomeSpawn.run(command)

if result.failure?
Expand Down
58 changes: 32 additions & 26 deletions app/models/service_template_transformation_plan_task.rb
Expand Up @@ -215,7 +215,14 @@ def update_options(opts)
def run_conversion
start_timestamp = Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')
updates = {}
updates[:virtv2v_wrapper] = conversion_host.run_conversion(conversion_options)
conversion_host.run_conversion(id, conversion_options)
updates[:virtv2v_wrapper] = {
"state_file" => "/var/lib/uci/#{id}/state.json",
"throttling_file" => "/var/lib/uci/#{id}/limits.json",
"cutover_file" => "/var/lib/uci/#{id}/cutover",
"v2v_log" => "/var/log/uci/#{id}/virt-v2v.log",
"wrapper_log" => "/var/log/uci/#{id}/virt-v2v-wrapper.log"
}
updates[:virtv2v_started_on] = start_timestamp
updates[:virtv2v_status] = 'active'
_log.info("InfraConversionJob run_conversion to update_options: #{updates}")
Expand All @@ -224,7 +231,7 @@ def run_conversion

def get_conversion_state
updates = {}
virtv2v_state = conversion_host.get_conversion_state(options[:virtv2v_wrapper]['state_file'])
virtv2v_state = conversion_host.get_conversion_state(id)
updated_disks = virtv2v_disks
updates[:virtv2v_pid] = virtv2v_state['pid'] if virtv2v_state['pid'].present?
updates[:virtv2v_message] = virtv2v_state['last_message']['message'] if virtv2v_state['last_message'].present?
Expand Down Expand Up @@ -258,28 +265,25 @@ def get_conversion_state
end

def cutover
if options[:virtv2v_wrapper]['cutover_file'].present?
unless conversion_host.create_cutover_file(options[:virtv2v_wrapper]['cutover_file'])
raise _("Couldn't create cutover file for #{source.name} on #{conversion_host.name}")
end
unless conversion_host.create_cutover_file(id)
raise _("Couldn't create cutover file for #{source.name} on #{conversion_host.name}")
end
end

def kill_virtv2v(signal = 'TERM')
def kill_virtv2v
get_conversion_state

unless virtv2v_running?
_log.info("virt-v2v is not running, so there is nothing to do.")
return false
end

unless options[:virtv2v_pid]
_log.info("No PID has been reported by virt-v2v-wrapper, so we can't kill it.")
return false
end
_log.info("Killing conversion pod for task '#{id}'.")
conversion_host.kill_virtv2v(id)
end

_log.info("Killing virt-v2v (PID: #{options[:virtv2v_pid]}) with #{signal} signal.")
conversion_host.kill_process(options[:virtv2v_pid], signal)
def virtv2v_running?
options[:virtv2v_started_on].present? && options[:virtv2v_finished_on].blank? && options[:virtv2v_wrapper].present?
end

private
Expand All @@ -288,10 +292,6 @@ def vm_resource
miq_request.vm_resources.find_by(:resource => source)
end

def virtv2v_running?
options[:virtv2v_started_on].present? && options[:virtv2v_finished_on].blank? && options[:virtv2v_wrapper].present?
end

def create_error_status_task(userid, msg)
MiqTask.create(
:name => "Download transformation log with ID: #{id}",
Expand Down Expand Up @@ -332,31 +332,37 @@ def calculate_network_mappings

def conversion_options_source_provider_vmwarews_vddk(_storage)
{
:vm_name => source.name,
:transport_method => 'vddk',
:vmware_fingerprint => source.host.thumbprint_sha1,
:vmware_uri => URI::Generic.build(
:vm_name => source.name,
:vm_uuid => source.uid_ems,
:conversion_host_uuid => conversion_host.resource.ems_ref,
:transport_method => 'vddk',
:vmware_fingerprint => source.host.thumbprint_sha1,
:vmware_uri => URI::Generic.build(
:scheme => 'esx',
:userinfo => CGI.escape(source.host.authentication_userid),
:host => source.host.ipaddress,
:path => '/',
:query => { :no_verify => 1 }.to_query
).to_s,
:vmware_password => source.host.authentication_password,
:two_phase => true,
:warm => warm_migration?
:vmware_password => source.host.authentication_password,
:two_phase => true,
:warm => warm_migration?,
:daemonize => false
}
end

def conversion_options_source_provider_vmwarews_ssh(storage)
{
:vm_name => URI::Generic.build(
:vm_name => URI::Generic.build(
:scheme => 'ssh',
:userinfo => 'root',
:host => source.host.ipaddress,
:path => "/vmfs/volumes/#{Addressable::URI.escape(storage.name)}/#{Addressable::URI.escape(source.location)}"
).to_s,
:transport_method => 'ssh'
:vm_uuid => source.uid_ems,
:conversion_host_uuid => conversion_host.resource.ems_ref,
:transport_method => 'ssh',
:daemonize => false
}
end

Expand Down
4 changes: 4 additions & 0 deletions config/settings.yml
Expand Up @@ -1097,6 +1097,10 @@
:max_concurrent_tasks_per_host: 10
:cpu_limit_per_host: unlimited
:network_limit_per_host: unlimited
:uci:
:container:
:registry:
:image: manageiq/v2v-conversion-host:latest
:ui:
:mark_translated_strings: false
:display_ops_database: false
Expand Down
6 changes: 3 additions & 3 deletions lib/infra_conversion_throttler.rb
Expand Up @@ -90,14 +90,14 @@ def self.apply_limits

jobs.each do |job|
migration_task = job.migration_task
throttling_file_path = migration_task.options.fetch_path(:virtv2v_wrapper, 'throttling_file')
next unless throttling_file_path
next unless migration_task.virtv2v_running?

limits = {
:cpu => cpu_limit,
:network => network_limit
}
unless migration_task.options[:virtv2v_limits] == limits
ch.apply_task_limits(throttling_file_path, limits)
ch.apply_task_limits(migration_task.id, limits)
migration_task.update_options(:virtv2v_limits => limits)
end
end
Expand Down

0 comments on commit 2de9665

Please sign in to comment.