Merge branch 'cpp-umount' into calamares

This commit is contained in:
Adriaan de Groot 2022-01-18 13:27:34 +01:00
commit 79683dd83d
11 changed files with 351 additions and 197 deletions

View File

@ -14,6 +14,7 @@
#include "partition/Sync.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/String.h"
#include <QDir>
#include <QTemporaryDir>
@ -92,7 +93,7 @@ struct TemporaryMount::Private
TemporaryMount::TemporaryMount( const QString& devicePath, const QString& filesystemName, const QString& options )
: m_d( std::make_unique<Private>() )
: m_d( std::make_unique< Private >() )
{
m_d->m_devicePath = devicePath;
m_d->m_mountDir.setAutoRemove( false );
@ -123,5 +124,32 @@ TemporaryMount::path() const
return m_d ? m_d->m_mountDir.path() : QString();
}
QList< MtabInfo >
MtabInfo::fromMtabFilteredByPrefix( const QString& mountPrefix, const QString& mtabPath )
{
QFile f( mtabPath.isEmpty() ? "/etc/mtab" : mtabPath );
if ( !f.open( QIODevice::ReadOnly ) )
{
return {};
}
QTextStream in( &f );
QList< MtabInfo > l;
while ( !f.atEnd() )
{
QStringList line = in.readLine().split( ' ', SplitSkipEmptyParts );
if ( line.length() == 3 && !line[ 0 ].startsWith( '#' ) )
{
// Lines have format: <device> <mountpoint> <options>, so check
// the mountpoint field. Everything starts with an empty string.
if ( line[ 1 ].startsWith( mountPrefix ) )
{
l.append( { line[ 0 ], line[ 1 ] } );
}
}
}
return l;
}
} // namespace Partition
} // namespace CalamaresUtils

View File

@ -14,6 +14,7 @@
#include "DllMacro.h"
#include <QList>
#include <QString>
#include <QStringList>
@ -50,6 +51,13 @@ DLLEXPORT int mount( const QString& devicePath,
*/
DLLEXPORT int unmount( const QString& path, const QStringList& options = QStringList() );
/** @brief Mount and automatically unmount a device
*
* The TemporaryMount object mounts a filesystem, and is like calling
* the mount() function, above. When the object is destroyed, unmount()
* is called with suitable options to undo the original mount.
*/
class DLLEXPORT TemporaryMount
{
public:
@ -68,6 +76,36 @@ private:
std::unique_ptr< Private > m_d;
};
/** @brief Information about a mount point from /etc/mtab
*
* Entries in /etc/mtab are of the form: <device> <mountpoint> <other>
* This struct only stores device and mountpoint.
*
* The main way of getting these structs is to call fromMtab() to read
* an /etc/mtab-like file and storing all of the entries from it.
*/
struct DLLEXPORT MtabInfo
{
QString device;
QString mountPoint;
/** @brief Reads an mtab-like file and returns the entries from it
*
* When @p mtabPath is given, that file is read. If the given name is
* empty (e.g. the default) then /etc/mtab is read, instead.
*
* If @p mountPrefix is given, then only entries that have a mount point
* that starts with that prefix are returned.
*/
static QList< MtabInfo > fromMtabFilteredByPrefix( const QString& mountPrefix = QString(),
const QString& mtabPath = QString() );
/// @brief Predicate to sort MtabInfo objects by device-name
static bool deviceOrder( const MtabInfo& a, const MtabInfo& b ) { return a.device > b.device; }
/// @brief Predicate to sort MtabInfo objects by mount-point
static bool mountPointOrder( const MtabInfo& a, const MtabInfo& b ) { return a.mountPoint > b.mountPoint; }
};
} // namespace Partition
} // namespace CalamaresUtils

View File

