[luksopenswaphookcfg] Port to C++

Merge pull request #1845 from calamares/issue-1659

FIXES #1659
FIXES #1644
This commit is contained in:
Adriaan de Groot 2021-12-17 20:31:05 +01:00 committed by GitHub
commit 7fc2859f23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 558 additions and 107 deletions

View File

@ -0,0 +1,22 @@
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2021 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: BSD-2-Clause
#
# Because LUKS Open Swap Hook (Job) is such a mouthful, we'll
# use LOSH all over the place as a shorthand.
calamares_add_plugin( luksopenswaphook
TYPE job
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
LOSHJob.cpp
SHARED_LIB
)
calamares_add_test(
luksopenswaphooktest
SOURCES
LOSHJob.cpp
Tests.cpp
)

View File

@ -0,0 +1,66 @@
/* === 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
*
*/
#ifndef LUKSOPENSWAPHOOKCFG_LOSHINFO_H
#define LUKSOPENSWAPHOOKCFG_LOSHINFO_H
#include <QString>
/** @brief Information needed to create a suitable config file
*
* The LUKS swap configuration has a handful of keys that need to
* be written to the config file. This struct holds those keys
* and can find the key values from Global Storage (where the
* *partition* module sets them).
*/
struct LOSHInfo
{
// Member names copied from Python code
QString swap_outer_uuid;
QString swap_mapper_name;
QString mountable_keyfile_device;
QString swap_device_path;
QString keyfile_device_mount_options;
bool isValid() const { return !swap_device_path.isEmpty(); }
/** @brief Helper method for doing key-value replacements
*
* Given a named @p key (e.g. "duck", or "swap_device"), returns the
* value set for that key. Invalid keys (e.g. "duck") return an empty string.
*/
QString replacementFor( const QString& key ) const
{
if ( key == QStringLiteral( "swap_device" ) )
{
return swap_device_path;
}
if ( key == QStringLiteral( "crypt_swap_name" ) )
{
return swap_mapper_name;
}
if ( key == QStringLiteral( "keyfile_device" ) )
{
return mountable_keyfile_device;
}
if ( key == QStringLiteral( "keyfile_filename" ) )
{
return QStringLiteral( "crypto_keyfile.bin" );
}
if ( key == QStringLiteral( "keyfile_device_mount_options" ) )
{
return keyfile_device_mount_options;
}
return QString();
}
/** @brief Creates a struct from information already set in GS
*
*/
static LOSHInfo fromGlobalStorage();
};
#endif

View File

