ACC SHELL
/**
* File:
* AddOnProduct.ycp
*
* Module:
* AddOnProduct
*
* Summary:
* This module provides integration of the add-on products
*
* Authors:
* Jiri Srain <jsrain@suse.de>
* Lukas Ocilka <locilka@suse.cz>
*/
{
module "AddOnProduct";
// IMPORTANT: maintainer of yast2-add-on is responsible for this module
textdomain "packager";
import "Label";
import "Mode";
import "ProductControl";
import "ProductFeatures";
import "Report";
import "XML";
import "Wizard";
import "FileUtils";
import "Language";
import "Popup";
import "InstShowInfo";
import "ProductLicense";
import "FileUtils";
import "Directory";
import "String";
import "WorkflowManager";
import "URL";
import "Mode";
import "Stage";
import "Icon";
import "PackageCallbacks";
import "PackagesProposal";
// variables for installation with product
/**
* ID for cache in the inst-sys
*/
integer src_cache_id = -1;
/**
* System proposals have already been prepared for merging?
*/
boolean system_proposals_prepared = false;
/**
* System workflows have already been prepared for merging?
*/
boolean system_workflows_prepared = false;
/**
* List of all selected repositories
*
* @struct add_on_products = [
* $[
* "media" : 4, // ID of the source
* "product_dir" : "/",
* "product" : "openSUSE version XX.Y",
* "autoyast_product" : "'PRODUCT' tag for AutoYaST Export",
* ],
* ...
* ]
*/
global list<map<string,any> > add_on_products = [];
/**
* ID of currently added repository for the add-on product
*/
global integer src_id = nil;
// for the add-on product workflow - needed for dialog skipping
/**
* return value of last step in the product adding workflow
*/
global symbol last_ret = nil;
global boolean modified = false;
global list <integer> mode_config_sources = [];
global map<string,any> current_addon = $[];
// Bugzilla #239630
// In installation: check for low-memory machines
global boolean low_memory_already_reported = false;
// Bugzilla #305554
// Both online-repositories and add-ons use the same function and variable
// if true, both are skipped at once without asking
global boolean skip_add_ons = false;
/**
* @struct $["src_id|media|filename" : "/path/to/the/file"]
*/
map <string, string> source_file_cache = $[];
string filecachedir = sformat ("%1/AddOns_CacheDir/", Directory::tmpdir);
integer filecachecounter = -1;
/**
* Downloads a requested file, caches it and returns path to that cached file.
* If a file is alerady cached, just returns the path to a cached file.
* Parameter 'sod' defines whether a file is 'signed' (file + file.asc) or 'digested'
* (file digest mentioned in signed content file).
*
* @param integer src_id
* @param integer media
* @param string filename
* @param string sod ("signed" or "digested")
* @param boolean optional (false if mandatory)
* @return string path to a cached file
*
* @example:
* // content file is usually signed with content.asc
* AddOnProduct::GetCachedFileFromSource (8, 1, "/content", "signed", false);
* // the other files are usually digested in content file
* AddOnProduct::GetCachedFileFromSource (8, 1, "/images/images.xml", "digested", true);
*/
global string GetCachedFileFromSource (integer src_id, integer media, string filename, string sod, boolean optional) {
// BNC #486785: Jukebox when using more physical media-based Add-Ons at once
string file_ID = sformat ("%1|%2|%3", src_id, media, filename);
string provided_file = source_file_cache[file_ID]:"";
if (provided_file != nil && provided_file != "") {
// Checking whether the cached file exists
if (FileUtils::Exists (provided_file)) {
y2milestone ("File %1 found in cache: %2", file_ID, provided_file);
return provided_file;
} else {
y2warning ("Cached file %1 not accessible!", provided_file);
source_file_cache = remove (source_file_cache, file_ID);
}
}
if (optional == nil) optional = true;
if (sod == "signed") {
provided_file = Pkg::SourceProvideSignedFile (src_id, media, filename, optional);
} else if (sod == "digested") {
provided_file = Pkg::SourceProvideDigestedFile (src_id, media, filename, optional);
} else {
y2error ("Unknown SoD: %1. It can be only 'signed' or 'digested'", sod);
provided_file = nil;
}
// A file has been found, caching...
if (provided_file != nil) {
filecachecounter = filecachecounter + 1;
// Where the file is finally cached
string cached_file = sformat ("%1%2", filecachedir, filecachecounter);
string cmd = sformat (
"/bin/mkdir -p '%1'; /bin/cp '%2' '%3'",
String::Quote (filecachedir), String::Quote (provided_file), String::Quote (cached_file)
);
map cmd_run = (map) SCR::Execute (.target.bash_output, cmd);
// Unable to cache a file, the original file will be returned
if (cmd_run["exit"]:-1 != 0) {
y2warning ("Error caching file: %1: %2", cmd, cmd_run);
} else {
y2milestone ("File %1 cached as %2", file_ID, cached_file);
// Writes entry into cache database
source_file_cache[file_ID] = cached_file;
// Path to a cached file will be returned
provided_file = cached_file;
}
}
return provided_file;
}
// Which part installation.xml will be used
string _inst_mode = "installation";
/**
* Returns the current add-on installation mode.
*
* @return string current mode
* @see SetMode()
*/
string GetMode () {
return _inst_mode;
}
/**
* Sets internal add-on installation mode to either "installation" or "update".
* Mode is used later when deciding which part of the installation.xml to use.
*
* @param string new_mode ("installation" or "update")
* @see GetMode();
*/
void SetMode (string new_mode) {
if (new_mode == nil || ! contains (["installation", "update"], new_mode)) {
y2error ("Wrong Add-On mode: %1", new_mode);
}
_inst_mode = new_mode;
}
/**
* Returns whether add-on product got as parameter (source id)
* replaces some already installed add-on or whether it is a new
* installation. Repositories and target have to be initialized.
*
* @param integer source_id
* @param string "installation" or "update" according the current state
*/
global string AddOnMode (integer source_id) {
list <map <string, any> > all_products = Pkg::ResolvableProperties ("", `product, "");
map <string, any> check_add_on = $[];
// Search for an add-on using source ID
foreach (map <string, any> one_product, all_products, {
if (one_product["source"]:-1 == source_id) {
check_add_on = one_product;
break;
}
});
string ret = "installation";
list <symbol> supported_statuses = [`installed, `selected];
boolean already_found = false;
// Found the
if (check_add_on != $[] && haskey (check_add_on, "replaces")) {
list <map <string, any> > product_replaces = (list <map <string, any> >) check_add_on["replaces"]:[];
// Run through through all products that the add-on can replace
foreach (map <string, any> one_replaces, product_replaces, {
if (already_found)
break;
// Run through all installed (or selected) products
foreach (map <string, any> one_product, all_products, {
// checking the status
if (! contains (supported_statuses, one_product["status"]:`unknown))
continue;
// ignore itself
if (one_product["source"]:-42 == source_id)
continue;
// check name to replace
if (one_product["name"]:"-A-" != one_replaces["name"]:"-B-")
continue;
// check version to replace
if (one_product["version"]:"-A-" != one_replaces["version"]:"-B-")
continue;
// check version to replace
if (one_product["arch"]:"-A-" != one_replaces["arch"]:"-B-")
continue;
y2milestone ("Found product matching update criteria: %1 -> %2", one_product, check_add_on);
ret = "update";
already_found = true;
break;
});
});
}
return ret;
}
// --> FATE #302123: Allow relative paths in "add_on_products" file
string base_product_url = nil;
global void SetBaseProductURL (string url) {
if (url == "" || url == nil)
y2warning ("Empty base url");
base_product_url = url;
y2milestone ("New base URL: %1", URL::HidePassword(base_product_url));
}
global string GetBaseProductURL () {
return base_product_url;
}
/**
* Returns an absolute URL from base + relative url.
* Relative URL needs to start with 'reulrl://' othewise
* it is not considered being relative and it's returned
* as it is (just the relative_url parameter).
*
* @param string base_url
* @param string relative_url
* @return string absolute_url
*
* @example
* AddOnProduct::GetAbsoluteURL (
* "http://www.example.org/some%20dir/another%20dir",
* "relurl://../AnotherProduct/"
* ) -> "http://www.example.org/some%20dir/AnotherProduct/"
* AddOnProduct::GetAbsoluteURL (
* "username:password@ftp://www.example.org/dir/",
* "relurl://./Product_CD1/"
* ) -> "username:password@ftp://www.example.org/dir/Product_CD1/"
*/
global string GetAbsoluteURL (string base_url, string url) {
if (! regexpmatch (url, "^relurl://")) {
y2debug ("Not a relative URL: %1", URL::HidePassword(url));
return url;
}
if (base_url == nil || base_url == "") {
y2error ("No base_url defined");
return url;
}
// bugzilla #306670
integer base_params_pos = search (base_url, "?");
string base_params = "";
if (base_params_pos != nil && base_params_pos >= 0) {
base_params = substring (base_url, (base_params_pos + 1));
base_url = substring (base_url, 0, base_params_pos);
}
integer added_params_pos = search (url, "?");
string added_params = "";
if (added_params_pos != nil && added_params_pos >= 0) {
added_params = substring (url, (added_params_pos + 1));
url = substring (url, 0, added_params_pos);
}
if (! regexpmatch (base_url, "/$")) {
base_url = base_url + "/";
}
y2milestone ("Merging '%1' (params '%2') to '%3' (params '%4')", url, added_params, base_url, base_params);
url = regexpsub (url, "^relurl://(.*)$", "\\1");
url = sformat ("%1%2", base_url, url);
// merge /something/../
integer max_count = 100;
while (max_count > 0 && regexpmatch (url, "(.*/)[^/]+/+\\.\\./")) {
max_count = max_count - 1;
list <integer> str_offset_l = (list <integer>) regexppos (url, "/\\.\\./");
integer str_offset = str_offset_l[0]:nil;
if (str_offset != nil && str_offset > 0) {
string stringfirst = substring (url, 0, str_offset);
string stringsecond = substring (url, str_offset);
y2debug ("Pos: %1 First: >%2< Second: >%3<", str_offset, stringfirst, stringsecond);
stringfirst = regexpsub (stringfirst, "^(.*/)[^/]+/*$", "\\1");
stringsecond = regexpsub (stringsecond, "^/\\.\\./(.*)$", "\\1");
url = stringfirst + stringsecond;
}
}
// remove /./
max_count = 100;
while (max_count > 0 && regexpmatch (url, "/\\./")) {
max_count = max_count - 1;
url = regexpsub (url, "^(.*)/\\./(.*)", "\\1/\\2");
}
map <string, string> base_params_map = URL::MakeMapFromParams (base_params);
map <string, string> added_params_map = URL::MakeMapFromParams (added_params);
map <string, string> final_params_map = (map <string, string>) union (base_params_map, added_params_map);
if (size (final_params_map) > 0) {
y2milestone ("%1 merge %2 -> %3", base_params_map, added_params_map, final_params_map);
url = url + "?" + URL::MakeParamsFromMap (final_params_map);
}
y2milestone ("Final URL: '%1'", URL::HidePassword(url));
return url;
}
// <--
/**
* Adapts the inst-sys from the tarball
* @param filename string the filename with the tarball to use to the update
* @return boolean true on success
*/
global boolean UpdateInstSys (string filename) {
src_cache_id = src_cache_id + 1;
string tmpdir = (string)SCR::Read (.target.tmpdir);
tmpdir = sformat ("%1/%2", tmpdir, src_cache_id);
map out = (map)SCR::Execute (.target.bash_output, sformat ("
/bin/mkdir %1;
cd %1;
/bin/tar -xvf %2;
/sbin/adddir %1 /;
", tmpdir, filename));
if (out["exit"]:0 != 0)
{
y2error ("Including installation image failed: %1", out);
return false;
}
y2milestone ("Including installation image succeeded");
return true;
}
/**
* New add-on product might add also new agents.
* Functions Rereads all available agents.
*
* @see bugzilla #239055, #245508
*/
global void RereadAllSCRAgents () {
y2milestone ("Registering new agents...");
boolean ret = (boolean) SCR::RegisterNewAgents();
if (ret) {
y2milestone ("Successful");
} else {
y2error ("Error occured during registering new agents!");
Report::Error (_("An error occurred while preparing the installation system."));
}
}
/**
* Remove the /y2update directory from the system
*/
void CleanY2Update() {
SCR::Execute (.target.bash, "/bin/rm -rf /y2update");
}
/**
* Show /media.1/info.txt file in a pop-up message if such file exists.
* Show license if such exists and return whether users accepts it.
* Returns 'nil' when did not succed.
*
* @return boolean whether the license has been accepted
*/
global boolean AcceptedLicenseAndInfoFile (integer src_id) {
symbol ret = ProductLicense::AskAddOnLicenseAgreement (src_id);
if (ret == nil)
return nil;
else if (ret == `abort || ret == `back)
{
y2milestone ("License confirmation failed");
return false;
}
return true;
}
boolean AnyPatternInRepo()
{
list<map> patterns = Pkg::ResolvableProperties("", `pattern, "");
y2milestone("Total number of patterns: %1", size(patterns));
patterns = filter(map pat, patterns,
{
return (pat["source"]:nil == src_id);
}
);
y2milestone("Found %1 add-on patterns", size(patterns));
y2debug("Found add-on patterns: %1", patterns);
return size(patterns) > 0;
}
symbol DoInstall_NoControlFile () {
y2milestone("File /installation.xml not found, running sw_single for this repository");
// display pattern the dialog when there is a pattern provided by the addon
// otherwise use search mode
symbol mode = AnyPatternInRepo() ? `patternSelector : `searchMode;
// enable repository management if not in installation mode
boolean enable_repo_management = Mode::normal();
map args = $[ "dialog_type" : mode, "repo_mgmt" : enable_repo_management ];
y2milestone ("Arguments for sw_single: %1", args);
any ret = WFM::CallFunction ("sw_single", [args]);
y2milestone ("sw_single returned: %1", ret);
if (ret == `abort || ret == `cancel || ret == `close)
return `abort;
return `register;
}
boolean IntegrateY2Update (integer src_id) {
string binaries = GetCachedFileFromSource (src_id, 1, "/y2update.tgz", "digested", true /* optional */);
// File /y2update.tgz exists
if (binaries != nil)
{
// Try to extract files from the archive
map out = (map)SCR::Execute (.target.bash_output, sformat ("
test -d /y2update && rm -rf /y2update;
/bin/mkdir -p /y2update/all;
cd /y2update/all;
/bin/tar -xvf %1;
cd /y2update;
ln -s all/usr/share/YaST2/* .;
ln -s all/usr/lib/YaST2/* .;
", binaries));
// Failed
if (out["exit"]:0 != 0)
{
// error report
Report::Error (_("An error occurred while preparing the installation system."));
CleanY2Update();
return false;
} else {
// bugzilla #239055
RereadAllSCRAgents();
}
}
else
{
y2milestone("File /y2update.tgz not provided");
}
return true;
}
symbol DoInstall_WithControlFile (string control) {
y2milestone ("File /installation.xml was found, running own workflow...");
// copy the control file to local filesystem - in case of media release
string tmp = (string)SCR::Read (.target.tmpdir);
tmp = tmp + "/installation.xml";
SCR::Execute (.target.bash, sformat ("/bin/cp %1 %2", control, tmp));
control = tmp;
if (! IntegrateY2Update (src_id)) return nil;
// set control file
ProductControl::custom_control_file = control;
if (!ProductControl::Init())
{
// error report
Report::Error (sformat (_("Control file %1 not found on media."),
control));
CleanY2Update();
return nil;
}
string current_stage = "normal";
string current_mode = "installation";
// Special add-on mode (GetMode()) returns the same
// add-on can be either installed (first time) or updated by another add-on
ProductControl::SetAdditionalWorkflowParams ($["add_on_mode" : AddOnMode (src_id)]);
list <map> steps = ProductControl::getModules (current_stage, current_mode, `enabled);
if (steps == nil || size (steps) < 1) {
y2warning ("Add-On product workflow for stage: %1, mode: %2 not defined", current_stage, current_mode);
ProductControl::ResetAdditionalWorkflowParams();
return nil;
}
// start workflow
Wizard::OpenNextBackStepsDialog();
// dialog caption
Wizard::SetContents(_("Initializing..."), `Empty (), "", false, false);
list <map> stage_mode = [$["stage": current_stage, "mode": current_mode, ]];
y2milestone ("Using Add-On control file parts: %1", stage_mode);
ProductControl::AddWizardSteps (stage_mode);
string old_mode = nil;
// Running system, not installation, not update
if (Stage::normal() && Mode::normal()) {
old_mode = Mode::mode();
Mode::SetMode (current_mode);
}
// Run the workflow
symbol ret = ProductControl::Run();
if (old_mode != nil) {
Mode::SetMode (old_mode);
}
UI::CloseDialog();
CleanY2Update();
ProductControl::ResetAdditionalWorkflowParams();
return ret;
}
/**
* Contains list of repository IDs that request registration
*/
list <integer> addons_requesting_registration = [];
global void ClearRegistrationRequest (integer src_id) {
y2milestone ("Clearing registration flag for repository ID %1", src_id);
if (src_id != nil) {
addons_requesting_registration = filter (integer one_source, addons_requesting_registration, {
return one_source != src_id;
});
}
}
/**
* Returns whether registration is requested by at least one of
* used Add-On products.
*
* @return boolean if requested
*/
global boolean ProcessRegistration () {
boolean force_registration = false;
// checking add-on products one by one
foreach (map<string,any> prod, AddOnProduct::add_on_products, {
integer srcid = (integer) prod["media"]:nil;
if (srcid != nil && contains (addons_requesting_registration, srcid)) {
force_registration = true;
break;
}
});
y2milestone ("Requesting registration: %1", force_registration);
return force_registration;
}
/**
* Add-On product might have been added into products requesting
* registration. This pruduct has been removed (during configuring
* list of add-on products).
*/
global void RemoveRegistrationFlag (integer src_id) {
// filtering out src_id
addons_requesting_registration = filter (integer one_id, addons_requesting_registration, {
return one_id != src_id;
});
// removing cached file
string tmpdir = (string) SCR::Read (.target.tmpdir) + "/add-on-content-files/";
string cachedfile = sformat ("%1content-%2", tmpdir, src_id);
if (FileUtils::Exists (cachedfile)) {
y2milestone ("Removing cached file %1", cachedfile);
SCR::Execute (.target.remove, cachedfile);
}
}
/**
* Checks whether the content file of the add-on has a flag REGISTERPRODUCT
* set to "true" or "yes". If it has, product is added into list of pruducts
* that need registration. Cached content file is used if possible.
*
* @param integer source id
*/
global void PrepareForRegistration (integer src_id) {
string tmpdir = (string) SCR::Read (.target.tmpdir) + "/add-on-content-files/";
// create directory if doesn't exist
if (! FileUtils::Exists (tmpdir)) {
integer run = (integer) SCR::Execute (.target.bash, sformat("/bin/mkdir -p '%1'", tmpdir));
if (run != 0) {
y2error ("Cannot create directory %1", tmpdir);
return nil;
}
}
// use cached file if possible
string contentfile = sformat ("%1content-%2", tmpdir, src_id);
if (FileUtils::Exists (contentfile)) {
y2milestone ("Using cached contentfile %1", contentfile);
} else {
y2milestone ("Checking contentfile from repository");
string sourcefile = GetCachedFileFromSource (src_id, 1, "/content", "signed", true);
if (sourcefile == nil) {
y2warning ("Cannot obtain content file!");
return nil;
}
// copying content file
integer run = (integer) SCR::Execute (.target.bash,
sformat ("/bin/cp '%1' '%2'", String::Quote (sourcefile), String::Quote (contentfile))
);
if (run != 0) {
y2error ("Cannot copy '%1' to '%2'", sourcefile, contentfile);
return nil;
}
}
// registering agent for the current content file
SCR::RegisterAgent (.addon.content, `ag_ini (
`IniAgent( contentfile, $[
"options" : [ "read_only", "global_values", "flat" ],
"comments" : [ "^#.*", "^[ \t]*$", ],
"params" : [
$[ "match" : [ "^[ \t]*([a-zA-Z0-9_\.]+)[ \t]*(.*)[ \t]*$", "%s %s" ] ]
]
]
)));
string register_product = (string) SCR::Read (.addon.content.REGISTERPRODUCT);
SCR::UnregisterAgent (.addon.content);
// evaluating REGISTERPRODUCT flag, default (nil == false)
y2milestone ("RegisterProduct flag for repository %1 is %2", src_id, register_product);
if (register_product == "yes" || register_product == "true") {
addons_requesting_registration = add (addons_requesting_registration, src_id);
}
}
/**
* Calls registration client if needed.
*
* @param integer source id
*/
global void RegisterAddOnProduct (integer src_id) {
if (contains (addons_requesting_registration, src_id)) {
y2milestone ("Repository ID %1 requests registration", src_id);
WFM::CallFunction ("inst_suse_register", []);
} else {
y2milestone ("Repository ID %1 doesn't need registration", src_id);
}
}
// function content defined later
void HandleProductPATTERNS (integer srcid);
/**
* Do installation of the add-on product within an installed system
* srcid is got via AddOnProduct::src_id
*
* @param string src_id
* @return symbol the result symbol from wizard sequencer
*/
global symbol DoInstall() {
// Display /media.1/info.txt if such file exists
// Display license and wait for agreement
// Not needed here, license already shown in the workflow
/*
boolean license_ret = AcceptedLicenseAndInfoFile(src_id);
if (license_ret != true) {
y2milestone("Removing the current source ID %1", src_id);
Pkg::SourceDelete(src_id);
return nil;
}
*/
// FATE #301312
PrepareForRegistration (src_id);
// FATE #302398: PATTERNS keyword in content file
HandleProductPATTERNS (src_id);
// FATE #301997: Support update of add-on products properly
string add_on_mode = AddOnMode (src_id);
SetMode (add_on_mode);
// BNC #468449
// Always store the current set of repositories as they might get
// changed by registration or the called add-on workflow
Pkg::SourceSaveAll();
symbol ret = nil;
string control = GetCachedFileFromSource (src_id, 1, "/installation.xml", "digested", true /* optional */);
if (control != nil) {
y2milestone ("Add-On has own control file");
ret = DoInstall_WithControlFile (control);
}
// Fallback -- Repository didn't provide needed control file
// or control file doesn't contain needed stage/mode
// Handling as it was a repository
if (control == nil || ret == nil) {
ret = DoInstall_NoControlFile();
}
y2milestone("Result of the add-on installation: %1", ret);
if (ret != nil && ret != `abort) {
// registers Add-On product if requested
RegisterAddOnProduct (src_id);
}
y2milestone ("Returning: %1", ret);
return ret;
}
/**
* Every Add-On can preselect some patterns.
* Only patterns that are not selected/installed yet will be used.
*
* @struct $[
* src_id : [
* "pattern_1", "pattern_2", "pattern_6"
* ]
* ]
*/
map <integer, list <string> > patterns_preselected_by_addon = $[];
string PackagesProposalAddonID (integer src_id) {
return sformat ("Add-On-Product-ID:%1", src_id);
}
// See also DeselectProductPatterns()
boolean SelectProductPatterns (string content_file, integer src_id) {
if (! FileUtils::Exists (content_file)) {
y2error ("No such file: %1", content_file);
return false;
}
map contentmap = (map) SCR::Read (.content_file, content_file);
// no PATTERNS defined
if (! haskey (contentmap, "PATTERNS")) {
y2milestone ("Add-On doesn't have any required patterns (PATTERNS in content)");
return true;
}
// parsing PATTERNS
list <string> patterns_to_select = splitstring (contentmap["PATTERNS"]:"", "\t ");
patterns_to_select = filter (string one_pattern, patterns_to_select, {
return (one_pattern != nil && one_pattern != "");
});
if (size (patterns_to_select) == 0) {
y2error ("Erroneous PATTERNS: %1", contentmap["PATTERNS"]:"");
return false;
}
y2milestone ("Add-On requires these PATTERNS: %1", patterns_to_select);
// clear/set
patterns_preselected_by_addon[src_id] = [];
// bnc #458297
// Using PackagesProposal to select the patterns itself
PackagesProposal::SetResolvables (PackagesProposalAddonID (src_id), `pattern, patterns_to_select);
if (Stage::initial()) {
y2milestone ("Using PackagesProposal to select Add-On patterns");
return true;
}
boolean ret = true;
foreach (string one_pattern, patterns_to_select, {
list <map <string,any> > pattern_properties = Pkg::ResolvableProperties (one_pattern, `pattern, "");
boolean already_selected = false;
foreach (map <string,any> one_pattern_found, pattern_properties, {
symbol patt_status = one_pattern_found["status"]:`unknown;
// patern is already selected
if (patt_status == `installed || patt_status == `selected) {
already_selected = true;
break;
}
});
if (already_selected) {
y2milestone ("Pattern %1 is already installed/selected", one_pattern);
return;
}
if (! Pkg::ResolvableInstall (one_pattern, `pattern)) {
y2error ("Cannot select pattern: %1, reason: %2", one_pattern, Pkg::LastError());
ret = false;
} else {
patterns_preselected_by_addon[src_id] = add (patterns_preselected_by_addon[src_id]:[], one_pattern);
}
});
return ret;
}
// See also SelectProductPatterns()
boolean DeselectProductPatterns (integer src_id) {
// bnc #458297
// Using PackagesProposal to deselect the patterns itself
PackagesProposal::SetResolvables (PackagesProposalAddonID (src_id), `pattern, []);
if (Stage::initial()) {
y2milestone ("Initial stage, using PackagesProposal to deselect patterns");
return true;
}
list <string> patterns_to_deselect = patterns_preselected_by_addon[src_id]:[];
if (size (patterns_to_deselect) == 0) {
y2milestone ("There's no pattern to be deselected");
return true;
}
boolean ret = true;
foreach (string one_pattern, patterns_to_deselect, {
if (! Pkg::ResolvableNeutral (one_pattern, `pattern, true)) {
y2error ("Cannot deselect pattern: %1, reason: %2", one_pattern, Pkg::LastError());
ret = false;
}
});
return ret;
}
/**
* Function checks whether the product content file contains
* PATTERNS tag and pre-selects patterns listed there.
*
* @param integer source ID
*/
void HandleProductPATTERNS (integer srcid) {
// FATE #302398: PATTERNS keyword in content file
string content_file = GetCachedFileFromSource (srcid, 1, "/content", "signed", true);
if (content_file == nil) {
y2warning ("Add-On %1 doesn't have a content file", srcid);
} else {
SelectProductPatterns (content_file, srcid);
}
}
/**
* Integrate the add-on product to the installation workflow, including
* preparations for 2nd stage and inst-sys update
* @param srcid integer the ID of the repository
* @return boolean true on success
*/
global boolean Integrate (integer srcid) {
y2milestone ("Integrating repository %1", srcid);
// Updating inst-sys
string y2update = GetCachedFileFromSource (srcid, 1, "/y2update.tgz", "digested", true /* optional */);
if (y2update == nil) {
y2milestone ("No YaST update found on the media");
} else {
UpdateInstSys (y2update);
}
// FATE #302398: PATTERNS keyword in content file
HandleProductPATTERNS (srcid);
// Adds workflow to the Workflow Store if any workflow exists
WorkflowManager::AddWorkflow (`addon, srcid, "");
return true;
}
/**
* Opposite to Integrate()
*
* @param srcid integer the ID of the repository
*/
global void Disintegrate (integer srcid) {
DeselectProductPatterns (srcid);
WorkflowManager::RemoveWorkflow (`addon, srcid, "");
}
/**
* Some product(s) were removed, reintegrating their control files from scratch.
*/
global boolean ReIntegrateFromScratch () {
y2milestone ("Reintegration workflows from scratch...");
// bugzilla #239055
RereadAllSCRAgents();
// Should have been done before (by calling AddOnProduct::Integrate()
// foreach (map<string,any> prod, AddOnProduct::add_on_products, {
// integer srcid = (integer) prod["media"]:nil;
//
// if (srcid == nil) {
// y2error ("Wrong definition of Add-on product: %1, cannot reintegrate", srcid);
// return;
// } else {
// y2milestone ("Reintegrating product %1", prod);
// Integrate (srcid);
// }
// });
boolean redraw = WorkflowManager::SomeWorkflowsWereChanged();
// New implementation: Control files are cached, just merging them into the Base Workflow
WorkflowManager::MergeWorkflows();
// steps might have been changed, forcing redraw
if (redraw) {
y2milestone ("Forcing RedrawWizardSteps()");
WorkflowManager::RedrawWizardSteps ();
}
return true;
}
global boolean CheckProductDependencies (list<string> products) {
// TODO check the dependencies of the product
return true;
}
string preselected_add_ons = "plain";
/**
* Sets an add_on_products file type ("plain" or "xml")
* @see FATE #303675
*
* @param string type "plain" or "xml"
*/
global void SetPreselectedAddOnProductsType (string type) {
if (type == "xml" || type == "plain") {
preselected_add_ons = type;
y2milestone ("add_on_products type set: %1", preselected_add_ons);
} else {
y2error ("Unknown type: %1", type);
}
}
/**
* Reads temporary add_on_products file, parses supported products,
* merges base URL if products use relative URL and returns list of
* maps defining additional products to add.
*
* @see FATE #303675
* @param string parse_file
* @param string base_url
* @return list <map> of products to add
*
* @struct
* [
* // product defined with URL and additional path (typically "/")
* $["url":(string) url, "path":(string) path]
* // additional list of products to install
* // media URL can contain several products at once
* $["url":(string) url, "path":(string) path, "install_products":(list <string>) pti]
* ]
*/
list <map> ParsePlainAddOnProductsFile (string parse_file, string base_url) {
if (! FileUtils::Exists (parse_file)) {
y2error ("Cannot parse missing file: %1", parse_file);
return [];
}
list <string> products = splitstring ((string) SCR::Read (.target.string, parse_file), "\r\n");
if (products == nil) {
// TRANSLATORS: error report
Report::Error (_("Unable to use additional products."));
y2error ("Erroneous file: %1", parse_file);
return [];
}
list <map> ret = [];
foreach (string p, products, {
if (p == "")
return;
list <string> elements = splitstring (p, " \t");
elements = filter (string e, elements, { return e != ""; });
string url = elements[0]:"";
string pth = elements[1]:"/";
if (elements[0]:nil != nil) elements = remove (elements, 0);
if (elements[0]:nil != nil) elements = remove (elements, 0);
// FATE #302123
if (base_url != nil && base_url != "") {
url = GetAbsoluteURL (base_url, url);
}
ret = add (ret, $[
"url" : url,
"path" : pth,
"install_products" : elements,
]);
});
return ret;
}
list <map> UserSelectsRequiredAddOns (list <map> products) {
if (products == nil || products == []) {
return [];
}
list <term> ask_user_products = [];
map <integer, map> ask_user_products_map = $[];
// key in ask_user_products_map
integer id_counter = -1;
string visible_string = "";
// filter those that are selected by default (without 'ask_user')
list <map> selected_products = filter (map one_product, products, {
if (one_product["ask_user"]:false == false) {
return true;
}
// wrong definition, 'url' is mandatory
if (! haskey (one_product, "url")) {
y2error ("No 'url' defined: %1", one_product);
return false;
}
// user is asked for the rest
id_counter = id_counter + 1;
// fill up internal map (used later when item selected)
ask_user_products_map[id_counter] = one_product;
if (haskey (one_product, "name")) {
visible_string = sformat (_("%1, URL: %2"), one_product["name"]:"", one_product["url"]:"");
} else if (haskey (one_product, "install_products")) {
visible_string = sformat (_("%1, URL: %2"), mergestring (one_product["install_products"]:[], ", "), one_product["url"]:"");
} else if (haskey (one_product, "path") && one_product["path"]:"/" != "/") {
visible_string = sformat (_("URL: %1, Path: %2"), one_product["url"]:"", one_product["path"]:"");
} else {
visible_string = sformat (_("URL: %1"), one_product["url"]:"");
}
// create items
ask_user_products = add (ask_user_products, `item (
`id (id_counter),
visible_string,
one_product["selected"]:false
));
return false;
});
ask_user_products = sort (term x, term y, ask_user_products, ``(x[1]:"" < y[1]:""));
UI::OpenDialog (
`VBox (
`HBox (
`HSquash (`MarginBox (0.5, 0.2, Icon::Simple ("yast-addon"))),
// TRANSLATORS: popup heading
`Left (`Heading(`id(`search_heading), _("Additional Products")))
),
`VSpacing (0.5),
// TRANSLATORS: additional dialog information
`Left (`Label (_("The installation repository contains also the listed additional repositories.
Select those you want to use."))),
`VSpacing (0.5),
`MinSize (
70, 16,
`MultiSelectionBox (
`id (`products),
_("Additional Products to Select"),
ask_user_products
)
),
`HBox (
`HStretch(),
// push button label
`PushButton (`id (`ok), _("Add Selected &Products")),
`HSpacing (1),
`PushButton (`id (`cancel), Label::CancelButton())
)
)
);
any ret = UI::UserInput();
y2milestone ("User ret: %1", ret);
// add also selected
if (ret == `ok) {
list <integer> selprods = (list <integer>) UI::QueryWidget (`products, `SelectedItems);
foreach (integer one_product, selprods, {
selected_products = add (selected_products, ask_user_products_map[one_product]:$[]);
});
}
UI::CloseDialog ();
y2milestone ("Selected products: %1", selected_products);
return selected_products;
}
list <map> ParseXMLBasedAddOnProductsFile (string parse_file, string base_url) {
if (! FileUtils::Exists (parse_file)) {
y2error ("Cannot parse missing file: %1", parse_file);
return [];
}
map <string, any> xmlfile_products = XML::XMLToYCPFile (parse_file);
if (xmlfile_products == nil) {
// TRANSLATORS: error report
Report::Error (_("Unable to use additional products."));
y2error ("Erroneous file %1", parse_file);
return [];
} else if (xmlfile_products["product_items"]:[] == []) {
y2warning ("Empty file %1", parse_file);
return [];
}
list <map> products = [];
boolean run_ask_user = false;
foreach (map one_prod, xmlfile_products["product_items"]:[], {
if (! haskey (one_prod, "url")) {
y2error ("No 'url' defined in %1", one_prod);
return;
}
// FATE #302123
if (base_url != nil && base_url != "") {
one_prod["url"] = GetAbsoluteURL (base_url, one_prod["url"]:"");
}
if (one_prod["ask_user"]:false == true) {
run_ask_user = true;
}
products = add (products, one_prod);
});
if (run_ask_user) {
products = UserSelectsRequiredAddOns (products);
}
return products;
}
/**
* Auto-integrate add-on products in specified file (usually add_on_products file)
*
* @param filelist string a file containing a list of add-on products to integrate
* @see FATE #303675: Support several add-ons on standard medium
* @return boolean true on exit
*
* @struct
* Format of /add_on_products.xml file on media root:
* <?xml version="1.0"?>
* <add_on_products xmlns="http://www.suse.com/1.0/yast2ns"
* xmlns:config="http://www.suse.com/1.0/configns">
* <product_items config:type="list">
* <product_item>
* <!-- Product name visible in UI when offered to user (optional item) -->
* <name>Add-on Name to Display</name>
* <!-- Product URL (mandatory item) -->
* <url>http://product.repository/url/</url>
* <!-- Product path, default is "/" (optional item) -->
* <path>/relative/product/path</path>
* <!--
* List of products to install from media, by default all products
* from media are installed (optional item)
* -->
* <install_products config:type="list">
* <!--
* Product to install - matching the metadata product 'name'
* (mandatory to fully define 'install_products')
* -->
* <product>Product-ID-From-Repository</product>
* <product>...</product>
* </install_products>
* <!--
* If set to 'true', user is asked whether to install this product,
* default is 'false' (optional)
* -->
* <ask_user config:type="boolean">true</ask_user>
* <!--
* Connected to 'ask_user', sets the default status of product,
* default is 'false' (optional)
* -->
* <selected config:type="boolean">true</selected>
* </product_item>
* <product_item>
* ...
* </product_item>
* </product_items>
* </add_on_products>
*/
global boolean AddPreselectedAddOnProducts (string filelist) {
if (filelist == nil)
{
y2milestone ("No add-on products defined on the media");
return true;
}
string base_url = GetBaseProductURL();
y2milestone ("Base URL: %1", URL::HidePassword(base_url));
list <map> add_products = [];
// new xml format
if (preselected_add_ons == "xml") {
add_products = ParseXMLBasedAddOnProductsFile (filelist, base_url);
// old fallback
} else if (preselected_add_ons == "plain") {
add_products = ParsePlainAddOnProductsFile (filelist, base_url);
} else {
y2error ("Unsupported type: %1", preselected_add_ons);
return false;
}
y2milestone ("Adding products: %1", add_products);
foreach (map one_product, add_products, {
string url = one_product["url"]:"";
string pth = one_product["path"]:"";
y2milestone ("Adding Repository: %1 %2", url, pth);
integer src = Pkg::SourceCreate (url, pth);
if (src == nil || src < 0) {
y2error ("Unable to add product: %1", url);
// TRANSLATORS: error message, %1 is replaced with product URL
Report::Error (sformat (_("Unable to add product %1."), url));
return;
}
if (! AcceptedLicenseAndInfoFile (src)) {
Pkg::SourceDelete (src);
return;
}
Integrate (src);
// adding the product to the list of products
// bugzilla #269625
map <string, string> prod = (map <string, string>) Pkg::SourceProductData (src);
AddOnProduct::add_on_products = add (AddOnProduct::add_on_products, $[
"media" : src,
"product" : prod["label"]:prod["productname"]:prod["productversion"]:"",
"autoyast_product" : prod["productname"]:"",
"media_url" : url,
"product_dir" : pth,
]);
list <string> prods_to_install = one_product["install_products"]:[];
// there are more products at the destination
// install the listed ones only
if (prods_to_install != nil && size (prods_to_install) > 0) {
foreach (string one_prod, prods_to_install, {
y2milestone ("Selecting product '%1' for installation", one_prod);
Pkg::ResolvableInstall (one_prod, `product);
});
// install all products from the destination
} else {
list<map<string,any> > products = Pkg::ResolvableProperties ("", `product, "");
// only those that come from the new source
products = filter (map<string,any> p, products, {
return p["source"]:-1 == src;
});
foreach (map<string,any> p, products, {
y2milestone ("Selecting product '%1' for installation", p["name"]:"");
Pkg::ResolvableInstall (p["name"]:"", `product);
});
}
});
// reread agents, redraw wizard steps, etc.
ReIntegrateFromScratch();
}
/* Export/Import --> */
/**
* Returns map describing all used add-ons.
*
* @return map
*
* @struct This is an XML file created from exported map:
* <add-on>
* <add_on_products config:type="list">
* <listentry>
* <media_url>ftp://server.name/.../</media_url>
* <product>NEEDS_TO_MATCH_"PRODUCT"_TAG_FROM_content_FILE!</product>
* <product_dir>/</product_dir>
* </listentry>
* ...
* </add_on_products>
* </add-on>
*/
global map Export () {
y2milestone ("Add-Ons Input: %1", add_on_products);
list<map<string,any> > exp = maplist (map<string,any> p, add_on_products, {
if (haskey (p, "media"))
p = remove (p, "media");
// bugzilla #279893
if (haskey (p, "autoyast_product")) {
p["product"] = p["autoyast_product"]:"";
p = remove (p, "autoyast_product");
}
return p;
});
y2milestone ("Add-Ons Output: %1", exp);
return $[
"add_on_products" : exp,
];
}
global boolean Import (map settings) {
add_on_products = settings["add_on_products"]:[];
modified = false;
if (Mode::config ())
{
foreach (map prod, add_on_products, {
string media = prod["media_url"]:"";
string pth = prod["product_dir"]:"/";
integer src = Pkg::SourceCreate (media, pth);
if (src != -1)
mode_config_sources = add (mode_config_sources, src);
});
}
return true;
}
global void CleanModeConfigSources () {
foreach (integer src, mode_config_sources, {
Pkg::SourceDelete (src);
});
mode_config_sources = [];
}
/**
* Returns the path where Add-Ons configuration is stored during the fist stage installation.
* This path reffers to the installed system.
*
* @see bugzilla #187558
*/
global string TmpExportFilename () {
return Directory::vardir + "/exported_add_ons_configuration";
}
/**
* Reads the Add-Ons configuration stored on disk during the first stage installation.
*
* @see bugzilla #187558
*/
global boolean ReadTmpExportFilename () {
string tmp_filename = TmpExportFilename();
modified = true;
if (FileUtils::Exists(tmp_filename)) {
y2milestone ("Reading %1 content", tmp_filename);
// there might be something already set, store the current configuration
list <map <string,any> > already_in_configuration = add_on_products;
map configuration_from_disk = (map) SCR::Read (.target.ycp, tmp_filename);
y2milestone ("Configuration from disk: %1", configuration_from_disk);
if (configuration_from_disk != nil) {
Import (configuration_from_disk);
if (already_in_configuration != [] && already_in_configuration != nil) {
add_on_products = (list <map <string,any> >) union (add_on_products, already_in_configuration);
}
return true;
} else {
y2error ("Reading %1 file returned nil result!", tmp_filename);
return false;
}
} else {
y2warning ("File %1 doesn't exists, skipping...", tmp_filename);
return true;
}
}
boolean AcceptUnsignedFile(string file, integer repo)
{
y2milestone("Accepting unsigned file %1 from repository %2", file, repo);
return true;
}
boolean RejectUnsignedFile(string file, integer repo)
{
y2milestone("Rejecting unsigned file %1 from repository %2", file, repo);
return false;
}
boolean AcceptFileWithoutChecksum(string file)
{
y2milestone("Accepting file without checksum: %1", file);
return true;
}
boolean RejectFileWithoutChecksum(string file)
{
y2milestone("Rejecting file without checksum: %1", file);
return false;
}
boolean AcceptVerificationFailed(string file, map<string,any> key, integer repo)
{
y2milestone("Accepting failed verification of file %1 with key %2 from repository %3", file, key, repo);
return true;
}
boolean RejectVerificationFailed(string file, map<string,any> key, integer repo)
{
y2milestone("Rejecting failed verification of file %1 with key %2 from repository %3", file, key, repo);
return false;
}
global boolean AcceptUnknownGpgKeyCallback( string filename, string keyid, integer repo)
{
y2milestone("AcceptUnknownGpgKeyCallback %1: %2 (from repository %3)", filename, keyid, repo);
return ( current_addon["signature-handling","accept_unknown_gpg_key","all"]:false ||
contains( current_addon["signature-handling","accept_unknown_gpg_key","keys"]:[], keyid ) );
}
global boolean ImportGpgKeyCallback(map<string,any> key, integer repo)
{
y2milestone("ImportGpgKeyCallback: %1 from repository %2", key, repo);
return ( current_addon["signature-handling","import_gpg_key","all"]:false ||
contains( current_addon["signature-handling","import_gpg_key","keys"]:[], key["id"]:"" ) );
}
global boolean AcceptNonTrustedGpgKeyCallback(map<string,any> key)
{
y2milestone("AcceptNonTrustedGpgKeyCallback %1", key);
return ( current_addon["signature-handling","accept_non_trusted_gpg_key","all"]:false ||
contains( current_addon["signature-handling","accept_non_trusted_gpg_key","keys"]:[], key["id"]:"" ) );
}
/* <-- Export/Import */
/*
<add-on>
<add_on_products config:type="list">
<listentry>
<media_url>http://software.opensuse.org/download/server:/dns/SLE_10/</media_url>
<product>buildservice</product>
<product_dir>/</product_dir>
<signature-handling>
<accept_unsigned_file config:type="boolean">true</accept_unsigned_file>
<accept_file_without_checksum config:type="boolean">true</accept_file_without_checksum>
<accept_verification_failed config:type="boolean">true</accept_verification_failed>
<accept_unknown_gpg_key>
<all config:type="boolean">true</all>
<keys config:type="list">
<keyid>...</keyid>
<keyid>3B3011B76B9D6523</keyid>
</keys>
</accept_unknown_gpg_key>
<accept_non_trusted_gpg_key>
<all config:type="boolean">true</all>
<keys config:type="list">
<keyid>...</keyid>
</keys>
</accept_non_trusted_gpg_key>
<import_gpg_key>
<all config:type="boolean">true</all>
<keys config:type="list">
<keyid>...</keyid>
</keys>
</import_gpg_key>
</signature-handling>
</listentry>
</add_on_products>
</add-on>
*/
global void SetSignatureCallbacks( string product ) {
current_addon = $[];
foreach( map<string,any> addon, add_on_products, ``{
if( addon["product"]:"" != product )
continue;
current_addon = addon; // remember the current addon for the Callbacks
if( haskey( addon["signature-handling"]:$[], "accept_unsigned_file" ) )
Pkg::CallbackAcceptUnsignedFile(
addon["signature-handling","accept_unsigned_file"]:false ? AcceptUnsignedFile : RejectUnsignedFile
);
if( haskey( addon["signature-handling"]:$[], "accept_file_without_checksum" ) )
Pkg::CallbackAcceptFileWithoutChecksum(
addon["signature-handling","accept_file_without_checksum"]:false ? AcceptFileWithoutChecksum : RejectFileWithoutChecksum
);
if( haskey( addon["signature-handling"]:$[], "accept_verification_failed") )
Pkg::CallbackAcceptVerificationFailed(
addon["signature-handling","accept_verification_failed"]:false ? AcceptVerificationFailed : RejectVerificationFailed
);
if( haskey( addon["signature-handling"]:$[], "accept_unknown_gpg_key") )
Pkg::CallbackAcceptUnknownGpgKey(AddOnProduct::AcceptUnknownGpgKeyCallback);
if( haskey( addon["signature-handling"]:$[], "import_gpg_key") )
Pkg::CallbackImportGpgKey(AddOnProduct::ImportGpgKeyCallback);
if( haskey( addon["signature-handling"]:$[], "accept_non_trusted_gpg_key") )
Pkg::CallbackAcceptNonTrustedGpgKey(AddOnProduct::AcceptNonTrustedGpgKeyCallback);
break;
});
return;
}
} // module end
ACC SHELL 2018