## Problem - When saving the Agama logs we save the system log by calling the `journactl` command and saving the output to a file. - However, the default output format misses some important data which might be neede for debugging, like source code locations. ## Solution - Export the whole system journal in JSON format to save complete data (even the values not displayed by default) - Implement our own `journalctl` filter which prints the journal in text format but with the additional data we need (source code locations). A simple text log is important for easy debugging. - The log file saved by `agama logs store` will contain 3 parts: - The complete log in JSON format - The text log with additional data (source location) - Additional libzypp log in the y2log format (preferred by the libzypp team), the Agama messages from `zypp-agama-sys` crate are included in the text log as well, this is just to make the libzypp debugging easier - Additional data can be always extracted from the complete JSON log if needed later ## Notes The `journalctl` command can export the journal data in a binary format which includes all data. But processing that binary dump is not trivial. You need to install the `systemd-journal-remote` package (not installed by default, fortunately present in SLES16) and convert the binary dump to the native binary journal format which then can be processed by the `journalctl` command and finally read it as text. That's quite complicated, simple JSON format is much easier to process (using `jq` even from shell). Moreover I'm a bit worried about the binary format compatibility, we need to be able to easily read and process the logs even after 10+ years in the future... ## Additional enhancements - Save separate libzypp log in the old y2log format, that should make debugging easier for the libzypp team as they will get the data in the format which they are familiar with. See more details below. - Redirect the output of the logging commands to files, avoid storing huge log files in memory. The whole system journal in JSON format can be huge (>=50MB), reading all data first into memory might be problematic on systems with low memory. So redirect the outputs to files and then delete empty files if nothing was printed. ## Technical details This PR adds two shell scripts which process the systemd journal and print it in text format with more details or in a different format. ### `agama-journal` script This script implements a custom `journalctl` output formatter. The output format is similar to the default `journalctl` output, but there some small (but important) changes: #### Additional values - Includes the error level for each message. *The displayed error level is the systemd/syslog level, which is different than the old y2log/libzypp error levels! Syslog uses 7 for debug, [the lower number the higher severity](https://en.wikipedia.org/wiki/Syslog#Severity_level)! Level 0 is emergency error.* - Source code location (source file name, line number, name of the function), this is supported only by few services/libararies. #### Removed values - Host name: I guess it is pointless to repeat the host name on each line of the output, especially when we use the generic name "agama" for all installations. (Though that can be changed by the `hostname=` boot parameter.) I was thinking about using subsecond precision for the time stamps in the output, the journal stores them with microsecond precision. But I think that in reality we need that precision very rarely and if needed the precise time can be always extracted from the saved JSON format, see below. Of course, the output format can be changed if needed, let's take it as the starting point... ### `agama-zypp-journal` script This script is similar to the `agama-journal` script, but it only exports the libzypp messages and saves them in the old YaST y2log format. It uses custom fields from the journal to print the original libzypp values. For example libzypp uses different error levels than syslog/systemd. Syslog uses level 7 as the lowest debug level, the higher level the lower number, 0 is emergency error. While libzypp/y2log uses 0 for debug and 5 for critical errors, with possibility to use level 999 for temporary debugging. 😃 So when logging a message to journal we have to remap the libzypp error level to journal value. Thanks to using the custom values in the journal we can easily restore the original libzypp levels in this script. ## Usage Both scripts are called when saving the logs and their output is included in the saved tarball log. ```console agama:~ # agama-journal | tail Mar 24 09:51:39 <6> avahi-daemon[1946]: Registering new address record for fd00::40e8:5a9b:9461:9b7b on enp0s3.*. Mar 24 09:51:39 <5> agama-web-server[4376]: agama-server/src/web/service.rs:130: request 274877906969: GET /api/v2/system Mar 24 09:51:39 <4> agama-web-server[4376]: agama-network/src/nm/client.rs:222: Skipped connection because of flags: 15 Mar 24 09:51:39 <5> agama-web-server[4376]: agama-server/src/web/service.rs:139: response for 274877906969: 200 OK 23.252194ms Mar 24 09:51:39 <6> agama-web-server[4376]: /home/lslezak/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-http-0.6.8/src/trace/on_eos.rs:105: end of stream Mar 24 10:12:18 <6> NetworkManager[1898]: ../src/core/dhcp/nm-dhcp-client.c:858: <info> [1774343538.5657] dhcp4 (enp0s3): state changed new lease, address=192.168.1.113 Mar 24 10:12:18 <6> systemd[1]: src/core/job.c(job_emit_start_message):622: Starting Network Manager Script Dispatcher Service... Mar 24 10:12:18 <6> systemd[1]: src/core/job.c(job_emit_done_message):815: Started Network Manager Script Dispatcher Service. Mar 24 10:12:18 <6> nm-dispatcher[5363]: sed: can't read /etc/fstab: No such file or directory Mar 24 10:12:28 <6> systemd[1]: src/core/unit.c(unit_log_success):6168: NetworkManager-dispatcher.service: Deactivated successfully. ``` ```console agama:~ # agama-zypp-journal | head 2026-03-24 09:17:08 <1> agama(4376) [zypp-agama-sys] lib.cxx(init_target):229 Initializing target: /run/agama/zypp 2026-03-24 09:17:08 <1> agama(4376) [zypp-agama-sys] lib.cxx(zypp_ptr):144 Initializing libzypp 2026-03-24 09:17:08 <1> agama(4376) [zypp-core] PathInfo.cc(assert_dir):372 mkdir /run/agama/zypp/run 00755 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppFactory.cc(_openLockFile):183 Open lockfile /run/agama/zypp/run/zypp.pid 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppFactory.cc(readLockFile):238 read: Lockfile /run/agama/zypp/run/zypp.pid has pid 0 (our pid: 4376) 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppFactory.cc(writeLockFile):250 write: Lockfile /run/agama/zypp/run/zypp.pid got pid 4376 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppFactory.cc(_closeLockFile):201 Close lockfile /run/agama/zypp/run/zypp.pid 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppImpl.cc(ZYppImpl):124 libzypp: 17.38.4 (01699:0:01) 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppImpl.cc(ZYppImpl):124 libsolv: 0.7.36 2026-03-24 09:17:08 <1> agama(4376) [zypp] ZYppImpl.cc(ZYppImpl):124 TextLocale: 'en_US' (en_US) ``` But the scripts can be used also interactively when debugging a running system. When running interactively the script colorizes the output based on the error level: <img width="1532" height="905" alt="image" src="https://github.com/user-attachments/assets/2d013830-5b4f-470b-8070-0fdbeae90dc4" /> It is possible to show precise time stamps if needed: ```console agama:~ # agama-journal --microseconds Mar 24 10:16:28.883915 <6> sshd-session[5445]: Accepted publickey for root from fd00::a7b4:553c:44ce:c6b2 port 34466 ssh2: ED25519 SHA256:bUer1LwyLcDFmNMTowvyFCVDJkV7ZkPKV3/pLIO1F/U Mar 24 10:16:28.889751 <6> systemd-logind[1875]: src/login/logind-session.c(session_start):837: New session '9' of user 'root' with class 'user' and type 'tty'. Mar 24 10:16:28.905420 <6> systemd[1]: src/core/job.c(job_emit_done_message):815: Started Session 9 of User root. Mar 24 10:16:28.906289 <6> sshd-session[5445]: pam_unix(sshd:session): session opened for user root(uid=0) by root(uid=0) Mar 24 10:16:29.047948 <6> sshd-session[5450]: Received disconnect from fd00::a7b4:553c:44ce:c6b2 port 34466:11: disconnected by user Mar 24 10:16:29.048602 <6> sshd-session[5450]: Disconnected from user root fd00::a7b4:553c:44ce:c6b2 port 34466 Mar 24 10:16:29.049754 <6> sshd-session[5445]: pam_unix(sshd:session): session closed for user root Mar 24 10:16:29.051696 <6> systemd[1]: src/core/unit.c(unit_log_success):6168: session-9.scope: Deactivated successfully. Mar 24 10:16:29.064042 <6> systemd-logind[1875]: src/login/logind-session.c(session_stop_scope):914: Session 9 logged out. Waiting for processes to exit. Mar 24 10:16:29.079808 <6> systemd-logind[1875]: src/login/logind-session.c(session_finalize):968: Removed session 9. ``` Or from a previously stored JSON dump: ```sh journalctl -o json > dump agama-journal --microseconds --input-file dump ``` The script passes all unknown parameters to the `journalctl` call so it is possible to do custom filtering, e.g. show all errors reported in the last 5 minutes: ```sh agama-journal -p err --since "5 minutes ago" ``` ### Storing the logs The new logs are included in the archive created by the `agama logs store` command: ```console agama:~ # agama logs store /tmp/agama-logs-1774355656.tar.gz agama:~ # tar tfzv /tmp/agama-logs-1774355656.tar.gz | grep 'out.log$' -rw-r--r-- root/root 31274 2026-03-24 13:34 agama-logs.z5Cp3M/rpm-qa.out.log -rw-r--r-- root/root 375089 2026-03-24 13:34 agama-logs.z5Cp3M/libzypp.out.log -rw-r--r-- root/root 611340 2026-03-24 13:34 agama-logs.z5Cp3M/journal.out.log -rw-r--r-- root/root 4671555 2026-03-24 13:34 agama-logs.z5Cp3M/journal_json.out.log ``` ## File sizes | Name | Size | Gzip compressed | |---|---|---| | Original text log| 6.1MiB| 0.4MiB| | New text log| 7.9MiB| 0.4MiB| | JSON dump | 48MiB | 4.3MiB | | Libzypp log | 6.9MiB | 0.3MiB | | Complete log tarball| - | 5.4MB | The most important part is that the whole saved tarball still fits into the bugzilla attachment size (20MB) and there is still some room available. ## Testing - Tested manually
931 lines
31 KiB
C++
931 lines
31 KiB
C++
#include "lib.h"
|
|
#include "callbacks.h"
|
|
#include "callbacks.hxx"
|
|
#include "helpers.hxx"
|
|
#include "repository.h"
|
|
|
|
#include <cstddef>
|
|
#include <cstdlib>
|
|
#include <exception>
|
|
#include <zypp-core/Pathname.h>
|
|
#include <zypp-core/Url.h>
|
|
#include <zypp/DiskUsageCounter.h>
|
|
#include <zypp/Pattern.h>
|
|
#include <zypp/Product.h>
|
|
#include <zypp/PublicKey.h>
|
|
#include <zypp/RepoInfo.h>
|
|
#include <zypp/RepoManager.h>
|
|
#include <zypp/RepoManagerFlags.h>
|
|
#include <zypp/ResKind.h>
|
|
#include <zypp/ResObject.h>
|
|
#include <zypp/ResStatus.h>
|
|
#include <zypp/Resolvable.h>
|
|
#include <zypp/ZYpp.h>
|
|
#include <zypp/ZYppCommit.h>
|
|
#include <zypp/ZYppFactory.h>
|
|
#define ZYPP_BASE_LOGGER_LOGGROUP "rust-bindings"
|
|
#include <zypp/base/LogControl.h>
|
|
#include <zypp/base/Logger.h>
|
|
|
|
#include <zypp/ui/Selectable.h>
|
|
|
|
extern "C" {
|
|
|
|
#include <systemd/sd-journal.h>
|
|
|
|
// helper macro for logging the code location from where libzypp is called
|
|
#define LOG_LOCATION(message) \
|
|
do { \
|
|
std::string line("CODE_LINE="); \
|
|
line.append(std::to_string(__LINE__)); \
|
|
sd_journal_send_with_location("CODE_FILE=" __FILE__, line.c_str(), \
|
|
__func__, "PRIORITY=%i", LOG_NOTICE, \
|
|
"MESSAGE=%s", (message), \
|
|
"COMPONENT=zypp-agama-sys", NULL); \
|
|
} while (0)
|
|
|
|
// helper function to hide the password in URL
|
|
std::string filter_url(const char *url) {
|
|
std::string result(url);
|
|
try {
|
|
zypp::Url z_url(url);
|
|
// asString() by default does not include the password
|
|
result = z_url.asString();
|
|
} catch (const zypp::Exception &excpt) {
|
|
// if the URL is invalid, just return the original string
|
|
}
|
|
return result;
|
|
}
|
|
|
|
struct Zypp {
|
|
zypp::ZYpp::Ptr zypp_pointer;
|
|
zypp::RepoManager *repo_manager;
|
|
};
|
|
|
|
static struct Zypp the_zypp{
|
|
.zypp_pointer = NULL,
|
|
.repo_manager = NULL,
|
|
};
|
|
|
|
// formatter which actually logs the messages to the systemd journal,
|
|
// that is a bit hacky but in the logger we receive an already formatted
|
|
// message as a single string and it would not be easy to get back the original
|
|
// components of the message
|
|
struct AgamaFormatter : public zypp::base::LogControl::LineFormater {
|
|
virtual std::string format(const std::string &zypp_group,
|
|
zypp::base::logger::LogLevel zypp_level,
|
|
const char *zypp_file, const char *zypp_func,
|
|
int zypp_line, const std::string &zypp_message) {
|
|
// the systemd/syslog compatible log level
|
|
int level;
|
|
|
|
// convert the zypp log level to the systemd/syslog log level
|
|
switch (zypp_level) {
|
|
// for details about the systemd levels see
|
|
// https://www.freedesktop.org/software/systemd/man/latest/sd-daemon.html
|
|
case zypp::base::logger::E_DBG:
|
|
level = LOG_DEBUG;
|
|
break;
|
|
case zypp::base::logger::E_MIL:
|
|
level = LOG_INFO;
|
|
break;
|
|
case zypp::base::logger::E_WAR:
|
|
level = LOG_WARNING;
|
|
break;
|
|
case zypp::base::logger::E_ERR:
|
|
level = LOG_ERR;
|
|
break;
|
|
case zypp::base::logger::E_SEC:
|
|
// security error => critical
|
|
level = LOG_CRIT;
|
|
break;
|
|
case zypp::base::logger::E_INT:
|
|
// internal error => critical
|
|
level = LOG_CRIT;
|
|
break;
|
|
// libzypp specific level
|
|
case zypp::base::logger::E_USR:
|
|
level = LOG_INFO;
|
|
break;
|
|
// libzypp specific level
|
|
case zypp::base::logger::E_XXX:
|
|
level = LOG_CRIT;
|
|
break;
|
|
}
|
|
|
|
// unlike the other values, the location needs to be sent in an already
|
|
// formatted strings
|
|
std::string file("CODE_FILE=");
|
|
file.append(zypp_file);
|
|
std::string line("CODE_LINE=");
|
|
line.append(std::to_string(zypp_line));
|
|
|
|
// this will log the message with libzypp location, not from *this* file,
|
|
// see "man sd_journal_send_with_location"
|
|
sd_journal_send_with_location(
|
|
file.c_str(), line.c_str(), zypp_func, "PRIORITY=%i", level,
|
|
"MESSAGE=%s", zypp_message.c_str(),
|
|
// some custom data to allow easy filtering of the libzypp messages
|
|
"COMPONENT=libzypp", "ZYPP_GROUP=%s", zypp_group.c_str(),
|
|
"ZYPP_LEVEL=%i", zypp_level, NULL);
|
|
|
|
// libzypp aborts when the returned message is empty,
|
|
// return some static fake data to make it happy
|
|
return "msg";
|
|
}
|
|
};
|
|
|
|
// a dummy logger
|
|
struct AgamaLogger : public zypp::base::LogControl::LineWriter {
|
|
virtual void writeOut(const std::string &formatted) {
|
|
// do nothing, the message has been already logged by the formatter
|
|
}
|
|
};
|
|
|
|
void free_zypp(struct Zypp *zypp) noexcept {
|
|
LOG_LOCATION("Dropping libzypp");
|
|
// ensure that target is unloaded otherwise nasty things can happen if new
|
|
// zypp is created in different thread
|
|
zypp->zypp_pointer->getTarget()->unload();
|
|
zypp->zypp_pointer =
|
|
NULL; // shared ptr assignment operator will free original pointer
|
|
delete (zypp->repo_manager);
|
|
zypp->repo_manager = NULL;
|
|
}
|
|
|
|
static zypp::ZYpp::Ptr zypp_ptr() {
|
|
LOG_LOCATION("Initializing libzypp");
|
|
sd_journal_print(LOG_NOTICE, "Redirecting libzypp logs to systemd journal");
|
|
|
|
// log to systemd journal using our specific formatter
|
|
boost::shared_ptr<AgamaFormatter> formatter(new AgamaFormatter);
|
|
zypp::base::LogControl::instance().setLineFormater(formatter);
|
|
// use a dummy logger, using a NULL logger would skip the formatter completely
|
|
// so the messages would not be logged in the end
|
|
boost::shared_ptr<AgamaLogger> logger(new AgamaLogger);
|
|
zypp::base::LogControl::instance().setLineWriter(logger);
|
|
|
|
// do not do any magic waiting for lock as in agama context we work
|
|
// on our own root, so there should be no need to wait
|
|
return zypp::getZYpp();
|
|
}
|
|
|
|
void switch_target(struct Zypp *zypp, const char *root,
|
|
struct Status *status) noexcept {
|
|
std::string message("Switching target to: ");
|
|
message.append(root);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
const std::string root_str(root);
|
|
try {
|
|
zypp->zypp_pointer->initializeTarget(root_str,
|
|
false /* rebuild rpmdb: no */);
|
|
|
|
// switch cache for repositories, otherwise we run out of space in tmpfs
|
|
// see
|
|
// https://github.com/yast/yast-pkg-bindings/blob/853496f527543e6d51730fd7e3126ad94b13c303/src/PkgFunctions.cc#L496
|
|
zypp::RepoManagerOptions repo_options(root);
|
|
zypp::Pathname packages_prefix = repo_options.repoPackagesCachePath;
|
|
|
|
zypp::ResPool pool = zypp->zypp_pointer->pool();
|
|
for_(it, pool.knownRepositoriesBegin(), pool.knownRepositoriesEnd()) {
|
|
zypp::RepoInfo repo = it->info();
|
|
repo.setPackagesPath(packages_prefix / repo.escaped_alias());
|
|
|
|
MIL << "Setting package cache for repository " << repo.alias().c_str()
|
|
<< ": " << repo.packagesPath().asString().c_str() << std::endl;
|
|
|
|
it->setInfo(repo);
|
|
}
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
return;
|
|
}
|
|
|
|
STATUS_OK(status);
|
|
}
|
|
|
|
bool commit(struct Zypp *zypp, struct Status *status,
|
|
struct DownloadResolvableCallbacks *download_callbacks,
|
|
struct SecurityCallbacks *security_callbacks,
|
|
struct InstallCallbacks *install_callbacks) noexcept {
|
|
LOG_LOCATION("Starting package installation");
|
|
|
|
try {
|
|
set_zypp_resolvable_download_callbacks(download_callbacks);
|
|
set_zypp_security_callbacks(security_callbacks);
|
|
set_zypp_install_callbacks(install_callbacks);
|
|
zypp::ZYppCommitPolicy policy;
|
|
// enable preload of rpms to speed up installation
|
|
policy.downloadMode(zypp::DownloadInAdvance);
|
|
zypp::ZYppCommitResult result = zypp->zypp_pointer->commit(policy);
|
|
STATUS_OK(status);
|
|
unset_zypp_resolvable_download_callbacks();
|
|
unset_zypp_security_callbacks();
|
|
unset_zypp_install_callbacks();
|
|
return result.allDone();
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
unset_zypp_resolvable_download_callbacks();
|
|
unset_zypp_security_callbacks();
|
|
unset_zypp_install_callbacks();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// TODO: split init target into set of repo manager, initialize target and load
|
|
// target and merge it in rust
|
|
struct Zypp *init_target(const char *root, struct Status *status,
|
|
ProgressCallback progress, void *user_data) noexcept {
|
|
std::string message("Initializing target: ");
|
|
message.append(root);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (the_zypp.zypp_pointer != NULL) {
|
|
STATUS_ERROR(status, "Cannot have two init_target concurrently, "
|
|
"libzypp not ready for this. Call free_zypp first.");
|
|
return NULL;
|
|
}
|
|
|
|
const std::string root_str(root);
|
|
|
|
// create the libzypp lock also in the target directory
|
|
setenv("ZYPP_LOCKFILE_ROOT", root, 1 /* allow overwrite */);
|
|
|
|
struct Zypp *zypp = NULL;
|
|
try {
|
|
zypp::RepoManagerOptions repo_manager_options(root);
|
|
// repository manager options cannot be replaced, a new repository manager
|
|
// is needed
|
|
zypp::RepoManager *new_repo_manager =
|
|
new zypp::RepoManager(repo_manager_options);
|
|
|
|
// replace the old repository manager
|
|
if (the_zypp.repo_manager)
|
|
delete the_zypp.repo_manager;
|
|
the_zypp.repo_manager = new_repo_manager;
|
|
|
|
// TODO: localization
|
|
if (progress != NULL)
|
|
progress("Initializing the Target System", 0, 2, user_data);
|
|
the_zypp.zypp_pointer = zypp_ptr();
|
|
if (the_zypp.zypp_pointer == NULL) {
|
|
STATUS_ERROR(status, "Failed to obtain zypp pointer. "
|
|
"See journalctl for details.");
|
|
return NULL;
|
|
}
|
|
zypp = &the_zypp;
|
|
zypp->zypp_pointer->initializeTarget(root_str, false);
|
|
if (progress != NULL)
|
|
progress("Reading Installed Packages", 1, 2, user_data);
|
|
zypp->zypp_pointer->target()->load();
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
the_zypp.zypp_pointer = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
STATUS_OK(status);
|
|
return zypp;
|
|
}
|
|
|
|
void free_repository(struct Repository *repo) {
|
|
free(repo->url);
|
|
free(repo->alias);
|
|
free(repo->userName);
|
|
free(repo->serviceName);
|
|
}
|
|
|
|
void free_repository_list(struct RepositoryList *list) noexcept {
|
|
for (unsigned i = 0; i < list->size; ++i) {
|
|
free_repository(list->repos + i);
|
|
}
|
|
free(list->repos);
|
|
}
|
|
|
|
void free_status(struct Status *status) noexcept {
|
|
if (status->error != NULL) {
|
|
free(status->error);
|
|
status->error = NULL;
|
|
}
|
|
}
|
|
|
|
static zypp::Resolvable::Kind kind_to_zypp_kind(RESOLVABLE_KIND kind) {
|
|
switch (kind) {
|
|
case RESOLVABLE_PACKAGE:
|
|
return zypp::Resolvable::Kind::package;
|
|
case RESOLVABLE_SRCPACKAGE:
|
|
return zypp::Resolvable::Kind::srcpackage;
|
|
case RESOLVABLE_PATTERN:
|
|
return zypp::Resolvable::Kind::pattern;
|
|
case RESOLVABLE_PRODUCT:
|
|
return zypp::Resolvable::Kind::product;
|
|
case RESOLVABLE_PATCH:
|
|
return zypp::Resolvable::Kind::patch;
|
|
}
|
|
PANIC("Unhandled case in resolvable kind switch %i", kind);
|
|
}
|
|
|
|
static zypp::ResStatus::TransactByValue
|
|
transactby_from(enum RESOLVABLE_SELECTED who) {
|
|
switch (who) {
|
|
case RESOLVABLE_SELECTED::SOLVER_SELECTED:
|
|
return zypp::ResStatus::SOLVER;
|
|
case RESOLVABLE_SELECTED::APPLICATION_SELECTED:
|
|
return zypp::ResStatus::APPL_HIGH;
|
|
case RESOLVABLE_SELECTED::USER_SELECTED:
|
|
return zypp::ResStatus::USER;
|
|
case RESOLVABLE_SELECTED::NOT_SELECTED: {
|
|
PANIC("Unexpected value RESOLVABLE_SELECTED::NOT_SELECTED.");
|
|
}
|
|
case RESOLVABLE_SELECTED::USER_REMOVED: {
|
|
PANIC("Unexpected value RESOLVABLE_SELECTED::USER_REMOVED.");
|
|
}
|
|
}
|
|
|
|
// should not happen
|
|
PANIC("Unexpected RESOLVABLE_SELECT value %i", who);
|
|
}
|
|
|
|
void resolvable_select(struct Zypp *_zypp, const char *name,
|
|
enum RESOLVABLE_KIND kind, enum RESOLVABLE_SELECTED who,
|
|
struct Status *status) noexcept {
|
|
std::string message("Selecting resolvable: ");
|
|
message.append(name);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (who == RESOLVABLE_SELECTED::NOT_SELECTED) {
|
|
STATUS_OK(status);
|
|
return;
|
|
}
|
|
|
|
zypp::Resolvable::Kind z_kind = kind_to_zypp_kind(kind);
|
|
auto selectable = zypp::ui::Selectable::get(z_kind, name);
|
|
if (!selectable) {
|
|
STATUS_ERROR(status, "Failed to find %s with name '%s'", z_kind.c_str(),
|
|
name);
|
|
return;
|
|
}
|
|
|
|
STATUS_OK(status);
|
|
auto value = transactby_from(who);
|
|
selectable->setToInstall(value);
|
|
}
|
|
|
|
void resolvable_unselect(struct Zypp *_zypp, const char *name,
|
|
enum RESOLVABLE_KIND kind,
|
|
enum RESOLVABLE_SELECTED who,
|
|
struct Status *status) noexcept {
|
|
std::string message("Unselecting resolvable: ");
|
|
message.append(name);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
STATUS_OK(status);
|
|
if (who == RESOLVABLE_SELECTED::NOT_SELECTED) {
|
|
return;
|
|
}
|
|
|
|
zypp::Resolvable::Kind z_kind = kind_to_zypp_kind(kind);
|
|
auto selectable = zypp::ui::Selectable::get(z_kind, name);
|
|
if (!selectable) {
|
|
STATUS_ERROR(status, "Failed to find %s with name '%s'", z_kind.c_str(),
|
|
name);
|
|
return;
|
|
}
|
|
|
|
auto value = transactby_from(who);
|
|
selectable->unset(value);
|
|
}
|
|
|
|
void resolvable_reset_all(struct Zypp *_zypp) noexcept {
|
|
LOG_LOCATION("Resetting status of all resolvables");
|
|
|
|
for (auto &item : zypp::ResPool::instance())
|
|
item.statusReset();
|
|
}
|
|
|
|
static RESOLVABLE_SELECTED convert_selected(zypp::ResStatus status) {
|
|
if (status.isToBeInstalled()) {
|
|
switch (status.getTransactByValue()) {
|
|
case zypp::ResStatus::TransactByValue::USER:
|
|
return RESOLVABLE_SELECTED::USER_SELECTED;
|
|
case zypp::ResStatus::TransactByValue::APPL_HIGH:
|
|
case zypp::ResStatus::TransactByValue::APPL_LOW:
|
|
return RESOLVABLE_SELECTED::APPLICATION_SELECTED;
|
|
case zypp::ResStatus::TransactByValue::SOLVER:
|
|
return RESOLVABLE_SELECTED::SOLVER_SELECTED;
|
|
}
|
|
} else {
|
|
// distinguish between the "not selected" and "explicitly removed by user"
|
|
// states
|
|
if (status.getTransactByValue() == zypp::ResStatus::TransactByValue::USER)
|
|
return RESOLVABLE_SELECTED::USER_REMOVED;
|
|
else
|
|
return RESOLVABLE_SELECTED::NOT_SELECTED;
|
|
}
|
|
|
|
return RESOLVABLE_SELECTED::NOT_SELECTED;
|
|
}
|
|
|
|
struct Patterns get_patterns(struct Zypp *zypp,
|
|
struct Status *status) noexcept {
|
|
LOG_LOCATION("Getting patterns");
|
|
|
|
auto iterator =
|
|
zypp->zypp_pointer->pool().proxy().byKind(zypp::ResKind::pattern);
|
|
|
|
Patterns result = {
|
|
(struct Pattern *)malloc(iterator.size() * sizeof(Pattern)),
|
|
0 // initialize with zero and increase after each successful add of
|
|
// pattern info
|
|
};
|
|
|
|
for (const auto &iter : iterator) {
|
|
Pattern &pattern = result.list[result.size];
|
|
auto zypp_pattern = iter->candidateAsKind<zypp::Pattern>();
|
|
pattern.name = strdup(iter->name().c_str());
|
|
pattern.category = strdup(zypp_pattern->category().c_str());
|
|
pattern.description = strdup(zypp_pattern->description().c_str());
|
|
pattern.icon = strdup(zypp_pattern->icon().c_str());
|
|
pattern.summary = strdup(zypp_pattern->summary().c_str());
|
|
pattern.order = strdup(zypp_pattern->order().c_str());
|
|
pattern.repo_alias = strdup(zypp_pattern->repoInfo().alias().c_str());
|
|
pattern.selected = convert_selected(iter->theObj().status());
|
|
result.size++;
|
|
}
|
|
|
|
STATUS_OK(status);
|
|
return result;
|
|
}
|
|
|
|
void free_patterns(const struct Patterns *patterns) noexcept {
|
|
for (unsigned i = 0; i < patterns->size; ++i) {
|
|
free((void *)patterns->list[i].name);
|
|
free((void *)patterns->list[i].category);
|
|
free((void *)patterns->list[i].description);
|
|
free((void *)patterns->list[i].icon);
|
|
free((void *)patterns->list[i].summary);
|
|
free((void *)patterns->list[i].order);
|
|
free((void *)patterns->list[i].repo_alias);
|
|
}
|
|
free((void *)patterns->list);
|
|
}
|
|
|
|
struct Products get_products(struct Zypp *zypp,
|
|
struct Status *status) noexcept {
|
|
LOG_LOCATION("Getting products");
|
|
auto iterator =
|
|
zypp->zypp_pointer->pool().proxy().byKind(zypp::ResKind::product);
|
|
|
|
Products result = {
|
|
(struct Product *)malloc(iterator.size() * sizeof(Product)),
|
|
0 // initialize with zero and increase after each successful add of
|
|
// product info
|
|
};
|
|
|
|
for (const auto &iter : iterator) {
|
|
Product &product = result.list[result.size];
|
|
auto zypp_product = iter->candidateAsKind<zypp::Product>();
|
|
product.name = strdup(iter->name().c_str());
|
|
product.repo_alias = strdup(zypp_product->repoInfo().alias().c_str());
|
|
product.service_alias = strdup(zypp_product->repoInfo().service().c_str());
|
|
result.size++;
|
|
}
|
|
|
|
STATUS_OK(status);
|
|
return result;
|
|
}
|
|
|
|
void free_products(const struct Products *products) noexcept {
|
|
for (unsigned i = 0; i < products->size; ++i) {
|
|
free((void *)products->list[i].name);
|
|
free((void *)products->list[i].repo_alias);
|
|
free((void *)products->list[i].service_alias);
|
|
}
|
|
|
|
free((void *)products->list);
|
|
}
|
|
|
|
bool run_solver(struct Zypp *zypp, bool only_required,
|
|
struct Status *status) noexcept {
|
|
LOG_LOCATION("Running solver");
|
|
try {
|
|
STATUS_OK(status);
|
|
auto resolver = zypp->zypp_pointer->resolver();
|
|
resolver->setOnlyRequires(only_required);
|
|
// needed to get hardware and locale specific provisioning
|
|
// @ma: On a fresh install I'd recommend to set INR if you
|
|
// want HW/Filesystem/Language supporting packages to be selected.
|
|
// INR is the mode e.g. 'zypper inr' operates in, AKA InstallNewRecommends.
|
|
// Recommendations of already installed packages are usually not evaluated
|
|
// again. With INR they are.
|
|
// The Resolver flag is setIgnoreAlreadyRecommended=0.
|
|
// (addalreadyrecommended=1 is libsolv world)
|
|
resolver->setIgnoreAlreadyRecommended(false);
|
|
return resolver->resolvePool();
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
return false; // do not matter much as status indicate failure
|
|
}
|
|
}
|
|
|
|
void add_service(struct Zypp *zypp, const char *alias, const char *url,
|
|
struct Status *status) noexcept {
|
|
std::string message("Adding service: ");
|
|
message.append(alias);
|
|
message.append(" (");
|
|
message.append(filter_url(url));
|
|
message.append(")");
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::ServiceInfo zypp_service = zypp::ServiceInfo(alias);
|
|
zypp_service.setUrl(zypp::Url(url));
|
|
|
|
zypp->repo_manager->addService(zypp_service);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
bool create_solver_testcase(struct Zypp *zypp, const char *dir) noexcept {
|
|
std::string message("Creating solver testcase in directory ");
|
|
message.append(dir);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
return zypp->zypp_pointer->resolver()->createSolverTestcase(dir);
|
|
}
|
|
|
|
void refresh_service(struct Zypp *zypp, const char *alias,
|
|
struct Status *status) noexcept {
|
|
std::string message("Refreshing service: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::ServiceInfo service = zypp->repo_manager->getService(alias);
|
|
if (service == zypp::ServiceInfo::noService) {
|
|
STATUS_ERROR(status,
|
|
"Cannot refresh service with alias %s. Service not found.",
|
|
alias);
|
|
return;
|
|
}
|
|
zypp->repo_manager->refreshService(service);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void refresh_repository(struct Zypp *zypp, const char *alias,
|
|
struct Status *status,
|
|
struct DownloadProgressCallbacks *callbacks,
|
|
struct SecurityCallbacks *security_callbacks) noexcept {
|
|
std::string message("Refreshing repository: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::RepoInfo zypp_repo = zypp->repo_manager->getRepo(alias);
|
|
if (zypp_repo == zypp::RepoInfo::noRepo) {
|
|
STATUS_ERROR(status, "Cannot refresh repo with alias %s. Repo not found.",
|
|
alias);
|
|
return;
|
|
}
|
|
|
|
set_zypp_download_callbacks(callbacks);
|
|
set_zypp_security_callbacks(security_callbacks);
|
|
zypp->repo_manager->refreshMetadata(
|
|
zypp_repo,
|
|
zypp::RepoManager::RawMetadataRefreshPolicy::RefreshIfNeeded);
|
|
STATUS_OK(status);
|
|
unset_zypp_download_callbacks();
|
|
unset_zypp_security_callbacks();
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
unset_zypp_security_callbacks();
|
|
unset_zypp_download_callbacks(); // TODO: we can add C++ final action helper
|
|
// if it is more common
|
|
}
|
|
}
|
|
|
|
bool is_local_url(const char *url, struct Status *status) noexcept {
|
|
try {
|
|
zypp::Url z_url(url);
|
|
STATUS_OK(status);
|
|
return z_url.schemeIsLocal();
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
unsigned packages_to_install(struct Zypp *zypp) noexcept {
|
|
LOG_LOCATION("Getting number of packages to install");
|
|
return zypp::ResPool::instance()
|
|
.byStatus(&zypp::ResStatus::isToBeInstalled)
|
|
.size();
|
|
}
|
|
|
|
static bool package_check(Zypp *zypp, const char *tag, bool selected,
|
|
Status *status) noexcept {
|
|
std::string message("Checking package: ");
|
|
message.append(tag);
|
|
if (selected)
|
|
message.append(" (selected)");
|
|
else
|
|
message.append(" (available)");
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
try {
|
|
std::string s_tag(tag);
|
|
if (s_tag.empty()) {
|
|
STATUS_ERROR(status, "Internal Error: Package tag is empty.");
|
|
return false;
|
|
}
|
|
|
|
// look for packages
|
|
zypp::Capability cap(s_tag, zypp::ResKind::package);
|
|
zypp::sat::WhatProvides possibleProviders(cap);
|
|
|
|
// if we check only for availability, then just check that quickly
|
|
if (!selected)
|
|
return !possibleProviders.empty();
|
|
|
|
for (auto iter = possibleProviders.begin(); iter != possibleProviders.end();
|
|
++iter) {
|
|
zypp::PoolItem provider = zypp::ResPool::instance().find(*iter);
|
|
// is it installed? if so return true, otherwise check next candidate
|
|
if (provider.status().isToBeInstalled())
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool is_package_available(Zypp *zypp, const char *tag,
|
|
Status *status) noexcept {
|
|
return package_check(zypp, tag, false, status);
|
|
}
|
|
|
|
bool is_package_selected(Zypp *zypp, const char *tag, Status *status) noexcept {
|
|
return package_check(zypp, tag, true, status);
|
|
}
|
|
|
|
void add_repository(struct Zypp *zypp, const char *alias, const char *url,
|
|
struct Status *status, ZyppProgressCallback callback,
|
|
void *user_data) noexcept {
|
|
std::string message("Adding repository: ");
|
|
message.append(alias);
|
|
message.append(" (");
|
|
message.append(filter_url(url));
|
|
message.append(")");
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
auto zypp_callback = create_progress_callback(callback, user_data);
|
|
zypp::RepoInfo zypp_repo = zypp::RepoInfo();
|
|
zypp_repo.setBaseUrl(zypp::Url(url));
|
|
zypp_repo.setAlias(alias);
|
|
|
|
zypp->repo_manager->addRepository(zypp_repo, zypp_callback);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void disable_repository(struct Zypp *zypp, const char *alias,
|
|
struct Status *status) noexcept {
|
|
std::string message("Disabling repository: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::RepoInfo r_info = zypp->repo_manager->getRepo(alias);
|
|
r_info.setEnabled(false);
|
|
zypp->repo_manager->modifyRepository(r_info);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void set_repository_url(struct Zypp *zypp, const char *alias, const char *url,
|
|
struct Status *status) noexcept {
|
|
|
|
std::string message("Setting repository url: ");
|
|
message.append(alias);
|
|
message.append(" (");
|
|
message.append(filter_url(url));
|
|
message.append(")");
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::RepoInfo r_info = zypp->repo_manager->getRepo(alias);
|
|
zypp::Url z_url(url);
|
|
r_info.setBaseUrl(z_url);
|
|
zypp->repo_manager->modifyRepository(r_info);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void remove_repository(struct Zypp *zypp, const char *alias,
|
|
struct Status *status, ZyppProgressCallback callback,
|
|
void *user_data) noexcept {
|
|
std::string message("Removing repository: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
auto zypp_callback = create_progress_callback(callback, user_data);
|
|
zypp::RepoInfo zypp_repo = zypp::RepoInfo();
|
|
zypp_repo.setAlias(alias); // alias should be unique, so it should always
|
|
// match correct repo
|
|
|
|
zypp->repo_manager->removeRepository(zypp_repo, zypp_callback);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
struct RepositoryList list_repositories(struct Zypp *zypp,
|
|
struct Status *status) noexcept {
|
|
LOG_LOCATION("Listing repositories");
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return {0, NULL};
|
|
}
|
|
|
|
std::list<zypp::RepoInfo> zypp_repos =
|
|
zypp->repo_manager->knownRepositories();
|
|
const std::list<zypp::RepoInfo>::size_type size = zypp_repos.size();
|
|
struct Repository *repos =
|
|
(struct Repository *)malloc(size * sizeof(struct Repository));
|
|
// TODO: error handling
|
|
unsigned res_i = 0;
|
|
for (auto iter = zypp_repos.begin(); iter != zypp_repos.end(); ++iter) {
|
|
struct Repository *new_repo = repos + res_i++;
|
|
new_repo->enabled = iter->enabled();
|
|
new_repo->url = strdup(iter->url().asString().c_str());
|
|
new_repo->alias = strdup(iter->alias().c_str());
|
|
new_repo->userName = strdup(iter->asUserString().c_str());
|
|
new_repo->serviceName = strdup(iter->service().c_str());
|
|
}
|
|
|
|
struct RepositoryList result = {static_cast<unsigned>(size), repos};
|
|
STATUS_OK(status);
|
|
return result;
|
|
}
|
|
|
|
void load_repository_cache(struct Zypp *zypp, const char *alias,
|
|
struct Status *status) noexcept {
|
|
std::string message("Loading repository cache: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
}
|
|
try {
|
|
zypp::RepoInfo zypp_repo = zypp->repo_manager->getRepo(alias);
|
|
if (zypp_repo == zypp::RepoInfo::noRepo) {
|
|
STATUS_ERROR(status, "Cannot load repo with alias %s. Repo not found.",
|
|
alias);
|
|
return;
|
|
}
|
|
|
|
// NOTE: loadFromCache has an optional `progress` parameter but it ignores
|
|
// it anyway
|
|
zypp->repo_manager->loadFromCache(zypp_repo);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void build_repository_cache(struct Zypp *zypp, const char *alias,
|
|
struct Status *status,
|
|
ZyppProgressCallback callback,
|
|
void *user_data) noexcept {
|
|
std::string message("Building repository cache: ");
|
|
message.append(alias);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
if (zypp->repo_manager == NULL) {
|
|
STATUS_ERROR(status, "Internal Error: Repo manager is not initialized.");
|
|
return;
|
|
}
|
|
try {
|
|
zypp::RepoInfo zypp_repo = zypp->repo_manager->getRepo(alias);
|
|
if (zypp_repo == zypp::RepoInfo::noRepo) {
|
|
STATUS_ERROR(status, "Cannot load repo with alias %s. Repo not found.",
|
|
alias);
|
|
return;
|
|
}
|
|
|
|
auto progress = create_progress_callback(callback, user_data);
|
|
zypp->repo_manager->buildCache(
|
|
zypp_repo, zypp::RepoManagerFlags::BuildIfNeeded, progress);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void import_gpg_key(struct Zypp *zypp, const char *const pathname,
|
|
struct Status *status) noexcept {
|
|
std::string message("Importing GPG key: ");
|
|
message.append(pathname);
|
|
LOG_LOCATION(message.c_str());
|
|
|
|
try {
|
|
zypp::filesystem::Pathname path(pathname);
|
|
zypp::PublicKey key(path);
|
|
// Keys that are unknown (not imported).
|
|
// or known-but-untrusted (weird in-between state, see KeyRing_test.cc)
|
|
// will trigger "Trust this?" callbacks.
|
|
bool trusted = true;
|
|
zypp->zypp_pointer->keyRing()->importKey(key, trusted);
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
|
|
void get_space_usage(struct Zypp *zypp, struct Status *status,
|
|
struct MountPoint *mount_points,
|
|
unsigned mount_points_size) noexcept {
|
|
LOG_LOCATION("Getting disk space usage");
|
|
|
|
try {
|
|
zypp::DiskUsageCounter::MountPointSet mount_points_set;
|
|
for (unsigned i = 0; i < mount_points_size; ++i) {
|
|
enum zypp::DiskUsageCounter::MountPoint::Hint hint =
|
|
mount_points[i].grow_only
|
|
? zypp::DiskUsageCounter::MountPoint::Hint::Hint_growonly
|
|
: zypp::DiskUsageCounter::MountPoint::Hint::NoHint;
|
|
zypp::DiskUsageCounter::MountPoint mp(mount_points[i].directory,
|
|
mount_points[i].filesystem, 0, 0, 0,
|
|
0, hint);
|
|
mount_points_set.insert(mp);
|
|
}
|
|
zypp->zypp_pointer->setPartitions(mount_points_set);
|
|
zypp::DiskUsageCounter::MountPointSet computed_set =
|
|
zypp->zypp_pointer->diskUsage();
|
|
for (unsigned i = 0; i < mount_points_size; ++i) {
|
|
auto mp =
|
|
std::find_if(computed_set.begin(), computed_set.end(),
|
|
[mount_points, i](zypp::DiskUsageCounter::MountPoint m) {
|
|
return m.dir == mount_points[i].directory;
|
|
});
|
|
if (mp == mount_points_set.end()) {
|
|
// mount point not found. Should not happen.
|
|
STATUS_ERROR(status, "Internal Error:Mount point not found.");
|
|
return;
|
|
}
|
|
mount_points[i].used_size = mp->pkg_size;
|
|
}
|
|
STATUS_OK(status);
|
|
} catch (zypp::Exception &excpt) {
|
|
STATUS_EXCEPT(status, excpt);
|
|
}
|
|
}
|
|
}
|