feat(config): add serverid configuration and use it for Snowflakes

Signed-off-by: Benjamin Gaussorgues <benjamin.gaussorgues@nextcloud.com>
pull/56095/head
Benjamin Gaussorgues 2025-10-30 11:40:35 +07:00
parent 69ec2ce26b
commit 1f74ed9239
No known key found for this signature in database
7 changed files with 99 additions and 3 deletions

@ -132,6 +132,7 @@ return array(
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => $baseDir . '/../lib/SetupChecks/ReadOnlyConfig.php',
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => $baseDir . '/../lib/SetupChecks/SchedulingTableSize.php',
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => $baseDir . '/../lib/SetupChecks/SecurityHeaders.php',
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => $baseDir . '/../lib/SetupChecks/ServerIdConfig.php',
'OCA\\Settings\\SetupChecks\\SupportedDatabase' => $baseDir . '/../lib/SetupChecks/SupportedDatabase.php',
'OCA\\Settings\\SetupChecks\\SystemIs64bit' => $baseDir . '/../lib/SetupChecks/SystemIs64bit.php',
'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => $baseDir . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php',

@ -147,6 +147,7 @@ class ComposerStaticInitSettings
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ReadOnlyConfig.php',
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => __DIR__ . '/..' . '/../lib/SetupChecks/SchedulingTableSize.php',
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => __DIR__ . '/..' . '/../lib/SetupChecks/SecurityHeaders.php',
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ServerIdConfig.php',
'OCA\\Settings\\SetupChecks\\SupportedDatabase' => __DIR__ . '/..' . '/../lib/SetupChecks/SupportedDatabase.php',
'OCA\\Settings\\SetupChecks\\SystemIs64bit' => __DIR__ . '/..' . '/../lib/SetupChecks/SystemIs64bit.php',
'OCA\\Settings\\SetupChecks\\TaskProcessingPickupSpeed' => __DIR__ . '/..' . '/../lib/SetupChecks/TaskProcessingPickupSpeed.php',

@ -69,6 +69,7 @@ use OCA\Settings\SetupChecks\RandomnessSecure;
use OCA\Settings\SetupChecks\ReadOnlyConfig;
use OCA\Settings\SetupChecks\SchedulingTableSize;
use OCA\Settings\SetupChecks\SecurityHeaders;
use OCA\Settings\SetupChecks\ServerIdConfig;
use OCA\Settings\SetupChecks\SupportedDatabase;
use OCA\Settings\SetupChecks\SystemIs64bit;
use OCA\Settings\SetupChecks\TaskProcessingPickupSpeed;
@ -207,6 +208,7 @@ class Application extends App implements IBootstrap {
$context->registerSetupCheck(RandomnessSecure::class);
$context->registerSetupCheck(ReadOnlyConfig::class);
$context->registerSetupCheck(SecurityHeaders::class);
$context->registerSetupCheck(ServerIdConfig::class);
$context->registerSetupCheck(SchedulingTableSize::class);
$context->registerSetupCheck(SupportedDatabase::class);
$context->registerSetupCheck(SystemIs64bit::class);

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Settings\SetupChecks;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
use Override;
final class ServerIdConfig implements ISetupCheck {
public function __construct(
private readonly IL10N $l10n,
private readonly IConfig $config,
private readonly IURLGenerator $urlGenerator,
) {
}
#[Override]
public function getName(): string {
return $this->l10n->t('Configuration server ID');
}
#[Override]
public function getCategory(): string {
return 'config';
}
#[Override]
public function run(): SetupResult {
$serverid = $this->config->getSystemValueInt('serverid', PHP_INT_MIN);
$linkToDoc = $this->urlGenerator->linkToDocs('admin-update');
if ($serverid === PHP_INT_MIN) {
return SetupResult::info(
$this->l10n->t('server identifier isnt configured. It is recommended if your Nextcloud instance is running on several PHP servers. Add a serverid in your configuration.'),
$linkToDoc,
);
}
if ($serverid < 0 || $serverid > 1023) {
return SetupResult::error(
$this->l10n->t('"%d" is not a valid server identifier. It must be between 0 and 1023.', [$serverid]),
$linkToDoc,
);
}
return SetupResult::success($this->l10n->t('server identifier is configured and valid.'));
}
}

@ -45,6 +45,20 @@ $CONFIG = [
*/
'instanceid' => '',
/**
* This is a unique identifier for your server.
* It is useful when your Nextcloud instance is using different PHP servers.
* Once it's set it shouldn't be changed.
*
* Value must be an integer, comprised between 0 and 1023.
*
* When config.php is shared between different servers, this value should be overriden with "NC_serverid=<int>" on each server.
* Note that it must be overriden for CLI and for your webserver.
*
* Example for CLI: NC_serverid=42 occ config:list system
*/
'serverid' => -1,
/**
* The salt used to hash all passwords, auto-generated by the Nextcloud
* installer. (There are also per-user salts.) If you lose this salt, you lose

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace OC\Snowflake;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use Override;
@ -23,6 +24,7 @@ use Override;
final class Generator implements IGenerator {
public function __construct(
private readonly ITimeFactory $timeFactory,
private readonly IConfig $config,
private readonly ISequence $sequenceGenerator,
) {
}
@ -100,8 +102,14 @@ final class Generator implements IGenerator {
];
}
/**
* Return configured serverid or generate one if not set
*/
private function getServerId(): int {
return crc32(gethostname() ?: random_bytes(8));
$serverid = $this->config->getSystemValueInt('serverid', -1);
return $serverid > 0
? $serverid
: crc32(gethostname() ?: random_bytes(8));
}
private function isCli(): bool {

@ -14,6 +14,7 @@ use OC\Snowflake\Decoder;
use OC\Snowflake\Generator;
use OC\Snowflake\ISequence;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\Snowflake\IGenerator;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\MockObject\MockObject;
@ -24,17 +25,25 @@ use Test\TestCase;
*/
class GeneratorTest extends TestCase {
private Decoder $decoder;
private IConfig&MockObject $config;
private ISequence&MockObject $sequence;
public function setUp():void {
$this->decoder = new Decoder();
$this->config = $this->createMock(IConfig::class);
$this->config->method('getSystemValueInt')
->with('serverid')
->willReturn(42);
$this->sequence = $this->createMock(ISequence::class);
$this->sequence->method('isAvailable')->willReturn(true);
$this->sequence->method('nextId')->willReturn(421);
}
public function testGenerator(): void {
$generator = new Generator(new TimeFactory(), $this->sequence);
$generator = new Generator(new TimeFactory(), $this->config, $this->sequence);
$snowflakeId = $generator->nextId();
$data = $this->decoder->decode($generator->nextId());
@ -52,6 +61,9 @@ class GeneratorTest extends TestCase {
// Check CLI
$this->assertTrue($data['isCli']);
// Check serverId
$this->assertEquals(42, $data['serverId']);
}
#[DataProvider('provideSnowflakeData')]
@ -60,11 +72,12 @@ class GeneratorTest extends TestCase {
$timeFactory = $this->createMock(ITimeFactory::class);
$timeFactory->method('now')->willReturn($dt);
$generator = new Generator($timeFactory, $this->sequence);
$generator = new Generator($timeFactory, $this->config, $this->sequence);
$data = $this->decoder->decode($generator->nextId());
$this->assertEquals($expectedSeconds, ($data['createdAt']->format('U') - IGenerator::TS_OFFSET));
$this->assertEquals($expectedMilliseconds, (int)$data['createdAt']->format('v'));
$this->assertEquals(42, $data['serverId']);
}
public static function provideSnowflakeData(): array {