@ -9,6 +9,7 @@
#include "ClearTempMountsJob.h"
#include "partition/Mount.h"
#include "utils/Logger.h"
#include "utils/String.h"
@ -45,51 +46,23 @@ ClearTempMountsJob::exec()
{
Logger::Once o;
// Fetch a list of current mounts to Calamares temporary directories.
QList< QPair< QString, QString > > lst;
QFile mtab( "/etc/mtab" );
if ( !mtab.open( QFile::ReadOnly | QFile::Text ) )
{
return Calamares::JobResult::error( tr( "Cannot get list of temporary mounts." ) );
}
using MtabInfo = CalamaresUtils::Partition::MtabInfo;
auto targetMounts = MtabInfo::fromMtabFilteredByPrefix( QStringLiteral( "/tmp/calamares-" ) );
cVerbose() << o << "Opened mtab. Lines:";
QTextStream in( &mtab );
QString lineIn = in.readLine();
while ( !lineIn.isNull() )
{
QStringList line = lineIn.split( ' ', SplitSkipEmptyParts );
cVerbose() << o << line.join( ' ' );
QString device = line.at( 0 );
QString mountPoint = line.at( 1 );
if ( mountPoint.startsWith( "/tmp/calamares-" ) )
{
lst.append( qMakePair( device, mountPoint ) );
}
lineIn = in.readLine();
}
if ( lst.empty() )
if ( targetMounts.isEmpty() )
{
return Calamares::JobResult::ok();
}
std::sort(
lst.begin(), lst.end(), []( const QPair< QString, QString >& a, const QPair< QString, QString >& b ) -> bool {
return a.first > b.first;
} );
std::sort( targetMounts.begin(), targetMounts.end(), MtabInfo::mountPointOrder );
QStringList goodNews;
QProcess process;
for ( const auto& line : qAsConst( lst ) )
for ( const auto& m : qAsConst( targetMounts ) )
{
QString partPath = line.second;
cDebug() << o << "Will try to umount path" << partPath;
process.start( "umount", { "-lv", partPath } );
process.waitForFinished();
if ( process.exitCode() == 0 )
cDebug() << o << "Will try to umount path" << m.mountPoint;
if ( CalamaresUtils::Partition::unmount( m.mountPoint, { "-lv" } ) == 0 )
{
goodNews.append( QString( "Successfully unmounted %1." ).arg( partPath ) );
// Returns the program's exit code, so 0 is success
goodNews.append( QString( "Successfully unmounted %1." ).arg( m.mountPoint ) );
}
}

View File

@ -0,0 +1,19 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
calamares_add_plugin( umount
TYPE job
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
UmountJob.cpp
SHARED_LIB
EMERGENCY
)
calamares_add_test(
umounttest
SOURCES
Tests.cpp
)

View File

@ -0,0 +1,52 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "UmountJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include <QDir>
#include <QFile>
#include <QtTest/QtTest>
// Internals of UmountJob.cpp
// Actual tests
class UmountTests : public QObject
{
Q_OBJECT
public:
UmountTests() {}
~UmountTests() override {}
private Q_SLOTS:
void initTestCase();
void testTrue();
};
void
UmountTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
}
void
UmountTests::testTrue()
{
QVERIFY( true );
}
QTEST_GUILESS_MAIN( UmountTests )
#include "utils/moc-warnings.h"
#include "Tests.moc"

View File

