diff --git a/CHANGES-3.2 b/CHANGES-3.2 index ea8beb764..a1387d99c 100644 --- a/CHANGES-3.2 +++ b/CHANGES-3.2 @@ -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) # diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index 371d98932..cebe45452 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -20,6 +20,8 @@ #include "utils/String.h" #include "utils/Variant.h" +#include + #include #include #include @@ -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; diff --git a/src/modules/users/Config.h b/src/modules/users/Config.h index e37ee77d5..c395dc1d4 100644 --- a/src/modules/users/Config.h +++ b/src/modules/users/Config.h @@ -20,17 +20,15 @@ #include #include -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; }; diff --git a/src/modules/users/MiscJobs.cpp b/src/modules/users/MiscJobs.cpp index 5cba202e0..fec546d96 100644 --- a/src/modules/users/MiscJobs.cpp +++ b/src/modules/users/MiscJobs.cpp @@ -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 diff --git a/src/modules/users/SetHostNameJob.cpp b/src/modules/users/SetHostNameJob.cpp index 9f81ddfb5..452f6a962 100644 --- a/src/modules/users/SetHostNameJob.cpp +++ b/src/modules/users/SetHostNameJob.cpp @@ -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 %1." ).arg( m_hostname ); + return tr( "Set hostname %1." ).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(); } diff --git a/src/modules/users/SetHostNameJob.h b/src/modules/users/SetHostNameJob.h index 9d44579cb..b32b1d7bb 100644 --- a/src/modules/users/SetHostNameJob.h +++ b/src/modules/users/SetHostNameJob.h @@ -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 diff --git a/src/modules/users/TestSetHostNameJob.cpp b/src/modules/users/TestSetHostNameJob.cpp index 84602a053..d1a556824 100644 --- a/src/modules/users/TestSetHostNameJob.cpp +++ b/src/modules/users/TestSetHostNameJob.cpp @@ -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 diff --git a/src/modules/users/Tests.cpp b/src/modules/users/Tests.cpp index acb0c9d6d..9a4005f21 100644 --- a/src/modules/users/Tests.cpp +++ b/src/modules/users/Tests.cpp @@ -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() { diff --git a/src/modules/users/UsersPage.cpp b/src/modules/users/UsersPage.cpp index 6ac6c5bf6..7936e9fc0 100644 --- a/src/modules/users/UsersPage.cpp +++ b/src/modules/users/UsersPage.cpp @@ -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 diff --git a/src/modules/users/page_usersetup.ui b/src/modules/users/page_usersetup.ui index ba1c0bc7d..daad98174 100644 --- a/src/modules/users/page_usersetup.ui +++ b/src/modules/users/page_usersetup.ui @@ -42,7 +42,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + @@ -129,7 +129,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + @@ -218,7 +218,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + What is the name of this computer? @@ -228,9 +228,9 @@ SPDX-License-Identifier: GPL-3.0-or-later - + - + 0 @@ -304,7 +304,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + Qt::Vertical @@ -330,7 +330,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + @@ -500,7 +500,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - + diff --git a/src/modules/users/users.conf b/src/modules/users/users.conf index 9f932c0a1..60284b74e 100644 --- a/src/modules/users/users.conf +++ b/src/modules/users/users.conf @@ -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: diff --git a/src/modules/users/users.schema.yaml b/src/modules/users/users.schema.yaml index 3b49061bc..fe45d5fb2 100644 --- a/src/modules/users/users.schema.yaml +++ b/src/modules/users/users.schema.yaml @@ -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 } diff --git a/src/modules/usersq/usersq.qml b/src/modules/usersq/usersq.qml index 71155af4b..9d49e6a7d 100644 --- a/src/modules/usersq/usersq.qml +++ b/src/modules/usersq/usersq.qml @@ -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