Merge branch 'issue-1654' into calamares

FIXES #1654
This commit is contained in:
Adriaan de Groot 2022-04-11 15:08:33 +02:00
commit 9c58f49c49
13 changed files with 345 additions and 131 deletions

View File

@ -17,7 +17,16 @@ This release contains contributions from (alphabetically by first name):
- No core changes yet
## Modules ##
- No module changes yet
- *users* module has rearranged configuration for setting the hostname.
Legacy settings are preserved, but produce a warning. Please see
`users.conf` for details.
- *users* module has a new hostname.location setting, *Transient*, which
will force the installed system to transient-hostname-setting by removing
the file `/etc/hostname`.
- *users* module has a new hostname.template setting, which allows some
tweaking of how the hostname suggestion is constructed. In particular,
it can be configured to use the current hostname (whatever that may be).
See the example `users.conf` for details on available keys.
# 3.2.54 (2022-03-21) #

View File

@ -20,6 +20,8 @@
#include "utils/String.h"
#include "utils/Variant.h"
#include <KMacroExpander>
#include <QCoreApplication>
#include <QFile>
#include <QMetaProperty>
@ -77,14 +79,16 @@ updateGSAutoLogin( bool doAutoLogin, const QString& login )
}
const NamedEnumTable< HostNameAction >&
hostNameActionNames()
hostnameActionNames()
{
// *INDENT-OFF*
// clang-format off
static const NamedEnumTable< HostNameAction > names {
{ QStringLiteral( "none" ), HostNameAction::None },
{ QStringLiteral( "etcfile" ), HostNameAction::EtcHostname },
{ QStringLiteral( "hostnamed" ), HostNameAction::SystemdHostname }
{ QStringLiteral( "etc" ), HostNameAction::EtcHostname },
{ QStringLiteral( "hostnamed" ), HostNameAction::SystemdHostname },
{ QStringLiteral( "transient" ), HostNameAction::Transient },
};
// clang-format on
// *INDENT-ON*
@ -98,7 +102,7 @@ Config::Config( QObject* parent )
emit readyChanged( m_isReady ); // false
// Gang together all the changes of status to one readyChanged() signal
connect( this, &Config::hostNameStatusChanged, this, &Config::checkReady );
connect( this, &Config::hostnameStatusChanged, this, &Config::checkReady );
connect( this, &Config::loginNameStatusChanged, this, &Config::checkReady );
connect( this, &Config::fullNameChanged, this, &Config::checkReady );
connect( this, &Config::userPasswordStatusChanged, this, &Config::checkReady );
@ -240,10 +244,15 @@ Config::loginNameStatus() const
void
Config::setHostName( const QString& host )
{
if ( host != m_hostName )
if ( hostnameAction() != HostNameAction::EtcHostname && hostnameAction() != HostNameAction::SystemdHostname )
{
cDebug() << "Ignoring hostname" << host << "No hostname will be set.";
return;
}
if ( host != m_hostname )
{
m_customHostName = !host.isEmpty();
m_hostName = host;
m_hostname = host;
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( host.isEmpty() )
{
@ -253,8 +262,8 @@ Config::setHostName( const QString& host )
{
gs->insert( "hostname", host );
}
emit hostNameChanged( host );
emit hostNameStatusChanged( hostNameStatus() );
emit hostnameChanged( host );
emit hostnameStatusChanged( hostnameStatus() );
}
}
@ -266,31 +275,31 @@ Config::forbiddenHostNames()
}
QString
Config::hostNameStatus() const
Config::hostnameStatus() const
{
// An empty hostname is "ok", even if it isn't really
if ( m_hostName.isEmpty() )
if ( m_hostname.isEmpty() )
{
return QString();
}
if ( m_hostName.length() < HOSTNAME_MIN_LENGTH )
if ( m_hostname.length() < HOSTNAME_MIN_LENGTH )
{
return tr( "Your hostname is too short." );
}
if ( m_hostName.length() > HOSTNAME_MAX_LENGTH )
if ( m_hostname.length() > HOSTNAME_MAX_LENGTH )
{
return tr( "Your hostname is too long." );
}
for ( const QString& badName : forbiddenHostNames() )
{
if ( 0 == QString::compare( badName, m_hostName, Qt::CaseSensitive ) )
if ( 0 == QString::compare( badName, m_hostname, Qt::CaseSensitive ) )
{
return tr( "'%1' is not allowed as hostname." ).arg( badName );
}
}
if ( !HOSTNAME_RX.exactMatch( m_hostName ) )
if ( !HOSTNAME_RX.exactMatch( m_hostname ) )
{
return tr( "Only letters, numbers, underscore and hyphen are allowed." );
}
@ -298,6 +307,12 @@ Config::hostNameStatus() const
return QString();
}
static QString
cleanupForHostname( const QString& s )
{
QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive );
return s.toLower().replace( dmirx, " " ).remove( ' ' );
}
/** @brief Guess the machine's name
*
@ -312,16 +327,11 @@ guessProductName()
if ( !tried )
{
// yes validateHostnameText() but these files can be a mess
QRegExp dmirx( "[^a-zA-Z0-9]", Qt::CaseInsensitive );
QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) );
if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) )
{
dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() )
.toLower()
.replace( dmirx, " " )
.remove( ' ' );
dmiProduct = cleanupForHostname( QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) );
}
if ( dmiProduct.isEmpty() )
{
@ -379,17 +389,37 @@ makeLoginNameSuggestion( const QStringList& parts )
return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString();
}
/** @brief Return an invalid string for use in a hostname, if @p s is empty
*
* Maps empty to "^" (which is invalid in a hostname), everything else
* returns @p s itself.
*/
static QString
makeHostnameSuggestion( const QStringList& parts )
invalidEmpty( const QString& s )
{
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
if ( parts.isEmpty() || parts.first().isEmpty() )
{
return QString();
}
return s.isEmpty() ? QStringLiteral( "^" ) : s;
}
QString productName = guessProductName();
QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName );
STATICTEST QString
makeHostnameSuggestion( const QString& templateString, const QStringList& fullNameParts, const QString& loginName )
{
QHash< QString, QString > replace;
// User data
replace.insert( QStringLiteral( "first" ),
invalidEmpty( fullNameParts.isEmpty() ? QString() : cleanupForHostname( fullNameParts.first() ) ) );
replace.insert( QStringLiteral( "name" ), invalidEmpty( cleanupForHostname( fullNameParts.join( QString() ) ) ) );
replace.insert( QStringLiteral( "login" ), invalidEmpty( cleanupForHostname( loginName ) ) );
// Hardware data
replace.insert( QStringLiteral( "product" ), guessProductName() );
replace.insert( QStringLiteral( "product2" ), cleanupForHostname( QSysInfo::prettyProductName() ) );
replace.insert( QStringLiteral( "cpu" ), cleanupForHostname( QSysInfo::currentCpuArchitecture() ) );
// Hostname data
replace.insert( QStringLiteral( "host" ), invalidEmpty( cleanupForHostname( QSysInfo::machineHostName() ) ) );
QString hostnameSuggestion = KMacroExpander::expandMacros( templateString, replace, '$' );
// RegExp for valid hostnames; if the suggestion produces a valid name, return it
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" );
return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString();
}
@ -420,18 +450,18 @@ Config::setFullName( const QString& name )
// Build login and hostname, if needed
static QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive );
QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) )
.replace( QRegExp( "[-']" ), "" )
.replace( rx, " " )
.toLower()
.simplified();
const QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) )
.replace( QRegExp( "[-']" ), "" )
.replace( rx, " " )
.toLower()
.simplified();
QStringList cleanParts = cleanName.split( ' ' );
if ( !m_customLoginName )
{
QString login = makeLoginNameSuggestion( cleanParts );
const QString login = makeLoginNameSuggestion( cleanParts );
if ( !login.isEmpty() && login != m_loginName )
{
setLoginName( login );
@ -441,8 +471,8 @@ Config::setFullName( const QString& name )
}
if ( !m_customHostName )
{
QString hostname = makeHostnameSuggestion( cleanParts );
if ( !hostname.isEmpty() && hostname != m_hostName )
const QString hostname = makeHostnameSuggestion( m_hostnameTemplate, cleanParts, loginName() );
if ( !hostname.isEmpty() && hostname != m_hostname )
{
setHostName( hostname );
// Still not custom
@ -653,7 +683,7 @@ bool
Config::isReady() const
{
bool readyFullName = !fullName().isEmpty(); // Needs some text
bool readyHostname = hostNameStatus().isEmpty(); // .. no warning message
bool readyHostname = hostnameStatus().isEmpty(); // .. no warning message
bool readyUsername = !loginName().isEmpty() && loginNameStatus().isEmpty(); // .. no warning message
bool readyUserPassword = userPasswordValidity() != Config::PasswordValidity::Invalid;
bool readyRootPassword = rootPasswordValidity() != Config::PasswordValidity::Invalid;
@ -734,25 +764,22 @@ setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >
}
}
STATICTEST HostNameActions
getHostNameActions( const QVariantMap& configurationMap )
STATICTEST HostNameAction
getHostNameAction( const QVariantMap& configurationMap )
{
HostNameAction setHostName = HostNameAction::EtcHostname;
QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" );
QString hostnameActionString = CalamaresUtils::getString( configurationMap, "location" );
if ( !hostnameActionString.isEmpty() )
{
bool ok = false;
setHostName = hostNameActionNames().find( hostnameActionString, ok );
setHostName = hostnameActionNames().find( hostnameActionString, ok );
if ( !ok )
{
setHostName = HostNameAction::EtcHostname; // Rather than none
}
}
HostNameAction writeHosts = CalamaresUtils::getBool( configurationMap, "writeHostsFile", true )
? HostNameAction::WriteEtcHosts
: HostNameAction::None;
return setHostName | writeHosts;
return setHostName;
}
/** @brief Process entries in the passwordRequirements config entry
@ -826,6 +853,25 @@ either( T ( *f )( const QVariantMap&, const QString&, U ),
}
}
// TODO:3.3: Remove
static void
copyLegacy( const QVariantMap& source, const QString& sourceKey, QVariantMap& target, const QString& targetKey )
{
if ( source.contains( sourceKey ) )
{
if ( target.contains( targetKey ) )
{
cWarning() << "Legacy *users* key" << sourceKey << "ignored.";
}
else
{
const QVariant legacyValue = source.value( sourceKey );
cWarning() << "Legacy *users* key" << sourceKey << "overrides hostname-settings.";
target.insert( targetKey, legacyValue );
}
}
}
void
Config::setConfigurationMap( const QVariantMap& configurationMap )
{
@ -844,7 +890,19 @@ Config::setConfigurationMap( const QVariantMap& configurationMap )
? SudoStyle::UserAndGroup
: SudoStyle::UserOnly;
m_hostNameActions = getHostNameActions( configurationMap );
// Handle *hostname* key and subkeys and legacy settings
{
bool ok = false; // Ignored
QVariantMap hostnameSettings = CalamaresUtils::getSubMap( configurationMap, "hostname", ok );
// TODO:3.3: Remove calls to copyLegacy
copyLegacy( configurationMap, "setHostname", hostnameSettings, "location" );
copyLegacy( configurationMap, "writeHostsFile", hostnameSettings, "writeHostsFile" );
m_hostnameAction = getHostNameAction( hostnameSettings );
m_writeEtcHosts = CalamaresUtils::getBool( hostnameSettings, "writeHostsFile", true );
m_hostnameTemplate
= CalamaresUtils::getString( hostnameSettings, "template", QStringLiteral( "${first}-${product}" ) );
}
setConfigurationDefaultGroups( configurationMap, m_defaultGroups );
@ -923,7 +981,7 @@ Config::createJobs() const
j = new SetPasswordJob( "root", rootPassword() );
jobs.append( Calamares::job_ptr( j ) );
j = new SetHostNameJob( hostName(), hostNameActions() );
j = new SetHostNameJob( this );
jobs.append( Calamares::job_ptr( j ) );
return jobs;

View File

@ -20,17 +20,15 @@
#include <QObject>
#include <QVariantMap>
enum HostNameAction
enum class HostNameAction
{
None = 0x0,
EtcHostname = 0x1, // Write to /etc/hostname directly
SystemdHostname = 0x2, // Set via hostnamed(1)
WriteEtcHosts = 0x4 // Write /etc/hosts (127.0.1.1 is this host)
None,
EtcHostname, // Write to /etc/hostname directly
SystemdHostname, // Set via hostnamed(1)
Transient, // Force target system transient, remove /etc/hostname
};
Q_DECLARE_FLAGS( HostNameActions, HostNameAction )
Q_DECLARE_OPERATORS_FOR_FLAGS( HostNameActions )
const NamedEnumTable< HostNameAction >& hostNameActionNames();
const NamedEnumTable< HostNameAction >& hostnameActionNames();
/** @brief Settings for a single group
*
@ -101,9 +99,9 @@ class PLUGINDLLEXPORT Config : public Calamares::ModuleSystem::Config
Q_PROPERTY( QString loginName READ loginName WRITE setLoginName NOTIFY loginNameChanged )
Q_PROPERTY( QString loginNameStatus READ loginNameStatus NOTIFY loginNameStatusChanged )
Q_PROPERTY( QString hostName READ hostName WRITE setHostName NOTIFY hostNameChanged )
Q_PROPERTY( QString hostNameStatus READ hostNameStatus NOTIFY hostNameStatusChanged )
Q_PROPERTY( HostNameActions hostNameActions READ hostNameActions CONSTANT )
Q_PROPERTY( QString hostname READ hostname WRITE setHostName NOTIFY hostnameChanged )
Q_PROPERTY( QString hostnameStatus READ hostnameStatus NOTIFY hostnameStatusChanged )
Q_PROPERTY( HostNameAction hostnameAction READ hostnameAction CONSTANT )
Q_PROPERTY( QString userPassword READ userPassword WRITE setUserPassword NOTIFY userPasswordChanged )
Q_PROPERTY( QString userPasswordSecondary READ userPasswordSecondary WRITE setUserPasswordSecondary NOTIFY
@ -204,11 +202,19 @@ public:
QString loginNameStatus() const;
/// The host name (name for the system)
QString hostName() const { return m_hostName; }
QString hostname() const
{
return ( ( hostnameAction() == HostNameAction::EtcHostname )
|| ( hostnameAction() == HostNameAction::SystemdHostname ) )
? m_hostname
: QString();
}
/// Status message about hostname -- empty for "ok"
QString hostNameStatus() const;
QString hostnameStatus() const;
/// How to write the hostname
HostNameActions hostNameActions() const { return m_hostNameActions; }
HostNameAction hostnameAction() const { return m_hostnameAction; }
/// Write /etc/hosts ?
bool writeEtcHosts() const { return m_writeEtcHosts; }
/// Should the user be automatically logged-in?
bool doAutoLogin() const { return m_doAutoLogin; }
@ -293,8 +299,8 @@ signals:
void fullNameChanged( const QString& );
void loginNameChanged( const QString& );
void loginNameStatusChanged( const QString& );
void hostNameChanged( const QString& );
void hostNameStatusChanged( const QString& );
void hostnameChanged( const QString& );
void hostnameStatusChanged( const QString& );
void autoLoginChanged( bool );
void reuseUserPasswordForRootChanged( bool );
void requireStrongPasswordsChanged( bool );
@ -317,7 +323,7 @@ private:
SudoStyle m_sudoStyle = SudoStyle::UserOnly;
QString m_fullName;
QString m_loginName;
QString m_hostName;
QString m_hostname;
QString m_userPassword;
QString m_userPasswordSecondary; // enter again to be sure
@ -337,7 +343,10 @@ private:
bool m_isReady = false; ///< Used to reduce readyChanged signals
HostNameActions m_hostNameActions;
HostNameAction m_hostnameAction = HostNameAction::EtcHostname;
bool m_writeEtcHosts = false;
QString m_hostnameTemplate;
PasswordCheckList m_passwordChecks;
};

View File

@ -41,13 +41,10 @@ designatorForStyle( Config::SudoStyle style )
{
case Config::SudoStyle::UserOnly:
return QStringLiteral( "(ALL)" );
break;
case Config::SudoStyle::UserAndGroup:
return QStringLiteral( "(ALL:ALL)" );
break;
}
__builtin_unreachable();
return QString();
}
Calamares::JobResult

View File

@ -24,31 +24,30 @@
using WriteMode = CalamaresUtils::System::WriteMode;
SetHostNameJob::SetHostNameJob( const QString& hostname, HostNameActions a )
SetHostNameJob::SetHostNameJob( const Config* c )
: Calamares::Job()
, m_hostname( hostname )
, m_actions( a )
, m_config( c )
{
}
QString
SetHostNameJob::prettyName() const
{
return tr( "Set hostname %1" ).arg( m_hostname );
return tr( "Set hostname %1" ).arg( m_config->hostname() );
}
QString
SetHostNameJob::prettyDescription() const
{
return tr( "Set hostname <strong>%1</strong>." ).arg( m_hostname );
return tr( "Set hostname <strong>%1</strong>." ).arg( m_config->hostname() );
}
QString
SetHostNameJob::prettyStatusMessage() const
{
return tr( "Setting hostname %1." ).arg( m_hostname );
return tr( "Setting hostname %1." ).arg( m_config->hostname() );
}
STATICTEST bool
@ -62,16 +61,19 @@ STATICTEST bool
writeFileEtcHosts( const QString& hostname )
{
// The actual hostname gets substituted in at %1
static const char etc_hosts[] = R"(# Host addresses
const QString standard_hosts = QStringLiteral( R"(# Standard host addresses
127.0.0.1 localhost
127.0.1.1 %1
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
)";
)" );
const QString this_host = QStringLiteral( R"(# This host address
127.0.1.1 %1
)" );
const QString etc_hosts = standard_hosts + ( hostname.isEmpty() ? QString() : this_host.arg( hostname ) );
return CalamaresUtils::System::instance()->createTargetFile(
QStringLiteral( "/etc/hosts" ), QString( etc_hosts ).arg( hostname ).toUtf8(), WriteMode::Overwrite );
QStringLiteral( "/etc/hosts" ), etc_hosts.toUtf8(), WriteMode::Overwrite );
}
STATICTEST bool
@ -129,29 +131,35 @@ SetHostNameJob::exec()
return Calamares::JobResult::error( tr( "Internal Error" ) );
}
if ( m_actions & HostNameAction::EtcHostname )
switch ( m_config->hostnameAction() )
{
if ( !setFileHostname( m_hostname ) )
case HostNameAction::None:
break;
case HostNameAction::EtcHostname:
if ( !setFileHostname( m_config->hostname() ) )
{
cError() << "Can't write to hostname file";
return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) );
}
break;
case HostNameAction::SystemdHostname:
// Does its own logging
setSystemdHostname( m_config->hostname() );
break;
case HostNameAction::Transient:
CalamaresUtils::System::instance()->removeTargetFile( QStringLiteral( "/etc/hostname" ) );
break;
}
if ( m_actions & HostNameAction::WriteEtcHosts )
if ( m_config->writeEtcHosts() )
{
if ( !writeFileEtcHosts( m_hostname ) )
if ( !writeFileEtcHosts( m_config->hostname() ) )
{
cError() << "Can't write to hosts file";
return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) );
}
}
if ( m_actions & HostNameAction::SystemdHostname )
{
// Does its own logging
setSystemdHostname( m_hostname );
}
return Calamares::JobResult::ok();
}

View File

@ -20,15 +20,14 @@ class SetHostNameJob : public Calamares::Job
{
Q_OBJECT
public:
SetHostNameJob( const QString& hostname, HostNameActions a );
SetHostNameJob( const Config* c );
QString prettyName() const override;
QString prettyDescription() const override;
QString prettyStatusMessage() const override;
Calamares::JobResult exec() override;
private:
const QString m_hostname;
const HostNameActions m_actions;
const Config* m_config;
};
#endif // SETHOSTNAMEJOB_CPP_H

View File

@ -92,18 +92,19 @@ UsersTests::testEtcHostname()
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( !QFile::exists( m_dir.filePath( "etc" ) ) );
const QString testHostname = QStringLiteral( "tubophone.calamares.io" );
// Doesn't create intermediate directories
QVERIFY( !setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( !setFileHostname( testHostname ) );
QVERIFY( CalamaresUtils::System::instance()->createTargetDirs( "/etc" ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
// Does write the file
QVERIFY( setFileHostname( QStringLiteral( "tubophone.calamares.io" ) ) );
QVERIFY( setFileHostname( testHostname ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hostname" ) ) );
// 22 for the test string, above, and 1 for the newline
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hostname" ) ).size(), 22 + 1 );
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hostname" ) ).size(), testHostname.length() + 1 );
}
void
@ -113,11 +114,12 @@ UsersTests::testEtcHosts()
QVERIFY( QFile::exists( m_dir.path() ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc" ) ) );
QVERIFY( writeFileEtcHosts( QStringLiteral( "tubophone.calamares.io" ) ) );
const QString testHostname = QStringLiteral( "tubophone.calamares.io" );
QVERIFY( writeFileEtcHosts( testHostname ) );
QVERIFY( QFile::exists( m_dir.filePath( "etc/hosts" ) ) );
// The skeleton contains %1 which has the hostname substituted in, so we lose two,
// and the rest of the blabla is 150 (according to Python)
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hosts" ) ).size(), 150 + 22 - 2 );
// and the rest of the blabla is 145 (the "standard" part) and 34 (the "for this host" part)
QCOMPARE( QFileInfo( m_dir.filePath( "etc/hosts" ) ).size(), 145 + 34 + testHostname.length() - 2 );
}
void

View File

@ -17,8 +17,10 @@
// Implementation details
extern void setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >& defaultGroups );
extern HostNameActions getHostNameActions( const QVariantMap& configurationMap );
extern HostNameAction getHostNameAction( const QVariantMap& configurationMap );
extern bool addPasswordCheck( const QString& key, const QVariant& value, PasswordCheckList& passwordChecks );
extern QString
makeHostnameSuggestion( const QString& templateString, const QStringList& fullNameParts, const QString& loginName );
/** @brief Test Config object methods and internals
*
@ -42,6 +44,10 @@ private Q_SLOTS:
void testHostActions_data();
void testHostActions();
void testHostActions2();
void testHostSuggestions_data();
void testHostSuggestions();
void testPasswordChecks();
void testUserPassword();
@ -228,6 +234,15 @@ UserTests::testHostActions_data()
QTest::newRow( "bad " ) << true << QString( "derp" ) << int( HostNameAction::EtcHostname );
QTest::newRow( "none " ) << true << QString( "none" ) << int( HostNameAction::None );
QTest::newRow( "systemd" ) << true << QString( "Hostnamed" ) << int( HostNameAction::SystemdHostname );
QTest::newRow( "etc(1) " ) << true << QString( "etcfile" ) << int( HostNameAction::EtcHostname );
QTest::newRow( "etc(2) " ) << true << QString( "etc" ) << int( HostNameAction::EtcHostname );
QTest::newRow( "etc-bad" )
<< true << QString( "etchost" )
<< int( HostNameAction::EtcHostname ); // This isn't a valid name, but defaults to EtcHostname
QTest::newRow( "ci-sysd" ) << true << QString( "hOsTnaMed" )
<< int( HostNameAction::SystemdHostname ); // Case-insensitive
QTest::newRow( "trbs " ) << true << QString( "transient" ) << int( HostNameAction::Transient );
QTest::newRow( "ci-trns" ) << true << QString( "trANSient" ) << int( HostNameAction::Transient );
}
void
@ -240,16 +255,75 @@ UserTests::testHostActions()
QVariantMap m;
if ( set )
{
m.insert( "setHostname", string );
m.insert( "location", string );
}
QCOMPARE( getHostNameActions( m ),
HostNameActions( result ) | HostNameAction::WriteEtcHosts ); // write bits default to true
// action is independent of writeHostsFile
QCOMPARE( getHostNameAction( m ), HostNameAction( result ) );
m.insert( "writeHostsFile", false );
QCOMPARE( getHostNameActions( m ), HostNameActions( result ) );
QCOMPARE( getHostNameAction( m ), HostNameAction( result ) );
m.insert( "writeHostsFile", true );
QCOMPARE( getHostNameActions( m ), HostNameActions( result ) | HostNameAction::WriteEtcHosts );
QCOMPARE( getHostNameAction( m ), HostNameAction( result ) );
}
void
UserTests::testHostActions2()
{
Config c;
QVariantMap legacy;
// Test defaults
c.setConfigurationMap( legacy );
QCOMPARE( c.hostnameAction(), HostNameAction::EtcHostname );
QCOMPARE( c.writeEtcHosts(), true );
legacy.insert( "writeHostsFile", false );
legacy.insert( "setHostname", "Hostnamed" );
c.setConfigurationMap( legacy );
QCOMPARE( c.hostnameAction(), HostNameAction::SystemdHostname );
QCOMPARE( c.writeEtcHosts(), false );
}
void
UserTests::testHostSuggestions_data()
{
QTest::addColumn< QString >( "templateString" );
QTest::addColumn< QString >( "result" );
QTest::newRow( "unset " ) << QString() << QString();
QTest::newRow( "const " ) << QStringLiteral( "derp" ) << QStringLiteral( "derp" );
QTest::newRow( "escaped" ) << QStringLiteral( "$$" ) << QString(); // Because invalid
QTest::newRow( "default" ) << QStringLiteral( "${first}-pc" )
<< QStringLiteral( "chuck-pc" ); // Avoid ${product} because it's DMI-based
QTest::newRow( "full " ) << QStringLiteral( "${name}" ) << QStringLiteral( "chuckyeager" );
QTest::newRow( "login+ " ) << QStringLiteral( "${login}-${first}" ) << QStringLiteral( "bill-chuck" );
// This is a bit dodgy: assumes CPU architecture of the testing host
QTest::newRow( " cpu " ) << QStringLiteral( "${cpu}X" ) << QStringLiteral( "x8664X" ); // Assume we don't test on non-amd64
// These have X X in the template to indicate that they are bogus. Mostly we want
// to see what the template engine does for these.
QTest::newRow( "@prod " ) << QStringLiteral( "X${product}X" ) << QString();
QTest::newRow( "@prod2 " ) << QStringLiteral( "X${product2}X" ) << QString();
QTest::newRow( "@host " ) << QStringLiteral( "X${host}X" ) << QString();
}
void
UserTests::testHostSuggestions()
{
const QStringList fullName { "Chuck", "Yeager" };
const QString login { "bill" };
QFETCH( QString, templateString );
QFETCH( QString, result );
if ( templateString.startsWith('X') && templateString.endsWith('X'))
{
QEXPECT_FAIL( "", "Test is too host-specific", Continue );
cWarning() << Logger::SubEntry << "Next test" << templateString << "->" << makeHostnameSuggestion( templateString, fullName, login );
}
QCOMPARE( makeHostnameSuggestion( templateString, fullName, login ), result );
}
void
UserTests::testPasswordChecks()
{

View File

@ -105,18 +105,31 @@ UsersPage::UsersPage( Config* config, QWidget* parent )
connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName );
connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited );
ui->textBoxHostName->setText( config->hostName() );
connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName );
connect( config,
&Config::hostNameChanged,
[ this ]( const QString& name )
{
if ( !ui->textBoxHostName->hasFocus() )
// If the hostname is going to be written out, then show the field
if ( ( m_config->hostnameAction() == HostNameAction::EtcHostname )
|| ( m_config->hostnameAction() == HostNameAction::SystemdHostname ) )
{
ui->textBoxHostname->setText( config->hostname() );
connect( ui->textBoxHostname, &QLineEdit::textEdited, config, &Config::setHostName );
connect( config,
&Config::hostnameChanged,
[ this ]( const QString& name )
{
ui->textBoxHostName->setText( name );
}
} );
connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus );
if ( !ui->textBoxHostname->hasFocus() )
{
ui->textBoxHostname->setText( name );
}
} );
connect( config, &Config::hostnameStatusChanged, this, &UsersPage::reportHostNameStatus );
}
else
{
// Need to hide the hostname parts individually because there's no widget-group
ui->hostnameLabel->hide();
ui->labelHostname->hide();
ui->textBoxHostname->hide();
ui->labelHostnameError->hide();
}
ui->textBoxLoginName->setText( config->loginName() );
connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName );
@ -155,7 +168,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent )
onReuseUserPasswordChanged( m_config->reuseUserPasswordForRoot() );
onFullNameTextEdited( m_config->fullName() );
reportLoginNameStatus( m_config->loginNameStatus() );
reportHostNameStatus( m_config->hostNameStatus() );
reportHostNameStatus( m_config->hostnameStatus() );
ui->textBoxLoginName->setEnabled( m_config->isEditable( "loginName" ) );
ui->textBoxFullName->setEnabled( m_config->isEditable( "fullName" ) );
@ -218,7 +231,7 @@ UsersPage::reportLoginNameStatus( const QString& status )
void
UsersPage::reportHostNameStatus( const QString& status )
{
labelStatus( ui->labelHostname, ui->labelHostnameError, m_config->hostName(), status );
labelStatus( ui->labelHostname, ui->labelHostnameError, m_config->hostname(), status );
}
static inline void

View File

@ -42,7 +42,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="fullNameLayout">
<item>
<widget class="QLineEdit" name="textBoxFullName">
<property name="minimumSize">
@ -129,7 +129,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="usernameLayout">
<item>
<widget class="QLineEdit" name="textBoxLoginName">
<property name="sizePolicy">
@ -218,7 +218,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</spacer>
</item>
<item>
<widget class="QLabel" name="hostname_label_2">
<widget class="QLabel" name="hostnameLabel">
<property name="text">
<string>What is the name of this computer?</string>
</property>
@ -228,9 +228,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<layout class="QHBoxLayout" name="hostnameLayout">
<item>
<widget class="QLineEdit" name="textBoxHostName">
<widget class="QLineEdit" name="textBoxHostname">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -304,7 +304,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</layout>
</item>
<item>
<spacer name="verticalSpacer_3">
<spacer name="hostnameVSpace">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
@ -330,7 +330,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<layout class="QHBoxLayout" name="userPasswordLayout">
<item>
<widget class="QLineEdit" name="textBoxUserPassword">
<property name="sizePolicy">
@ -500,7 +500,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<layout class="QHBoxLayout" name="rootPasswordLayout">
<item>
<widget class="QLineEdit" name="textBoxRootPassword">
<property name="sizePolicy">

View File

@ -149,19 +149,57 @@ allowWeakPasswordsDefault: false
# that the shell actually exists or is executable.
userShell: /bin/bash
# Hostname setting
# Hostname settings
#
# The user can enter a hostname; this is configured into the system
# in some way; pick one of:
# in some way. There are settings for how a hostname is guessed (as
# a default / suggestion) and where (or how) the hostname is set in
# the target system.
#
# Key *hostname* has the following sub-keys:
#
# - *location* How the hostname is set in the target system:
# - *None*, to not set the hostname at all
# - *EtcFile*, to write to `/etc/hostname` directly
# - *Etc*, identical to above
# - *Hostnamed*, to use systemd hostnamed(1) over DBus
# The default is *EtcFile*.
# - *Transient*, to remove `/etc/hostname` from the target
# The default is *EtcFile*. Setting this to *None* or *Transient* will
# hide the hostname field.
# - *writeHostsFile* Should /etc/hosts be written with a hostname for
# this machine (also adds localhost and some ipv6 standard entries).
# Defaults to *true*.
# - *template* Is a simple template for making a suggestion for the
# hostname, based on user data. The default is "${first}-${product}".
# This is used only if the hostname field is shown. KMacroExpander is
# used; write `${key}` where `key` is one of the following:
# - *first* User's first name (whatever is first in the User Name field,
# which is first-in-order but not necessarily a "first name" as in
# "given name" or "name by which you call someone"; beware of western bias)
# - *name* All the text in the User Name field.
# - *login* The login name (which may be suggested based on User Name)
# - *product* The hardware product, based on DMI data
# - *product2* The product as described by Qt
# - *cpu* CPU name
# - *host* Current hostname (which may be a transient hostname)
# Literal text in the template is preserved. Calamares tries to map
# `${key}` values to something that will fit in a hostname, but does not
# apply the same to literal text in the template. Do not use invalid
# characters in the literal text, or no suggeston will be done.
hostname:
location: EtcFile
writeHostsFile: true
template: "derp-${cpu}"
# TODO:3.3: Remove this setting
#
# This is a legacy setting for hostname.location; if it is set
# at all, and there is no setting for hostname.location, it is used.
setHostname: EtcFile
# Should /etc/hosts be written with a hostname for this machine
# (also adds localhost and some ipv6 standard entries).
# Defaults to *true*.
# TODO:3.3: Remove this setting
#
# This is a legacy setting for hostname.writeHostsFile
writeHostsFile: true
presets:

View File

@ -40,7 +40,14 @@ properties:
minLength: { type: number }
maxLength: { type: number }
libpwquality: { type: array, items: { type: string } } # Don't know what libpwquality supports
# Hostname setting
hostname:
additionalProperties: false
type: object
properties:
location: { type: string, enum: [ None, EtcFile, Hostnamed, Transient ] }
writeHostsFile: { type: boolean, default: true }
template: { type: string, default: "${first}-${product}" }
# Legacy Hostname setting
setHostname: { type: string, enum: [ None, EtcFile, Hostnamed ] }
writeHostsFile: { type: boolean, default: true }

View File

@ -149,7 +149,7 @@ Kirigami.ScrollablePage {
id: _hostName
width: parent.width
placeholderText: qsTr("Computer Name")
text: config.hostName
text: config.hostname
validator: RegularExpressionValidator { regularExpression: /[a-zA-Z0-9][-a-zA-Z0-9_]+/ }
onTextChanged: acceptableInput