library/packages/src/lib/y2packager/release_notes_fetchers/url.rb
# ------------------------------------------------------------------------------
# Copyright (c) 2017 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.
# ------------------------------------------------------------------------------
require "yast"
require "y2packager/release_notes"
require "y2packager/release_notes_fetchers/base"
require "tmpdir"
Yast.import "Pkg"
Yast.import "String"
module Y2Packager
module ReleaseNotesFetchers
# This class reads release notes from the relnotes_url product property
#
# The code was extracted from the old version of the InstDownloadReleaseNotesClient
# and adapted. See https://github.com/yast/yast-installation/blob/62596684d6de242667a0957765c85712874e77ef/src/lib/installation/clients/inst_download_release_notes.rb
#
# @see Base
class Url < Base
class << self
# Enable downloading release notes
#
# This method is intended to be used during testing.
def enable!
@enabled = true
end
# Disable downloading release notes due to communication errors
def disable!
@enabled = false
end
# Determine if release notes would be downloaded
#
# @return [Boolean]
# @see disable!
def enabled?
return true if @enabled.nil?
@enabled
end
# Blacklist of URLs that failed to download and should not be retried
#
# @return [Array<String>] List of URLs
def blacklist
@blacklist ||= []
end
# Add an URL to the blacklist
#
# @param url [String] URL
def add_to_blacklist(url)
blacklist << url
end
# Determine whether an URL is blacklisted or not
#
# @return [Boolean]
def blacklisted?(url)
blacklist.include?(url)
end
# Clear products blackist
def clear_blacklist
blacklist.clear
end
end
# When cURL returns one of those codes, the download won't be retried
# @see man curl
CURL_GIVE_UP_RETURN_CODES = {
5 => "Couldn't resolve proxy.",
6 => "Couldn't resolve host.",
7 => "Failed to connect to host.",
28 => "Operation timeout."
}.freeze
# Get release notes for the given product
#
# Release notes are downloaded and extracted to work directory. When
# release notes for a language "xx_XX" are not found, it will fallback to
# "xx".
#
# @param prefs [ReleaseNotesContentPrefs] Content preferences
# @return [String,nil] Release notes or nil if a release notes were not found
# (no package providing release notes or notes not found in the package)
def release_notes(prefs)
if !self.class.enabled?
log.info("Skipping release notes download due to previous download issues")
return nil
end
if self.class.blacklisted?(relnotes_url)
log.info("Skipping release notes download for #{product.name} due to previous issues")
return nil
end
if !relnotes_url_valid?
log.warn("Skipping release notes download for #{product.name}: '#{relnotes_url}'")
return nil
end
relnotes = fetch_release_notes(prefs)
if relnotes
log.info "Got release notes for #{product.name} from URL #{relnotes_url} " \
"with #{prefs}"
return relnotes
end
log.warn("Release notes for product #{product.name} not found. " \
"Blacklisting #{relnotes_url}...")
self.class.add_to_blacklist(relnotes_url)
nil
end
# Return release notes latest version identifier
#
# Release notes that lives in relnotes_url are considered always to be the
# latest version, hence this method always returns :latest.
#
# @return [:latest]
def latest_version
:latest
end
private
# Return the release notes instance
#
# It relies on #release_notes_content to get release notes content.
#
# @param prefs [ReleaseNotesContentPrefs] Content preferences
# @return [ReleaseNotes,nil] Release notes or nil if a release notes were not found
# @see release_notes_content
def fetch_release_notes(prefs)
content, lang = release_notes_content(prefs)
return nil if content.nil?
Y2Packager::ReleaseNotes.new(
product_name: product.name,
content: content,
user_lang: prefs.user_lang,
lang: lang,
format: prefs.format,
version: :latest
)
end
# Search for release notes content
#
# @param prefs [ReleaseNotesContentPrefs] Content preferences
# @return [String,nil] Return release notes content or nil if it release
# notes were not found
def release_notes_content(prefs)
langs = [prefs.user_lang]
langs << prefs.user_lang.split("_", 2).first if prefs.user_lang.include?("_")
langs << prefs.fallback_lang
langs.uniq.each do |lang|
content = release_notes_content_for_lang_and_format(lang, prefs.format)
return [content, lang] if content
end
nil
end
# Return release notes content for a given language and format
#
# @return [String,nil] Return release notes content or nil if it release
def release_notes_content_for_lang_and_format(lang, format)
# If there is an index and the language is not indexed
return nil unless release_notes_index.empty? || indexed_release_notes_for?(lang, format)
# Where we want to store the downloaded release notes
tmpfile = Tempfile.new("relnotes-")
return nil unless curl_download(release_notes_file_url(lang, format), tmpfile.path)
log.info("Release notes downloaded successfully")
File.read(tmpfile.path)
ensure
if tmpfile
tmpfile.close
tmpfile.unlink
end
end
# Determine whether the relnotes URL is valid
#
# @return [Boolean]
def relnotes_url_valid?
if relnotes_url.nil?
log.error "No release notes URL for #{product.name}"
return false
end
if relnotes_url.rindex("/").nil?
log.error "Broken URL for release notes: #{relnotes_url}"
return false
end
true
end
# Return release notes URL from libzypp
#
# @return [String] Release notes URL
def relnotes_url
@relnotes_url ||= product.relnotes_url
end
# Return release notes URL
#
# @return [String] Release notes full URL
# @see #release_notes_filename
def release_notes_file_url(user_lang, format)
File.join(relnotes_url_base, release_notes_file(user_lang, format))
end
# Return release notes base URL
#
# @return [String] Release notes base URL
def relnotes_url_base
return @relnotes_url_base if @relnotes_url_base
pos = relnotes_url.rindex("/")
@relnotes_url_base = relnotes_url[0, pos]
end
# Return release notes filename including language and format
#
# @return [String] Release notes filename
def release_notes_file(user_lang, format)
"RELEASE-NOTES.#{user_lang}.#{format}"
end
# curl proxy options
#
# @return [String] to be interpolated in a .target.bash command, unquoted
def curl_proxy_args
# Import the module when it is needed to avoid building projects (bsc#1079045)
Yast.import "Proxy"
return @curl_proxy_args if @curl_proxy_args
@curl_proxy_args = ""
# Proxy should be set by inst_install_inf if set via Linuxrc
Yast::Proxy.Read
return @curl_proxy_args unless Yast::Proxy.enabled
# Test if proxy works
#
# It is enough to test http proxy, release notes are downloaded via http
proxy_ret = Yast::Proxy.RunTestProxy(
Yast::Proxy.http,
"",
"",
Yast::Proxy.user,
Yast::Proxy.pass
)
http_ret = proxy_ret.fetch("HTTP", {})
proxy_ok = http_ret.fetch("tested", true) == true && http_ret.fetch("exit", 1) == 0
return @curl_proxy_args unless proxy_ok
user_pass = (Yast::Proxy.user != "") ? "#{Yast::Proxy.user}:#{Yast::Proxy.pass}" : ""
@curl_proxy_args = "--proxy #{Yast::Proxy.http}"
@curl_proxy_args << " --proxy-user '#{user_pass}'" unless user_pass.empty?
@curl_proxy_args
end
# Release notes index for the given product
#
# @return [Array<String>] Index containing the list of release notes files
# @see #download_release_notes_index
def release_notes_index
return @release_notes_index if @release_notes_index
@release_notes_index = download_release_notes_index(relnotes_url_base) || []
end
# Determine whether the release notes index contains an entry for the given
# language and format
#
# @return [Boolean]
def indexed_release_notes_for?(user_lang, format)
release_notes_index.include?(release_notes_file(user_lang, format))
end
# Download of index of release notes for a specific product
#
# @param url_base URL pointing to directory with the index
# @return [Array<String>,nil] filenames, nil if not found
def download_release_notes_index(url_base)
url_index = url_base + "/directory.yast"
log.info("Index with available files: #{url_index}")
tmpfile = Tempfile.new("directory.yast-")
# download the index with much shorter time-out
ok = curl_download(url_index, tmpfile.path, max_time: 30)
if ok
log.info("Release notes index downloaded successfully")
index_file = File.read(tmpfile.path)
if index_file.nil? || index_file.empty?
log.info("Release notes index empty, not filtering further downloads")
nil
else
rn_index = index_file.split("\n")
log.info("Index of RN files at the server: #{rn_index}")
rn_index
end
elsif ok.nil?
nil
else
log.info "Downloading index failed, trying all files according to selected language"
nil
end
ensure
tmpfile.close
tmpfile.unlink
end
# Download *url* to *filename*
#
# May disable release notes downloading by calling .disable!.
#
# @return [Boolean,nil] true: success, false: failure, nil: failure+dont retry
def curl_download(url, filename, max_time: 300)
return nil unless self.class.enabled?
cmd = Yast::Builtins.sformat(
"/usr/bin/curl --location --verbose --fail --max-time %6 " \
"--connect-timeout 15 %1 '%2' --output '%3' > '%4/%5' 2>&1",
curl_proxy_args,
url,
Yast::String.Quote(filename),
Yast::String.Quote(Yast::Directory.logdir),
"curl_log",
max_time
)
ret = Yast::SCR.Execute(Yast::Path.new(".target.bash"), cmd)
log.info("#{cmd.sub(curl_proxy_args, "--proxy-user @PROXYPASSWORD@")} returned #{ret}")
reason = CURL_GIVE_UP_RETURN_CODES[ret]
if reason
log.info "Communication with server failed (#{reason}), skipping further attempts."
self.class.disable!
return nil
end
ret == 0
end
end
end
end