@ -0,0 +1,158 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* Tags from the Python version of this module:
* SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org>
* SPDX-FileCopyrightText: 2016 Anke Boersma <demm@kaosx.us>
* SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
* Tags for the C++ version of this module:
* SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#include "UmountJob.h"
#include "partition/Mount.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include <QCoreApplication>
#include <QDir>
#include <QList>
UmountJob::UmountJob( QObject* parent )
: Calamares::CppJob( parent )
{
}
UmountJob::~UmountJob() {}
QString
UmountJob::prettyName() const
{
return tr( "Unmount file systems." );
}
static Calamares::JobResult
unmountTargetMounts( const QString& rootMountPoint )
{
QDir targetMount( rootMountPoint );
if ( !targetMount.exists() )
{
return Calamares::JobResult::internalError(
QCoreApplication::translate( UmountJob::staticMetaObject.className(), "Could not unmount target system." ),
QCoreApplication::translate( UmountJob::staticMetaObject.className(),
"The target system is not mounted at '%1'." )
.arg( rootMountPoint ),
Calamares::JobResult::GenericError );
}
QString targetMountPath = targetMount.absolutePath();
if ( !targetMountPath.endsWith( '/' ) )
{
targetMountPath.append( '/' );
}
using MtabInfo = CalamaresUtils::Partition::MtabInfo;
auto targetMounts = MtabInfo::fromMtabFilteredByPrefix( targetMountPath );
std::sort( targetMounts.begin(), targetMounts.end(), MtabInfo::mountPointOrder );
for ( const auto& m : qAsConst( targetMounts ) )
{
if ( CalamaresUtils::Partition::unmount( m.mountPoint, { "-lv" } ) )
{
// Returns the program's exit code, so 0 is success
return Calamares::JobResult::error(
QCoreApplication::translate( UmountJob::staticMetaObject.className(),
"Could not unmount target system." ),
QCoreApplication::translate( UmountJob::staticMetaObject.className(),
"The device '%1' is mounted in the target system. It is mounted at '%2'. "
"The device could not be unmounted." )
.arg( m.device, m.mountPoint ) );
}
}
return Calamares::JobResult::ok();
}
static Calamares::JobResult
exportZFSPools( const QString& rootMountPoint )
{
auto* gs = Calamares::JobQueue::instance()->globalStorage();
QStringList poolNames;
{
// The pools are dictionaries / VariantMaps
auto zfs_pool_list = gs->value( "zfsPoolInfo" ).toList();
for ( const auto& v : zfs_pool_list )
{
auto m = v.toMap();
QString poolName = m.value( "poolName" ).toString();
if ( !poolName.isEmpty() )
{
poolNames.append( poolName );
}
}
poolNames.sort();
}
for ( const auto& poolName : poolNames )
{
auto result = CalamaresUtils::System::runCommand( { "zpool", "export", poolName }, std::chrono::seconds( 30 ) );
if ( result.getExitCode() )
{
cWarning() << "Failed to export pool" << result.getOutput();
}
}
// Exporting ZFS pools does not cause the install to fail
return Calamares::JobResult::ok();
}
Calamares::JobResult
UmountJob::exec()
{
const auto* sys = CalamaresUtils::System::instance();
if ( !sys )
{
return Calamares::JobResult::internalError(
"UMount", tr( "No target system available." ), Calamares::JobResult::InvalidConfiguration );
}
Calamares::GlobalStorage* gs
= Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
if ( !gs || gs->value( "rootMountPoint" ).toString().isEmpty() )
{
return Calamares::JobResult::internalError(
"UMount", tr( "No rootMountPoint is set." ), Calamares::JobResult::InvalidConfiguration );
}
// Do the unmounting of target-system filesystems
{
auto r = unmountTargetMounts( gs->value( "rootMountPoint" ).toString() );
if ( !r )
{
return r;
}
}
// For ZFS systems, export the pools
{
auto r = exportZFSPools( gs->value( "rootMountPoint" ).toString() );
if ( !r )
{
return r;
}
}
return Calamares::JobResult::ok();
}
void
UmountJob::setConfigurationMap( const QVariantMap& map )
{
}
CALAMARES_PLUGIN_FACTORY_DEFINITION( UmountJobFactory, registerPlugin< UmountJob >(); )

View File

