config: keep Store mirrors authoritative
Debian Trixie package bundle / packages (push) Successful in 22m24s

This commit is contained in:
Mario Fetka
2026-07-27 21:33:35 +02:00
parent 1056e0c489
commit fbb5c97ab0
4 changed files with 135 additions and 12 deletions
+16 -1
View File
@@ -96,10 +96,25 @@ def read_configuration(name: str) -> dict:
def replace_configuration(name: str, value: dict) -> None:
serialized = json.dumps(
value, ensure_ascii=False, separators=(",", ":")
)
SMTP07.run(
[SMTP07.ADMIN, "__config-replace", name],
input_text=json.dumps(value, separators=(",", ":")),
input_text=serialized,
)
if "/" not in name:
path = Path("/etc/bongo/config.d") / name
try:
mirrored = json.loads(path.read_bytes())
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
raise SMTP26Error(
f"configuration mirror {path} is not valid UTF-8 JSON"
) from error
if mirrored != value:
raise SMTP26Error(
f"configuration mirror {path} does not match the Store update"
)
def restart_bongo(host: str, port: int) -> None:
+7 -8
View File
@@ -44,14 +44,13 @@ is the administrator- and configuration-management-friendly representation.
`config check` validates those local files without changing the Store;
`config sync` validates them and asks the running manager to apply them. The
manager reconciles the same mirror at startup. It uses the Store object's
existing Unix modification time as the common revision: a newer valid local
file is an intentional emergency edit and is imported, while a newer Store
document refreshes the file. Store mirror timestamps have zero nanoseconds,
so an editor save in the same second is still recognized as a local edit; an
otherwise equal timestamp belongs to the primary Store. `config sync` explicitly
selects the validated local files when an administrator needs to force an
emergency edit into the Store. Listener changes still require the affected
agents to be restarted.
existing Unix modification time as the common revision: a valid local file
whose whole-second timestamp is strictly newer is an intentional emergency
edit and is imported, while a newer or equally dated Store document refreshes
the file. This tie-break deliberately keeps the Store authoritative; use
`config sync` to force a valid local edit made within the same timestamp
second into the Store. Store mirror timestamps have zero nanoseconds.
Listener changes still require the affected agents to be restarted.
If a local mirror is missing but its operative Store document still exists,
`config sync` reconstructs the file from the Store before committing the
+111
View File
@@ -28,12 +28,14 @@
#include <nmlib.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <syslog.h>
#include <time.h>
@@ -273,12 +275,103 @@ ReadConfigurationInput(char **content, size_t *length)
return 0;
}
static int
WriteConfigurationMirror(const char *name, const char *content, size_t length,
uint64_t modified)
{
char path[XPL_MAX_PATH + 1];
char temporary[XPL_MAX_PATH + 32];
struct stat details;
struct timespec times[2];
const char *cursor = content;
size_t remaining = length;
int descriptor = -1;
int directory = -1;
int saved_error = 0;
int result = 0;
temporary[0] = '\0';
/* Alias Store documents are a compiled per-domain view of aliases.d,
* rather than a one-to-one local document. Public alias operations
* therefore update aliases.d transactionally before the manager compiles
* that view. */
if (strchr(name, '/'))
return 1;
if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_CONFIG_DIR, name) >=
(int)sizeof(path) ||
snprintf(temporary, sizeof(temporary), "%s.tmp.XXXXXX", path) >=
(int)sizeof(temporary)) {
errno = ENAMETOOLONG;
goto done;
}
if (lstat(path, &details) != 0 || !S_ISREG(details.st_mode)) {
fprintf(stderr,
"bongo-admin: configuration mirror is missing or unsafe: %s\n",
path);
errno = EINVAL;
goto done;
}
times[0] = details.st_atim;
times[1].tv_sec = (time_t)modified;
times[1].tv_nsec = 0;
if ((uint64_t)times[1].tv_sec != modified ||
(descriptor = mkstemp(temporary)) < 0)
goto done;
while (remaining) {
ssize_t written = write(descriptor, cursor, remaining);
if (written < 0) {
if (errno == EINTR)
continue;
goto done;
}
cursor += written;
remaining -= (size_t)written;
}
if (fchmod(descriptor, details.st_mode & 0777) != 0 ||
fchown(descriptor, details.st_uid, details.st_gid) != 0 ||
futimens(descriptor, times) != 0 || fsync(descriptor) != 0)
goto done;
if (close(descriptor) != 0) {
descriptor = -1;
goto done;
}
descriptor = -1;
if (rename(temporary, path) != 0)
goto done;
temporary[0] = '\0';
result = 1;
directory = open(XPL_DEFAULT_CONFIG_DIR,
O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (directory < 0 || fsync(directory) != 0)
fprintf(stderr,
"bongo-admin: warning: cannot sync configuration directory: %s\n",
strerror(errno));
done:
saved_error = errno;
if (descriptor >= 0)
close(descriptor);
if (directory >= 0)
close(directory);
if (!result && temporary[0])
unlink(temporary);
errno = saved_error;
if (!result)
fprintf(stderr,
"bongo-admin: cannot update configuration mirror %s: %s\n",
path, strerror(errno));
return result;
}
static int
AccessConfiguration(const char *name, int replace)
{
BongoJsonNode *root = NULL;
char *content = NULL;
char *original = NULL;
size_t length = 0;
uint64_t modified = 0;
int conn_started = 0;
int msg_started = 0;
int result = 1;
@@ -309,11 +402,27 @@ AccessConfiguration(const char *name, int replace)
goto done;
}
if (replace) {
if (!strchr(name, '/') &&
(!NMAPReadConfigFile(name, &original) || !original)) {
fprintf(stderr,
"bongo-admin: cannot snapshot configuration %s\n", name);
goto done;
}
if (!NMAPReplaceConfigFile(name, content, length)) {
fprintf(stderr, "bongo-admin: cannot replace configuration %s\n",
name);
goto done;
}
if (!strchr(name, '/') &&
(!NMAPReadConfigFileModified(name, &modified) ||
!WriteConfigurationMirror(name, content, length, modified))) {
if (!original ||
!NMAPReplaceConfigFile(name, original, strlen(original)))
fprintf(stderr,
"bongo-admin: CRITICAL: cannot roll back configuration %s\n",
name);
goto done;
}
openlog("bongo-admin", LOG_PID, LOG_AUTHPRIV);
syslog(LOG_NOTICE, "administrator replaced Bongo configuration %s",
name);
@@ -334,6 +443,8 @@ AccessConfiguration(const char *name, int replace)
done:
if (root)
BongoJsonNodeFree(root);
if (original)
MemFree(original);
if (content) {
if (replace)
free(content);
+1 -3
View File
@@ -834,9 +834,7 @@ SyncConfigurationDocument(const char *storeName, const char *path)
result = SetConfigurationFileTime(path, &details, storeModified);
goto done;
}
if ((uint64_t)details.st_mtime > storeModified ||
((uint64_t)details.st_mtime == storeModified &&
details.st_mtim.tv_nsec != 0)) {
if ((uint64_t)details.st_mtime > storeModified) {
if (!SyncConfigurationContent(storeName, path, local) ||
!NMAPReadConfigFileModified(storeName, &storeModified))
goto done;