src/lib/transfer/file_from_url.rb
# ------------------------------------------------------------------------------
# Copyright (c) 2016 SUSE, LLC. All Rights Reserved.
#
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of version 2 of the GNU General Public License as published by the
# Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, contact SUSE.
#
# To contact SUSE about this file by physical or electronic mail, you may find
# current contact information at www.suse.com.
# ------------------------------------------------------------------------------
require "yast"
require "yast2/execute"
require "yast2/rel_url"
# rubocop:disable all
module Yast::Transfer
module FileFromUrl
include Yast
include Yast::Logger
# lazy initialization
def initialize_file_from_url
return if @file_from_url_initialized
@file_from_url_initialized = true
textdomain "installation"
Yast.import "URL"
Yast.import "FTP"
Yast.import "Installation"
Yast.import "HTTP"
Yast.import "TFTP"
Yast.import "InstURL"
end
# Basename
# @param filePath [String] path
# @return [String] basename
def basename(filePath)
pathComponents = Builtins.splitstring(filePath, "/")
ret = Ops.get_string(
pathComponents,
Ops.subtract(Builtins.size(pathComponents), 1),
""
)
ret
end
# Get directory name
# @param filePath [String] path
# @return [String] dirname
def dirname(filePath)
pathComponents = Builtins.splitstring(filePath, "/")
last = Ops.get_string(
pathComponents,
Ops.subtract(Builtins.size(pathComponents), 1),
""
)
ret = Builtins.substring(
filePath,
0,
Ops.subtract(Builtins.size(filePath), Builtins.size(last))
)
ret
end
# Copy a file from a URL to a local path
# The URL allows autoyast-specific schemes:
# https://www.suse.com/documentation/sles-12/singlehtml/book_autoyast/book_autoyast.html#Commandline.ay
#
# @note Some arguments are duplicated in the urltok hash. Where they
# differ, the explicitly passed arguments replace their counterparts in urltok.
#
# @param scheme [String] ftp, tftp, http, https, cifs, nfs, device, cd, dvd, hd, usb, file, label, repo
# @param host [String]
# @param urlpath [String]
# @param localfile [String] destination filename
# @param urltok [Hash{String => String}] same url as above, but better
# @param destdir [String] chroot (with crazy juggling)
#
# @return [Boolean] true on success
def get_file_from_url(scheme:, host:, urlpath:, localfile:,
urltok:, destdir:)
# adapt sane API to legacy implementation
_Scheme = scheme
_Host = host
_Path = urlpath
_Localfile = localfile
initialize_file_from_url
@GET_error = ""
ok = false
res = {}
# ensure both sets of parameters are in sync
toks = deep_copy(urltok)
toks["scheme"] = _Scheme
toks["host"] = _Host
toks["path"] = _Path
Builtins.y2milestone(
"Scheme:%1 Host:%2 Path:%3 Localfile:%4",
_Scheme,
_Host,
_Path,
_Localfile
)
log.info "toks initial: #{hide_password(toks).inspect}"
if _Scheme == "repo"
base_url = InstURL.installInf2Url("")
if base_url.empty?
log.err "no ZyppRepoURL in /etc/install.inf"
return false
end
log.info("installation path from install.inf: #{URL.HidePassword(base_url)}")
toks["scheme"] = "relurl"
rel_url = URL.Build(toks)
log.info("relative url: #{rel_url}")
absolute_url = Yast2::RelURL.from_installation_repository(rel_url).absolute_url.to_s
log.info("absolute url: #{URL.HidePassword(absolute_url)}")
toks = URL.Parse(absolute_url)
log.info "toks absolute: #{hide_password(toks).inspect}"
end
# convert 'cd', "dvd', and 'hd' Zypp schemes to 'device' schema
if ["cd", "dvd", "hd"].include?(toks["scheme"])
dev_name = toks["query"].match(/devices?=\/dev\/(.*)/)
if !dev_name.nil?
toks["scheme"] = "device"
toks["host"] = dev_name[1]
toks["query"] = ""
end
end
# convert 'label' scheme to 'device' scheme
if toks["scheme"] == "label"
toks["scheme"] = "device"
toks["host"] = "disk/by-label/#{toks["host"]}"
end
_Scheme = toks["scheme"]
_Host = toks["host"]
_Path = toks["path"]
if Builtins.regexpsub(_Path, "(.*)//(.*)", "\\1/\\2") != nil
_Path = Builtins.regexpsub(_Path, "(.*)//(.*)", "\\1/\\2")
log.info "path changed from #{toks["path"]} to #{_Path}"
end
toks["path"] = _Path
log.info "toks final: #{hide_password(toks).inspect}"
# URL.Build does not reconstruct the URL in all cases; notably it has
# some ideas about what the host part might look like - which conflicts
# with the host part being used for device names in local disk URIs.
#
# It does not matter much as full_url is only used for ftp/http(s).
full_url = URL.Build(toks)
log.info("full url (host part might be missing): #{URL.HidePassword(full_url)}")
tmp_dir = Convert.to_string(WFM.Read(path(".local.tmpdir"), []))
mount_point = Ops.add(tmp_dir, "/tmp_mount")
mp_in_local = mount_point
chr = WFM.SCRGetName(WFM.SCRGetDefault)
if Builtins.search(chr, "chroot=/mnt:") == 0
mp_in_local = Ops.add(destdir, mount_point)
end
Builtins.y2milestone("Chr:%3 TmpDir:%1 Mp:%2", tmp_dir, mp_in_local, chr)
WFM.Execute(path(".local.mkdir"), mp_in_local)
if _Scheme == "http" || _Scheme == "https"
HTTP.easySSL(true)
if Ops.greater_than(
SCR.Read(
path(".target.size"),
"/etc/ssl/clientcerts/client-cert.pem"
),
0
)
HTTP.clientCertSSL("/etc/ssl/clientcerts/client-cert.pem")
end
if Ops.greater_than(
SCR.Read(
path(".target.size"),
"/etc/ssl/clientcerts/client-key.pem"
),
0
)
HTTP.clientKeySSL("/etc/ssl/clientcerts/client-key.pem")
end
res = HTTP.Get(full_url, _Localfile)
if Ops.get_integer(res, "code", 0) == 200
@GET_error = ""
return true
else
Builtins.y2error("Can't find URL: %1", full_url)
# autoyast tried to read a file but had no success.
@GET_error = Builtins.sformat(
_(
"Cannot find URL '%1' via protocol HTTP(S). Server returned code %2."
),
full_url,
Ops.get_integer(res, "code", 0)
)
return false
end
end
if _Scheme == "ftp"
res = FTP.Get(full_url, _Localfile)
if Ops.greater_or_equal(Ops.get_integer(res, "code", -1), 200) &&
Ops.less_than(Ops.get_integer(res, "code", -1), 300) &&
Ops.greater_than(SCR.Read(path(".target.size"), _Localfile), 0)
@GET_error = ""
return true
else
Builtins.y2error("Can't find URL: %1", full_url)
# autoyast tried to read a file but had no success.
@GET_error = Builtins.sformat(
_("Cannot find URL '%1' via protocol FTP. Server returned code %2."),
full_url,
Ops.get_integer(res, "code", 0)
)
return false
end
elsif _Scheme == "file"
file = Builtins.sformat("%1/%2", Installation.sourcedir, _Path) # FIXME: I have doubts this will ever work. Too early.
if Ops.greater_than(SCR.Read(path(".target.size"), file), 0)
copy_local_file(file, _Localfile)
else
@GET_error = Ops.add(
@GET_error,
Builtins.sformat(
_("Reading file on %1/%2 failed.\n"),
Installation.sourcedir,
_Path
)
)
copy_local_file(_Path, _Localfile)
end
if File.exist?(_Localfile)
@GET_error = ""
ok = true
else
@GET_error = Ops.add(
@GET_error,
Builtins.sformat(_("Reading file on %1 failed.\n"), _Path)
)
Builtins.y2milestone(
"Trying to find file on installation media: %1",
Installation.boot
)
# The Cdrom entry in install.inf is obsolete. So we are using the
# entry which is defined in InstUrl module. (bnc#908271)
install_url = InstURL.installInf2Url("")
# Builtins.regexpsub can also return nil (bnc#959723)
cdrom_device = install_url ? (Builtins.regexpsub(install_url, "devices=(.*)$", "\\1") || "") : ""
if Installation.boot == "cd" && !cdrom_device.empty?
mtab = File.read("/proc/mounts")
m = mtab.match(/^#{cdrom_device}\s+(\S+)/)
if m
Builtins.y2milestone(
"%1 is already mounted, trying to bind mount...",
cdrom_device
)
am1 = Convert.to_map(
SCR.Execute(path(".target.bash_output"),
"/bin/mount -v --bind #{m[1]} #{mount_point}")
)
if Ops.get_integer(am1, "exit", -1) == 0
ok = true
else
Builtins.y2warning(
"can't bind mount %1 failing...",
cdrom_device
)
ok = false
end
else
try_again = 10
while Ops.greater_than(try_again, 0)
if !Convert.to_boolean(
WFM.Execute(
path(".local.mount"),
[cdrom_device, mount_point, Installation.mountlog]
)
)
# autoyast tried to mount the CD but had no success.
@GET_error = Ops.add(
@GET_error,
Builtins.sformat(_("Mounting %1 failed."), cdrom_device)
)
Builtins.y2warning("Mount failed")
ok = false
try_again = Ops.subtract(try_again, 1)
Builtins.sleep(3000)
else
ok = true
try_again = 0
end
end
end
if ok
copy_local_file(File.join(mount_point, _Path), _Localfile)
WFM.Execute(path(".local.umount"), mount_point)
if File.exist?(_Localfile)
@GET_error = ""
return true
end
end
end
# autoyast tried to read a file but had no success.
@GET_error = Ops.add(
@GET_error,
Builtins.sformat(
_("Reading a file on CD failed. Path: %1/%2."),
mount_point,
_Path
)
)
ok = false
end
elsif _Scheme == "nfs" # NFS
nfs_host = find_nfs_host(_Host)
if !Convert.to_boolean(
SCR.Execute(
path(".target.mount"),
[Ops.add(Ops.add(nfs_host, ":"), dirname(_Path)), mount_point],
"-o noatime,nolock"
)
) &&
!Convert.to_boolean(
SCR.Execute(
path(".target.mount"),
[Ops.add(Ops.add(nfs_host, ":"), dirname(_Path)), mount_point],
"-o noatime -t nfs4"
)
)
Builtins.y2warning("Mount failed")
# autoyast tried to mount a NFS directory which failed
@GET_error = Builtins.sformat(
_("Mounting %1 failed."),
Ops.add(Ops.add(_Host, ":"), dirname(_Path))
)
return false
end
copyCmd = Ops.add(
Ops.add(
Ops.add(
Ops.add(Ops.add("/bin/cp ", mp_in_local), "/"),
basename(_Path)
),
" "
),
_Localfile
)
Builtins.y2milestone("Copy Command: %1", copyCmd)
if WFM.Execute(path(".local.bash"), copyCmd) == 0
@GET_error = ""
ok = true
else
# autoyast tried to copy a file via NFS which failed
@GET_error = Builtins.sformat(
_("Remote file %1 cannot be retrieved"),
Ops.add(Ops.add(mount_point, "/"), basename(_Path))
)
Builtins.y2error(
"remote file %1 can't be retrieved",
Ops.add(Ops.add(mount_point, "/"), basename(_Path))
)
end
SCR.Execute(path(".target.umount"), mount_point)
elsif _Scheme == "cifs" # CIFS
if !Convert.to_boolean(
SCR.Execute(
path(".target.mount"),
[Ops.add(Ops.add("//", _Host), dirname(_Path)), mount_point],
"-t cifs -o guest,ro,noatime"
)
)
Builtins.y2warning("Mount failed")
# autoyast tried to mount a NFS directory which failed
@GET_error = Builtins.sformat(
_("Mounting %1 failed."),
Ops.add(Ops.add("//", _Host), dirname(_Path))
)
return false
end
copyCmd = Ops.add(
Ops.add(
Ops.add(
Ops.add(Ops.add("/bin/cp ", mp_in_local), "/"),
basename(_Path)
),
" "
),
_Localfile
)
Builtins.y2milestone("Copy Command: %1", copyCmd)
if WFM.Execute(path(".local.bash"), copyCmd) == 0
@GET_error = ""
ok = true
else
# autoyast tried to copy a file via NFS which failed
@GET_error = Builtins.sformat(
_("Remote file %1 cannot be retrieved"),
Ops.add(Ops.add(mount_point, "/"), basename(_Path))
)
Builtins.y2error(
"remote file %1 can't be retrieved",
Ops.add(Ops.add(mount_point, "/"), basename(_Path))
)
end
SCR.Execute(path(".target.umount"), mount_point)
elsif _Scheme == "floppy"
@GET_error = _("URLs starting with 'floppy:/' are not longer supported")
ok = false
elsif _Scheme == "device" || _Scheme == "usb" # Device or USB
if _Path != ""
deviceList = []
if _Host == ""
disks = _Scheme == "device" ?
Convert.convert(
SCR.Read(path(".probe.disk")),
from: "any",
to: "list <map>"
) :
Convert.convert(
SCR.Read(path(".probe.usb")),
from: "any",
to: "list <map>"
)
Builtins.foreach(disks) do |m|
if _Scheme == "usb" && Ops.get_string(m, "bus", "USB") != "SCSI"
next
end
if Builtins.haskey(m, "dev_name")
i = 0
dev = Ops.get_string(m, "dev_name", "")
deviceList = Builtins.add(
deviceList,
Builtins.substring(dev, 5)
)
begin
i = Ops.add(i, 1)
dev = Ops.add(
Ops.get_string(m, "dev_name", ""),
Builtins.sformat("%1", i)
)
if SCR.Read(path(".target.lstat"), dev) != {}
deviceList = Builtins.add(
deviceList,
Builtins.substring(dev, 5)
)
end
end while SCR.Read(path(".target.lstat"), dev) != {} ||
Ops.less_than(i, 5) # not uncommon for USB sticks to have no partition
end
end
Builtins.y2milestone("devices to look on: %1", deviceList)
else
# sometimes we have devices like /dev/cciss/c1d0p5
# those "nested" devices will be catched here
# as long as we find a directory where we expect a device,
# we cut down the Path and enhance the Host (device name)
while SCR.Read(path(".target.dir"), Ops.add("/dev/", _Host)) != nil
Builtins.y2milestone("nested device found")
l = Builtins.splitstring(_Path, "/")
_Host = Ops.add(Ops.add(_Host, "/"), Ops.get(l, 0, ""))
l = Builtins.remove(l, 0)
_Path = Builtins.mergestring(l, "/")
Builtins.y2milestone("Host=%1 Path=%2", _Host, _Path)
end
# catching nested devices done
deviceList = [_Host]
end
Builtins.foreach(deviceList) do |_Host2|
Builtins.y2milestone("looking for profile on %1", _Host2)
# checking if device has already been mounted. Taking new mountpoint
# if it has already been done.
# storage-ng: This should be move to storage-ng
findmnt = ["/usr/bin/findmnt", "--first-only", "--noheadings", "--output=target", "/dev/#{_Host2}"]
mp = Yast::Execute.locally!.stdout(*findmnt).split.last
already_mounted = !mp.nil?
mount_point = mp if already_mounted
Builtins.y2milestone(
"already mounted=%1 mountpoint=%2 mp=%3",
already_mounted,
mount_point,
mp
)
if !already_mounted &&
!Convert.to_boolean(
SCR.Execute(
path(".target.mount"),
[Builtins.sformat("/dev/%1", _Host2), mount_point],
"-o noatime"
)
)
Builtins.y2milestone(
"%1 is not mounted and mount failed",
Builtins.sformat("/dev/%1", _Host2)
)
@GET_error = Builtins.sformat(
_("%1 is not mounted and mount failed"),
Builtins.sformat("/dev/%1", _Host2)
)
next
end
if WFM.Execute(
path(".local.bash"),
Ops.add(
Ops.add(
Ops.add(
Ops.add(Ops.add("/bin/cp ", mount_point), "/"),
_Path
),
" "
),
_Localfile
)
) != 0
# autoyast tried to copy a file but that file can't be found
@GET_error = Builtins.sformat(
_("File %1 cannot be found"),
Ops.add(mount_point, _Path)
)
Builtins.y2milestone(
"file %1 can't be found",
Ops.add(mount_point, _Path)
)
else
@GET_error = ""
ok = true
Builtins.y2milestone("found")
end
WFM.Execute(path(".local.umount"), mount_point) if !already_mounted
raise Break if ok == true
end
end
elsif _Scheme == "tftp" # Device
if TFTP.Get(_Host, _Path, _Localfile)
@GET_error = ""
ok = true
else
@GET_error = Builtins.sformat(
_("Cannot find URL '%1' via protocol TFTP."),
Ops.add(Ops.add(_Host, ":"), _Path)
)
Builtins.y2error("file %1 can't be found", _Path)
end
else
# the user wanted autoyast to fetch it's profile via an unknown protocol
@GET_error = Builtins.sformat(_("Unknown protocol %1."), _Scheme)
Builtins.y2error("Protocol not supported")
ok = false
end
if !Builtins.isempty(@GET_error)
Builtins.y2warning("GET_error:%1", @GET_error)
end
ok
end
private
# Copy a file
#
# @param source [String] Source file path
# @param destination [String] Destination file path
def copy_local_file(source, destination)
log.info "Copying #{source} to #{destination}"
::FileUtils.cp(source, destination)
rescue SystemCallError => e
log.warn "Could not copy #{source} to #{destination}: #{e.inspect}"
end
# Replace password with 'PASSWORD', if one was set.
#
# This is used to keep logs clean.
#
# @param toks [Hash{String => String}]
#
# @return [Hash{String => String}]
def hide_password(toks)
tmp = toks.dup
tmp["pass"] &&= "PASSWORD"
tmp
end
# Determines the host to use when trying to mount an NFS volume.
#
# IPv6 addresses should be enclosed between square brackets.
#
# @param host [String] Hostname or IP address.
def find_nfs_host(host)
ip = IPAddr.new(host)
ip.ipv6? ? "[#{host}]" : host
rescue IPAddr::InvalidAddressError
host
end
end
end