@ -0,0 +1,180 @@
/* === 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
*
*/
#include "LOSHJob.h"
#include "LOSHInfo.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Permissions.h"
#include "utils/PluginFactory.h"
#include "utils/String.h"
#include "utils/Variant.h"
#include <QList>
#include <QObject>
#include <QVariantMap>
LOSHJob::LOSHJob( QObject* parent )
: Calamares::CppJob( parent )
{
}
LOSHJob::~LOSHJob() {}
QString
LOSHJob::prettyName() const
{
return tr( "Configuring encrypted swap." );
}
STATICTEST QString
get_assignment_part( const QString& line )
{
static QRegularExpression re( "^[# \\t]*([A-Za-z_]+)[ \\t]*=" );
auto m = re.match( line );
if ( m.hasMatch() )
{
return m.captured( 1 );
}
return QString();
}
/** Writes the config file at @p path
*
* NOTE: @p path is relative to the target system, not an absolute path.
*/
STATICTEST void
write_openswap_conf( const QString& path, QStringList& contents, const LOSHInfo& info )
{
if ( info.isValid() )
{
for ( auto& line : contents )
{
const QString key = get_assignment_part( line );
QString replacement = info.replacementFor( key );
if ( !replacement.isEmpty() )
{
line.clear();
line.append( QStringLiteral( "%1=%2" ).arg( key, replacement ) );
}
}
cDebug() << "Writing" << contents.length() << "line configuration to" << path;
// \n between each two lines, and a \n at the end
CalamaresUtils::System::instance()->createTargetFile(
path, contents.join( '\n' ).append( '\n' ).toUtf8(), CalamaresUtils::System::WriteMode::Overwrite );
}
else
{
cDebug() << "Will not write an invalid configuration to" << path;
}
}
Calamares::JobResult
LOSHJob::exec()
{
const auto* sys = CalamaresUtils::System::instance();
if ( !sys )
{
return Calamares::JobResult::internalError(
"LuksOpenSwapHook", 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(
"LuksOpenSwapHook", tr( "No rootMountPoint is set." ), Calamares::JobResult::InvalidConfiguration );
}
if ( m_configFilePath.isEmpty() )
{
return Calamares::JobResult::internalError(
"LuksOpenSwapHook", tr( "No configFilePath is set." ), Calamares::JobResult::InvalidConfiguration );
}
QStringList contents = sys->readTargetFile( m_configFilePath );
if ( contents.isEmpty() )
{
contents << QStringLiteral( "# swap_device=" ) << QStringLiteral( "# crypt_swap_name=" )
<< QStringLiteral( "# keyfile_device=" ) << QStringLiteral( "# keyfile_filename=" )
<< QStringLiteral( "# keyfile_device_mount_options" );
}
write_openswap_conf( m_configFilePath, contents, LOSHInfo::fromGlobalStorage() );
return Calamares::JobResult::ok();
}
void
LOSHJob::setConfigurationMap( const QVariantMap& configurationMap )
{
m_configFilePath = CalamaresUtils::getString(
configurationMap, QStringLiteral( "configFilePath" ), QStringLiteral( "/etc/openswap.conf" ) );
}
STATICTEST void
globalStoragePartitionInfo( Calamares::GlobalStorage* gs, LOSHInfo& info )
{
if ( !gs )
{
return;
}
QVariantList l = gs->value( "partitions" ).toList();
if ( l.isEmpty() )
{
return;
}
for ( const auto& pv : l )
{
const QVariantMap partition = pv.toMap();
if ( !partition.isEmpty() )
{
QString mountPoint = partition.value( "mountPoint" ).toString();
QString fileSystem = partition.value( "fs" ).toString();
QString luksMapperName = partition.value( "luksMapperName" ).toString();
// if partition["fs"] == "linuxswap" and "luksMapperName" in partition:
if ( fileSystem == QStringLiteral( "linuxswap" ) && !luksMapperName.isEmpty() )
{
info.swap_outer_uuid = partition.value( "luksUuid" ).toString();
info.swap_mapper_name = luksMapperName;
}
else if ( mountPoint == QStringLiteral( "/" ) && !luksMapperName.isEmpty() )
{
info.mountable_keyfile_device = QStringLiteral( "/dev/mapper/" ) + luksMapperName;
}
}
}
if ( !info.mountable_keyfile_device.isEmpty() && !info.swap_outer_uuid.isEmpty() )
{
info.swap_device_path = QStringLiteral( "/dev/disk/by-uuid/" ) + info.swap_outer_uuid;
}
QString btrfsRootSubvolume = gs->value( "btrfsRootSubvolume" ).toString();
if ( !btrfsRootSubvolume.isEmpty() )
{
CalamaresUtils::removeLeading( btrfsRootSubvolume, '/' );
info.keyfile_device_mount_options
= QStringLiteral( "keyfile_device_mount_options=--options=subvol=" ) + btrfsRootSubvolume;
}
}
LOSHInfo
LOSHInfo::fromGlobalStorage()
{
LOSHInfo i {};
globalStoragePartitionInfo(
Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr, i );
return i;
}
CALAMARES_PLUGIN_FACTORY_DEFINITION( LOSHJobFactory, registerPlugin< LOSHJob >(); )

View File

@ -0,0 +1,37 @@
/* === 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
*
*/
#ifndef LUKSOPENSWAPHOOKCFG_LOSHJOB_H
#define LUKSOPENSWAPHOOKCFG_LOSHJOB_H
#include "CppJob.h"
#include "DllMacro.h"
#include "utils/PluginFactory.h"
#include <QString>
#include <QVariantMap>
class PLUGINDLLEXPORT LOSHJob : public Calamares::CppJob
{
Q_OBJECT
public:
explicit LOSHJob( QObject* parent = nullptr );
~LOSHJob() override;
QString prettyName() const override;
Calamares::JobResult exec() override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
private:
QString m_configFilePath;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( LOSHJobFactory )
#endif

View File

@ -0,0 +1,253 @@
/* === 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
*
*/
#include "LOSHInfo.h"
#include "LOSHJob.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include <QtTest/QtTest>
// LOSH = LUKS Open Swap Hook (Job)
// Implementation details
extern QString get_assignment_part( const QString& line );
extern void write_openswap_conf( const QString& path, QStringList& contents, const LOSHInfo& info );
class LOSHTests : public QObject
{
Q_OBJECT
public:
LOSHTests();
~LOSHTests() override {}
private Q_SLOTS:
void initTestCase();
void testAssignmentExtraction_data();
void testAssignmentExtraction();
void testLOSHInfo();
void testConfigWriting();
void testJob();
};
LOSHTests::LOSHTests() {}
void
LOSHTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
cDebug() << "LOSH test started.";
}
void
LOSHTests::testAssignmentExtraction_data()
{
QTest::addColumn< QString >( "line" );
QTest::addColumn< QString >( "match" );
QTest::newRow( "empty" ) << QString() << QString();
QTest::newRow( "comment-only1" ) << QStringLiteral( "# " ) << QString();
QTest::newRow( "comment-only2" ) << QStringLiteral( "###" ) << QString();
QTest::newRow( "comment-only3" ) << QStringLiteral( "# # #" ) << QString();
QTest::newRow( "comment-text" ) << QStringLiteral( "# NOTE:" ) << QString();
QTest::newRow( "comment-story" ) << QStringLiteral( "# This is a shell comment" ) << QString();
// We look for assignments, but only for single-words
QTest::newRow( "comment-space-eq" ) << QStringLiteral( "# Check that a = b" ) << QString();
QTest::newRow( "assignment1" ) << QStringLiteral( "a=1" ) << QStringLiteral( "a" );
QTest::newRow( "assignment2" ) << QStringLiteral( "a = 1" ) << QStringLiteral( "a" );
QTest::newRow( "assignment3" ) << QStringLiteral( "# a=1" ) << QStringLiteral( "a" );
QTest::newRow( "assignment4" ) << QStringLiteral( "cows = 12" ) << QStringLiteral( "cows" );
QTest::newRow( "assignment5" ) << QStringLiteral( "# # cows=1" ) << QStringLiteral( "cows" );
QTest::newRow( "assignment6" ) << QStringLiteral( "# moose='cool' # not cows" ) << QStringLiteral( "moose" );
QTest::newRow( "assignment7" ) << QStringLiteral( " moose=cows=42" ) << QStringLiteral( "moose" );
QTest::newRow( "assignment8" ) << QStringLiteral( "#swap_device=/dev/something" )
<< QStringLiteral( "swap_device" );
QTest::newRow( "assignment9" ) << QStringLiteral( "# swap_device=/dev/something" )
<< QStringLiteral( "swap_device" );
QTest::newRow( "assignment10" ) << QStringLiteral( "swap_device=/dev/something" )
<< QStringLiteral( "swap_device" );
}
void
LOSHTests::testAssignmentExtraction()
{
QFETCH( QString, line );
QFETCH( QString, match );
QCOMPARE( get_assignment_part( line ), match );
}
static CalamaresUtils::System*
file_setup( const QTemporaryDir& tempRoot )
{
CalamaresUtils::System* ss = CalamaresUtils::System::instance();
if ( !ss )
{
ss = new CalamaresUtils::System( true );
}
Calamares::GlobalStorage* gs
= Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
if ( !gs )
{
cDebug() << "Creating new JobQueue";
(void)new Calamares::JobQueue();
gs = Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
}
if ( gs )
{
// Working with a rootMountPoint set
gs->insert( "rootMountPoint", tempRoot.path() );
}
return ss;
}
static void
make_valid_loshinfo( LOSHInfo& i )
{
i.swap_outer_uuid = QStringLiteral( "UUID-0000" );
i.swap_mapper_name = QStringLiteral( "/dev/mapper/0000" );
i.swap_device_path = QStringLiteral( "/dev/sda0" );
i.mountable_keyfile_device = QStringLiteral( "/dev/ada0p0s0" );
}
void
LOSHTests::testLOSHInfo()
{
LOSHInfo i {};
QVERIFY( !i.isValid() );
make_valid_loshinfo( i );
QVERIFY( i.isValid() );
QCOMPARE( i.replacementFor( QStringLiteral( "swap_device" ) ), QStringLiteral( "/dev/sda0" ) );
QCOMPARE( i.replacementFor( QStringLiteral( "duck" ) ), QString() );
}
void
LOSHTests::testConfigWriting()
{
QTemporaryDir tempRoot( QDir::tempPath() + QStringLiteral( "/test-job-XXXXXX" ) );
QVERIFY( tempRoot.isValid() );
auto* ss = file_setup( tempRoot );
QVERIFY( ss );
QVERIFY( Calamares::JobQueue::instance()->globalStorage() );
QVERIFY( QFile::exists( tempRoot.path() ) );
QVERIFY( QFileInfo( tempRoot.path() ).isDir() );
const QString targetFilePath = QStringLiteral( "losh.conf" );
const QString filePath = tempRoot.filePath( targetFilePath );
QStringList contents { QStringLiteral( "# Calamares demo" ),
QStringLiteral( "# swap_device=a thing" ),
QStringLiteral( "# duck duck swap_device=another" ) };
// When the information is invalid, file contents are unchanged,
// and no file is written either.
LOSHInfo i {};
QVERIFY( !i.isValid() );
QVERIFY( !QFile::exists( filePath ) );
write_openswap_conf( targetFilePath, contents, i ); // Invalid i
QVERIFY( !QFile::exists( filePath ) );
QCOMPARE( contents.length(), 3 );
QCOMPARE( contents.at( 1 ).left( 4 ), QStringLiteral( "# s" ) );
// Can we write there at all?
QFile derp( filePath );
QVERIFY( derp.open( QIODevice::WriteOnly ) );
QVERIFY( derp.write( "xx", 2 ) );
derp.close();
QVERIFY( QFile::exists( filePath ) );
QVERIFY( QFile::remove( filePath ) );
// Once the information is valid, though, the file is written
make_valid_loshinfo( i );
QVERIFY( i.isValid() );
QVERIFY( !QFile::exists( filePath ) );
write_openswap_conf( targetFilePath, contents, i ); // Now it is valid
QVERIFY( QFile::exists( filePath ) );
QCOMPARE( contents.length(), 3 );
QCOMPARE( i.swap_device_path, QStringLiteral( "/dev/sda0" ) ); // expected key value
QCOMPARE( contents.at( 1 ), QStringLiteral( "swap_device=/dev/sda0" ) ); // expected line
// readLine() returns with newlines-added
QFile f( filePath );
QVERIFY( f.open( QIODevice::ReadOnly ) );
QCOMPARE( f.readLine(), QStringLiteral( "# Calamares demo\n" ) );
QCOMPARE( f.readLine(), QStringLiteral( "swap_device=/dev/sda0\n" ) );
QCOMPARE( f.readLine(), QStringLiteral( "# duck duck swap_device=another\n" ) );
QCOMPARE( f.readLine(), QString() );
QVERIFY( f.atEnd() );
// Note how the contents is updated on every write_openswap_conf()
i.swap_device_path = QStringLiteral( "/dev/zram/0.zram" );
write_openswap_conf( targetFilePath, contents, i ); // Still valid
QCOMPARE( contents.length(), 3 );
QCOMPARE( i.swap_device_path, QStringLiteral( "/dev/zram/0.zram" ) ); // expected key value
QCOMPARE( contents.at( 1 ), QStringLiteral( "swap_device=/dev/zram/0.zram" ) ); // expected line
}
void
LOSHTests::testJob()
{
QTemporaryDir tempRoot( QDir::tempPath() + QStringLiteral( "/test-job-XXXXXX" ) );
QVERIFY( tempRoot.isValid() );
auto* ss = file_setup( tempRoot );
QVERIFY( ss );
Calamares::GlobalStorage* gs
= Calamares::JobQueue::instance() ? Calamares::JobQueue::instance()->globalStorage() : nullptr;
QVERIFY( gs );
{
QDir d( tempRoot.path() );
d.mkdir( "etc" );
}
QVERIFY( !LOSHInfo::fromGlobalStorage().isValid() );
QVariantList outerPartition;
QVariantMap innerPartition;
innerPartition.insert( "mountPoint", "/" );
innerPartition.insert( "fs", "ext4" );
innerPartition.insert( "luksMapperName", "root" );
innerPartition.insert( "luksUUID", "0000" );
outerPartition.append( innerPartition );
innerPartition.remove( "mountPoint" );
innerPartition.insert( "fs", "linuxswap" );
innerPartition.insert( "luksMapperName", "swap" );
innerPartition.insert( "luksUuid", "0001" );
outerPartition.append( innerPartition );
gs->insert( "partitions", outerPartition );
QVERIFY( LOSHInfo::fromGlobalStorage().isValid() );
LOSHJob j;
j.setConfigurationMap( QVariantMap() );
auto jobresult = j.exec();
QVERIFY( jobresult );
{
QFile f( tempRoot.filePath( "etc/openswap.conf" ) );
QVERIFY( f.exists() );
QVERIFY( f.open( QIODevice::ReadOnly ) );
cDebug() << f.readAll();
}
}
QTEST_GUILESS_MAIN( LOSHTests )
#include "utils/moc-warnings.h"
#include "Tests.moc"

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://calamares.io> ===
#
# SPDX-FileCopyrightText: 2016 Teo Mrnjavac <teo@kde.org>
# SPDX-FileCopyrightText: 2017 Alf Gaida <agaida@siduction.org>
# SPDX-FileCopyrightText: 2019 Adriaan de Groot <groot@kde.org>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Calamares is Free Software: see the License-Identifier above.
#
import libcalamares
import os.path
import gettext
_ = gettext.translation("calamares-python",
localedir=libcalamares.utils.gettext_path(),
languages=libcalamares.utils.gettext_languages(),
fallback=True).gettext
def pretty_name():
return _("Configuring encrypted swap.")
def write_openswap_conf(partitions, root_mount_point, openswap_conf_path):
swap_outer_uuid = ""
swap_mapper_name = ""
mountable_keyfile_device = ""
for partition in partitions:
if partition["fs"] == "linuxswap" and "luksMapperName" in partition:
swap_outer_uuid = partition["luksUuid"]
swap_mapper_name = partition["luksMapperName"]
elif partition["mountPoint"] == "/" and "luksMapperName" in partition:
mountable_keyfile_device = (
"/dev/mapper/{!s}".format(partition["luksMapperName"])
)
if not mountable_keyfile_device or not swap_outer_uuid:
return None
swap_device_path = "/dev/disk/by-uuid/{!s}".format(swap_outer_uuid)
lines = []
with open(os.path.join(root_mount_point,
openswap_conf_path), 'r') as openswap_file:
lines = [x.strip() for x in openswap_file.readlines()]
for i in range(len(lines)):
if lines[i].startswith("swap_device"):
lines[i] = "swap_device={!s}".format(swap_device_path)
elif lines[i].startswith("crypt_swap_name"):
lines[i] = "crypt_swap_name={!s}".format(swap_mapper_name)
elif lines[i].startswith("keyfile_device"):
lines[i] = "keyfile_device={!s}".format(mountable_keyfile_device)
elif lines[i].startswith("keyfile_filename"):
lines[i] = "keyfile_filename=crypto_keyfile.bin"
elif lines[i].startswith("#keyfile_device_mount_options"):
if libcalamares.globalstorage.contains("btrfsRootSubvolume"):
btrfs_root_subvolume = libcalamares.globalstorage.value("btrfsRootSubvolume")
lines[i] = "keyfile_device_mount_options=--options=subvol=" + btrfs_root_subvolume.lstrip("/")
with open(os.path.join(root_mount_point,
openswap_conf_path), 'w') as openswap_file:
openswap_file.write("\n".join(lines) + "\n")
return None
def run():
"""
This module sets up the openswap hook for a resumable encrypted swap.
:return:
"""
root_mount_point = libcalamares.globalstorage.value("rootMountPoint")
openswap_conf_path = libcalamares.job.configuration["configFilePath"]
partitions = libcalamares.globalstorage.value("partitions")
if not partitions:
libcalamares.utils.warning("partitions is empty, {!s}".format(partitions))
return (_("Configuration Error"),
_("No partitions are defined for <pre>{!s}</pre> to use.").format("luksopenswaphookcfg"))
if not root_mount_point:
libcalamares.utils.warning("rootMountPoint is empty, {!s}".format(root_mount_point))
return (_("Configuration Error"),
_("No root mount point is given for <pre>{!s}</pre> to use.").format("luksopenswaphookcfg"))
openswap_conf_path = openswap_conf_path.lstrip('/')
return write_openswap_conf(partitions, root_mount_point, openswap_conf_path)

View File

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