@ -0,0 +1,41 @@
/* === This file is part of Calamares - <https://calamares.io> ===
*
* SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Calamares is Free Software: see the License-Identifier above.
*
*/
#ifndef UMOUNTJOB_H
#define UMOUNTJOB_H
#include "CppJob.h"
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include <QObject>
#include <QStringList>
#include <QVariantMap>
/** @brief Write 'random' data: machine id, entropy, UUIDs
*
*/
class PLUGINDLLEXPORT UmountJob : public Calamares::CppJob
{
Q_OBJECT
public:
explicit UmountJob( QObject* parent = nullptr );
~UmountJob() override;
QString prettyName() const override;
Calamares::JobResult exec() override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( UmountJobFactory )
#endif // UMOUNTJOB_H

View File

@ -1,126 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org>
# SPDX-FileCopyrightText: 2016 Anke Boersma <demm@kaosx.us>
# SPDX-FileCopyrightText: 2018 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Calamares is Free Software: see the License-Identifier above.
#
import os
import subprocess
import shutil
import libcalamares
from libcalamares.utils import gettext_path, gettext_languages
import gettext
_translation = gettext.translation("calamares-python",
localedir=gettext_path(),
languages=gettext_languages(),
fallback=True)
_ = _translation.gettext
_n = _translation.ngettext
def pretty_name():
return _( "Unmount file systems." )
def list_mounts(root_mount_point):
""" List mount points.
:param root_mount_point:
:return:
"""
lst = []
root_mount_point = os.path.normpath(root_mount_point)
for line in open("/etc/mtab").readlines():
device, mount_point, _ = line.split(" ", 2)
if os.path.commonprefix([root_mount_point, mount_point]) == root_mount_point:
lst.append((device, mount_point))
return lst
def export_zpools(root_mount_point):
""" Exports the zpools if defined in global storage
:param root_mount_point: The absolute path to the root of the install
:return:
"""
try:
zfs_pool_list = libcalamares.globalstorage.value("zfsPoolInfo")
zfs_pool_list.sort(reverse=True, key=lambda x: x["poolName"])
if zfs_pool_list:
for zfs_pool in zfs_pool_list:
try:
libcalamares.utils.host_env_process_output(['zpool', 'export', zfs_pool["poolName"]])
except subprocess.CalledProcessError:
libcalamares.utils.warning("Failed to export zpool")
except Exception as e:
# If this fails it shouldn't cause the installation to fail
libcalamares.utils.warning("Received exception while exporting zpools: " + format(e))
pass
def run():
""" Unmounts given mountpoints in decreasing order.
:return:
"""
root_mount_point = libcalamares.globalstorage.value("rootMountPoint")
if(libcalamares.job.configuration and
"srcLog" in libcalamares.job.configuration or
"destLog" in libcalamares.job.configuration):
libcalamares.utils.error("Log-file preserving is **deprecated** in the *umount* module and removed in the next release")
if(libcalamares.job.configuration and
"srcLog" in libcalamares.job.configuration and
"destLog" in libcalamares.job.configuration):
log_source = libcalamares.job.configuration["srcLog"]
log_destination = libcalamares.job.configuration["destLog"]
# Relocate log_destination into target system
log_destination = '{!s}/{!s}'.format(root_mount_point, log_destination)
# Make sure source is a string
log_source = '{!s}'.format(log_source)
# copy installation log before umount
if os.path.exists(log_source):
try:
shutil.copy2(log_source, log_destination)
except Exception as e:
libcalamares.utils.warning("Could not preserve file {!s}, "
"error {!s}".format(log_source, e))
if not root_mount_point:
return ("No mount point for root partition in globalstorage",
"globalstorage does not contain a \"rootMountPoint\" key, "
"doing nothing")
if not os.path.exists(root_mount_point):
return ("Bad mount point for root partition in globalstorage",
"globalstorage[\"rootMountPoint\"] is \"{}\", which does not "
"exist, doing nothing".format(root_mount_point))
lst = list_mounts(root_mount_point)
# Sort the list by mount point in decreasing order. This way we can be sure
# we unmount deeper dirs first.
lst.sort(key=lambda x: x[1], reverse=True)
for device, mount_point in lst:
# On success, no output; if the command fails, its output is
# in the exception object.
subprocess.check_output(["umount", "-lv", mount_point], stderr=subprocess.STDOUT)
export_zpools(root_mount_point)
os.rmdir(root_mount_point)
return None

View File

@ -1,8 +0,0 @@
# SPDX-FileCopyrightText: no
# SPDX-License-Identifier: CC0-1.0
---
type: "job"
name: "umount"
interface: "python"
script: "main.py"
emergency: true

View File

@ -4,31 +4,11 @@
### Umount Module
#
# This module represents the last part of the installation, the unmounting
# of partitions used for the install. It is also the last place where it
# is possible to copy files to the target system.
#
# The "copy log files" functionality is deprecated; use the *preservefiles*
# module instead, which is more flexible.
#
# of partitions used for the install. After this, there is no regular way
# to modify the target system anymore.
#
---
# This is a **deprecated** example. Use the *preservefiles* module
# instead, where the equivalent configuration is this:
#
# files:
# - from: log
# dest: /var/log/installation.log
#
# Note that the "equivalent configuration" always finds the log,
# and is not dependent on specific user names or the vagaries of
# polkit configuration -- so it is a **better** "equivalent".
#
# example when using a log created by `sudo calamares -d`:
#srcLog: "/home/live/installation.log"
#destLog: "/var/log/installation.log"
srcLog: "/bogus/just/do/not/use/this/anymore.txt"
# Setting emergency to true will make it so this module is still run
# when a prior module fails
# emergency: true
emergency: false

View File

@ -6,5 +6,4 @@ $id: https://calamares.io/schemas/umount
additionalProperties: false
type: object
properties:
srcLog: { type: string }
destLog: { type: string }
emergency: { type: boolean }