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 - No core changes yet
## Modules ## ## 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) # # 3.2.54 (2022-03-21) #

View File

@ -20,6 +20,8 @@
#include "utils/String.h" #include "utils/String.h"
#include "utils/Variant.h" #include "utils/Variant.h"
#include <KMacroExpander>
#include <QCoreApplication> #include <QCoreApplication>
#include <QFile> #include <QFile>
#include <QMetaProperty> #include <QMetaProperty>
@ -77,14 +79,16 @@ updateGSAutoLogin( bool doAutoLogin, const QString& login )
} }
const NamedEnumTable< HostNameAction >& const NamedEnumTable< HostNameAction >&
hostNameActionNames() hostnameActionNames()
{ {
// *INDENT-OFF* // *INDENT-OFF*
// clang-format off // clang-format off
static const NamedEnumTable< HostNameAction > names { static const NamedEnumTable< HostNameAction > names {
{ QStringLiteral( "none" ), HostNameAction::None }, { QStringLiteral( "none" ), HostNameAction::None },
{ QStringLiteral( "etcfile" ), HostNameAction::EtcHostname }, { QStringLiteral( "etcfile" ), HostNameAction::EtcHostname },
{ QStringLiteral( "hostnamed" ), HostNameAction::SystemdHostname } { QStringLiteral( "etc" ), HostNameAction::EtcHostname },
{ QStringLiteral( "hostnamed" ), HostNameAction::SystemdHostname },
{ QStringLiteral( "transient" ), HostNameAction::Transient },
}; };
// clang-format on // clang-format on
// *INDENT-ON* // *INDENT-ON*
@ -98,7 +102,7 @@ Config::Config( QObject* parent )
emit readyChanged( m_isReady ); // false emit readyChanged( m_isReady ); // false
// Gang together all the changes of status to one readyChanged() signal // 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::loginNameStatusChanged, this, &Config::checkReady );
connect( this, &Config::fullNameChanged, this, &Config::checkReady ); connect( this, &Config::fullNameChanged, this, &Config::checkReady );
connect( this, &Config::userPasswordStatusChanged, this, &Config::checkReady ); connect( this, &Config::userPasswordStatusChanged, this, &Config::checkReady );
@ -240,10 +244,15 @@ Config::loginNameStatus() const
void void
Config::setHostName( const QString& host ) 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_customHostName = !host.isEmpty();
m_hostName = host; m_hostname = host;
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( host.isEmpty() ) if ( host.isEmpty() )
{ {
@ -253,8 +262,8 @@ Config::setHostName( const QString& host )
{ {
gs->insert( "hostname", host ); gs->insert( "hostname", host );
} }
emit hostNameChanged( host ); emit hostnameChanged( host );
emit hostNameStatusChanged( hostNameStatus() ); emit hostnameStatusChanged( hostnameStatus() );
} }
} }
@ -266,31 +275,31 @@ Config::forbiddenHostNames()
} }
QString QString
Config::hostNameStatus() const Config::hostnameStatus() const
{ {
// An empty hostname is "ok", even if it isn't really // An empty hostname is "ok", even if it isn't really
if ( m_hostName.isEmpty() ) if ( m_hostname.isEmpty() )
{ {
return QString(); return QString();
} }
if ( m_hostName.length() < HOSTNAME_MIN_LENGTH ) if ( m_hostname.length() < HOSTNAME_MIN_LENGTH )
{ {
return tr( "Your hostname is too short." ); 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." ); return tr( "Your hostname is too long." );
} }
for ( const QString& badName : forbiddenHostNames() ) 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 ); 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." ); return tr( "Only letters, numbers, underscore and hyphen are allowed." );
} }
@ -298,6 +307,12 @@ Config::hostNameStatus() const
return QString(); 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 /** @brief Guess the machine's name
* *
@ -312,16 +327,11 @@ guessProductName()
if ( !tried ) 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" ) ); QFile dmiFile( QStringLiteral( "/sys/devices/virtual/dmi/id/product_name" ) );
if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) ) if ( dmiFile.exists() && dmiFile.open( QIODevice::ReadOnly ) )
{ {
dmiProduct = QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) dmiProduct = cleanupForHostname( QString::fromLocal8Bit( dmiFile.readAll().simplified().data() ) );
.toLower()
.replace( dmirx, " " )
.remove( ' ' );
} }
if ( dmiProduct.isEmpty() ) if ( dmiProduct.isEmpty() )
{ {
@ -379,17 +389,37 @@ makeLoginNameSuggestion( const QStringList& parts )
return USERNAME_RX.indexIn( usernameSuggestion ) != -1 ? usernameSuggestion : QString(); 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 static QString
makeHostnameSuggestion( const QStringList& parts ) invalidEmpty( const QString& s )
{ {
static const QRegExp HOSTNAME_RX( "^[a-zA-Z0-9][-a-zA-Z0-9_]*$" ); return s.isEmpty() ? QStringLiteral( "^" ) : s;
if ( parts.isEmpty() || parts.first().isEmpty() ) }
{
return QString();
}
QString productName = guessProductName(); STATICTEST QString
QString hostnameSuggestion = QStringLiteral( "%1-%2" ).arg( parts.first() ).arg( productName ); 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(); return HOSTNAME_RX.indexIn( hostnameSuggestion ) != -1 ? hostnameSuggestion : QString();
} }
@ -420,7 +450,7 @@ Config::setFullName( const QString& name )
// Build login and hostname, if needed // Build login and hostname, if needed
static QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive ); static QRegExp rx( "[^a-zA-Z0-9 ]", Qt::CaseInsensitive );
QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) ) const QString cleanName = CalamaresUtils::removeDiacritics( transliterate( name ) )
.replace( QRegExp( "[-']" ), "" ) .replace( QRegExp( "[-']" ), "" )
.replace( rx, " " ) .replace( rx, " " )
.toLower() .toLower()
@ -431,7 +461,7 @@ Config::setFullName( const QString& name )
if ( !m_customLoginName ) if ( !m_customLoginName )
{ {
QString login = makeLoginNameSuggestion( cleanParts ); const QString login = makeLoginNameSuggestion( cleanParts );
if ( !login.isEmpty() && login != m_loginName ) if ( !login.isEmpty() && login != m_loginName )
{ {
setLoginName( login ); setLoginName( login );
@ -441,8 +471,8 @@ Config::setFullName( const QString& name )
} }
if ( !m_customHostName ) if ( !m_customHostName )
{ {
QString hostname = makeHostnameSuggestion( cleanParts ); const QString hostname = makeHostnameSuggestion( m_hostnameTemplate, cleanParts, loginName() );
if ( !hostname.isEmpty() && hostname != m_hostName ) if ( !hostname.isEmpty() && hostname != m_hostname )
{ {
setHostName( hostname ); setHostName( hostname );
// Still not custom // Still not custom
@ -653,7 +683,7 @@ bool
Config::isReady() const Config::isReady() const
{ {
bool readyFullName = !fullName().isEmpty(); // Needs some text 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 readyUsername = !loginName().isEmpty() && loginNameStatus().isEmpty(); // .. no warning message
bool readyUserPassword = userPasswordValidity() != Config::PasswordValidity::Invalid; bool readyUserPassword = userPasswordValidity() != Config::PasswordValidity::Invalid;
bool readyRootPassword = rootPasswordValidity() != Config::PasswordValidity::Invalid; bool readyRootPassword = rootPasswordValidity() != Config::PasswordValidity::Invalid;
@ -734,25 +764,22 @@ setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >
} }
} }
STATICTEST HostNameActions STATICTEST HostNameAction
getHostNameActions( const QVariantMap& configurationMap ) getHostNameAction( const QVariantMap& configurationMap )
{ {
HostNameAction setHostName = HostNameAction::EtcHostname; HostNameAction setHostName = HostNameAction::EtcHostname;
QString hostnameActionString = CalamaresUtils::getString( configurationMap, "setHostname" ); QString hostnameActionString = CalamaresUtils::getString( configurationMap, "location" );
if ( !hostnameActionString.isEmpty() ) if ( !hostnameActionString.isEmpty() )
{ {
bool ok = false; bool ok = false;
setHostName = hostNameActionNames().find( hostnameActionString, ok ); setHostName = hostnameActionNames().find( hostnameActionString, ok );
if ( !ok ) if ( !ok )
{ {
setHostName = HostNameAction::EtcHostname; // Rather than none setHostName = HostNameAction::EtcHostname; // Rather than none
} }
} }
HostNameAction writeHosts = CalamaresUtils::getBool( configurationMap, "writeHostsFile", true ) return setHostName;
? HostNameAction::WriteEtcHosts
: HostNameAction::None;
return setHostName | writeHosts;
} }
/** @brief Process entries in the passwordRequirements config entry /** @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 void
Config::setConfigurationMap( const QVariantMap& configurationMap ) Config::setConfigurationMap( const QVariantMap& configurationMap )
{ {
@ -844,7 +890,19 @@ Config::setConfigurationMap( const QVariantMap& configurationMap )
? SudoStyle::UserAndGroup ? SudoStyle::UserAndGroup
: SudoStyle::UserOnly; : 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 ); setConfigurationDefaultGroups( configurationMap, m_defaultGroups );
@ -923,7 +981,7 @@ Config::createJobs() const
j = new SetPasswordJob( "root", rootPassword() ); j = new SetPasswordJob( "root", rootPassword() );
jobs.append( Calamares::job_ptr( j ) ); jobs.append( Calamares::job_ptr( j ) );
j = new SetHostNameJob( hostName(), hostNameActions() ); j = new SetHostNameJob( this );
jobs.append( Calamares::job_ptr( j ) ); jobs.append( Calamares::job_ptr( j ) );
return jobs; return jobs;

View File

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

View File

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

View File

@ -24,31 +24,30 @@
using WriteMode = CalamaresUtils::System::WriteMode; using WriteMode = CalamaresUtils::System::WriteMode;
SetHostNameJob::SetHostNameJob( const QString& hostname, HostNameActions a ) SetHostNameJob::SetHostNameJob( const Config* c )
: Calamares::Job() : Calamares::Job()
, m_hostname( hostname ) , m_config( c )
, m_actions( a )
{ {
} }
QString QString
SetHostNameJob::prettyName() const SetHostNameJob::prettyName() const
{ {
return tr( "Set hostname %1" ).arg( m_hostname ); return tr( "Set hostname %1" ).arg( m_config->hostname() );
} }
QString QString
SetHostNameJob::prettyDescription() const 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 QString
SetHostNameJob::prettyStatusMessage() const SetHostNameJob::prettyStatusMessage() const
{ {
return tr( "Setting hostname %1." ).arg( m_hostname ); return tr( "Setting hostname %1." ).arg( m_config->hostname() );
} }
STATICTEST bool STATICTEST bool
@ -62,16 +61,19 @@ STATICTEST bool
writeFileEtcHosts( const QString& hostname ) writeFileEtcHosts( const QString& hostname )
{ {
// The actual hostname gets substituted in at %1 // 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.0.1 localhost
127.0.1.1 %1
::1 localhost ip6-localhost ip6-loopback ::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes ff02::1 ip6-allnodes
ff02::2 ip6-allrouters 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( 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 STATICTEST bool
@ -129,29 +131,35 @@ SetHostNameJob::exec()
return Calamares::JobResult::error( tr( "Internal Error" ) ); 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"; cError() << "Can't write to hostname file";
return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) ); 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"; cError() << "Can't write to hosts file";
return Calamares::JobResult::error( tr( "Cannot write hostname to target system" ) ); 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(); return Calamares::JobResult::ok();
} }

View File

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

View File

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

View File

@ -17,8 +17,10 @@
// Implementation details // Implementation details
extern void setConfigurationDefaultGroups( const QVariantMap& map, QList< GroupDescription >& defaultGroups ); 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 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 /** @brief Test Config object methods and internals
* *
@ -42,6 +44,10 @@ private Q_SLOTS:
void testHostActions_data(); void testHostActions_data();
void testHostActions(); void testHostActions();
void testHostActions2();
void testHostSuggestions_data();
void testHostSuggestions();
void testPasswordChecks(); void testPasswordChecks();
void testUserPassword(); void testUserPassword();
@ -228,6 +234,15 @@ UserTests::testHostActions_data()
QTest::newRow( "bad " ) << true << QString( "derp" ) << int( HostNameAction::EtcHostname ); QTest::newRow( "bad " ) << true << QString( "derp" ) << int( HostNameAction::EtcHostname );
QTest::newRow( "none " ) << true << QString( "none" ) << int( HostNameAction::None ); QTest::newRow( "none " ) << true << QString( "none" ) << int( HostNameAction::None );
QTest::newRow( "systemd" ) << true << QString( "Hostnamed" ) << int( HostNameAction::SystemdHostname ); 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 void
@ -240,16 +255,75 @@ UserTests::testHostActions()
QVariantMap m; QVariantMap m;
if ( set ) if ( set )
{ {
m.insert( "setHostname", string ); m.insert( "location", string );
} }
QCOMPARE( getHostNameActions( m ), // action is independent of writeHostsFile
HostNameActions( result ) | HostNameAction::WriteEtcHosts ); // write bits default to true QCOMPARE( getHostNameAction( m ), HostNameAction( result ) );
m.insert( "writeHostsFile", false ); m.insert( "writeHostsFile", false );
QCOMPARE( getHostNameActions( m ), HostNameActions( result ) ); QCOMPARE( getHostNameAction( m ), HostNameAction( result ) );
m.insert( "writeHostsFile", true ); 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 void
UserTests::testPasswordChecks() UserTests::testPasswordChecks()
{ {

View File

@ -105,18 +105,31 @@ UsersPage::UsersPage( Config* config, QWidget* parent )
connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName ); connect( ui->textBoxFullName, &QLineEdit::textEdited, config, &Config::setFullName );
connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited ); connect( config, &Config::fullNameChanged, this, &UsersPage::onFullNameTextEdited );
ui->textBoxHostName->setText( config->hostName() ); // If the hostname is going to be written out, then show the field
connect( ui->textBoxHostName, &QLineEdit::textEdited, config, &Config::setHostName ); 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, connect( config,
&Config::hostNameChanged, &Config::hostnameChanged,
[ this ]( const QString& name ) [ this ]( const QString& name )
{ {
if ( !ui->textBoxHostName->hasFocus() ) if ( !ui->textBoxHostname->hasFocus() )
{ {
ui->textBoxHostName->setText( name ); ui->textBoxHostname->setText( name );
} }
} ); } );
connect( config, &Config::hostNameStatusChanged, this, &UsersPage::reportHostNameStatus ); 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() ); ui->textBoxLoginName->setText( config->loginName() );
connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName ); connect( ui->textBoxLoginName, &QLineEdit::textEdited, config, &Config::setLoginName );
@ -155,7 +168,7 @@ UsersPage::UsersPage( Config* config, QWidget* parent )
onReuseUserPasswordChanged( m_config->reuseUserPasswordForRoot() ); onReuseUserPasswordChanged( m_config->reuseUserPasswordForRoot() );
onFullNameTextEdited( m_config->fullName() ); onFullNameTextEdited( m_config->fullName() );
reportLoginNameStatus( m_config->loginNameStatus() ); reportLoginNameStatus( m_config->loginNameStatus() );
reportHostNameStatus( m_config->hostNameStatus() ); reportHostNameStatus( m_config->hostnameStatus() );
ui->textBoxLoginName->setEnabled( m_config->isEditable( "loginName" ) ); ui->textBoxLoginName->setEnabled( m_config->isEditable( "loginName" ) );
ui->textBoxFullName->setEnabled( m_config->isEditable( "fullName" ) ); ui->textBoxFullName->setEnabled( m_config->isEditable( "fullName" ) );
@ -218,7 +231,7 @@ UsersPage::reportLoginNameStatus( const QString& status )
void void
UsersPage::reportHostNameStatus( const QString& status ) 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 static inline void

View File

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

View File

@ -149,19 +149,57 @@ allowWeakPasswordsDefault: false
# that the shell actually exists or is executable. # that the shell actually exists or is executable.
userShell: /bin/bash userShell: /bin/bash
# Hostname setting # Hostname settings
# #
# The user can enter a hostname; this is configured into the system # 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 # - *None*, to not set the hostname at all
# - *EtcFile*, to write to `/etc/hostname` directly # - *EtcFile*, to write to `/etc/hostname` directly
# - *Etc*, identical to above
# - *Hostnamed*, to use systemd hostnamed(1) over DBus # - *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 setHostname: EtcFile
# Should /etc/hosts be written with a hostname for this machine # TODO:3.3: Remove this setting
# (also adds localhost and some ipv6 standard entries). #
# Defaults to *true*. # This is a legacy setting for hostname.writeHostsFile
writeHostsFile: true writeHostsFile: true
presets: presets:

View File

@ -40,7 +40,14 @@ properties:
minLength: { type: number } minLength: { type: number }
maxLength: { type: number } maxLength: { type: number }
libpwquality: { type: array, items: { type: string } } # Don't know what libpwquality supports 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 ] } setHostname: { type: string, enum: [ None, EtcFile, Hostnamed ] }
writeHostsFile: { type: boolean, default: true } writeHostsFile: { type: boolean, default: true }

View File

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